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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3790 Β· 7 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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

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_testable
baseline no→ patch + rebuild →patched clean

Runtime 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)
↓ fix.diffper-fix-DF-3790

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.