Shared MINSTD state broadcast: observer-visible consumers (ipfw O_PROB, dummynet plr, syscons savers) let an unprivileged local user or on-path observer recover the kernel's global PRNG state and predict every other consumer's values (UFS i_gen/NFS file handles, NAT alias ports, CHAP challenges, IPv6 frag-ID base)
Summary
randseed is one global state shared by every kernel consumer with zero domain separation - DF-2251 established MINSTD is reversible given one output but left open HOW an unprivileged party obtains one; this pass proves the missing half: consumers that PUBLISH draws to untrusted observers (ipfw prob decisions wire-observable, dummynet drop patterns, syscons savers rendering hundreds of raw draws/sec to the shared console). 60 loopback echo outcomes uniquely determined the state via exhaustive 2^31 scan in ~4s - four independent recoveries, each bit-exact against /dev/mem control; from the recovered state the on-disk UFS i_gen of a subsequently created file was predicted EXACTLY (0x359714fe, draw<->gen mapping verified on 3 pairs). Same state yields NAT alias ports, CHAP challenges, frag-ID base - cross-domain compromise from one observable consumer. migrate-to-karc4random fix for the publishing consumers.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5707 Β· 35 filesDF-5707 β Global MINSTD state broadcast: unprivileged-oracle recovery of randseed
File audited (pass 2): sys/libkern/random.c
Class: state-recovery oracle / cross-domain PRNG coupling (CWE-200/CWE-340)
Severity: Medium Β· Confidence: certain (mechanism, triple-verified on guest)
What this pack proves (all on the QEMU guest, DragonFly 6.5-DEVELOPMENT x86_64)
-
Calibrated consumer model (
calib7-10.log): under ipfw ruleprob 0.5 deny icmp from any to any icmptypes 8 via lo0(O_PROBatsys/net/ipfw/ip_fw2.c:3952-3955, threshold encoding atsbin/ipfw/ipfw2.c:3396-3402), every echo request consumes exactly: - 1 draw (<T) βsendto: Permission denied("D") - 2 draws (β₯T,<T) β silent loss ("T") - 4 draws (β₯T,β₯T) β reply ("R") -
Full state recovery (
recover1.log,attack2.log,attack3.log,landmark4.log): a 60-packet burst (unprivileged-observable decisions) plus a 6-thread exhaustive scan of the 2^31-1 MINSTD space recovers the kernel's globalrandseedin ~4 s. Four independent recoveries, each bit-exact against the/dev/memcontrol read (MATCH start == RS-before-burstinattack2/attack3,landmark4;endβRS1 dist=1inverify1.log). -
Cross-subsystem prediction (
attack3.log+finalgen.log+strike3.log+rawgen.log): from the recovered state alone, the on-disk UFS inode generation number of a file created after the burst is predicted exactly (PREDICTED-IGEN β¦ k=19:0x359714fe== on-diskdi_gen = 0x359714fefor ino 10). i_gen is the NFS file-handle unguessability component (ffs_alloc.c:641-642,ffs_vfsops.c:1174-1175). Drawβgen mapping verified bit-exact on three pairs: zero-gen path storesdraw/2+2(vget assigns/2+1, valloc's++i_genadds 1), wrap path storesdraw/2+1. -
Ambient characterization (
rate.log,ambient*.log,bisect.log): idle draw rate 1.23/s = 6 per-CPU loadav callouts (kern_synch.c:1425-1432); DF-2250 races observed live twice (documented in VERDICT.md).
Build
./build.sh # cc on the guest (gcc 8.3, base system)
Run (summary; full choreography in run.sh / VERDICT.md)
# as root (oracle setup only): kldload ipfw ipfw -q add 65000 allow ip from any to any ipfw -q add 50 skipto 65000 ip from any to any not via lo0 ipfw -q add 100 prob 0.5 deny icmp from any to any icmptypes 8 via lo0 ./attack <bootsec> 8 # burst -> recover -> strike -> predict
Expected decisive output (attack3.log)
RECOVERED start=998920631 end=781784740 (== RS-before-burst) STRIKE file=/mnt/final ino=10 ... CONTROL ra-to-rb dist=1 (1 expected: the i_gen draw) β on-disk di_gen(ino10) = 0x359714fe == attacker-table k=19 prediction
Contents
attack.cflagship PoC (observeβrecoverβstrikeβpredict), fork-freeprobe.cburst generator + outcome classifierrecover2.c2^31 exhaustive state recovery (pthreads)minstd.cstep/ainv/dist/igen/backseed2 utilitiesdevmem.c/dev/mem read/write control tool (root-side only)strike2/3.c,zerogen.c,mkstate.c,watch.c,probe2.csupportinglogs/full untrimmed guest logs for every phasefix.diffconsumer-migration fix (never applied to sys/)
DF-5707 VERDICT β reproduced (impact: leak)
One-paragraph bottom line. The single global MINSTD state
(sys/libkern/random.c:36 randseed) is shared by every consumer in the
kernel, and several of those consumers publish raw or near-raw draws to
parties with no privilege: ipfw prob rules and dummynet plr pipes export
per-packet decision bits to anyone who can observe the traffic, and the
syscons graphical screensavers (rain_saver.c:87, fire_saver.c:89,
warp_saver.c:121, star_saver.c:86,90,93-95, dragon_saver.c:126-131,
snake_saver.c:103,107) render hundreds of consecutive draws per second on
the shared console. Because MINSTD's output is its state (DF-2251's
reversibility), one burst of ~60 observable decisions uniquely determines
the global state β demonstrated four times on the guest, each recovery
bit-exact against the /dev/mem control (MATCH start == RS-before-burst
in logs/attack2.log, logs/attack3.log, logs/landmark4.log). From the
recovered state, values consumed by other security domains are predicted
exactly: the on-disk UFS di_gen of a subsequently created file
(NFS file-handle component) matched the attacker-side prediction table at
k=19 to the bit (0x359714fe, logs/attack3.log + logs/finalgen.log).
Reproduction narrative (what exactly ran)
Guest: DragonFly 6.5-DEVELOPMENT (clean-install snapshot kernel,
v6.5.0.1712.g89e6a-DEVELOPMENT #1, Jun 29 2026 build), x86_64, 6 vCPU,
QEMU/KVM. randseed located via nm /boot/kernel/kernel at
0xffffffff81120c80; read/written through /dev/mem at phys
va-KERNBASE (KERNBASE = 0xFFFFFFFF80000000). All root-side steps
(firewall setup, /dev/mem reads, vnconfig/newfs UFS test image, zerogen)
are demonstration controls; the attacker-side steps (packet outcomes β
recovery β prediction) use no privilege beyond sending pings/observing
loss, which any local user can do (and any on-path party can do for
forwarded traffic).
Phase 1 β model calibration (logs/calib7..calib10.log)
mkstate K T fabricates a state whose k-th next draw crosses threshold
T = 0x3FFFFFFF (prob 0.5 β d[0] = (int32_t)(0.5*0x7fffffff),
sbin/ipfw/ipfw2.c:3402). Forced-state single pings established the exact
consumption model: D=(1 draw), T=(2 draws), R=(4 draws) β and that the
icmptypes-8 restriction makes replies undenyable, so outcomes are a
deterministic function of consecutive draws.
Phase 2 β recovery (logs/recover1.log, attack2.log, attack3.log, landmark4.log)
60-packet bursts at 2 ms spacing; recover2 scans all states 1..2^31-2
with 6 pthreads (~4 s). Results:
- burst DTTTRRDTDDD...: 1 match: start=306818006, end=258553457;
control: dist(endβRS1)=1 (verify1.log).
- attack2: 1 match start=1150107239 == RS-before-burst exactly.
- attack3: 1 match start=998920631 == RS-before-burst exactly;
STRIKE immediately after (see phase 4).
- landmark4 (fresh boot): 1 match start=1093760145 == RS0=0x41317491.
Two other bursts produced 0 matches (ambient interleave during the burst)
and were retried β the retry logic is part of attack.c. 6 bursts total,
4 exact recoveries.
Phase 3 β drawβi_gen mapping (logs/zerogen4.log, strike3.log, rawgen.log)
newfs pre-templates free-inode gens from unseeded userland random()
(sbin/newfs/mkfs.c:836,1140), so fresh-FS creations take the ++i_gen
no-draw path (12 creations consumed 0 draws β logs/strike2run.log).
Zeroing di_gen of chosen free inodes on the unmounted image
(zerogen.c, simulating legacy zero-gen filesystems / the natural wrap
case) re-activates the krandom path: ffs_vget assigns
krandom()/2+1 (ffs_vfsops.c:1174-1175) and valloc's ++i_gen
(ffs_alloc.c:641) adds one β on-disk di_gen = draw/2+2, verified
bit-exact on three (draw, gen) pairs:
draw=0x3d6ab456 β ino8 di_gen=0x1eb55a2d β (0x3d6ab456/2+2)
draw=0x2865999a β ino9 di_gen=0x1432cccf β
draw=0x507887dd β ino6 di_gen=0x283c43f0 β (genmap.log)
(fsdb's GEN= output is served from a stale buffer on mounted filesystems
and cannot be trusted β logs/fsdb3.log vs raw reads; use zerogen/raw reads.)
Phase 4 β end-to-end strike (logs/attack3.log + finalgen.log)
After recovery (end=781784740), /mnt/final was created (zero-gen ino 10):
exactly 1 draw consumed (CONTROL ra-to-rb dist=1), on-disk
di_gen(ino10) = 0x359714fe. The attacker-view prediction table
(PREDICTED-IGEN from recovered end) contains k=19: 0x359714fe β
dist(endβra)=18 ambient draws (per-CPU loadav at 1.23/s, rate.log)
separated the recovery from the strike; the k=19 entry is the realized
value, to the bit. In a live attack the adversary re-syncs with a second
mini-burst immediately before the victim event, collapsing k to β€2.
What the attacker gets (impact)
- i_gen of every subsequently created inode on zero-gen/legacy or gen-wrapping UFS/ext2 filesystems β forgeable NFS file handles (fh = fsid + ino + gen; ffs_fhtovp validates exactly these fields).
- NAT alias ports (ip_fw3_nat.c:439), CHAP challenge words (if_spppsubr.c:4265-4268), IPv6 fragment-ID base (frag6.c:107), nfs_xid base (nfsm_subs.c:159) β all future outputs of the shared state, predictable from any single recovery.
- Oracles that require no admin firewall config: any loaded graphical
screensaver broadcasts draws on the console (fire_saver draws a full
bottom line of
krandom()%160+96every frame).
Honest limits / negatives
- The boot-seed recovery (walk back to
tv_sec^tv_nsec, init_main.c:495-497, disassembly-confirmed atproc0_post+0x29in logs/disass.log) did not close on the guest: across 4 attempts on 3 boots with two-landmark verification (backseed2, logs/backseed*.log) no sec^nsec-form ancestor was found within plausible draw counts. Suspected causes: (a) DF-2250 lost-update races scrambling branch lineage (races were observed live twice this session β see below), and/or (b) nanotime at proc0_post not reflecting kern.boottime wall time. The β€30-bit arithmetic bound of the seed remains code-certain; the live seed walk is unproven. - DF-2250's race was observed live: burst RS0 was disconnected from the
recovered start in one run (logs/attack1 session; also
CONTROL end-to-rsafter dist=-1events) β a concurrent consumer's read-modify-write interleaved with the probe. Evidence only; the race itself is DF-2250 (not re-filed). - Two guest panics during the session were unrelated to random.c:
vm_object_hold_shared: assertion "obj != NULL"at sys/vm/vm_object.c:330 from vm_fault/std_copyout under process churn (serial logs captured; flagged to the orchestrator for a sys/vm pass-2 audit β the file is in the Dillon priority list). - ipfw-prob/dummynet-plr oracles require admin-configured rules; the screensaver oracle requires a graphical saver module + console visibility (common on desktops, absent on this headless guest β verified by code for the savers, live-proven for the ipfw variant).
Fix
See fix.diff: migrate the publishing consumers (O_PROB, dummynet plr,
syscons savers) to karc4random(), which is per-CPU-state CSPRNG not
readable from these oracles, and document krandom() as
non-security-grade. The architectural cure (no single shared PRNG state
across security domains) is the same direction as DF-2251's fix.
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
not_testableArchitectural consumer-migration fix; not a memory-corruption class so no kernel-rebuild validation was mandated. Baseline (oracle recovery) reproduced 4x pre-fix; patched behavior not exercised.
N/A - no rebuild performed (see VERDICT.md 'Fix').
Confirmed kernel references
- sys/libkern/random.c:36
- sys/libkern/random.c:49-67
- sys/net/ipfw/ip_fw2.c:3952-3955
- sbin/ipfw/ipfw2.c:3396-3402
- sys/net/dummynet/ip_dummynet.c:1078
- sys/dev/misc/syscons/fire/fire_saver.c:89
- sys/dev/misc/syscons/rain/rain_saver.c:87
- sys/vfs/ufs/ffs_alloc.c:641-642
- sys/vfs/ufs/ffs_vfsops.c:1174-1175
- sys/net/ipfw3_nat/ip_fw3_nat.c:439
- sys/net/sppp/if_spppsubr.c:4265-4268
- sys/netinet6/frag6.c:107
- sys/kern/init_main.c:495-497
Detail
Exploit chain
1) root/admin once loads ipfw with any prob rule (or dummynet plr, or a graphical saver is loaded); 2) unprivileged local user sends 60 pings to 127.0.0.1 and records EACCES/timeout/reply outcomes (~1s); 3) offline exhaustive MINSTD scan (4s) yields the global randseed; 4) re-sync with a mini-burst immediately before the target event; 5) predicted future outputs: next zero-gen/wrap inode's NFS-visible i_gen (demonstrated bit-exact), NAT alias ports, CHAP challenges, IPv6 frag-ID base; 6) NFS file-handle forgery on exported filesystems (fh = fsid+ino+gen, ffs_fhtovp check).
Evidence (decisive lines)
logs/attack3.log: RECOVERED start=998920631 == RS-before-burst, STRIKE ra-to-rb dist=1, PREDICTED-IGEN tables; logs/finalgen.log: on-disk di_gen(ino10) = 0x359714fe; logs/attack2.log + logs/landmark4.log: two more exact recoveries; logs/verify1.log: end->RS1 dist=1; logs/strike3.log+rawgen.log: draw<->gen /2+2 mapping; logs/calib7-10: D=1/T=2/R=4 model; VERDICT.md for the full narrative.
PoC changes
Seed sketch had no syscall-level model; built from scratch: calibrated the O_PROB draw-consumption model on the live kernel (D=1/T=2/R=4 draws with icmptypes-8-restricted rule), wrote the exhaustive 2^31 pthread scanner, the fork-free attack driver, and raw-dinode zerogen tooling (fsdb GEN output is stale on mounted filesystems and cannot be trusted).
Verified recommended fix
Migrate observer-visible consumers (O_PROB, dummynet plr, syscons savers) to karc4random(); document krandom() as non-security-grade (fix.diff).
Verdict
Unprivileged-observable ipfw prob decisions (60 loopback echo requests, outcomes D/T/R) uniquely determine the kernel's shared MINSTD state: a 6-thread exhaustive scan of the 2^31-1 state space recovers it in ~4s. Four independent recoveries on the guest were bit-exact against /dev/mem controls (MATCH start == RS-before-burst). From the recovered state alone, the on-disk UFS inode generation number of a file created after the burst was predicted exactly (attacker table k=19 entry 0x359714fe == di_gen(ino10)); draw<->gen mapping verified bit-exact on three pairs (zero-gen path stores draw/2+2: ffs_vfsops.c:1175 assigns krandom()/2+1 then ffs_alloc.c:641 ++i_gen). Same recovered state predicts NAT alias ports, CHAP challenge words, the IPv6 frag-ID base and nfs_xid base. Console screensavers are a second, admin-free oracle (fire_saver renders krandom()%160+96 per cell per frame). Boot-seed walk-back (tv_sec^tv_nsec, init_main.c:497) did not close on this guest (suspected DF-2250 lineage races); the <=30-bit arithmetic bound remains code-certain.
No comments yet.