# DF-5766 — VERDICT

**Finding:** `csprng_get_random()` permanently suppresses reseeding on the
BSP's Fortuna state (and on *the* state of uniprocessor systems) because the
first `ratecheck()` at boot stores a garbage, astronomically large
`getmicrouptime()` value into `state->last_reseed`; every later delta is
negative, so `ratecheck()` returns 0 forever. All entropy harvested after
boot is never mixed into that CPU's CSPRNG key for the entire uptime.

**Verdict: REPRODUCED** (live, kernel-state introspection via /dev/kmem;
three reproductions across two independent boots).

---

## Root cause (path:line)

1. `sys/kern/kern_nrandom.c:562` — `rand_initialize` runs at
   `SI_BOOT2_POST_SMP`. Per CPU it seeds the pools and then calls
   `read_random(buf, 64, 1)` (`:558`) **on the CPU executing the SYSINIT —
   the BSP (cpu0)**. This is the first `csprng_get_random()` call in the
   system for `csprng_pcpu[0]` (and on `ncpus == 1` systems, for the only
   state, `csprng_boot` — `kern_nrandom.c:495-496`).
2. `sys/kern/subr_csprng.c:134-137` —
   ```c
   if (!state->callout_based_reseed &&
        ratecheck(&state->last_reseed, &csprng_reseed_interval)) {
           csprng_reseed(state);
   }
   ```
   `ratecheck()` (`sys/kern/kern_time.c:1140-1159`) has a first-call special
   case (`lasttime == {0,0}`) that returns 1 **and stores the current
   `getmicrouptime()` into `last_reseed`**. At `SI_BOOT2_POST_SMP` the uptime
   machinery is not yet coherent — `getmicrouptime()`
   (`sys/kern/kern_clock.c:1372-1393`) computes
   `delta = gd->gd_hardclock.time - gd->gd_cpuclock_base` in **unsigned
   64-bit** (`sysclock_t` is `__uint64_t`, `sys/sys/systimer.h:51`); at this
   boot stage the subtraction underflows and `delta / sys_cputimer->freq`
   explodes. Observed stored value: **`tv_sec = 184467440735`** with a
   well-formed `tv_usec` (~646-670 ms) — reproduced on two independent boots
   (`.646387` and `.670544`), i.e. a deterministic early-boot artifact, not
   stale memory.
3. `sys/kern/kern_time.c:1151-1157` — every subsequent call computes
   `delta = sane_uptime − 184467440735` → negative →
   `timevalcmp(&delta, mininterval, >=)` is false → returns 0. `lasttime` is
   only updated on success, so the poisoned baseline never self-corrects.
   **`csprng_reseed()` is never called again on that state — ever.**
4. The boot reseed itself (`reseed_cnt = 1`) did happen (the
   `{0,0}`-special-case fires regardless of clock sanity, and pool[0] is
   pre-fed with the 14976-byte `struct globaldata` blob during
   `rand_initialize` — see DF-0220's verification). So the state looks
   "keyed" and never blocks — the failure is silent.

## Live proof (guest: DragonFly 6.5-DEVELOPMENT #0, 6 vCPU KVM)

`csprng_dump.c` reads `csprng_pcpu[]` through `/dev/kmem` (symbol
`0xffffffff81199140`; offsets from `kernel.debug` DWARF: `reseed_cnt@32`,
`pool[i].bytes@104+112i`, `last_reseed@4256`, `sizeof=6656`).

Boot #1 (uptime ~1130 s, `run.1.log`):
- `state[0]`: `reseed_cnt=1`, `pool0=29944` (**311x over the 96-byte
  MIN_POOL_SIZE reseed gate**), `last_reseed=184467440735.670544`.
- states 1-5: `reseed_cnt=7..13`, sane `last_reseed` (~1004-1009 s uptime).
- **8 MiB of /dev/urandom drained pinned to cpu0** (`usched :1 dd`, 142
  MB/s — hundreds of `csprng_get_random` entries): `reseed_cnt` **still 1**,
  `last_reseed` **unchanged**. In the same window cpus 1-5 reseeded
  repeatedly (their `last_reseed` advanced to ~1227 s).

Boot #2 (fresh `vm.sh reset with-src`, uptime 21 s, `run.2.log`):
- `state[0]`: already poisoned — `reseed_cnt=1`,
  `last_reseed=184467440735.646387`, `pool0=11716` (eligible).
- states 1-5: `reseed_cnt=4..8`, sane `last_reseed=21.3-21.8`.
- After a pinned 8 MiB cpu0 read (214 MB/s): `reseed_cnt=1` still,
  `last_reseed` untouched; cpu1 had reseeded twice more (7→9, sane 40.2 s).

The pool *selection* arithmetic itself is fine — the dumps show exactly the
Fortuna drain pattern (pool i reset to 0 when `reseed_cnt % 2^i == 0`, deep
pools accumulating 20-30 KB between uses) on the healthy CPUs.

## Impact

- On the BSP state, for the whole uptime, the ChaCha key chain evolves only
  through the per-request forward rekey (`subr_csprng.c:158-161`) — a pure
  PRF chain rooted in the **boot-time key alone**. None of the entropy
  continuously harvested into that state's pools (interrupt timing, TSC,
  virtio-rng/RDRAND when present, `/dev/random` seeding writes) is ever mixed
  in. An attacker who can estimate the boot key (VM clones / deterministic
  boots / thin entropy at `SI_BOOT2_POST_SMP`, the DF-0220 domain) gets
  lifetime prediction of **all** of that CPU's output — including root-owned
  consumers' (`sshd` session keys via libcrypto, `karc4rand` stirs, TCP
  ISNs, `kern_uuid`, argv/stack randomization on that CPU).
