DF-5766 / verdict.json
{ "finding_id": "DF-5766", "status": "reproduced", "verdict": "Live-reproduced on two independent boots: the BSPs (cpu0s) Fortuna state permanently stops reseeding after the single boot-time reseed. The first csprng_get_random() of the system runs inside rand_initialize (SI_BOOT2_POST_SMP, kern_nrandom.c:558) on the BSP; ratecheck()s {0,0} first-call path (kern_time.c:1154) stores a garbage, astronomically large getmicrouptime() value (observed tv_sec=184467440735, a deterministic early-boot artifact of the unsigned sysclock_t delta underflow in getmicrouptime, kern_clock.c:1379) into state->last_reseed. Every later ratecheck computes a negative delta and returns 0 forever, so csprng_reseed() is never called again on that state: no entropy harvested after boot is ever mixed into that CPUs CSPRNG key for the whole uptime, while pool[0] sits eligible at 15-32KB (observed frozen at reseed_cnt=1 through 8MiB pinned /dev/urandom reads at 134-214 MB/s). On ncpus==1 systems csprng_pcpu = &csprng_boot (kern_nrandom.c:495-496): the single kernel CSPRNG never reseeds after boot. Root-cause line: subr_csprng.c:134-137 trusts ratecheck/getmicrouptime sanity with no backwards-time defense. Impact: silent permanent entropy starvation of the kernel RNG on the most-used CPU; anyone who estimates the boot-time key (DF-0220 domain: VM clones, deterministic boots) predicts that CPUs entire /dev/urandom, getrandom(2), kern.random and karc4rand output lifetime. Fix validated: single-line-guard kernel (fix.diff) self-heals the baseline on the first sane call; patched kernel shows cpu0 reseed_cnt=37 within 74s of boot and sane last_reseed, behavior gone.", "reproduced": true, "impact": "leak", "confidence": "certain", "exploit_chain": "estimate/guess the boot-time first-reseed key K1 = SHA256(0^256 || pool digests at SI_BOOT2_POST_SMP) (thin entropy: timing deltas + largely deterministic rgd blob on no-KASLR/VM systems) -> the per-request forward rekeys (subr_csprng.c:158-161) are a deterministic PRF chain from K1 -> replay them given the request-size sequence -> predict all future and derive all past cpu0 /dev/urandom, getrandom(2), kern.random, karc4rand output for the entire uptime -> recover secrets minted by privileged consumers on that CPU (session keys, nonces, ISNs, uuids). The new defect is the never-recovery: harvested entropy can never displace a weak K1 on the BSP (on UP systems, on the whole machine).", "evidence": "run.2.log (fresh boot, uptime 21s: state[0] reseed_cnt=1 last_reseed=184467440735.646387 pool0=11716 eligible; after pinned 8MiB read: unchanged; states 1-5 reseeding at sane 21.3-21.8s); run.1.log (boot #1: pool0=32176 eligible, frozen through pinned 8MiB @142MB/s; healthy cpus at ~1227s); fix_run.log (patched kernel: state[0] reseed_cnt=37@74s, 39 after reads, last_reseed=73.8/76.9 sane); csprng_dump.c (/dev/kmem dumper, DWARF offsets); env.txt (symbols, offsets); fix.diff + fix_build.log (validated fix)", "kernel_refs": [ "sys/kern/subr_csprng.c:134-137", "sys/kern/subr_csprng.c:64", "sys/kern/subr_csprng.c:146", "sys/kern/subr_csprng.c:158-161", "sys/kern/kern_time.c:1140-1159", "sys/kern/kern_clock.c:1372-1393", "sys/sys/systimer.h:51", "sys/kern/kern_nrandom.c:558", "sys/kern/kern_nrandom.c:495-496", "sys/kern/kern_nrandom.c:562" ], "poc_changes": "No seed existed (new pass-2 finding). Authored csprng_dump.c (/dev/kmem reader; static symbols cannot be linked from a KLD); struct offsets extracted from the guests kernel.debug via gdb and cross-validated by 5 healthy states reading sane timestamps at the same offsets.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 2400, "guest_dirty": 1, "build_cmd": "cc -O2 -o /tmp/rngtest/csprng_dump /tmp/rngtest/csprng_dump.c", "run_cmd": "sh run.sh # csprng_dump; usched :1 dd if=/dev/urandom of=/dev/null bs=1048576 count=8; csprng_dump", "code_hash": "220bea21463435c412791052d386742ac97aecd2f55c3d85365fa53e34fa8ad2", "notes": "Pass-2 negative results recorded with live weight: ChaCha20 core byte-exact vs independent RFC 7539 implementation over 300 streams incl. counter-carry 12->13->14->15 (chacha differential test); 8 concurrent 1MiB /dev/urandom readers: 0 duplicate 4K blocks, 0 identical streams, getrandom(2) never short; chi-square byte frequency 242-256 (255+/-22.6) over 3x8MiB with 0 duplicate 16-byte windows; pool selection arithmetic live-verified correct (drain pattern matches ctz(reseed_cnt) Fortuna rule); ssleep-with-spinlock protocol correct (kern_synch.c:805-826). Guest was reset with-src after the fix validation.", "recommended_fix": "In csprng_get_random, before ratecheck(), bzero last_reseed when it lies in the future of getmicrouptime() so the poisoned boot baseline self-heals (see fix.diff); optionally also clamp getmicrouptime consumers against early-boot garbage.", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Single-fix kernel (fix.diff applied to guest /usr/src, nativekernel build, -Werror clean) boots and self-heals: cpu0 reseed_cnt reached 37 within 74s of boot and 39 after pinned reads, last_reseed=73.831166/76.851196 (sane uptime), pools cycling normally; cpus 1-5 unchanged. The frozen-state bad behavior is gone. Baseline re-reproduced on the pre-fix boot of the same session (run.2.log).", "fix_evidence": "findings/poc/DF-5766/fix_run.log (before/after dumps on patched kernel), fix_build.log (build+install transcript), fix.diff" } |