β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5694

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5694 Β· 17 files
FileTypeDescriptionSize
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 << 24 sets bit 31 β€” not representable in int, UB per C11 6.5.7p4 β€” which real compilers (guest gcc 8.3, kernel built with the same) implement as shl wrap, yielding a negative int. The |= into the uint64_t ret converts that negative int modulo 2^64: sign extension β€” bits 32..63 all become 1. The subsequent |= (uint64_t) byte << 32..56 cannot clear them. Result: whenever byte3 >= 0x80, the upper 32 bits of karc4random64() are exactly 0xffffffff β€” 64-bit output carries only ~32.5 bits of entropy instead of 64. (karc4random() is unaffected: conversion to uint32_t truncates the sign extension away.)

Impact

  • In-tree consumers: vm_map.c:4408 (mmap ASLR addend) and dev/drm/include/linux/random.h:49 (get_random_long() β†’ i915 GTT randomization, i915_gem_gtt.c:3986).
  • With the default RLIMIT_DATA of 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). Default vm.randomize_mmap=0 disables 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.

VERDICT.md
↓ download raw

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

  1. In-kernel (KLD arc4samp.ko, loaded into the live guest kernel, X86_64_GENERIC DragonFly 6.5-DEVELOPMENT, gcc 8.3): * /dev/arc4srep (2^20 karc4random64() draws, counted in-kernel): top32==0xffffffff at 0.4992 / 0.5000 across runs; and in every single run top32ones == b3high identically β€” the bias is exactly the event "4th byte >= 0x80". * /dev/arc4s64 stream, 2,097,152 words: 0.500496 biased. * Control: /dev/arc4s32 (64MB karc4random) and /dev/arc4sbuf (64MB karc4random_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.
  2. 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. Default vm.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() returns karc4random64() verbatim; used at i915_gem_gtt.c:3986 for 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-r key[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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-5694

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.