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)
PoC verification
Evidence pack
findings/poc/DF-1805 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
fixedVALIDATED 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)
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- b
- k
- t
- r
- /
- b
- k
- t
- r
- _
- o
- s
- .
- c
- :
- 7
- 3
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- v
- i
- d
- e
- o
- /
- b
- k
- t
- r
- /
- b
- k
- t
- r
- _
- o
- s
- .
- c
- :
- 7
- 1
- 9
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).
No comments yet.