What changed, and why it matters
This commit adds safety checks to the QR-code scanning library (quirc) used in Passport firmware. It rejects QR grids that are larger than the standard maximum size (version 40, or 177×177 cells) before they can be processed. Without these checks, a malformed or oversized QR code could cause memory corruption or crashes. The commit also fixes an off-by-one bug in grid index validation and adds tests to verify the new limits.
Treat as a security hardening fix and include in release notes. Review whether other quirc entry points (e.g., quirc_end/quirc_extract callers) enforce the same limits and consider fuzzing the QR pipeline with malformed inputs.
Security signals we found
Bounds check added on QR grid size before decode/extract
Off-by-one fix in grid index validation
Output struct zeroed before bounds check to prevent information leak/use of uninitialized data
New regression tests with ASan/UBSan canaries confirm no buffer overflow on oversized grid
QUIRC_MAX_BITMAP now derived from maximum grid size rather than hardcoded constant
Evidence from the diff
The patch hardens quirc by introducing a QUIRC_MAX_GRID_SIZE derived from QUIRC_MAX_VERSION (40), and rejects code->size larger than that in quirc_decode() and qr->grid_size larger than that in measure_timing_pattern() and quirc_extract(). It also fixes an off-by-one in quirc_extract() where index <= num_grids was accepted instead of index < num_grids, and moves memset before the bounds check so the output is always cleared. A new test file with AddressSanitizer/UBSan verifies oversized grids are rejected and canary bytes around the output struct remain intact.
Changed components
extmod/quirc/decode.cextmod/quirc/identify.cextmod/quirc/quirc.hextmod/quirc/quirc_internal.hextmod/quirc/tests/Makefileextmod/quirc/tests/test_grid_bounds.cInspect captured patch +159 / −5
### 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
@@ -729,6 +729,8 @@ static int measure_timing_pattern(struct quirc *q, int index)
size = scan * 2 + 13;
ver = (size - 15) / 4;
qr->grid_size = ver * 4 + 17;
+ if (qr->grid_size > QUIRC_MAX_GRID_SIZE)
+ return -1;
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]);
@@ -1233,6 +1237,8 @@ void quirc_extract(const struct quirc *q, int index,
perspective_map(qr->c, 0.0, qr->grid_size, &code->corners[3]);
code->size = qr->grid_size;
+ if (code->size > QUIRC_MAX_GRID_SIZE)
+ return;
for (y = 0; y < qr->grid_size; y++)
{
### extmod/quirc/quirc.h
@@ -91,7 +91,9 @@ typedef enum {
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
+#define QUIRC_MAX_VERSION 40
+#define QUIRC_MAX_GRID_SIZE (QUIRC_MAX_VERSION * 4 + 17)
+#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 += -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 63/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.