# DF-5780 VERDICT — reproduced (kernel memory corruption → panic), unprivileged trigger

## Bottom line

The unsynchronized `hci_memos` lifecycle in sys/netbt/hci_misc.c is a real,
guest-verified kernel memory-corruption bug reachable in its decisive step by
an **unprivileged local user**. Two independent fresh-boot runs of the same
PoC produced two different slab-allocator panics, both caused by the memo
race; a third run with the fix applied produced zero panics under 6x heavier
load. Impact on this kernel: reliable local DoS (panic) with an underlying
double-free / write-after-free primitive whose silent exploitation is blocked
by the kernel's allocator assertions (details below).

## Root cause (path:line)

* `hci_memo_find()` walks and **mutates** `unit->hci_memos` with no lock:
  sys/netbt/hci_misc.c:120-134 (expiry free at :124-128).
* `hci_memo_new()` allocates/inserts and timestamps with no lock:
  hci_misc.c:149-172 (UAF-write candidate `microtime(&memo->time)` at :171).
* `hci_memo_free()` unlinks+frees with no lock: hci_misc.c:175-181.
* Concurrent contexts (verified on the guest):
  - unprivileged user: L2CAP `connect()` → `l2cap_sconnect` (l2cap_socket.c:351)
    → `l2cap_connect` (l2cap_upper.c:140) → `hci_route_lookup` (hci_misc.c:55)
    → `hci_memo_find`. No privilege gate anywhere on this path (the only
    `caps_priv_check_self` calls in netbt are hci_socket.c:644 and
    hci_ioctl.c:224-287 — not on this path).
  - netisr-0: `hci_input_event` → `hci_intr` — which **releases hci_devlock
    before** calling `hci_event` (hci_unit.c:340→349) — → inquiry/RSSI
    handlers (hci_event.c:463,500) → `hci_memo_new` → `hci_memo_find`.
  - softclock: `hci_link_free` → `hci_memo_new` (hci_link.c:1016) — observed
    live in panic run 1's backtrace (slotimer/softclock frames).
* The three loops of `hci_route_lookup` (hci_misc.c:65/82/95) and the
  `hci_unit_list` mutations `hci_attach`/`hci_detach` (hci_unit.c:113-115,
  123-127, crit_enter only) are additionally racy — that wider hole is filed
  separately as DF-5781 (Low).

## Evidence

Baseline (unfixed), run 1 — `panic.txt`:
`panic: assertion "(((intptr_t)chunk ^ (intptr_t)z) & ZoneMask) == 0" failed
in chunk_mark_free at /usr/src/sys/kern/kern_slaballoc.c:1675`, trace includes
`chunk_mark_free ← kfree ← hci_memo_*` with softclock/timer frames; CPU0
panicked, 5 CPUs stopped.

Baseline (unfixed), run 2 after `vm.sh reset with-src` (clean state) —
`panic_run2.txt` / `boot_run2_panic.log` last lines:

    hci_memo_find: memo 0xfffff801188e77e8 too old (expiring)
    panic: assertion "kuhp < 0" failed ... /usr/src/sys/kern/kern_slaballoc.c

The usage-counter assertion (`kuhp < 0` in kfree) fires **only** when a chunk
is freed more times than it was allocated — i.e., the double free happened in
`hci_memo_find`'s expiry path on the very print line above the panic.

Trigger: `su -m maxx -c "/tmp/race 6 30"` (uid 1001) during the burst window;
time-to-panic ≈ 5–35 s per run.

Primitive characterization:
* object: `struct hci_memo`, sizeof == 48 (x86_64), zone class 41–48 bytes,
  **global per-size-class zone** shared by every 41–48-byte kmalloc
  kernel-wide (kern_slaballoc.c:638 `zoneindex()` — no type parameter).
* double `kfree()` of the same 48-byte chunk (silent freelist dup on kernels
  without allocator asserts; `hcir_check_dup` detects duplicate live
  addresses).
* write-after-free: 16-byte `microtime` write at offset 0 of a freed chunk
  (hci_misc.c:171) — clobbers the slab freelist link (`c_Next` at offset 0).
* list self-link: an aliased block double-`LIST_INSERT_HEAD`ed yields
  `memo->next == memo` — observed as multi-second `hci_memo_find` stalls
  (watchdog `stuck=1/5` in baseline run 2) before the fatal double free.

## Why not uid=0 (hard blocker, documented)

Primary objective push: the primitive is a zone-restricted double-free plus a
semi-controlled (wall-clock valued) 16-byte write-after-free at a fixed offset
of a freed 48-byte chunk. On this guest kernel (and any DragonFly kernel built
with the stock slab allocator), `kfree()` carries live assertions —
`chunk_mark_free` zone/bitmap checks (kern_slaballoc.c:1675-1681) and the
`kmemusage` counter check — that convert **every** duplicate free into an
immediate panic before the freelist dup can be allocated through. That closes
the classic dup-allocation → victim-aliasing → pointer-control route. The
remaining route (UAF-write at :171 turning a freed chunk's `c_Next` into a
timestamp-valued fake chunk pointer) offers no attacker-chosen value
(tv_sec/tv_usec are wall clock), no address control, and no leak dependency
relief sufficient to forge a credible victim object; a reliable uid=0 chain
was therefore not achievable within this verification window and is documented
as blocked by allocator self-checks on the target kernel. (No KASLR/SMAP/SMEP
on this guest — those are NOT the blockers; the allocator asserts are.)

## Fix validation

`fix.diff` (hci_misc.c + hci.h + hci_unit.c): serialize memo ops on the
unit's existing `hci_devlock` (locked/unlocked variants; `hci_memo_free`
gains a `unit` argument; `hci_memo_new` performs find+alloc+insert+timestamp
under one lock — also closing the :171 UAF-write for the found path).
`hci_intr` drops devlock before `hci_event` (hci_unit.c:340) so no recursion;
`hci_disable` purge updated.

* Baseline: 2/2 runs panic (both slab asserts above).
* Patched (`patch -p1 < fix.diff` in guest /usr/src, module rebuilt from the
  fixed production sources): 6/6 burst cycles + one 12.4M-connect debug-off
  stress with live watchdog: **0 panics, dup=0, stuck=0, guest up**
  (`fix_run.log` last lines). The `WALK STUCK` entries recorded during
  `bluetooth_debug=1` windows are print-bound devlock hold times (~5000
  expiry prints × ~4ms ≈ the measured 19.9 s), not corruption; with debug off
  (production default): `watchdog totals: dup=0 stuck=0`.

Residual (documented, not reproduced): callers that write fields of the memo
returned by `hci_memo_new`/`hci_memo_find` after the lock is dropped
(hci_event.c:466-470, hci_link.c:101-103, hci_link.c:1017-1018) still have a
narrow window against a concurrent expiry free at end-of-life; the fix note
recommends moving those writes under `hci_devlock` in a follow-up.

## 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.
