# DF-5613 VERDICT

**Status: REPRODUCED** — both primitives (claim race → controlled heap
overflow; teardown → UAF write with attacker-controlled content) were
demonstrated on production code paths in the DFbsd 6.5-DEVELOPMENT guest.
Confidence: certain.

## Reproduction narrative

### Test 1 — unsynchronized `FW_STSTREAM` claim → shared queue → heap overflow

Production path: `fw_ioctl(FW_SSTBUF)` (fwdev.c:504) stores the bufspec
unvalidated → `fw_ioctl(FW_STSTREAM)` (fwdev.c:447-467): scan for a
non-`FWXFERQ_OPEN` queue → `fwdev_allocbuf()` (fwdev.c:94-137) →
`fwdma_malloc_multiseg()` (fwdma.c:144-205, sleeping) → only then
`it->flag |= FWXFERQ_OPEN` (fwdev.c:461).

Harness (df5613.c): fd1 runs `SSTBUF(64,64,2048)` + `STSTREAM` in a
kernel thread; the main thread waits until fd1 is provably inside
`fwdma_malloc_multiseg` (`q->bulkxfer` assigned at fwdev.c:103 but
`q->buf` still NULL, `OPEN` clear), then fd2 runs
`SSTBUF(2,2,512)` + `STSTREAM` on a second cdev. Observed (run.log):

* fd2's scan still sees `it[0]` unclaimed → both claim it:
  `final: d1->it=0xfffff80068ed2240 d2->it=0xfffff80068ed2240 shared=YES`
* fd2's `kmalloc`'d 2-entry (64-byte) `bulkxfer` became `q->bulkxfer`;
  fd1's chunk loop (fwdev.c:126-130) then wrote 64 entries through it:
  `q->bulkxfer[8].poffset = 512 ... [20].poffset = 1280` —
  **62 of 62 out-of-bounds entries carry fd1's poffset=i*64 pattern →
  62 × 32 = 1984 bytes of linear heap overflow past a 64-byte kmalloc,
  values attacker-chosen via `npacket`** (`poffset = i*npacket`,
  `npacket` an unvalidated 32-bit user value).
* The loser's `fwdma_alloc_multi` + all its segment pages are orphaned
  (pointer overwrite at fwdev.c:103/108 with no free) — code-certain;
  the leak probe ("probe differs") didn't catch the address this run
  because fd1's allocation had already completed before the capture
  read, so this sub-claim is noted as code-certain, not
  runtime-demonstrated.

### Test 2 — `fw_close` teardown vs in-flight `fw_write` → UAF write

Precondition is exactly test 1's post-race state (two `fw_drv1` owning
one queue — reached through production ioctls above). fd2 is inside
`fw_write()` at fwdev.c:388-392: it has computed
`fp = fwdma_v_addr(it->buf, stproc->poffset + queued)` (fwdma.h:59-66,
element 0 → `seg[0].v_addr`) and is about to
`uiomove(hdr,4)` + `uiomove(payload, fp->mode.stream.len)`.

* fd1 calls production `fw_close()` → `fwdev_freebuf()` →
  **`fwdma_free_multiseg()`** (fwdma.c:207-218) freeing all 16 segment
  pages, `bus_dma_tag_destroy`, `kfree(am)`; then `kfree(bulkxfer)`
  (fwdev.c:144-147). `fp` is now a dangling pointer into freed kernel
  memory (`q->buf=0 bulkxfer=0 (both freed); fd2's fp=... is DANGLING`).
* Spray reclaims the freed 4096-byte segment pages (on this 2GB-RAM
  guest they are `kmalloc(4096, M_DEVBUF)`-backed per
  busdma_machdep.c:88-91/511 — i.e. ordinary reclaimable kernel heap,
  as on every sub-4GB-RAM machine, the classic FireWire laptop).
* fd2 resumes and performs fwdev.c:390-392's two uiomoves. Observed
  (run.log): the payload landed in the reclaimed page — `UAF WRITE
  landed in spray block 15 ... at offset 11`, context
  `"...13-UAF!DF5613-UAF!DF5613-UAF"` — **2048 bytes of
  attacker-controlled iso data written into freed-then-reallocated
  kernel heap**. Note `fp->mode.stream.len` itself was read as 42432
  from the reclaimed (spray-influenced) page — the length dance is
  attacker-steerable too.
* The same race taken *mid-flight* by real production code (no
  replayed interleaving) was captured on the 4GB-RAM guest: the
  concurrent `fw_close` freed the contigmalloc'd segments under the
  hammering production `fw_write` and its `uiomove` took
  **`Fatal trap 12: page fault ... supervisor write ... in memmove`**
  writing the stale segment pointer (panic.txt; fault VA in the freed
  segment region, far from `am`). That is the same UAF write — on
  ≥4GB-RAM hosts contigfree unmaps the page so the write faults; on
  <4GB hosts it lands silently in reclaimable heap (test 2).

