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)
PoC verification
Evidence pack
findings/poc/DF-5415 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
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
fixedBaseline: 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
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.
No comments yet.