# DF-5763 — VERDICT

**Status: reproduced (mechanism; production-config deadness proven by
control). Severity Info — the trigger precondition does not exist in
the shipped kernel. Fix validated.**

## What the finding claims

`sys/netbt/bt_proto.c:128-134` defines `netbt_dispose()`, which calls
`zdestroy()` on the three **global** netbt vm_zones
(`l2cap_pdu_pool`, `l2cap_req_pool`, `rfcomm_credit_pool`) and ignores
its `struct mbuf *` argument. `sys/netbt/bt_proto.c:163` wires it as
`btdomain.dom_dispose`.

In DragonFly, `dom_dispose` is **not** a domain-teardown hook. Both of
its only call sites — `sys/kern/uipc_socket.c:1609-1611` (receive
cleanup) and `sys/kern/uipc_socket.c:1986-1987` (`sorflush`) — invoke it
**per socket flush**, gated on `pr->pr_flags & PR_RIGHTS`, passing the
flushed receive-buffer mbuf chain. The unix domain uses it to release
externalized descriptors (`unp_dispose`). netbt's `btsw[]` protosw
entries set only `PR_ADDR|PR_ATOMIC` / `PR_CONNREQUIRED|PR_ATOMIC` /
`PR_CONNREQUIRED|PR_WANTRCVD` (bt_proto.c:73, 87, 102, 116) — **none
sets `PR_RIGHTS`** — so production never calls `netbt_dispose`. The
module's real teardown protection is `netbt_modevent` returning `EBUSY`
on `MOD_UNLOAD` (bt_proto.c:48-49); the pools are created exactly once
by `dom_init` (`netbt_init`, SI_SUB_PROTO_DOMAIN via `DOMAIN_SET`,
uipc_domain.c:86-87).

## Why it is a finding anyway (the booby trap)

If `netbt_dispose` ever ran it would, on the **first** flush of any
bluetooth socket, destroy three vm_zones that are still in global use;
`zdestroy()` frees the zone structure itself (`sys/vm/vm_zone.c:543`)
and releases the backing slabs (`:534-538`). The **second** flush of
*any* bt socket (same or different socket: `sorflush` runs on
`shutdown(SHUT_RD)` and on socket close) calls `zdestroy()` on the
freed zone — use-after-free read *and* write (`LIST_REMOVE` at
vm_zone.c:527, `kfree` again at :543, double-free). Any subsequent
`zalloc` from a destroyed pool is a UAF dereference of freed memory.
This is not hypothetical hardening polish: the distance from armed to
firing is exactly one flag bit on any protosw of the domain — the kind
of bit a "add SCM_RIGHTS to raw HCI sockets" refactor or a copy-paste
from `unp_protosw` would set, at which point **every** socket close
becomes a mass-UAF of the global bluetooth state.

## Evidence (see README for file map)

* **Verbatim production code under test:** `build.sh` copies
  `/usr/src/sys/netbt/bt_proto.c` into the rig; md5
  `e68d6f9f555dc28d0d4ff6d9b26fe4ed` equals the audit tree's
  `sys/netbt/bt_proto.c` (`setup.log`). `nm` shows `netbt_dispose` at
  module offset `0x200`; at runtime `hw.btdisp.info` printed
  `dom_dispose=0xffffffff82600200` with the module linked at
  `0xffffffff82600000` — the domain hook **is** production
  `netbt_dispose`.
* **Control (production flags):** `run.control.log` — kldload
  registers the production domain (`netbt_init` zinit's the pools:
  `pdu=0xfffff80118580000 req=0xfffff80118585000
  credit=0xfffff8011858a000`), 8 × socket+shutdown(SHUT_RD) rounds, all
  succeed, pools bit-identical before/after, guest healthy. Production
  config never fires `dom_dispose`. (This is the deadness proof.)
* **Armed (one bit set):** `sysctl hw.btdisp.arm=1` sets
  `btsw[0].pr_flags |= PR_RIGHTS` (`0x00000003 -> 0x00000013`, console
  kprintf). The next trigger round ran production
  `sorflush -> dom_dispose -> netbt_dispose -> zdestroy` and the guest
  took a deterministic page fault — **3/3 runs across 2 clean boots**
  (`panic.txt`, `panic.2.txt`, `run.log`):
  `Fatal trap 12 ... supervisor read data, page not present ... Stopped
  at zdestroy+0x19: testb $0x40,0x4068(%rdi)` — the `zflags` test on a
  destroyed zone whose slab was already released (`fault virtual
  address = pdu_pool + 0x4068`). `current process = Idle` = the netisr
  protocol port thread where `soshutdown` netmsgs execute
  (uipc_socket.c:259 `so->so_port = netisr_cpuport(0)`).
* **Fix validation:** `fix.diff` (one line: `.dom_dispose = NULL`,
  plus a contract comment) applied to the guest's `/usr/src`
  (`patch` "Hunk #1 succeeded at 160", md5 `78082bfb…`), rig rebuilt
  (`fix_build.log`, `setup.log` md5s match patched source),
  `hw.btdisp.info` now shows `dom_dispose=0`; control clean **and** the
  armed phase (PR_RIGHTS set) completes all 4 rounds with pools
  untouched and the guest healthy (`fix_run.log`).
  baseline panic 3/3 → patched 0/1 → **fixed**.

## Classification reasoning

* Not reachable in the shipped kernel (control-proven) → not
  High/Medium; no attacker-controlled input path reaches it.
* Demonstrated catastrophic behavior at one bit of distance → more
  than style; it is exactly the "hardening opportunity /
  defense-in-depth" tier: **Info**, confidence **certain**.
* Impact recorded as `panic` refers to the armed-precondition
  demonstration (the code's actual behavior when its documented gate
  passes), not to any production exposure.

## Fix

`fix.diff`: `.dom_dispose = NULL` on `btdomain` (keep `netbt_dispose`
itself — `netbt_init`'s boot-fail path at bt_proto.c:154 still calls
it). If domain teardown is ever wanted, it belongs in `MOD_UNLOAD`
handling (after draining users), not in the per-flush rights hook.

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