The read side is symmetric: `fw_read()` at fwdev.c:321-330 computes
`fwdma_v_addr(ir->buf, ...)` and `uiomove`s `fp->mode.stream.len + 4`
bytes *out* — from the same freed/reclaimed memory — an
arbitrary-kernel-read shape; not separately run (the write side is the
stronger primitive and shares the identical lifetime hole).

## Why this is a fwdma.c-family finding

`fwdma_free_multiseg` (fwdma.c:207-218) and the buffers it releases
have **no lifetime protocol**: no refcount, no lock, no
"quiesce in-flight users" step, and its only consumer-side free sites
(fwdev.c:144, fwohci.c:1178) are equally unsynchronized against the
I/O paths that dereference `am->seg[]` (fwdma.h:59-75) and the segment
pages. DF-5304 (Info) covered the narrower *hardware* DMA window after
`fwohci_{i,t}x_disable`; this finding is the software-side hole (two
fds, claim race + close-vs-I/O), which reaches `fwdma_free_multiseg`
directly and yields controlled kernel heap corruption.

## Exploit chain (operator-group local → kernel)

open(/dev/fw0.0) + open(/dev/fw0.1) → race FW_STSTREAM (window =
`fwdma_malloc_multiseg` duration, widened at will via `nchunk*npacket`)
→ shared xferq → either (a) bulkxfer heap overflow with chosen
`i*npacket` ints + STAILQ link pointers, or (b) close-vs-write UAF:
`fwdma_v_addr` returns a pointer read from freed/reclaimed `am`/segment
memory → `uiomove` writes up to 64KB (`stream.len:16`) of chosen bytes
per packet at reclaimed-heap addresses. On the no-SMAP/SMEP/KASLR
guest, (b) with a heap groom (the 4096-byte zone is trivially
sprayable, as test 2 shows) is a write-what-where-style primitive into
reallocated kernel objects → credential/function-pointer overwrite →
uid=0. The full chain to uid=0 was not constructed (hardware-gated
entry in QEMU: /dev/fw* requires an attached OHCI), matching the
DF-5413/DF-5548 evidence-class standard for this family.

## Fix validation

`fix.diff` (against `sys/bus/firewire/fwdev.c`): atomic queue claim
under `crit_enter()` **before** the sleeping `fwdev_allocbuf`, OPEN
cleared on failure; `fwdev_freebuf` clears `stproc` and re-inits the
chunk lists; `fw_read`/`fw_write` re-check `it->buf == NULL` at loop
top. Applied inside the guest's /usr/src, module rebuilt (fix_build.log),
test 1 rerun (fix_run.log):

* baseline: `shared=YES`, 62/62 overflow entries → heap overflow CONFIRMED
* patched:  `OPEN=1` already set while fd1 is inside
  `fwdma_malloc_multiseg`; fd2 claims a different queue
  (`d2->it != fc.it[0]`); `shared=no overflow=0`

The claim gate being closed also removes the reachability of test 2's
two-fds-one-queue precondition. Residual: a single instruction-level
window between the loop-top check and the uiomove remains in principle
(nanoseconds wide, per iteration) — closing it fully needs a refcount
or lock held across the I/O loops; the shipped fix reduces the window
from milliseconds (the whole `fwdma_free_multiseg`) to that sliver.

## kernel_refs

* sys/bus/firewire/fwdev.c:100-101 (check-then-act EXTBUF re-check)
* sys/bus/firewire/fwdev.c:103-109 (bulkxfer/buf overwrite, no free)
* sys/bus/firewire/fwdev.c:126-130 (chunk loop -> bulkxfer overflow)
* sys/bus/firewire/fwdev.c:139-153 (fwdev_freebuf, no sync, stproc kept)
* sys/bus/firewire/fwdev.c:144 (calls fwdma_free_multiseg)
* sys/bus/firewire/fwdev.c:447-467, 475-496 (claim races)
* sys/bus/firewire/fwdev.c:366-405 / 280-345 (in-flight fw_write/fw_read)
* sys/bus/firewire/fwdma.c:144-205 (fwdma_malloc_multiseg, sleeping window)
* sys/bus/firewire/fwdma.c:207-218 (fwdma_free_multiseg, no lifetime protocol)
* sys/bus/firewire/fwdma.h:59-75 (fwdma_v_addr/fwdma_bus_addr deref am)
* sys/platform/pc64/x86_64/busdma_machdep.c:88-91,511 (kmalloc-backed segs <4GB)

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