Repository navigation
Fix integer-overflow crash in qrng_range32/qrng_range64 (SIGFPE on full-span inputs) - #1
Conversation
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.
|
Thank you, Richard. Your report correctly identified both the full-span PR #2 has now replaced that engine with the pinned 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. |
Summary
qrng_range32can crash the process withSIGFPEon caller-controlled input. Because/v1/qrng/rangeforwardsmin/maxstraight 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
max - min + 1is evaluated in signedint32. At the full span (min = INT32_MIN, max = INT32_MAX) that overflows, which is undefined behavior. At-O2the compiler is entitled to assume signed overflow never happens, so it provesmax - min + 1 >= 1⇒range != 0and deletes theif (range == 0)guard. At runtime the value actually wraps to0, andr % rangebecomesr % 0→ SIGFPE. (Confirmed: crashes at-O2, not at-O0— the signature of a guard the optimizer removed under UB.)qrng_range64has the sibling bug:max - min + 1is unsigned there (defined wraparound), so the guard survives — but on the full 2⁶⁴ span it returns the constantUINT64_MAXinstead 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 whenmin < 0.qrng_range64: on the full-span (range == 0) case, returnqrng_uint64(ctx)instead of the constantmax.Verification
Built and tested with the real generator (
qrng_init+qrng_range*) at-O2:qrng_range32(ctx, INT32_MIN, INT32_MAX)→SIGFPE(exit 136);[-2147480997 .. 2147481639](genuine negatives + positives);[-3..6]: all 10 buckets hit; ordinary[1..6]unaffected;qrng_range64full span: pre-fix returns constant18446744073709551615, post-fix draws vary;A one-command standalone reproducer is available — happy to drop it in a comment if useful.