# DF-2539 — VERDICT

**status: reproduced — impact: panic (kernel DoS from a malformed controller
event; no escalation primitive). confidence: certain.**

## What was verified and how

The rig is a verbatim-interposition harness compiled INTO a custom DragonFly
kernel (`X86_64_HCIPOC`, identical to stock `X86_64_GENERIC` INVARIANTS build
plus `options NETGRAPH7`, `NETGRAPH7_BLUETOOTH`, `NETGRAPH7_BLUETOOTH_HCI`,
`device hcipoc`):

1. The function under test, `num_compl_pkts()` (ng_hci_evnt.c:876–932), is
   extracted byte-for-byte from the pristine source by
   `trigger/gen_snippet.sh` (anchor + content assertions, sha256 attested at
   boot and via sysctl):
   * `ng_hci_evnt.c` sha256 `14d26fb7c9610763fef6ea78383119a882217a1f04336a4d79618fc1af9ae2a4`
   * verbatim 57-line extraction sha256 `ed5ab16ea4b248af10ad4cbdbf420a02acf37d53509f9a35d739296111dfafde`
   * gen_snippet proved `ng_hci_evnt.c:891` is
     `for (; ep->num_con_handles > 0; ep->num_con_handles --) {`
2. Everything the verbatim copy calls is the REAL compiled-in netgraph7
   ng_hci code (`ng_hci_con_by_handle`, `ng_hci_send_data` from the linked
   pristine objects). Fabricated state uses the REAL
   `struct ng_node` / `ng_hci_unit_t` / `ng_hci_unit_con_t` types.
3. The REAL dispatcher `ng_hci_process_event()` (also compiled in, pristine)
   is driven directly with full HCI event packets — the exact entry the
   driver hook uses.

## Baseline results (vulnerable kernel)

* `control` (verbatim copy, num_con_handles=2, 2 pairs): **PASS** —
  rc=0, `con.pending` 5→3, `buffer.acl_free` 1→3, expected
  `invalid connection handle=2457` (0x999) alert
  (`run.control.log`). Real struct semantics confirmed: handle wire value
  0x1234 → `NG_HCI_CON_HANDLE()` mask → con 0x0234 match, `pending -= p`
  applied, `NG_HCI_BUFF_ACL_FREE` applied, unknown handle ignored.
* `real-control` (REAL `ng_hci_process_event`, well-formed event):
  **PASS, bit-identical values** (`run.real-control.log`).
* `malformed` (verbatim copy, num_con_handles=255 with a single 4-byte
  pair): **PANIC**
  ```
  panic: m_copydata: length > size of mbuf chain
  m_copydata() at m_copydata+0xd0
  num_compl_pkts.constprop.3() at num_compl_pkts+0xac
  hcipoc_sysctl() at hcipoc_sysctl+0x1e0
  sysctl_root ... userland_sysctl ...
  ```
  (`panic.txt`). Guest sat in DDB; vm.sh status ⇒ down.
* `real-malformed` (REAL `ng_hci_process_event` → REAL static
  `num_compl_pkts`, event `[04 13 05 FF 34 12 02 00]`): **PANIC**, same
  signature, crash dump written, reboot path then wedged in `send_ipiq`
  (guest dead either way) (`panic.real-malformed.txt`,
  `run.real-malformed.log`).

Mechanics (why it always panics, traced at sys/kern/uipc_mbuf.c:1671–1695):
`m_adj()` front-trim drives every mbuf's `m_len` to 0 without freeing the
chain; the next `m_copydata(event, 0, 2, &h)` sees `m != NULL` with
`m_len == 0`, computes `count = min(0, 2) = 0`, advances `m = m->m_next ==
NULL`, and the second pass hits
`KASSERT(m != NULL, "length > size of mbuf chain")` — a panic on INVARIANTS
kernels; on non-INVARIANTS kernels the same path dereferences NULL
(`m->m_len`) and takes a fatal page fault. Deterministic on the first
iteration that exceeds the payload.

## Fix validation (fix.diff applied to guest /usr/src, incremental rebuild)

* `real-control`: **PASS, bit-identical** (rc=0, pending=3, acl_free=3,
  invalid-handle alert) — well-formed events unaffected; the boundary
  `m_pkthdr.len == 4*n` still passes (`fix.real-control.log`).
* `real-malformed`: **DROPPED, no panic** —
  `num_compl_pkts: hcipoc0 - invalid Number Of Completed Packets event,
  num_con_handles=255, payload=4 bytes`, handler returned EINVAL, guest
  stayed up, `con.pending`/`acl_free` untouched (event rejected before any
  state mutation) (`fix.real-malformed.log`).
  The harness line `RETURNED WITHOUT PANIC -- FAIL` is baseline-relative
  wording; on the fixed kernel that outcome is the success criterion.

