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

fw_kqfilter dereferences d->ir with no NULL check: knote_insert on NULL+offsetof(fw_xferq,rkq) -> kernel panic (EVFILT_WRITE also hooked to the receive queue)

Summary

ir stays NULL until the first FW_SRSTREAM succeeds but fw_kqfilter does knote_insert(&ir->rkq.ki_note) at NULL+0x98; EVFILT_WRITE is also hooked to ir rather than d->it. open+kqueue+kevent(EVFILT_READ) before stream setup = deterministic panic. Guest-proven: panic at fw_kqfilter+0x8e pinned in the trace; with the fix ENODEV and the guest survives. NULL-check + hook-EVFILT_WRITE-to-it fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5415 Β· 14 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 1.8 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 β€” 811 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 β€” 2.9 KB view raw

DF-5415 β€” fw_kqfilter dereferences d->ir without a NULL check β†’ kernel panic (and EVFILT_WRITE hooked to the receive queue)

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_kqfilter() reads ir = ((struct fw_drv1 *)dev->si_drv1)->ir (fwdev.c:721) and, for both EVFILT_READ and EVFILT_WRITE (fwdev.c:725-737 β€” the write filter should use d->it, it uses ir), executes klist = &ir->rkq.ki_note; knote_insert(klist, kn) (fwdev.c:739-740). ir is NULL until the first FW_SRSTREAM succeeds (fwdev.c:494), so knote_insert operates at NULL + offsetof(struct fw_xferq, rkq) = NULL+0x98.

Any holder of an open /dev/fwN.M descriptor (device mode 0660, group operator) can reach this before configuring a stream:

fd = open("/dev/fw0.0", O_RDWR);
kq = kqueue();
kevent(kq, &(struct kevent){ .filter=EVFILT_READ, .flags=EV_ADD }, 1, ...); /* panic */

Demonstration (guest, test 5, production fw_kqfilter)

df5415: production fw_kqfilter with d->ir=NULL, offsetof(fw_xferq, rkq)=152
panic: assertion "kn->kn_status & KN_PROCESSING" failed in knote_insert at kern_event.c:1890
knote_insert() at knote_insert+0x5a
fw_kqfilter() at fw_kqfilter+0x8e
df5413_fire_sysctl() ...

The NULL-adjacent memory access inside knote_insert (reading the knote status through the NULL-derived klist) panics the kernel with fw_kqfilter pinned in the trace (panic.txt). Guest down.

What to run

./build.sh
sh run.sh      # kern.df5413_test=5  -> guest panics; capture serial console

Fix

fix.diff (shared) returns ENODEV when ir == NULL before touching rkq. Validated: fw_kqfilter returned 19 (ENODEV), guest alive (fix_run.log). Upstream should also hook EVFILT_WRITE to d->it (as modern FreeBSD does).

VERDICT.md
↓ download raw

DF-5415 β€” VERDICT

reproduced Β· impact: kernel panic (local DoS) Β· confidence: certain

The production fw_kqfilter() (fwdev.c:706-743) was invoked with the exact state an open(2) without FW_SRSTREAM leaves: d->ir == NULL, d->it == NULL. fwdev.c:721 assigns ir without checking; for EVFILT_READ (and equally for EVFILT_WRITE, which is wrongly hooked to ir instead of d->it at fwdev.c:730-732), fwdev.c:739 computes klist = &ir->rkq.ki_note = NULL+0x98 and fwdev.c:740's knote_insert() dereferences it.

Guest result (fresh boot):

panic: assertion "kn->kn_status & KN_PROCESSING" failed in knote_insert
        at /usr/src/sys/kern/kern_event.c:1890
knote_insert() at knote_insert+0x5a
fw_kqfilter() at fw_kqfilter+0x8e        <-- production path pinned
df5413_fire_sysctl() ...

Guest down (ssh dead; vm.sh status down). The manifestation is the knote-status read through the NULL-derived klist inside knote_insert; the trap frame and trace pin fw_kqfilter. Equivalent userland trigger: open("/dev/fw0.0") + kqueue() + kevent(EVFILT_READ, EV_ADD).

Reachability: any user who can open /dev/fwN.M β€” mode 0660 root:operator (fwdev.c:173-175 pattern). The node itself requires a FireWire controller (absent in QEMU β€” evidence class DF-5302/DF-5321); the production fw_kqfilter code executed and panicked.

Fix validated (fix.diff: if (ir == NULL) { a_result = ENODEV; return 0; }): "df5415: ... fw_kqfilter returned 19" (ENODEV), guest alive through the remaining tests (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: kernel panic in fw_kqfilter (guest down). Patched (fix.diff on /usr/src, module rebuilt, same trigger): fw_kqfilter returns 19 (ENODEV), no panic, guest alive through the rest of the battery.

fix_run.log ('df5415: ... returned 19'), fix_build.log, fix.diff
↓ fix.diffper-fix-DF-5415

Confirmed kernel references

Detail

Exploit chain

open /dev/fw0.0 (operator) -> kqueue() -> kevent(EVFILT_READ, EV_ADD) -> fw_kqfilter with d->ir==NULL -> knote_insert writes/reads at NULL+offsetof(fw_xferq,rkq)=0x98 -> kernel panic (reliable local DoS)

Evidence (decisive lines)

panic.txt (panic signature + trace with fw_kqfilter+0x8e), fix_run.log ('df5415: ... returned 19' on the patched module, guest alive), fix.diff, build.log, fix_build.log

PoC changes

Same shared harness (df5413.c test 5): fake cdev with fw_drv1{ir=NULL} and a real knote (EVFILT_READ); production fw_kqfilter does the rest.

Verified recommended fix

fw_kqfilter: if (ir == NULL) { ap->a_result = ENODEV; return 0; } (and hook EVFILT_WRITE to d->it as modern FreeBSD does) - as in fix.diff.

Verdict

Reproduced on the QEMU guest by calling the production fw_kqfilter with d->ir == NULL (the state after open(2) before any FW_SRSTREAM): fwdev.c:739-740 builds klist = &ir->rkq.ki_note at NULL+0x98 and knote_insert() dereferences it, panicking the kernel ('assertion kn->kn_status & KN_PROCESSING failed in knote_insert', trace pins fw_kqfilter+0x8e). The guest went down (ssh dead, vm.sh status down). Local DoS for any user able to open /dev/fwN.M (0660 root:operator). The same hole exists for EVFILT_WRITE when only the transmit stream is configured, because fwdev.c:731-732 hooks ir instead of it. Fix validated: NULL check returns ENODEV (fw_kqfilter returned 19), no panic, guest alive.