# DF-3146 — VERDICT

**reproduced** — unprivileged post-mount trigger, two independent panic
signatures plus a silent controlled wild write, on the stock INVARIANTS
kernel (`DragonFly dfbsd 6.5-DEVELOPMENT #0`, 2026-07-02).

## How it was proven

### 1. Warm variant — deterministic panic (`bcopy` with `(size_t)-1`)

Entry `W` SUSP area: `NM(len=21,payload)` then `NM(len=4)` then filler.
Second NM: `wlen = 4-5 = -1` (cd9660_rrip.c:258), `*outlen = 16-1 = 15`
(≤ maxlen) → `bcopy(in, out, (size_t)-1)` (rrip.c:276).

Observed (serial console, guest down in DDB):

```
Fatal trap 12: page fault while in kernel mode
fault virtual address  = 0xfffff8005d685ff9
fault code             = supervisor read data, page not present
Stopped at  memmove+0x24f:  repe movsq (%rsi),%es:(%rdi)
```

(bcopy → memmove on amd64; the copy walks forward until the unmapped page
after the source buffer-cache page.) → `panic.txt`.

### 2. Cold variant — silent OOB write at `d_name − 65536·N` + heap disclosure

Entry `C` SUSP area: `[NM(len=4)+filler]×N` + `NM(len=21,payload)`.
First NM: `*outlen = 0-1` wraps u_short → 65535 > NAME_MAX →
`outbuf -= 65535-(-1) = 65536` exactly (rrip.c:268-271). After N such
entries the output pointer sits `65536·N` below `idp->current.de.d_name`;
the payload NM then `bcopy`s 16 fully attacker-chosen bytes there.

* N=1 and N=20: guest **survived** (the target pages are mapped kernel
  heap) and the trigger entry shipped with `namlen=16` and a name of
  **stale idp heap bytes** (`2e 2e 00 00 …` — leftovers of the previous
  entry's `defname` strcpy into the same buffer) while `ABCDEFGHIJKLMNOP`
  went to the wild address. Direct evidence of both the write and a
  corroborating 16-byte stale-heap disclosure in the same dirent.
* N=16140 (CE-chained, −1.02 GB, image `df3146_ce64.iso`): the write
  reached far below the malloc arena and killed the VM subsystem:

```
panic: assertion "obj != NULL" failed in vm_object_hold_shared at vm/vm_object.c:330
vm_fault() at vm_fault+0x408
trap_pfault() at trap_pfault+0x9a
--- trap 000000000000000c, rip = ... isofntrans() at isofntrans+0xe1 ---
```

The faulting `isofntrans` is the `defname` fallback writing at the rewound
pointer (same mechanism as cold1, scaled to −1 GB) → `panic2.txt`.

### 3. Address-control analysis (what the attacker gets)

* Write address: `(output buffer) − 65536·N`, N ≥ 1 arbitrary (CE
  continuations scale N without bound — each 2048-byte CE block carries 252
  rewind pairs = −16 MB per block; ce64 used 64 chained blocks).
* Write content: arbitrary bytes from the image (payload NM up to 250
  bytes; `defname` fallback writes the entry's ISO name, up to ~222 bytes).
* Repeatable: every readdir allocates a fresh `isoreaddir`; the objcache
  recycles the same chunk, so the grid anchor is stable within a boot.
* The 65536 residue is intrinsic: every "epoch" (outlen reset) rewinds by
  exactly `65536 + (bytes advanced since last reset)`, i.e. the pointer
  always returns to `start − 65536·k`. No byte-granular placement within the
  grid is possible from this bug alone.

### 4. uid0 escalation — attempted, blocked in-session (documented honestly)

Primitive: arbitrary-content write at any address ≡ idp (mod 65536) below
the idp heap allocation, repeatable, unprivileged post-mount. To reach
`uid=0` one needs a juicy victim object (ucred/`struct file`/kstack of
another thread — the lookup call site writes below a **kernel stack**
`altname[]`) whose address is (a) below the anchor and (b) on the anchor's
64KB residue, plus knowledge of the anchor address (heap leak).

In-session blockers, rigorously:

1. **Residue constraint** (intrinsic, proven above): the victim must lie on
   the anchor's 64KB grid. Real victims (ucred ~96 B, file ~144 B) occupy
   16-byte-aligned addresses; for a given boot only ~1/4096 of sprayed
   victims land on the required residue, and their addresses must then be
   *known* to pick N.
2. **No anchor-address leak in this pack**: the stale-name disclosure
   (cold runs) reads bytes of the *previous dirent content*, not pointers;
   the DF-3147 leak reads the stale `uio_off` slot (offsets, not pointers).
   A pointer-class leak of the idp/anchor address was not available without
   pulling in other findings (DF-0878's read primitive would be the natural
   complement, and on this no-KASLR guest kernel text is fixed — the
   missing piece is the *heap* address).
3. Combined (1)+(2) make a reliable single-session chain infeasible; a
   realistic weaponization needs a heap-address leak plus long-run grooming
   (fork-spray ucreds until one lands on the residue, or pivot off the
   kstack-relative variant in `cd9660_lookup`). The primitive itself —
   write-what-at-chosen-64KB-grid-address, arbitrary content, repeatable —
   is fully demonstrated above.

Impact recorded as `panic` (both deterministic warm panic and deep-write
panic reproduced); the write primitive is the memcorrupt escalation vector,
not completed to uid0 this session.

## Distinction from known findings

* DF-0878 = rrip_loop walk **upper-bound** OOB reads (large lengths → read
  past buffer). This finding = **lower-bound** violation (length < 5) →
  u_short wrap → **write** at a rewound pointer + negative-length bcopy.
  Different direction, different primitive class, separate fix site.
* DF-0866 = dirent pad disclosure. Different sink (here the stale name bytes
  are corroboration, not the finding).
* DF-0879 = CE infinite loop. The CE chain here is *bounded* (validated,
  forward-progress); it is used only to scale N.

## Fix validation

`fix.diff` (NM `h.length < 5` → ignore field; plus the DF-3147 `uio_off`
init) applied to the guest's `/usr/src`, `make -j8 nativekernel`,
`make installkernel`, reboot. Re-runs of all three triggers on the patched
kernel: no panic, no stale-name entries — the crafted `W`/`C` entries now
parse as their payload names (`ABCDEFGHIJKLMNOP`), i.e. the NM walk behaves
as if the bogus 4-byte NMs did not exist. See fix_run.log.

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