# DF-4673 — VERDICT (verify run, 2026-10-15)

## Verdict

**REPRODUCED (impact: leak — cross-privilege kernel-memory disclosure).**
Fix behaviorally VALIDATED on a rebuilt kernel (`fixed`).

The grow path of the **production, unmodified** `bktr.ko`
(`video_ioctl` @`0xffffffff82606190`) installed an **unzeroed**
`contigmalloc`'d frame buffer, and the **verbatim** `bktr_mmap` fault
path (bktr_os.c:656-685, copied line-for-line into the rig because
`bktr_mmap` is a local symbol, `nm`: `t bktr_mmap @0x7840`) mapped its
recycled pages read-only into an **unprivileged** process (uid 1001),
which read root's memory contents back.

### Decisive numbers (`run.log`, stock kernel, guest RAM 4 GB)

- Root sprayed 2600 MiB of anonymous memory with per-page markers
  (`"ROOTSECR"`, tag, spray page index) and exited (pages → PQ_FREE,
  contents intact).
- uid 1001 ran 24 adaptive `METEORSETGEO` grows; each grow's wire delta
  (0 = reserve-served, >0 = **scan-served/recycled**) was printed; every
  scan-served buffer was immediately `mmap(PROT_READ)`'d and scanned.
- **8 scan-path buffers, 16,444 pages scanned: 14,117 pages carried the
  ROOT spray secret** (e.g. page 0: `524f4f5453454352 0000000000000029
  000000000001da0b 004446343637332d ...` = MAGIC0, tag=41,
  spraypage=121355, MAGIC3, 0xAA fill) — disclosed to an unprivileged
  user across the privilege boundary.
- The run ends with `Cannot allocate memory` — `get_bktr_mem` exhaustion
  (incidentally also demonstrating DF-4672's phase C).

### Important DragonFly-specific precondition (honest scoping)

The seed's claim "pages come from the free/cache queues" is only the
*fallback* path on this kernel: `contigmalloc` first serves from the
256 MiB low-phys **DMA reserve** (vm_page.c:2765) whose fictitious pages
are pristine zero, so with a full reserve the grown buffer reads as all
zero (demonstrated by the control rounds, `control_rounds.log`: 700 MiB
and 2600 MiB sprays, all scans 0/…  non-zero with the reserve intact —
this also *confirms* the two-pool model). The disclosure becomes
reachable the moment the reserve cannot satisfy the request — and the
SAME unprivileged ioctl deterministically drains the reserve (that is
DF-4672, +203.7 MiB verified in the same session). After the drain, the
pfn-ascending first-fit scan takes sprayed PQ_FREE pages (small sprays
lose to pristine low-pfn runs; ~85% coverage wins deterministically:
P(clean 33-page run) ≈ 0.15^33 ≈ 1e-27).

Chain for an unprivileged attacker on real hardware: open /dev/bktr0
(0444) → N SETGEO grows to drain the reserve (each leaks, DF-4672) →
one more grow → mmap → read recycled page-cache/heap/user pages of other
users, including root. No privilege check anywhere in the path
(`rg priv_check sys/dev/video/bktr/` → none).

### Root cause re-confirmed in source

- attach zeroes: bktr_core.c:432 `bzero(bigbuf, BROOKTREE_ALLOC)`;
  grow path installs `get_bktr_mem()`'d buffer with **no** zeroing
  (bktr_core.c:1667-1673; `get_bktr_mem` passes only `M_NOWAIT`,
  bktr_os.c:403-407 — no `M_ZERO`).
- scan-path pages come from PQ_FREE/PQ_CACHE with contents intact
  (vm_contig.c:308-323, 398-449; zeroing happens only on fault via
  `vm_page_grab(VM_ALLOC_*ZERO)` paths, not for contig pages).
- `bktr_mmap` maps them at first fault with no exec-only restriction
  (device_pager.c:348-390 `old_dev_pager_fault` → `dev_dmmap`).

### Rig fidelity

Same table as DF-4672 (production ioctl+allocation path, verbatim
production mmap, synthetic softc/fake register page, 0666 rig node vs
0444 real node — both unpriv-openable). The disclosure observed is
strictly in-bounds mapped frame-buffer memory; no OOB read is involved.

## Fix validation

`fix.diff` adds `bzero(new buffer)` at install (plus the contigfree and
ordering fixes). On the DF467XFIX kernel, the same spray+grow+scan
protocol yields **all-zero buffers and 0 secret pages**
(`fix_run.log`). Baseline bad behavior GONE ⇒ `fix_status: fixed`.

## Files

| File | Purpose |
|---|---|
| `poc_stalebuf.c` | adaptive unpriv grow+mmap+scan PoC (wire-delta phase detection) |
| `spray.c` | root-side page-queue groomer (marked anon pages → PQ_FREE) |
| `run.log` | decisive run (14,117/16,444 secret pages) |
| `control_rounds.log` | control rounds (reserve intact → all zero; two-pool proof) |
| `fix_run.log` | same attack on the fixed kernel |
| `fix.diff` | verified fix |
| `env.txt`, `manifest.json`, `verdict.json` | environment + machine records |

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