# 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

```c
// 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.
