FW_CBINDADDR unbinds a foreign bind via the global fw_bindlookup and STAILQ_REMOVEs it from the wrong xferq's (empty) bind list -- unguarded walk-off-NULL -> kernel panic; cross-node bind theft
Summary
fw_bindlookup searches the GLOBAL fc->binds so FW_CBINDADDR on fw0.1 can find and remove a bind registered through fw0.0 (ownership never verified), then STAILQ_REMOVE from B's empty list walks NULL->chlist (DFly STAILQ_REMOVE has no absent-element guard). Guest-proven entirely through the production fw_ioctl path with two synthetic nodes: Fatal trap 12 va 0x28 at fw_ioctl+0x7f6; membership-check + crit + xfer-drain fix returns EINVAL. Purely software trigger. Also unsynchronized vs the fw_rcv interrupt walker.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5416 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 2.1 KB | β raw | |
| df5413.c | β | 21.5 KB | view raw | |
| Makefile | β | 1.8 KB | β download | |
| shim/ | β | 4.0 KB | β download | |
| build.sh | β | 103 B | view raw | |
| run.sh | β | 167 B | view raw | |
| build.log | β | 7.0 KB | view raw | |
| panic.txt | β | 907 B | view raw | |
| fix.diff | β | 3.4 KB | view raw | |
| fix_build.log | β | 7.0 KB | view raw | |
| fix_run.log | β | 2.6 KB | view raw | |
| env.txt | β | 615 B | view raw | |
| verdict.json | β | 3.9 KB | view raw |
DF-5416 β cross-node FW_CBINDADDR unbinds a foreign bind: unguarded STAILQ_REMOVE walks off NULL β kernel panic
File: sys/bus/firewire/fwdev.c Β· Severity: Medium Β· Bucket: base:hw (local DoS) Β· Confidence: certain
Status: reproduced (kernel panic) on the QEMU guest.
Root cause (production path:line)
FW_CBINDADDR (fwdev.c:592-602) looks the bind up with
fw_bindlookup() β which searches the global fc->binds list
(firewire.c:804-814), i.e. binds created through any file descriptor on
any /dev/fwN.M node β and then unlinks it from this descriptor's
xferq:
STAILQ_REMOVE(&sc->fc->binds, fwb, fw_bind, fclist); /* ok: fwb is there */
STAILQ_REMOVE(&ir->binds, fwb, fw_bind, chlist); /* fwdev.c:600: NOT there */
kfree(fwb, M_FW);
DragonFly's STAILQ_REMOVE (sys/sys/queue.h:319-331) has no
absent-element guard: the walk while (STAILQ_NEXT(curelm, field) != elm)
curelm = STAILQ_NEXT(curelm, field); runs off the end of the list and
dereferences NULL->chlist (offset 0x28). With fd B's ir->binds empty
the very first curelm is NULL.
Sequence (all software β no FireWire bus traffic needed, only the device nodes, which need the controller):
- A:
open("/dev/fw0.0"),FW_SSTBUF,FW_SRSTREAM(getsfc->ir[0]),FW_SBINDADDR {start=0x1_00000000, len=0x1000}β productionfw_bindadd()links the bind intofc->bindsandfc->ir[0]->binds(firewire.c:847-848). - B:
open("/dev/fw0.1")(separatesi_drv1),FW_SSTBUF,FW_SRSTREAM(getsfc->ir[1]). - B:
FW_CBINDADDR {same start}βfw_bindlookupfinds A's bind βfwdev.c:600removes it from B's empty bind list β NULL-walk β panic.
Beyond the panic, the same primitive is a bind theft: B removes A's bind
from fc->binds and frees it while A's ir->binds still references it
(A's later fw_close drain at fwdev.c:224-229 would double-free it if
the walk survived) β ownership is never verified.
Demonstration (guest, test 4, production fw_ioctl)
df5416: A SBINDADDR err=0 fwb=0xfffff8008d3e7f28 start=100000000 end=100001000 sub=0 ... df5416: B SRSTREAM err=0 ir=0xfffff8008fa923a0 (binds list EMPTY: 0) df5416: B FW_CBINDADDR -- expect Fatal trap 12 in fw_ioctl Fatal trap 12: page fault while in kernel mode ... fault virtual address = 0x28 Stopped at fw_ioctl+0x7f6: movq 0x28(%rdx),%rax /* rdx = curelm = NULL */
fwdev.c's own ioctl path pinned in the trap (panic.txt). Guest down.
What to run
./build.sh sh run.sh # kern.df5413_test=4 -> guest panics; capture serial console
Fix
fix.diff (shared) makes FW_CBINDADDR verify the bind actually belongs
to this ir (STAILQ_FOREACH over ir->binds) before unlinking, wraps
the removals in crit_enter(), and frees the bind's xferlist xfer
(also the DF-5417 leak). Validated: CBINDADDR returned 22 (EINVAL), no
panic, guest alive (fix_run.log).
DF-5416 β VERDICT
reproduced Β· impact: kernel panic (local DoS) Β· confidence: certain
Everything ran through the production fw_ioctl() dispatcher with two fake /dev/fw nodes (separate fw_drv1 via separate cdev si_drv1 β exactly what devfs gives for fw0.0 vs fw0.1; a fake devclass/device pair supplies the softc that an attached fwohci card would).
Sequence and result (fresh boot):
df5416: A SRSTREAM err=0 ir=0xfffff8008fa92240
df5416: A SBINDADDR err=0 fwb=... start=100000000 end=100001000 sub=0
(production fw_bindadd linked it into fc->binds + ir[0]->binds)
df5416: B SRSTREAM err=0 ir=0xfffff8008fa923a0 (binds list EMPTY: 0)
df5416: B FW_CBINDADDR ...
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x28
Stopped at fw_ioctl+0x7f6: movq 0x28(%rdx),%rax <-- rdx = curelm = NULL
Mechanism: fw_bindlookup() (fwdev.c:593 -> firewire.c:804-814) searches the GLOBAL fc->binds, so node B found node A's bind; fwdev.c:599 removed it from fc->binds (legitimate), then fwdev.c:600 STAILQ_REMOVE(&ir->binds, fwb) attempted to unlink it from B's EMPTY bind list. DragonFly's STAILQ_REMOVE (sys/sys/queue.h:319-331) has no absent-element guard β the walk dereferenced NULL->chlist (offset 0x28) and faulted inside the production ioctl. Guest down.
No bus traffic is required β only two open nodes, so on real hardware this is a deterministic two-syscall-after-setup DoS for operator-group users. The same missing ownership check is a bind-theft primitive (cross-fd); with a guarded STAILQ_REMOVE it would instead leave a freed bind on the owner's ir->binds for a close-time double free (fwdev.c:224-229).
Fix validated (fix.diff: STAILQ_FOREACH(ir->binds) membership check + crit sections + xferlist free): "df5416: ... CBINDADDR returned 22" (EINVAL), no panic, guest alive (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.
Fix verification
fixedBaseline: Fatal trap 12 at fw_ioctl+0x7f6 (fault va 0x28), guest down. Patched (fix.diff on /usr/src, module rebuilt, same trigger): FW_CBINDADDR returns 22 (EINVAL) for the foreign bind, no panic, guest alive through the full battery.
fix_run.log ('df5416: UNREACHED: CBINDADDR returned 22'), fix_build.log, fix.diff
Confirmed kernel references
Detail
Exploit chain
open /dev/fw0.0 and /dev/fw0.1 (operator) -> A: SSTBUF+SRSTREAM+SBINDADDR(addr) -> B: SSTBUF+SRSTREAM -> B: CBINDADDR(addr) -> production fwdev.c:600 STAILQ_REMOVE of a foreign bind from an empty list -> NULL-walk page fault at fw_ioctl+0x7f6 -> reliable kernel panic; conceptually also cross-fd bind theft/double-free on the owner's close path
Evidence (decisive lines)
panic.txt (Fatal trap 12, fault va 0x28, Stopped at fw_ioctl+0x7f6, full pre-panic dmesg), fix_run.log ('df5416: ... CBINDADDR returned 22' on patched module, guest alive), fix.diff, build.log, fix_build.log
PoC changes
Same shared harness (df5413.c test 4): the fake devclass/device pair makes the production fw_ioctl fetch a valid softc, two fake cdevs model the two /dev/fw nodes devfs gives (separate si_drv1), everything else (SSTBUF/SRSTREAM/SBINDADDR/CBINDADDR, fw_bindadd, fw_bindlookup, the removals, the kfree) is production code.
Verified recommended fix
FW_CBINDADDR: verify the looked-up bind is on this ir's binds list (STAILQ_FOREACH) before unlinking, protect the removals with crit_enter/exit, and free fwb->xferlist xfers - as in fix.diff.
Verdict
Reproduced on the QEMU guest entirely through the production fw_ioctl path with two synthetic /dev/fw nodes (fw0.0/fw0.1 with separate fw_drv1, as devfs provides): node A registered a FW_SBINDADDR bind (production fw_bindadd links it into fc->binds and fc->ir[0]->binds), node B then issued FW_CBINDADDR with the same address; fw_bindlookup() is global (firewire.c:804-814) so it returned A's bind, fwdev.c:599 removed it from fc->binds, and fwdev.c:600 STAILQ_REMOVE(&ir->binds, fwb) tried to unlink it from B's EMPTY list - DragonFly's STAILQ_REMOVE (queue.h:319-331) has no absent-element guard, the walk dereferenced NULL and the kernel took 'Fatal trap 12, fault virtual address 0x28, Stopped at fw_ioctl+0x7f6: movq 0x28(%rdx),%rax'. Guest down. Deterministic local DoS for operator-group users with two open nodes; it is also a bind-theft primitive (cross-fd ownership is never verified; absent the walk-off it would leave a freed bind on the owner's list for a close-time double free). Fix validated: ownership check returns EINVAL (22), no panic, guest alive.
No comments yet.