bktr_filter_detach() calls knote_insert() instead of knote_remove() β destroyed knote left on (or self-looped into) the VBI klist β KNOTE() from the frame interrupt dereferences freed memory / hard hang (root cause bktr_os.c, sink bktr_core.c:705)
Summary
bktr_kqfilter inserts the EVFILT_READ knote into bktr->vbi_kq.ki_note; on knote destruction the framework calls f_detach, which must REMOVE it - but bktr_filter_detach re-INSERTS it (every other DF driver calls knote_remove). SLIST_INSERT_HEAD of an already-listed element corrupts the list (self-loop when head); knote_drop then frees the knote, leaving a dangling entry that the VBI even-field interrupt walks via KNOTE(&bktr->vbi_kq.ki_note, 0) - UAF f_event call on cached/reused knote memory, or an infinite walk holding the pool token. Source-certain (runtime needs the device, absent on the guest). Unprivileged UAF/lockup from interrupt context: keep /dev/vbi0 open, delete the knote, run PAL capture. One-line inversion fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3790 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_knote_detach.c | β | 1.9 KB | view raw | |
| env.txt | β | 412 B | view raw | |
| poc_build_and_device_check.log | β | 1 B | view raw | |
| fix.diff | β | 236 B | view raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| manifest.json | β | 991 B | view raw | |
| verdict.json | β | 2.1 KB | view raw |
DF-3790 β verdict
INCONCLUSIVE | reproduced=0 | impact=none | confidence=certain
INCONCLUSIVE at runtime (needs the bktr device + VBI interrupts; absent on this guest). Source-certain: the inverted call is unambiguous and the knote lifecycle (kern_event.c:1764-1774, 2084-2097 knote_free cache) confirms the dangling entry and interrupt-context consumer.
Root cause (citations)
bktr_kqfilter() (sys/dev/video/bktr/bktr_os.c:718-719) inserts the knote into bktr->vbi_kq.ki_note. When the knote is destroyed (EV_DELETE, kqueue close, fd close), knote_detach_and_drop() (sys/kern/kern_event.c:1764-1774) calls f_detach = bktr_filter_detach(), which must REMOVE the knote - but it calls knote_INSERT() again (bktr_os.c:724-732; every other DF driver's f_detach calls knote_remove()). SLIST_INSERT_HEAD of an element already on the list corrupts the list (self-loop when it is head). knote_drop() then frees the knote, leaving a dangling (possibly self-referential) entry on vbi_kq.ki_note. The VBI even-field interrupt path KNOTE(&bktr->vbi_kq.ki_note, 0) at bktr_core.c:705 then walks the corrupted list from interrupt context: deref of freed knote (knote cache memory, reused for unrelated knotes -> f_event on wrong filterops/hook) and/or infinite self-loop walk holding the pool token -> hard hang. Root cause is in bktr_os.c (pass-1 marked that file clean and missed it); the consuming KNOTE() is in this file. Unprivileged: /dev/vbi0 is 0444.
Guest reality (why no runtime kernel trigger)
The audit guest is DragonFly 6.5-DEVELOPMENT x86_64 (X86_64_GENERIC). The bktr
driver is NOT compiled into the GENERIC kernel (sys/conf/files: optional
bktr pci), there is no /boot/modules tree, no /dev/bktr, /dev/tuner,
/dev/vbi* nodes, no Brooktree PCI device (QEMU has no Bt848/Bt878 device
model; pciconf -lv shows no vendor 109e). Therefore the kernel code path is
unreachable ON THIS GUEST. Verification is source-level: faithful harnesses of
the exact kernel arithmetic (the same methodology as the accepted DF-1216
evidence pack), run on the DragonFly guest with its native cc, plus fix.diff
compile-validation against the guest's /usr/src. All PoC triggers compile
cleanly against the guest's kernel headers (poc_build_and_device_check.log).
Exploit chain
open /dev/vbi0 (0444) -> kqueue EVFILT_READ ADD on it -> EV_DELETE the note (f_detach re-inserts; knote freed + cached) -> keep /dev/vbi0 open, start PAL continuous capture -> even-field interrupt KNOTE() walks the dangling/self-looped entry -> UAF call or infinite loop with pool token held.
Decisive evidence
poc_knote_detach.c (compiles on guest vs /usr/src headers; /dev/vbi0 absent). VERDICT.md cites the lifecycle chain.
Fix validation
fix.diff (in this directory) applies cleanly against the pristine sys/ tree
(git apply --check) and compiles as part of the bktr KLD module on the
guest's /usr/src with -Werror together with the seven other pass-2 bktr fixes
(fix_build.log, MAKE_RC=0, bktr.ko + bktr_mem.ko produced). Runtime
patched-behavior validation is not possible on this guest (no device), hence
fix_status=not_testable (compile-validated + logic-reviewed).
Kernel references
- sys/dev/video/bktr/bktr_os.c:724
- sys/dev/video/bktr/bktr_os.c:731
- sys/dev/video/bktr/bktr_os.c:718
- sys/dev/video/bktr/bktr_core.c:705
- sys/kern/kern_event.c:1764
- sys/kern/kern_event.c:1902
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
not_testableRuntime fix validation impossible on this guest (no Bt848/878 hardware, driver not in GENERIC, no module dir). fix.diff applies cleanly to sys/ and the patched bktr KLD module compiles with -Werror on the guest's /usr/src (fix_build.log).
fix.diff; fix_build.log (all-8-fixes module build, MAKE_RC=0)
Confirmed kernel references
Detail
Exploit chain
open /dev/vbi0 (0444) -> kqueue EVFILT_READ ADD on it -> EV_DELETE the note (f_detach re-inserts; knote freed + cached) -> keep /dev/vbi0 open, start PAL continuous capture -> even-field interrupt KNOTE() walks the dangling/self-looped entry -> UAF call or infinite loop with pool token held.
Evidence (decisive lines)
poc_knote_detach.c (compiles on guest vs /usr/src headers; /dev/vbi0 absent). VERDICT.md cites the lifecycle chain.
PoC changes
none
Verified recommended fix
knote_remove() in bktr_filter_detach (fix.diff)
Verdict
INCONCLUSIVE at runtime (needs the bktr device + VBI interrupts; absent on this guest). Source-certain: the inverted call is unambiguous and the knote lifecycle (kern_event.c:1764-1774, 2084-2097 knote_free cache) confirms the dangling entry and interrupt-context consumer.
No comments yet.