- Fortuna's compromise-recovery property (fast pool-0 reseeds every >=100 ms
  absorb fresh entropy; deep pools recover from state compromise) is
  structurally dead on the BSP — the state most heavily used early in boot.
- **Uniprocessor systems are fully exposed**: `ncpus == 1` →
  `csprng_pcpu = &csprng_boot` (`kern_nrandom.c:495-496`) — the single
  kernel CSPRNG never reseeds after boot, for the machine's lifetime, while
  appearing healthy (`reseed_cnt >= 1` keeps `/dev/random` non-blocking).
- No memory-safety impact; no panic/DoS (output keeps flowing); the damage
  is silent, permanent, cryptographic.

Impact class: `leak` (RNG output predictability → disclosure of everything
derived from it). Not memory corruption — no uid0 chain attempted.

## Exploit chain (sketch, why this matters)

`read boot-entropy estimate (VM snapshot / deterministic appliance boot)`
→ `brute/guess first-reseed key K1 = SHA256(0^256 || digest_0)` →
`replay the per-request forward rekeys (deterministic given K1 and the
sequence of request sizes)` → `predict /dev/urandom|getrandom output on cpu0
for the entire uptime` → `recover session keys/nonces minted by privileged
consumers`. Each step is the DF-0220 family's economics; the *new* defect is
that the system can never recover by mixing fresh entropy on that CPU.

## Fix

`fix.diff` (in-file, self-healing): before calling `ratecheck()`, read
`getmicrouptime()`; if the stored baseline is in the future (time warp —
only possible from the boot garbage, uptime is otherwise monotonic), zero it
so `ratecheck()`'s `{0,0}` path fires, the reseed attempt proceeds, and a
sane baseline is stored. Normal operation is byte-identical to before.

## Fix validation (Phase 8)

Built the single-fix kernel in the guest (`patch` to `/usr/src`, incremental
`make buildkernel KERNCONF=X86_64_GENERIC`, install, reboot):

- Baseline (unpatched): cpu0 `reseed_cnt=1` frozen, `last_reseed=184467440735.x`,
  pool0 eligible — reproduced again on the pre-fix boot.
- Patched: after the first post-boot read, cpu0 `reseed_cnt >= 2` and
  `last_reseed` holds a sane small-uptime value; reseeds continue at
  >=100 ms spacing while pool[0] is eligible; cpus 1-5 unchanged. Fixed.
  Full transcripts: `fix_build.log`, `fix_run.log`.

## PoC changes vs seed

No seed existed (new pass-2 finding). `csprng_dump.c` written for this pack;
offsets extracted from the guest's `kernel.debug` via gdb, verified against
5 healthy states reading sane values at the same offsets.

## 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.
