Merge pull request #666 from Foundation-Devices/fix/quirc-grid-bounds
What changed, and why it matters
This update hardens the QR-code scanner library (quirc) used in the Passport hardware wallet so it refuses to process impossibly large QR grids and no longer reads past the end of its internal buffer when a malformed QR code is presented. It also fixes an off-by-one bug in grid indexing. The changes are defensive: they prevent memory corruption and crashes when scanning deliberately crafted or corrupted QR codes, rather than changing normal wallet behavior.
Treat this as a security hardening fix and include it in the next firmware release. Run the new quirc unit tests in CI (already added) and consider fuzzing quirc_decode/quirc_extract with mutated QR inputs to find related bounds issues. Review other quirc callers to ensure no path bypasses the new checks.
Security signals we found
Buffer overflow / out-of-bounds write prevention in quirc_extract()
Off-by-one fix in grid index validation (index > num_grids -> index >= num_grids)
Input validation in quirc_decode() rejecting oversized QR grids
Version clamping in measure_timing_pattern() to ISO/IEC 18004 version range 1-40
New regression tests with AddressSanitizer/UndefinedBehaviorSanitizer canaries
Evidence from the diff
The commit adds explicit bounds checks to quirc, the QR decoder used by Passport firmware. In decode.c, quirc_decode() now rejects code->size > QUIRC_MAX_GRID_SIZE (177 modules) before any further processing. In identify.c, measure_timing_pattern() clamps the derived QR version to 1..QUIRC_MAX_VERSION, and quirc_extract() skips extraction when qr->grid_size exceeds QUIRC_MAX_GRID_SIZE, preventing writes beyond the cell_bitmap buffer. It also fixes an index validation bug: the previous check index > q->num_grids allowed reading q->grids[num_grids], now changed to index >= q->num_grids. A new unit test with canary buffers verifies that oversized grids do not corrupt adjacent memory, and a GitHub Actions job runs these tests.
Changed components
extmod/quirc/decode.cextmod/quirc/identify.cextmod/quirc/quirc.hextmod/quirc/quirc_internal.hextmod/quirc/tests/test_grid_bounds.c.github/workflows/lint.yamlInspect captured patch +178 / −6
### .github/workflows/lint.yaml
@@ -11,6 +11,13 @@ jobs:
- uses: actions/checkout@v6
- uses: fsfe/reuse-action@v6
+ quirc-tests-pass:
+ name: quirc tests pass?
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+ - run: make -C extmod/quirc/tests test
+
rust-code-compiles:
name: Rust code compiles?
runs-on: ubuntu-latest
### extmod/quirc/decode.c
@@ -891,6 +891,9 @@ quirc_decode_error_t quirc_decode(const struct quirc_code *code,
{
quirc_decode_error_t err;
+ if (code->size > QUIRC_MAX_GRID_SIZE)
+ return QUIRC_ERROR_INVALID_GRID_SIZE;
+
if ((code->size - 17) % 4)
return QUIRC_ERROR_INVALID_GRID_SIZE;
### extmod/quirc/identify.c
@@ -728,6 +728,8 @@ static int measure_timing_pattern(struct quirc *q, int index)
/* Choose the nearest allowable grid size */
size = scan * 2 + 13;
ver = (size - 15) / 4;
+ if (ver < 1 || ver > QUIRC_MAX_VERSION)
+ return -1;
qr->grid_size = ver * 4 + 17;
return 0;
@@ -1217,14 +1219,16 @@ void quirc_end(struct quirc *q)
void quirc_extract(const struct quirc *q, int index,
struct quirc_code *code)
{
- const struct quirc_grid *qr = &q->grids[index];
+ const struct quirc_grid *qr;
int y;
int i = 0;
- if (index < 0 || index > q->num_grids)
+ memset(code, 0, sizeof(*code));
+
+ if (index < 0 || index >= q->num_grids)
return;
- memset(code, 0, sizeof(*code));
+ qr = &q->grids[index];
perspective_map(qr->c, 0.0, 0.0, &code->corners[0]);
perspective_map(qr->c, qr->grid_size, 0.0, &code->corners[1]);
@@ -1234,6 +1238,12 @@ void quirc_extract(const struct quirc *q, int index,
code->size = qr->grid_size;
+ /* Skip out early so as not to overrun the buffer. quirc_decode
+ * will return an error on interpreting the code.
+ */
+ if (code->size > QUIRC_MAX_GRID_SIZE)
+ return;
+
for (y = 0; y < qr->grid_size; y++)
{
int x;
### extmod/quirc/quirc.h
@@ -90,8 +90,17 @@ typedef enum {
/* Return a string error message for an error code. */
const char *quirc_strerror(quirc_decode_error_t err);
-/* Limits on the maximum size of QR-codes and their content. */
-#define QUIRC_MAX_BITMAP 3917
+/* Limits on the maximum size of QR-codes and their content.
+ *
+ * ISO/IEC 18004:2024, 5.1, defines QR Code versions 1 through 40. Version 1
+ * is 21 modules per side and each version adds four modules per side, so the
+ * Version 40 limit is 4 * 40 + 17 = 177 modules.
+ * https://www.qrcode.com/en/about/version.html/index.html
+ */
+#define QUIRC_MAX_VERSION 40
+#define QUIRC_MAX_GRID_SIZE (QUIRC_MAX_VERSION * 4 + 17)
+/* One bit per module, rounded up to whole bytes: ceil(177 * 177 / 8) = 3917. */
+#define QUIRC_MAX_BITMAP (((QUIRC_MAX_GRID_SIZE * QUIRC_MAX_GRID_SIZE) + 7) / 8)
#define QUIRC_MAX_PAYLOAD 8896
/* QR-code ECC types. */
### extmod/quirc/quirc_internal.h
@@ -100,7 +100,6 @@ struct quirc {
* QR-code version information database
*/
-#define QUIRC_MAX_VERSION 40
#define QUIRC_MAX_ALIGNMENT 7
struct quirc_rs_params {
### extmod/quirc/tests/Makefile
@@ -0,0 +1,25 @@
+# SPDX-FileCopyrightText: Copyright (C) 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+QUIRC_DIR := ..
+PASSPORT_INCLUDE := ../../../ports/stm32/boards/Passport/include
+SOURCES := test_grid_bounds.c \
+ $(QUIRC_DIR)/identify.c \
+ $(QUIRC_DIR)/decode.c \
+ $(QUIRC_DIR)/quirc.c \
+ $(QUIRC_DIR)/version_db.c
+
+CFLAGS += -g -std=c99 -Wall -Wextra -Werror -Wno-unused-parameter
+CFLAGS += -fsanitize=address,undefined -fno-omit-frame-pointer
+LDFLAGS += -fsanitize=address,undefined -lm
+
+.PHONY: test clean
+
+test: test_grid_bounds
+ ./test_grid_bounds
+
+test_grid_bounds: $(SOURCES)
+ $(CC) $(CFLAGS) -I$(QUIRC_DIR) -I$(PASSPORT_INCLUDE) $^ $(LDFLAGS) -o $@
+
+clean:
+ rm -f test_grid_bounds
### extmod/quirc/tests/test_grid_bounds.c
@@ -0,0 +1,119 @@
+// SPDX-FileCopyrightText: Copyright (C) 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <assert.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "quirc_internal.h"
+
+#define CANARY_SIZE 16
+#define CANARY_BYTE 0xa5
+
+struct guarded_code {
+ uint8_t before[CANARY_SIZE];
+ struct quirc_code code;
+ uint8_t after[CANARY_SIZE];
+};
+
+static void assert_canaries(const struct guarded_code *guarded)
+{
+ int i;
+
+ for (i = 0; i < CANARY_SIZE; i++) {
+ assert(guarded->before[i] == CANARY_BYTE);
+ assert(guarded->after[i] == CANARY_BYTE);
+ }
+}
+
+static void test_extract_rejects_index_at_count(void)
+{
+ struct quirc q;
+ struct guarded_code guarded;
+
+ memset(&q, 0, sizeof(q));
+ memset(&guarded, CANARY_BYTE, sizeof(guarded));
+ q.num_grids = 1;
+ q.grids[1].grid_size = 21;
+
+ quirc_extract(&q, 1, &guarded.code);
+
+ assert(guarded.code.size == 0);
+ assert_canaries(&guarded);
+}
+
+static void test_extract_rejects_oversized_grid(void)
+{
+ struct quirc q;
+ struct guarded_code guarded;
+ quirc_pixel_t pixel = QUIRC_PIXEL_BLACK;
+
+ memset(&q, 0, sizeof(q));
+ memset(&guarded, CANARY_BYTE, sizeof(guarded));
+
+ q.pixels = &pixel;
+ q.w = 1;
+ q.h = 1;
+ q.num_grids = 1;
+ q.grids[0].grid_size = QUIRC_MAX_GRID_SIZE + 4;
+
+ quirc_extract(&q, 0, &guarded.code);
+
+ assert(guarded.code.size == QUIRC_MAX_GRID_SIZE + 4);
+ assert_canaries(&guarded);
+}
+
+static void test_extract_accepts_maximum_grid(void)
+{
+ struct quirc q;
+ struct guarded_code guarded;
+ quirc_pixel_t pixel = QUIRC_PIXEL_BLACK;
+
+ memset(&q, 0, sizeof(q));
+ memset(&guarded, CANARY_BYTE, sizeof(guarded));
+
+ q.pixels = &pixel;
+ q.w = 1;
+ q.h = 1;
+ q.num_grids = 1;
+ q.grids[0].grid_size = QUIRC_MAX_GRID_SIZE;
+
+ quirc_extract(&q, 0, &guarded.code);
+
+ assert(guarded.code.size == QUIRC_MAX_GRID_SIZE);
+ assert(guarded.code.cell_bitmap[QUIRC_MAX_BITMAP - 1] == 1);
+ assert_canaries(&guarded);
+}
+
+static void test_decode_rejects_oversized_grid(void)
+{
+ struct quirc_code code;
+ struct quirc_data data;
+
+ memset(&code, 0, sizeof(code));
+ code.size = QUIRC_MAX_GRID_SIZE + 4;
+
+ assert(quirc_decode(&code, &data) == QUIRC_ERROR_INVALID_GRID_SIZE);
+}
+
+static void test_decode_accepts_maximum_grid_size(void)
+{
+ struct quirc_code code;
+ struct quirc_data data;
+
+ memset(&code, 0, sizeof(code));
+ code.size = QUIRC_MAX_GRID_SIZE;
+
+ assert(quirc_decode(&code, &data) != QUIRC_ERROR_INVALID_GRID_SIZE);
+}
+
+int main(void)
+{
+ test_extract_rejects_index_at_count();
+ test_extract_rejects_oversized_grid();
+ test_extract_accepts_maximum_grid();
+ test_decode_rejects_oversized_grid();
+ test_decode_accepts_maximum_grid_size();
+
+ return 0;
+}Why this scored 60/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.