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

bktr_os: bktr_filter_detach calls knote_insert instead of knote_remove -> UAF via corrupted klist

Summary

bktr_filter_detach at 724-732 calls knote_insert(klist,kn) instead of knote_remove(klist,kn). Every other _filter_detach in tree uses knote_remove (cyapa,cxm,atmel_mxt,ipmi,drm_file,psm). knote_insert does SLIST_INSERT_HEAD no dup check; subsequent knote_drop frees knote but leaves bktr->vbi_kq.ki_note SLIST head pointing at freed knote (knote_drop only removes kn_link/kn_kqlink never kn_next). Next KNOTE(&vbi_kq.ki_note,0) from VBI interrupt (bktr_core.c:705) walks freed memory; also creates self-referential cycle kn->kn_next==kn -> infinite SLIST_FOREACH loop. /dev/vbi0 mode 0444 any local user can open O_RDONLY + kqfilter. Requires Bt848/Bt878 PCI card. Ring0 escalation with slab grooming: kn->kn_fop->f_event controlled function pointer. Fix: knote_remove(klist,kn).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1805 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness reproducing the buggy arithmetic/control-flow 2.6 KB view raw
VERDICT.md verdict full verification narrative 2.5 KB ↓ raw
build.sh build-script exact build command 88 B view raw
run.sh run-script exact run invocation 41 B view raw
harness_run.log run-log harness output on guest 320 B view raw
fix.diff suggested-fix git-apply-able unified diff 292 B view raw
env.txt environment guest uname, cc version, kernel config 768 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full verification narrative
↓ download raw

DF-1805 β€” Verification Verdict

Verdict: REPRODUCED (source-confirmed β€” typo is self-evident)

The bug is a plain typo at sys/dev/video/bktr/bktr_os.c:731: bktr_filter_detach calls knote_insert instead of knote_remove. Every other *_filter_detach in the tree uses knote_remove.

Mechanism

// sys/dev/video/bktr/bktr_os.c:724-732
static void
bktr_filter_detach(struct knote *kn)
{
    bktr_ptr_t bktr = (bktr_ptr_t)kn->kn_hook;
    struct klist *klist;
    klist = &bktr->vbi_kq.ki_note;
    knote_insert(klist, kn);    // <-- BUG: should be knote_remove
}

knote_insert does SLIST_INSERT_HEAD with no duplicate check. On detach this creates a self-referential cycle (kn->kn_next == kn) and re-adds the knote to the head. When knote_drop later frees the knote (it removes kn_link/kn_kqlink but never the driver's SLIST), the vbi_kq.ki_note head is left dangling at the freed knote. The next KNOTE(&vbi_kq.ki_note, 0) from a VBI interrupt (bktr_core.c:705) walks freed memory and/or loops forever on the self-cycle. A slab-groom attacker who reclaims the freed knote controls kn->kn_fop->f_event, which the kqueue subsystem dereferences as a function pointer.

Harness evidence

after attach: head=0x8004902c0 kn=0x8004902c0 alive=1
after buggy detach (knote_insert): head=0x8004902c0 kn=0x8004902c0 kn->kn_next=0x8004902c0 (self-cycle=1)
after knote_drop: head=0x8004902c0 -> freed kn (alive=0) = DANGLING
after FIXED detach+drop: head=0x0 (clean)

Why no live trigger on this guest

bktr_filter_detach is registered as the kqfilter detach hook for /dev/vbi0, which requires a Bt848/Bt878 PCI video capture card. The audit guest has no such card; bktr.ko is present but not loaded. Valid Phase-6 hard blocker.

Exploit chain

Not applicable (bktr-HW-gated). No uid=0 claim. On real HW: any local user who can open /dev/vbi0 (mode 0444) and register a kqfilter can trigger the dangling-head UAF; with slab grooming of the freed knote it becomes a function-pointer hijack β†’ kernel code execution.

PoC changes

  • Added harness.c: SLIST model showing the self-cycle and dangling head.
  • Added fix.diff: knote_insert β†’ knote_remove (one-line fix).

Fix

fix.diff changes line 731 from knote_insert(klist, kn) to knote_remove(klist, kn), matching every other filter_detach in the tree.

  • BEFORE: harness shows self-cycle + dangling head after detach+drop.
  • AFTER: harness shows clean head (NULL) after detach+drop.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED at compile+boot level: all 13 fixes applied cleanly to /usr/src, built into a single X86_64_GENERIC kernel (make -j6 nativekernel rc=0, kernel linked), installed as /boot/kernel/kernel, and the patched kernel booted cleanly (kern.version #1 vs baseline #0). The live PoC cannot run on this guest (HW/config-gated per the verdict), so before/after is at source+harness level: baseline harness: 'head -> freed kn (alive=0) = DANGLING' + self-cycle | patched harness: 'head=0x0 (clean)'

baseline (#0 unpatched): baseline harness: 'head -> freed kn (alive=0) = DANGLING' + self-cycle
patched (#1 kernel, all 13 fixes, booted clean): patched harness: 'head=0x0 (clean)'
kernel sha256 c3fff85f... (patched, booted) vs 5dc83dac... (baseline #0)
↓ fix.diffDragonFly 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Mon Jul 20 19:12:20 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

HW-gated (bktr PCI video capture card absent on guest). No uid=0 escalation claimed. Primitive characterized in harness.c (SLIST model showing self-cycle + dangling head). Live ceiling on real bktr HW: any local user opening /dev/vbi0 (mode 0444) + kqfilter triggers dangling-head UAF; slab-groom of freed knote -> function-pointer hijack via kn_fops->f_event.

Evidence (decisive lines)

after attach: head=0x8004902c0 kn=0x8004902c0 alive=1
after buggy detach (knote_insert): head=0x8004902c0 kn=0x8004902c0 kn->kn_next=0x8004902c0 (self-cycle=1)
after knote_drop: head=0x8004902c0 -> freed kn (alive=0) = DANGLING
after FIXED detach+drop: head=0x0 (clean)

PoC changes

Added harness.c (SLIST model) and fix.diff (knote_insert -> knote_remove, one-line fix).

Verified recommended fix

fix.diff changes bktr_os.c:731 from knote_insert(klist,kn) to knote_remove(klist,kn). matches finding proposal exactly.

Verdict

REPRODUCED (typo is self-evident). bktr_filter_detach at bktr_os.c:731 calls knote_INSERT instead of knote_REMOVE. Every other *_filter_detach in tree uses knote_remove. SLIST harness shows this creates a self-cycle (kn_next==kn) and, after knote_drop frees the knote, leaves vbi_kq.ki_note head dangling at freed memory. HW-gated (Bt848/Bt878 PCI card absent).