# 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+96` every frame).

## Honest limits / negatives

- The boot-seed recovery (walk back to `tv_sec^tv_nsec`,
  init_main.c:495-497, disassembly-confirmed at
  `proc0_post+0x29` in 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=-1` events) — 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.
