# DF-5548 VERDICT — unsynchronized si_drv1/refcount lifecycle in fwmem open/close/ioctl/strategy

## Reproduced: YES — live race kernel panic + controlled UAF write with slab-freelist poisoning (production code)

`fwmem_open()` (`sys/bus/firewire/fwmem.c:266-278`) does a check-then-act on
`dev->si_drv1` / `fms->refcount` with **no lock**; `fwmem_close()`
(`fwmem.c:291-298`) decrements, `kfree`s and NULLs `si_drv1` with **no lock**;
`fwmem_ioctl()` (`fwmem.c:407-410`) and `fwmem_strategy()` (`fwmem.c:342-343`)
consume `si_drv1` with **no lock**. devfs serializes neither: two processes
open()/close()ing `/dev/fwmem0.0` (0660 root:operator, created by `fwdev_clone()`,
`fwdev.c:827-849`) race these windows directly. `crit_enter()` in strategy does not
help across CPUs.

## Evidence (all production entry points, df5547.ko harness)

1. **Live race → kernel panic (run.log test 4).** 4 kernel threads racing production
   `fwmem_open`/`fwmem_close` on one shared cdev — the in-kernel equivalent of two
   processes open()+close()ing the device. Result within moments:

   ```
   Fatal trap 12: page fault while in kernel mode
   fault virtual address = 0x8  (supervisor READ)
   Stopped at dfw_fwmem_close+0x13: movl 0x8(%rbx),%eax
   ```

   `0x8` is `offsetof(struct fwmem_softc, refcount)` — production `fwmem.c:292`
   dereferenced a NULL `si_drv1`, constructible only through the race (every close in
   the racer was preceded by a successful open).

2. **Post-race state → NULL write panic (run.log test 3).** The state "fd alive,
   `si_drv1 == NULL`" (open's `fwmem.c:266` check passed, the final close's
   `fwmem.c:295-297` freed + NULLed in between): production `fwmem_ioctl(FW_SDEUI64)`
   `bcopy`s 8 bytes to `&fms->eui` with `fms == NULL`:

   ```
   Fatal trap 12 ... fault virtual address = 0x0, supervisor WRITE
   ```

3. **Controlled UAF write → slab freelist poisoning (run.log test 2 / test 5).**
   Stepwise replay of the ioctl-vs-final-close interleaving (ioctl had loaded
   `si_drv1` at `fwmem.c:407` before the close's `kfree` at `fwmem.c:296`):

   ```
   df5548: after production close: si_drv1=0, freed chunk head (c_Next) = 0xfffff8008d5e15f0
   df5548: after production ioctl(FW_SDEUI64): FREED chunk 0xfffff8008d5f05d0 now holds 0xfffff8008d5ef5e0 (== ATTACKER VALUE, slab freelist poisoned)
   df5548: kmalloc#1 = 0xfffff8008d5f05d0 (== freed chunk) -- zone head now walks to the poisoned pointer
   panic: memory chunk 0xfffff8008d5ef5e0 is already allocated!
   ```

   `FW_SDEUI64` writes the user-supplied 8-byte EUI64 at offset 0 of the freed 16-byte
   zone chunk — exactly where DragonFly's SLAB allocator keeps the freelist next
   pointer (`sys/kern/kern_slaballoc.c`). `kmalloc#2` followed the attacker value onto
   a LIVE chunk ("already allocated" INVARIANTS panic on this kernel; on a production
   non-INVARIANTS kernel this is a **silent double allocation** — two owners of one
   kernel object). With an arbitrary kernel address as the value, `kmalloc` handed out
   the attacker-chosen address (test 5: `chunk_mark_allocated` zone-membership assert —
   again, the only thing that stopped a silent arbitrary-address allocation here).
   The racing variant is the mid-interleaving of `fwmem_ioctl` vs `fwmem_close`; the
   equivalent open-side window (`fwmem.c:266→269-270`) yields the same freed-chunk
   increment and the NULL+8 write observed live in (1).

4. **Double-close / desync consequences (code-read, consistent with (1)):** two closes
   racing `fms->refcount--` can both take 1→0 → `kfree` twice, and the second
   `kfree(dev->si_drv1, ...)` with `si_drv1 == NULL` panics ("trying to free NULL
   pointer", `sys/kern/kern_slaballoc.c:1410`).

## Exploit chain (demonstrated skeleton)

race open-vs-close (or ioctl-vs-close) → 8-byte fully-controlled write at offset 0 of
a freed 16-byte-zone chunk (the SLAB freelist next pointer) → next-next `kmalloc(16)`
returns the attacker-chosen address / duplicates a live chunk → overlap with a live
kernel object of the 16-byte zone → object-field corruption. On this INVARIANTS guest
the allocator's asserts convert the last step into a reliable kernel panic
(deterministic DoS); the arbitrary-address hand-out itself was observed.

## Fix validation

`fix.diff`: a `spinlock` serializing the whole si_drv1/refcount lifecycle (open's
check-increment and alloc-install, close's decrement-free-NULL, ioctl's locked read +
`ENXIO` NULL guard, strategy's locked EUI copy), plus the `M_FW`→`M_FWMEM` type fix
(DF-5549). Applied to the guest's `/usr/src`, module rebuilt from the patched
production file (`fix_validation.log`):

| case | baseline | fixed |
|---|---|---|
| test 4 live race (4 threads) | Fatal trap 12 in `fwmem_close+0x13` moments after start | **769,792 open+close pairs, guest up, zero damage** |
| test 3 `si_drv1==NULL` ioctl | Fatal trap 12, supervisor WRITE at 0x0 | returns `ENXIO`, guest up |

**fix_status: fixed.** (The stepwise replay test 2 deliberately hand-constructs the
mid-race state, bypassing the added serialization; fix validation therefore uses the
production-path live race and the NULL-guard path.)

## Notes

* Related-but-distinct from DF-5321 (firewire.c xfer/tlabel completion race): same
  missing-serialization family, different objects (`si_drv1`/`fwmem_softc`/bp vs
  tlabel/xfer) and different entry points.
* The `kfree(dev->si_drv1, M_FW)` wrong-malloc-type free (`fwmem.c:296` vs the
  `M_FWMEM` alloc at `fwmem.c:272`) is DF-5549 (accounting-only on DFly's allocator,
  which derives the zone from the chunk header) and is fixed in this pack's diff.

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