## Impact ceiling — why this is panic/DoS, not privesc

* The primitive is an unbounded mbuf-chain WALK, not a wild read:
  `m_copydata` only follows `m_next`; at chain end `m_next == NULL`, so the
  kernel faults at NULL (or KASSERTs) — it never reads adjacent heap as
  event data.
* The 2-byte values read (`h`, `p`) only mutate `con->pending`
  (clamped ≥ 0) and `buffer.acl_free/sco_free` (clamped ≤ *_pkts) and feed
  log lines. No copyout of kernel memory, no write primitive, no
  info-leak channel of stolen bytes.
* Therefore: reliable, attacker-chosen-timing kernel panic from malformed
  controller input = local DoS. Per the team directive
  ("panic/garbage-read class from hardware — close or flag per facts"):
  **close as panic**.

## Reachability quantification (as demanded)

* Full path, exactly what the harness drives for `real-*`:
  USB dongle / H4 UART / bt3c → netgraph7 driver node → ng_hci `drv` hook →
  `ng_hci_drv_rcvdata()` (ng_hci_main.c:691) → `case NG_HCI_EVENT_PKT`
  (ng_hci_main.c:749) → `ng_hci_process_event()` (ng_hci_evnt.c:87,
  m_adj(3), switch on `hdr->event`) → `case NG_HCI_EVENT_NUM_COMPL_PKTS
  0x13` (ng_hci_evnt.c:171) → `num_compl_pkts()` (ng_hci_evnt.c:878).
  Note `hdr->length` (ng_hci_evnt.c:97–104) is never validated either — the
  length field is decorative for this path; `num_con_handles` alone drives
  the loop.
* Preconditions: the ng_hci stack must be configured by root (kernel
  options or kldload of ng_hci + ng_ubt/ng_h4/bt3c + ngctl wiring). The
  STOCK guest kernel ships no netgraph at all (verified: no /dev/ng*, no
  NETGRAPH in X86_64_GENERIC) — out of the box, DragonFly is not reachable.
* Attacker models that reach the bug: (a) malicious or compromised BT
  controller/dongle firmware — this is a controller-initiated flow-control
  event, emittable at any time with no pairing/auth (the finding's model);
  (b) root injecting on the drv hook (no privilege boundary crossed).
  An unprivileged LOCAL user has no injection path into ng_hci's drv hook:
  the raw-HCI socket (ng_btsocket_hci_raw) taps events upstream but does
  not originate them into ng_hci; l2cap/rfcomm sockets carry data, not HCI
  events.
* Net: Medium severity stands — root-configured niche stack, attacker is
  the hardware (or root), impact = reliable kernel panic.

## Sibling (same class, same file — for the orchestrator, NOT re-filed here)

`inquiry_result()` (ng_hci_evnt.c:373, loop at :387) has the identical
unvalidated-count pattern: `ep->num_responses` × 14 bytes via `m_copydata`
and `*mtod` with no payload check — same `m_copydata` panic from a
malformed Inquiry Result event. Worth a follow-up finding; the same
`m_pkthdr.len` guard style fixes it.

## Environment

* Guest: DragonFly 6.5-DEVELOPMENT x86_64, custom `X86_64_HCIPOC`
  INVARIANTS kernel built from the guest's pristine `/usr/src` snapshot
  (`reset with-src`), gcc 8.3 (gcc80), 6 vCPU, 4 GB RAM, QEMU/KVM.
  Harness kernel: `DragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Oct 19 22:08:10
  UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_HCIPOC x86_64`
  (`env.txt`, `dmesg.txt`).
* `buildkernel` was blocked by `bk_tools` on this snapshot ("use
  nativekernel"); `make -j6 nativekernel KERNCONF=X86_64_HCIPOC` +
  `make installkernel ... DESTKERNNAME=kernel` was used (build.sh).
* The original baseline full-build log was lost with `/tmp` (tmpfs) across
  a panic reboot; `build.log` is a full untrimmed recaptured build (same
  tree, fix applied, ng_hci_evnt.c + ng_hci_poc.c forced to recompile,
  zero warnings/errors under -Werror) proving the exact compile path; the
  panic/test outputs come from the host-side serial log, not tmpfs.

## Guest hygiene

Guest was reset (`vm.sh reset with-src`) and the mid-run
`hcipoc-baseline` snapshot was deleted before returning — next spawn starts
clean.

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