β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5416

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5416 Β· 14 files
FileTypeDescriptionSize
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):

  1. A: open("/dev/fw0.0"), FW_SSTBUF, FW_SRSTREAM (gets fc->ir[0]), FW_SBINDADDR {start=0x1_00000000, len=0x1000} β€” production fw_bindadd() links the bind into fc->binds and fc->ir[0]->binds (firewire.c:847-848).
  2. B: open("/dev/fw0.1") (separate si_drv1), FW_SSTBUF, FW_SRSTREAM (gets fc->ir[1]).
  3. B: FW_CBINDADDR {same start} β†’ fw_bindlookup finds A's bind β†’ fwdev.c:600 removes 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).

VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline: 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
↓ fix.diffper-fix-DF-5416

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.