Skip to content

Harden QR decoder against a crafted-image hang (and a NULL memcpy) - #159

Open
AlexandrKhromov2005 wants to merge 3 commits into
dlbeer:masterfrom
AlexandrKhromov2005:fix/qr-decoder-hardening
Open

Harden QR decoder against a crafted-image hang (and a NULL memcpy)#159
AlexandrKhromov2005 wants to merge 3 commits into
dlbeer:masterfrom
AlexandrKhromov2005:fix/qr-decoder-hardening

Conversation

@AlexandrKhromov2005

Copy link
Copy Markdown

Harden the QR decoder against a crafted-image hang (and a NULL memcpy)

Two fixes found by fuzzing quirc with libFuzzer + ASan/UBSan.

1. DoS: unbounded alignment-pattern spiral (identify.c).
find_alignment_pattern() spirals outward while step_size*step_size < size_estimate*100.
size_estimate is derived from coordinates produced by the perspective transform; a degenerate
transform (near-zero denominator) makes those coordinates huge, so size_estimate becomes enormous
and the loop runs for hundreds of millions of iterations. A 246×246 image (about the size of a
real screenshot QR) makes quirc_end() run for over 90 seconds on ~28 MB of RAM — a pure CPU
hang on attacker-supplied image data. Since the alignment pattern must lie within the image, this
caps the search radius at the image size. Verified with the fuzzer reproducer: the 90s+ hang drops
to ~12 ms and real QR codes still decode.

2. UB: memcpy(dst, NULL, 0) on first resize (quirc.c).
On the first quirc_resize(), q->image is NULL and q->w == q->h == 0, so min == 0 and the
copy is memcpy(image, NULL, 0) — UB (memcpy's source is declared nonnull), flagged by UBSan on
the first decode of every fresh handle. Guarded with if (min).

These reproduce on current master. Both are also present in copies of quirc vendored by downstream
projects (I hit #1 fuzzing one such client). Minimal reproducer for the hang available on request.

On the first quirc_resize() the previous image buffer q->image is NULL and
q->w == q->h == 0, so min == 0 and the copy becomes memcpy(image, NULL, 0).
That is undefined behaviour (memcpy's source is declared nonnull) and is
flagged by UBSan on the first decode of every fresh quirc handle.

Skip the copy when there is nothing to copy.
find_alignment_pattern() spirals outward while
step_size*step_size < size_estimate*100. size_estimate is derived from
coordinates produced by the perspective transform; a degenerate transform
(near-zero denominator) makes those coordinates huge, so size_estimate becomes
enormous and the loop runs for hundreds of millions of iterations.

A 246x246 image (about the size of a real screenshot QR) makes quirc_end() run
for over 90 seconds on ~28 MB of RAM - a pure CPU hang on attacker-supplied
image data. The alignment pattern must lie within the image, so cap the search
radius at the image size. Verified with a fuzzer-found reproducer: the 90s+ hang
drops to ~12 ms and real QR codes still decode.

Found by fuzzing (libFuzzer + ASan/UBSan).

@kaworu kaworu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @AlexandrKhromov2005, couple of nitpicking comments, but the patch LGTM.

Comment thread lib/identify.c Outdated
* radius at the image size.
*/
while (step_size * step_size < size_estimate * 100) {
int max_step = (q->w > q->h ? q->w : q->h) + 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const?

Suggested change
int max_step = (q->w > q->h ? q->w : q->h) + 1;
const int max_step = (q->w > q->h ? q->w : q->h) + 1;

Comment thread lib/quirc.c Outdated
* new buffer when the new size is smaller, hence the min computation.
*/
(void)memcpy(image, q->image, min);
if (min)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking

Suggested change
if (min)
if (min > 0)

@AlexandrKhromov2005

Copy link
Copy Markdown
Author

Thanks for the review @kaworu — applied both in b1d371f: const int max_step and the explicit if (min > 0). min is a size_t so it's a no-op behaviorally, but the intent reads better. Let me know if there's anything else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants