Skip to content

Fix integer-overflow crash in qrng_range32/qrng_range64 (SIGFPE on full-span inputs) - #1

Closed
RichardHoekstra wants to merge 1 commit into
Tsotchke-Corporation:masterfrom
RichardHoekstra:fix/qrng-range-overflow-sigfpe
Closed

RichardHoekstra wants to merge 1 commit into
Tsotchke-Corporation:masterfrom
RichardHoekstra:fix/qrng-range-overflow-sigfpe

Conversation

@RichardHoekstra

Copy link
Copy Markdown

Summary

qrng_range32 can crash the process with SIGFPE on caller-controlled input. Because /v1/qrng/range forwards min/max straight to it (server.js → rng.getRange32(min, max)), a single request at the full 32-bit span takes the server down — an unauthenticated denial of service.

Root cause

uint32_t range = (uint32_t)(max - min + 1);   // max - min + 1 is signed int32
if (range == 0) return max;                    // guard
...
return min + (r % range);

max - min + 1 is evaluated in signed int32. At the full span (min = INT32_MIN, max = INT32_MAX) that overflows, which is undefined behavior. At -O2 the compiler is entitled to assume signed overflow never happens, so it proves max - min + 1 >= 1 ⇒ range != 0 and deletes the if (range == 0) guard. At runtime the value actually wraps to 0, and r % range becomes r % 0 → SIGFPE. (Confirmed: crashes at -O2, not at -O0 — the signature of a guard the optimizer removed under UB.)

qrng_range64 has the sibling bug: max - min + 1 is unsigned there (defined wraparound), so the guard survives — but on the full 2⁶⁴ span it returns the constant UINT64_MAX instead of a uniform draw.

Fix

  • qrng_range32: compute the span in 64-bit ((uint64_t)((int64_t)max - (int64_t)min) + 1), which cannot overflow; branch out the full-span case (return a raw draw, no rejection or modulo); unsigned-add on the return to avoid the residual signed overflow when min < 0.
  • qrng_range64: on the full-span (range == 0) case, return qrng_uint64(ctx) instead of the constant max.

Verification

Built and tested with the real generator (qrng_init + qrng_range*) at -O2:

  • pre-fix, qrng_range32(ctx, INT32_MIN, INT32_MAX) → SIGFPE (exit 136);
  • post-fix, same call → no crash; over 3M draws the output spans [-2147480997 .. 2147481639] (genuine negatives + positives);
  • bounded [-3..6]: all 10 buckets hit; ordinary [1..6] unaffected;
  • qrng_range64 full span: pre-fix returns constant 18446744073709551615, post-fix draws vary;
  • the bundled edge-case suite ("Range Edge Cases") still passes on the patched tree.

A one-command standalone reproducer is available — happy to drop it in a comment if useful.

qrng_range32 computed the span (max - min + 1) in signed int32. At the
full span (min=INT32_MIN, max=INT32_MAX) this is signed-overflow UB; at
-O2 the compiler then proves range != 0 and eliminates the (range == 0)
guard, so the subsequent r % range divides by zero and the process takes
SIGFPE. Because /v1/qrng/range passes caller-supplied min/max straight
through, a single request at the full span crashes the server.

Fix: compute the span in 64-bit (uint64_t)((int64_t)max-(int64_t)min)+1,
which cannot overflow, and branch out the full-span case (return a raw
draw, no modulo). Unsigned add on the return avoids the residual signed
overflow when min<0.

qrng_range64 had the sibling bug: on the full 2^64 span the range wraps
to 0 (defined, so the guard survived) and it returned the constant max
instead of a uniform draw. Now returns qrng_uint64(ctx).

Verified at -O2: no crash on the full span, output covers negatives and
positives, all buckets hit, range64 full-span now varies.
@tsotchke

Copy link
Copy Markdown
Member

Thank you, Richard. Your report correctly identified both the full-span qrng_range32 SIGFPE and the constant-output qrng_range64 behavior in the legacy engine.

PR #2 has now replaced that engine with the pinned quantum_rng v3 implementation. The API bridge includes an overflow-safe full signed 32-bit range implementation, and the pinned v3 revision includes the corrected full-width 64-bit conditioning. The new native regression suite explicitly exercises both full-span cases so they cannot silently return.

Because the files changed by this PR no longer back the API, I’m closing this as superseded rather than merging obsolete code. The report was valuable and directly informed the regression coverage—thank you for taking the time to investigate and document it so thoroughly.

@tsotchke tsotchke closed this Jul 14, 2026
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