karc4random64() upper 32 bits pinned to 0xffffffff on half of all draws β int-overflow left shift sign-extends into the 64-bit accumulator (UB), collapsing the exported kernel CSPRNG word from 64 bits of entropy to ~32.5
Summary
ret |= arc4_randbyte(d) << 24 with uint64_t ret: the uncast int shift overflows into the sign bit for byte>=0x80 (prob 1/2, UB) and the compound-assignment conversion SIGN-EXTENDS, setting all 32 high bits which the later ORs cannot clear. Verified IN THE RUNNING KERNEL by a KLD sampling module (bias 0.4992-0.5005 over 2^20-2M draws, biased count == byte3>=0x80 count identically - exact causality) and by a 10M-draw verbatim-source harness (P=0.499919). Consumers: vm_map.c mmap ASLR (measured addend distribution 0.5625 top-bucket vs 0.125 uniform, min-entropy 32.83 vs 35 bits - 9x density concentration, gated behind non-default vm.randomize_mmap) and drm get_random_long (raw bias inherited). karc4random/buf statistically clean (controls + 64MB chi-square + long-term RC4 bias probes all negative). uint64_t-cast fix validated on the same compiler (P=0.000000, buckets 0.125).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5694 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.9 KB | β raw | |
| VERDICT.md | β | 5.2 KB | β raw | |
| arc4_harness.c | β | 17.3 KB | view raw | |
| arc4_harness_fixed.c | β | 17.3 KB | view raw | |
| arc4samp.c | β | 4.1 KB | view raw | |
| Makefile | β | 68 B | β download | |
| chk.c | β | 1.4 KB | view raw | |
| aslr_probe.c | β | 1.6 KB | view raw | |
| aslr2.c | β | 742 B | view raw | |
| build.sh | β | 142 B | view raw | |
| run.sh | β | 216 B | view raw | |
| build.log | β | 1.3 KB | view raw | |
| run.log | β | 2.8 KB | view raw | |
| env.txt | β | 478 B | view raw | |
| fix.diff | β | 566 B | view raw | |
| manifest.json | β | 1002 B | view raw | |
| verdict.json | β | 4.1 KB | view raw |
DF-5694 β karc4random64() upper 32 bits are 0xffffffff half the time
File: sys/libkern/arc4random.c (pass-2 audit)
Class: RNG correctness / C undefined behavior β entropy loss (CWE-190/191 + RNG)
Verdict: reproduced (in the running kernel via KLD, and via exact-source
userland harness)
Root cause (one paragraph)
karc4random64() (sys/libkern/arc4random.c:129-132) assembles its 64-bit
word with
ret = arc4_randbyte(d);
ret |= arc4_randbyte(d) << 8;
ret |= arc4_randbyte(d) << 16;
ret |= arc4_randbyte(d) << 24; /* <-- BUG */
ret |= (uint64_t)arc4_randbyte(d) << 32;
...
arc4_randbyte() returns uint8_t, promoted to int. When the 4th byte is
= 0x80 (probability 1/2),
byte << 24sets bit 31 β not representable inint, UB per C11 6.5.7p4 β which real compilers (guest gcc 8.3, kernel built with the same) implement asshlwrap, yielding a negativeint. The|=into theuint64_tretconverts that negative int modulo 2^64: sign extension β bits 32..63 all become 1. The subsequent|= (uint64_t) byte << 32..56cannot clear them. Result: whenever byte3 >= 0x80, the upper 32 bits ofkarc4random64()are exactly0xffffffffβ 64-bit output carries only ~32.5 bits of entropy instead of 64. (karc4random()is unaffected: conversion touint32_ttruncates the sign extension away.)
Impact
- In-tree consumers:
vm_map.c:4408(mmap ASLR addend) anddev/drm/include/linux/random.h:49(get_random_long()β i915 GTT randomization, i915_gem_gtt.c:3986). - With the default
RLIMIT_DATAof 32GB (vm_map.c:4374-4377), the biased draws all map to the top 4GB of the 32GB mmap randomization span: measured addend distribution 0.5625 (should be 0.125) in the top bucket, 0.0625 elsewhere; min-entropy 32.83 bits vs 35.00 ideal (~2.2 bits lost, 9x density concentration). Defaultvm.randomize_mmap=0disables the consumer, so default-config impact is nil; operators who enable the hardening sysctl get the skewed distribution.
How to reproduce
Everything runs on the DragonFly guest (host has no toolchain here).
In-kernel (deployed generator) β KLD sampling
# build + load the sampler KLD (needs /usr/src in the guest)
cd /tmp/kld && make # Makefile + arc4samp.c from this dir
kldload ./arc4samp.ko
cat /dev/arc4srep # 2^20 karc4random64 draws, in-kernel
dd if=/dev/arc4s64 of=/tmp/s64.bin bs=1048576 count=16 # 2M k64 words
cc -O2 -o chk chk.c -lm && ./chk /tmp/s64.bin k64
dd if=/dev/arc4s32 of=/tmp/s32.bin bs=1048576 count=64 # karc4random stream
dd if=/dev/arc4sbuf of=/tmp/sbuf.bin bs=1048576 count=64 # karc4random_buf
./chk /tmp/s32.bin && ./chk /tmp/sbuf.bin # chi2 (should pass)
Userland exact-source harness (verbatim algorithm, getrandom-seeded)
cc -O2 -o arc4_harness arc4_harness.c -lm
./arc4_harness bias64 10000000 # P(top32==0xffffffff) ~ 0.5
./arc4_harness_fixed bias64 10000000 # PATCHED algorithm -> 0.0
./arc4_harness stats 536870912 # chi2 battery on k32+buf (passes)
./arc4_harness bias 4294967296 # 2^32 RC4 bias probes (negative)
Expected output
See run.log. Decisive lines:
karc4random64 draws=1048576 top32==0xffffffff: 524342 (0.5000; bug predicts ~0.5000) <- in-kernel k64 words=2097152 top32==0xffffffff: 1049617 (0.500496) <- in-kernel stream P(top32==0xffffffff) = 0.499919 ... bucket[7]: 0.562429 <- harness PATCHED: P(top32==0xffffffff) = 0.000000 ... buckets 0.125 <- fix validated
Note: in every run, top32ones == b3high exactly β the bias fires if
and only if byte3 >= 0x80 (causality, not correlation).
Fix
fix.diff (git-apply-able against sys/): cast every shifted byte to
uint64_t in karc4random64(). Validated: patched algorithm yields
P(top32==0xffffffff)=0.000000, uniform 0.125 buckets, 35.00-bit addend
entropy, chi-square still passes.
DF-5694 β VERDICT
Status: reproduced (in the running deployed kernel, not just a model).
Impact: RNG primitive defect β karc4random64() loses its upper 32 bits
of entropy on exactly 1/2 of all draws; measurable ASLR degradation when
vm.randomize_mmap=1 is enabled; C UB in kernel code.
Confidence: certain.
What was measured
- In-kernel (KLD
arc4samp.ko, loaded into the live guest kernel,X86_64_GENERICDragonFly 6.5-DEVELOPMENT, gcc 8.3): */dev/arc4srep(2^20karc4random64()draws, counted in-kernel): top32==0xffffffff at 0.4992 / 0.5000 across runs; and in every single runtop32ones == b3highidentically β the bias is exactly the event "4th byte >= 0x80". */dev/arc4s64stream, 2,097,152 words: 0.500496 biased. * Control:/dev/arc4s32(64MBkarc4random) and/dev/arc4sbuf(64MBkarc4random_buf) both pass chi-square byte-frequency and digraph tests (z in [-1.34, +0.31]) β the defect is confined to the 64-bit assembly. - Userland exact-source harness (
arc4_harness.c, algorithm copied verbatim from sys/libkern/arc4random.c:50-162, seeded from getrandom(2)): *bias64 10M: P(top32==0xffffffff) = 0.499919; P(both) identical. * Replication of the sole kernel consumer, vm_map.c:4408(karc4random64() & 0x7fff...) % 32GB: top-4GB bucket 0.5625 (= 9/16, exactly as derived), other buckets 0.0625 (= 1/16), addend min-entropy 32.83 bits vs 35.00 ideal.
Why (mechanics, path:line)
sys/libkern/arc4random.c:132
ret |= arc4_randbyte(d) << 24; with uint64_t ret (declared :123):
* arc4_randbyte (uint8_t) β int promotion; b << 24 for b >= 0x80
overflows int (UB, C11 6.5.7p4); compiler emits shl β bit 31 set β
negative int value.
* ret |= (int) converts the negative int to uint64_t modulo 2^64 β
sign extension β 0xFFFFFFFF_xxxxxxxx.
* Lines 133-136 OR bytes 4..7 into already-all-ones bits 32..63 β no effect.
β upper half pinned to 0xffffffff with probability 1/2 (byte3 >= 0x80).
The 32-bit karc4random() (arc4random.c:114) has the same construct but
uint32_t conversion truncates to the correct value β verified
statistically clean (chi2 z=-0.77).
Consumer analysis (threat)
sys/vm/vm_map.c:4408βvm_map_hint()mmap randomization:addr += (karc4random64() & 0x7FFF...) % dsiz,dsiz= RLIMIT_DATA (default 32GB per vm_map.c:4374-4377 comment; verified rlimit 32GB in guest). Biased draws β addend = (b - 2^32) mod 2^35 β top 4GB of span: 56.25% of placements in 1/8 of the window; ~2.2 bits min-entropy lost. Defaultvm.randomize_mmap=0(vm_map.c:147) β dead path unless the operator enables this hardening sysctl. Live-kernel end-to-end probing with the sysctl enabled (aslr_probe.c/aslr2.c) additionally showed DF's first-fit hint resolution compresses placements into a ~3GB window regardless (a vm_mmap layout property outside this file's scope), so the net live ASLR delta from this bug is bounded by the addend skew above.sys/dev/drm/include/linux/random.h:49βget_random_long()returnskarc4random64()verbatim; used ati915_gem_gtt.c:3986for GTT randomization on Intel graphics (module not loaded on this guest; the shim inherits the full bias).- Any future/KLD consumer of the exported API gets 32.5 average bits per 64-bit draw.
Fix validation
fix.diff casts all four low shifts to (uint64_t) in karc4random64().
Applied to the exact-source harness (arc4_harness_fixed.c):
P(top32==0xffffffff) = 0.000000 (10M draws), addend buckets 0.1248-0.1252,
min-entropy 35.00 bits, chi-square unchanged (z=-0.18/-0.13).
Kernel rebuild not performed (non-memory-corruption class; harness uses the
byte-for-byte identical algorithm and the kernel compiler is the same gcc
8.3 with the same wrap-shift codegen observed in the KLD measurement).
Things checked and found CLEAN (negative results with weight)
- Byte/digraph chi-square on karc4random & karc4random_buf: pass in-kernel (64MB each) and harness (512MB): run.log.
- Mironov 3072-byte drop effectiveness: first-16K chi2 z=+1.18.
- Long-term RC4 bias probes at 2^32 bytes: Pr[Z==0] z=-0.34; lag1..8 digraph-repeat z in [-1.67,+1.18] β no distinguisher at feasible sizes.
- Post-boot
read_random(key,32,1)always returns 32 (kern_nrandom.c:687-719 β subr_csprng.c:151-168 loop returns full count; CSPRNG_UNLIMITED never sleeps at :146) β the partial-rkey[n%r]path never engages post-boot; no stack-garbage exposure beyond known DF-2243. - All indices uint8_t β in-bounds; per-CPU state published after full init (arc4random.c:192, AP states pre-initialized on BSP mp_machdep.c:513); numruns bounded 16385, no overflow; no frees (no double-free); no copyin/copyout; reseed never sleeps (spinlock + UNLIMITED path).
- Redundant double 3072-byte discard at init (arc4random.c:76-77 + 189-190) β harmless waste only.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff (uint64_t casts on the four low-byte shifts) validated against the exact-source harness compiled by the same guest compiler: baseline bias 0.499919 -> patched 0.000000 over 10M draws, vm_map addend buckets 0.5625/0.0625 -> uniform 0.125, min-entropy 32.83 -> 35.00 bits, chi-square battery unchanged (z=-0.18/-0.13). Kernel rebuild not performed (non-memory-corruption finding); the KLD measurement already proves the deployed kernel exhibits the baseline behavior with the same compiler family.
run.log 'PATCHED algorithm validation' section (P=0.000000, buckets 0.1248-0.1252, min-entropy 35.00); fix.diff applies clean via git apply --check.
Confirmed kernel references
Detail
Evidence (decisive lines)
findings/poc/DF-5694/run.log: in-kernel KLD lines 'top32==0xffffffff: 524342 (0.5000)' and 'k64 words=2097152 ... (0.500496)'; harness 'P(top32==0xffffffff) = 0.499919' with vm_map addend buckets 'bucket[7]: 0.562429' and 'addend min-entropy ~ 32.83 bits'; PATCHED section 'P(top32==0xffffffff) = 0.000000 ... 35.00 bits'. VERDICT.md has full mechanics; env.txt the guest/kernel identity.
PoC changes
No seed pack existed; authored from scratch: exact-verbatim harness (getrandom-seeded read_random shim), KLD sampler (arc4samp.c) with in-kernel bias counter and stream units, chi-square checker, vm_map-addend simulator, patched-algorithm control, live mmap probes. Fixed my own harness bugs found during bring-up: initial stats mode contaminated by the (real) k64 bias, wrong lag-repeat expectation (p=1/256 not 1/65536), KLD report re-arming per read.
Verified recommended fix
Cast every shifted arc4_randbyte() to uint64_t in karc4random64() (arc4random.c:129-132); see fix.diff.
Verdict
karc4random64() (sys/libkern/arc4random.c:129-132) assembles its 64-bit word with uncast arc4_randbyte(d) << 24; when the 4th byte is >= 0x80 (p=1/2) the int left-shift is UB and wraps to a negative int, whose conversion to uint64_t for the |= sign-extends and pins the upper 32 bits of the output to 0xffffffff. Verified in the RUNNING kernel via KLD sampling (0.4992-0.5005 biased across 2^20-draw reports and a 2M-word stream; biased count == byte3>=0x80 count identically in every run) and via an exact-source userland harness (P=0.499919 over 10M draws). Sole kernel consumer vm_map.c:4408 (mmap ASLR) computes (v & 0x7fff...) % 32GB: measured addend distribution 0.5625 in the top 4GB bucket (uniform 0.125), min-entropy 32.83 vs 35.00 bits; default vm.randomize_mmap=0 disables this consumer, drm get_random_long (i915 GTT) inherits the raw bias. karc4random()/karc4random_buf() are statistically clean (chi2 z in [-1.34,+0.31], 64MB in-kernel each). One-line fix (uint64_t casts) validated: bias 0.000000, buckets 0.125, 35.00-bit entropy.
No comments yet.