# DF-5413 — VERDICT

**reproduced · impact: arbitrary kernel write (memcorrupt; privesc-class primitive) · confidence: certain**

## What ran

The guest (DragonFly 6.5-DEVELOPMENT X86_64_GENERIC, no FireWire hardware)
compiled the **unmodified production** `/usr/src/sys/bus/firewire/firewire.c`,
`fwdma.c` and `fwdev.c` into KLD `df5413.ko` (globals `-D`-renamed, guard
shims for the unguarded firewire headers — `firewirereg.h`, `fwdma.h`,
`fwmem.h`, `iec68113.h`, `firewire_phy.h` have no include guards).  The
harness drives the production entry points with exactly the state devfs +
fwohci_attach would create (`fw_drv1` via a fake cdev `si_drv1`,
`firewire_comm` with `fc->it[]/ir[]`, `dmach`, `nisodma`, `dmat`,
`irx_enable` stub; a fake `devclass`/`device` pair makes
`devclass_get_softc(firewire_devclass, 0)` return the synthetic softc the
way an attached card would).

## The chain that ran (all production code)

1. `fw_ioctl(FW_SSTBUF, {tx={0x10000,0x10000,2048}})` — `fwdev.c:504-506`
   bcopy of the user struct, no validation.
2. `fw_ioctl(FW_STSTREAM)` — `fwdev.c:447-467` scans `fc->it[]`, calls
   `fwdev_allocbuf()` (`fwdev.c:95-137`):
   * `fwdev.c:109` `b->nchunk * b->npacket` → `0x10000 * 0x10000` wraps to
     **0** in 32 bits → `fwdma_malloc_multiseg(fc, 4, 2048, 0, …)`
     (`fwdma.c:145-205`) computes `nseg = howmany(0, 2) = 0` and returns a
     32-byte `fwdma_alloc_multi` with **zero segments**
     (`kmalloc(32 + 0)`, `fwdma.c:162-163`);
   * `fwdev.c:116-117` sets `bnchunk = bnpacket = 0x10000` anyway.
3. Heap groom (harness-only): a bank of 64×32-byte `M_FW` chunks is
   allocated, two *adjacent* slots are found, freed in LIFO order so that
   the production `kmalloc` inside `fwdma_malloc_multiseg` pops the lower
   slot for `am` and the harness's follow-up `kmalloc` pops the upper slot,
   which is filled with `V = &df5413_marker[0]`.
4. `fw_write()` (`fwdev.c:348-406`) — production: `fp = fwdma_v_addr(buf,
   0)` (`fwdev.c:388-389`, `fwdma.h:60-66`) reads `am->seg[0].v_addr` from
   `am+40` — **16 bytes past the 32-byte allocation** — and returns the
   groomed `V`; `uiomove(fp, 4)` then `uiomove(fp->mode.stream.payload,
   fp->mode.stream.len)` write the user's 4+32 bytes at `V`.

## Decisive output (`run.log`)

```
df5413: attempt 0 bank ... pair=9
df5413:   &am->seg[0]=0xfffff8004f1026e0 (allocation is 32 bytes) -- seg[0] is OUT OF BOUNDS
df5413:   OOB-read seg[0].v_addr = 0xffffffff82606180 (want 0xffffffff82606180 == GROOMED)
df5413: fw_write err=0 resid=0 marker after = 343546440020abcd 52544e4f432d3331 454b2d44454c4c4f
df5413:   payload="13-CONTROLLED-KERNEL-WRITE!!..."
df5413: DF-5413 REPRODUCED: controlled kernel write (header 0x0020ABCD + 32 payload
        bytes) via OOB seg[0].v_addr to 0xffffffff82606180
```

`0x343546440020abcd` little-endian = `cd ab 20 00 44 46 35 34` — the
4-byte header `0x0020ABCD` followed by "DF54…", i.e. the payload starts at
`V+4` exactly where `fp->mode.stream.payload` points.  Bytes, address, and
length are all attacker-chosen; the write went through with `err=0` and the
guest stayed up (corruption is silent — ideal for exploitation).

## Why this is a user→root primitive (not just a crash)

* The value read OOB (`seg[0].v_addr`) is fully controllable by heap
  grooming (demonstrated); the data written is fully controlled (the uio
  bytes); `fw_write` loops over the uio in packet-sized steps, giving a
  *repeatable* write-what-where per `write(2)` call.
* The only reason the PoC stops at a module symbol is that the guest has no
  FireWire controller, so the unprivileged `/dev/fwN.N` entry point cannot
  exist.  On real hardware the exact same production path is reachable by
  any user in group `operator` (device mode 0660, `fwdev.c:173-175` /
  FreeBSD-conditional clone path aside, DragonFly creates the nodes on
  attach), with no bus traffic required for the *write* direction.
* With the write primitive, `uid=0` on this class of guest (no KASLR/SMAP/
  SMEP) is mechanical: overwrite the caller's `p_ucred` (uid/gid fields) or
  a `cred`-adjacent target, then `execve("/bin/sh")`.

## Fix validation

`fix.diff` (in this pack) adds bufreq validation to `fwdev_allocbuf()`
(`nchunk/npacket/psize` sane, product can no longer wrap).  Applied to the
guest `/usr/src`, module rebuilt from the patched production source:

```
df5413: attempt 0 STSTREAM err=22      (EINVAL — request rejected)
df5413: attempt 1 STSTREAM err=22 …    (no allocation, no fw_write, no write)
```

Guest alive after all tests (`fix_run.log`).  Baseline vs patched:
controlled write present → gone.

## Notes / deviations

* The harness's `fw_ioctl` calls go through the production dispatcher; the
  only synthetic state is the controller/softc scaffolding (fwohci_attach's
  job) and the heap groom.
* Bring-up oddity (not a finding): adding an extra `DECLARE_MODULE` in the
  same translation unit corrupts the modmetadata linker set on this kernel
  and panics `module_register()` — the module therefore uses the included
  firewire.c `DRIVER_MODULE` as its only module entry (DF-5321 pattern).

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