β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3513

NG_BT_ITEMQ_DRAIN uses flag-only NG_FREE_ITEM: every packet queued in an HCI conq at connection destruction permanently leaks ng_item + mbuf + node/hook references (unbounded kernel memory exhaustion)

Summary

ng_hci_acl/sco_rcvdata take a real item reference (ng_ref_item) before enqueueing into con->conq; when the connection is destroyed (discon_compl event, unit_clean, shutdown, timeouts), ng_hci_free_con drains conq via NG_BT_ITEMQ_DESTROY whose NG_FREE_ITEM only sets the NGQF_FREE flag in netgraph7 (no unref, nothing consumes the flag). Each drained packet leaks a struct ng_item (objcache netgraph, pool 4096), its mbuf, and the item's node+hook references - the pinned ng_node keeps ng_nodes elevated forever, feeding the DF-3514 rehash panic. VERIFIED live: 20000 UNPRIVILEGED rounds (uid 65534; ng7 NG_DATA sockets have NO privilege check at attach) exhausted the item objcache ('Warning: objcache(netgraph) exhausted on cpu0!', ENOMEM) and wedged the guest; also triggers in normal operation (every disconnect with flow-controlled queued packets). One-line fix validated (ng_unref_item in the DRAIN macro; the correct pattern already exists at ng_btsocket_hci_raw.c:617): clean 3000+20000-round runs, flat memory.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3513 Β· 11 files
FileTypeDescriptionSize
README.md β€” 3.2 KB ↓ raw
VERDICT.md β€” 4.3 KB ↓ raw
poc_leak.c β€” 14.0 KB view raw
build.sh β€” 110 B view raw
run.sh β€” 139 B view raw
build.log β€” 363 B view raw
run.log β€” 1.2 KB view raw
env.txt β€” 888 B view raw
fix.diff β€” 922 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 2.5 KB view raw

DF-3513 β€” ng_hci conq drain leaks ng_item + mbuf (flag-only NG_FREE_ITEM)

What

NG_BT_ITEMQ_DRAIN() (sys/netgraph7/bluetooth/include/ng_bluetooth.h:202-213) frees drained items with NG_FREE_ITEM(i). In netgraph7 NG_FREE_ITEM is a flag-only macro (sys/netgraph7/netgraph.h:816-820: KKASSERT + set NGQF_FREE; no unref, no free β€” nothing ever consumes the flag). The queue holds a real reference taken by ng_ref_item() at enqueue time (ng_hci_main.c:873 and :993). So every item drained by ng_hci_free_con() β†’ NG_BT_ITEMQ_DESTROY() (ng_hci_misc.c:320) leaks:

  • the struct ng_item (objcache netgraph, pool cap net.graph.maxalloc=4096),
  • its attached mbuf (and cluster if any),
  • the item's node reference (el_dest = the hci node) and hook reference β€” which permanently pins the ng_node, keeping ng_nodes elevated.

The parallel drain code in ng_btsocket_hci_raw.c:617-618 does it correctly: NG_FREE_ITEM(item); ng_unref_item(item, 0); β€” only the shared macro (and ng_hci_free_con through it) was left behind by the netgraph7 port.

Trigger (PoC: poc_leak.c)

Per round (all HCI inputs are attacker data; events enter via the drv hook, ACL data via the acl hook):

  1. Connection_Complete event (status=0, handle=1, ACL) β†’ con_compl() creates an OPEN connection (ng_hci_evnt.c:479-503).
  2. Two ACL packets via the acl hook: #1 is forwarded to the driver (acl_free 1β†’0), #2 stays flow-controlled in con->conq (ng_hci_main.c:859-878).
  3. Disconnection_Complete event (status=0, handle=1) β†’ discon_compl() (ng_hci_evnt.c:664-674) β†’ ng_hci_free_con() β†’ DRAIN β†’ leak.

Repeatable without limit. Setup (root, once): create ng7 hci node "hci0", wire two ng7 data sockets to its drv/acl hooks (models a driver + upper layer installed by the administrator); the process then setuid(65534) and runs the whole attack loop as nobody. On a real Bluetooth system the same events/ACL flow arrive from the remote peer through the radio.

Reproduce

# in guest (root):
cd /usr/src/sys/netgraph7/netgraph && make
cd ../socket && make
cd ../bluetooth/bluetooth && make
cd ../hci && make
kldload .../netgraph.ko .../ng_socket.ko .../ng_bluetooth.ko .../ng_hci.ko
cc -O2 -o poc_leak poc_leak.c
./poc_leak 20000

Success criterion (baseline, unfixed): kernel console shows Warning: objcache(netgraph) exhausted on cpu0!, netgraph allocations fail with ENOMEM (ng_hci_mtap: Could not forward packet, error=12), the run wedges and the guest stops answering. With the one-line fix: the run completes, vmstat -m "netgraph" count is flat, zero exhaustion warnings.

Observed (baseline, 20000 rounds)

Warning: objcache(netgraph) exhausted on cpu0!
ng_hci_mtap: hci0 - Could not forward packet, error=12
... guest wedged (ssh dead), run never completed

Observed (fixed, 3000 + 20000 rounds)

done: 3000 rounds (queued=yes)   EXIT=0
netgraph   Count=7  MemUse=1008   (flat, no growth)
0 objcache-exhaustion warnings, guest healthy

The leaked node references also feed the ng_ID_rehash/ng_name_rehash panic chain (DF-3514): pinned nodes keep ng_nodes/named-node counts above the rehash threshold, so the next node creation runs the (separately broken) rehash and panics the kernel.

VERDICT.md
↓ download raw

DF-3513 β€” VERDICT

Status: reproduced (impact: dos β€” kernel memory exhaustion; with the DF-3514 chain, kernel panic). Fix validated on the guest (module-level).

Root cause (certain, source-proven)

  • sys/netgraph7/bluetooth/include/ng_bluetooth.h:202-213 β€” NG_BT_ITEMQ_DRAIN uses NG_FREE_ITEM(i).
  • sys/netgraph7/netgraph.h:816-820 β€” NG_FREE_ITEM only sets the NGQF_FREE flag (and KKASSERTs it wasn't already set). No reference is dropped; nothing consumes the flag later.
  • Every NG_BT_ITEMQ_ENQUEUE site takes a real reference first: ng_ref_item(item) at ng_hci_main.c:873 (ACL), :993 (SCO), and the btsocket queues (ng_btsocket_hci_raw.c:280/334, ng_btsocket_l2cap*.c). The correct drain pattern exists in the tree: NG_FREE_ITEM(item); ng_unref_item(item, 0); (ng_btsocket_hci_raw.c:617-618).
  • Sole user of the broken macro: ng_hci_free_con() (sys/netgraph7/bluetooth/hci/ng_hci_misc.c:320).
  • ng_hci_free_con is called from my audited files: discon_compl() (ng_hci_evnt.c:674, remote-peer-triggerable), con_compl() (ng_hci_evnt.c:501), and via ng_hci_unit_clean() from ng_hci_disconnect/ng_hci_shutdown (ng_hci_main.c:258/170).

Per leaked round: one struct ng_item + one mbuf (+cluster) + one node reference + one hook reference. The pinned node keeps the hci ng_node alive forever β†’ ng_nodes never returns to baseline β†’ drives DF-3514.

Verification narrative

  1. Built the netgraph7 stack as modules in the guest (core, socket, bluetooth, hci) β€” the stock kernel ships none of them.
  2. Wrote poc_leak.c (this dir): root wires an hci node named hci0 with two ng7 data sockets on drv/acl (+ control socket for graph messages), then setuid(65534); the unprivileged loop sends ConCompl / 2Γ—ACL / DisconCompl events per round. NOTE: the guest's NGM_HCI_NODE_INIT bdaddr gate had to be bypassed in the guest-local module copy (measurement scaffolding only) because the HCI command-complete handshake is wedged by an independent kernel bug (DF-3515: callout_stop() returns 0 for an armed callout, so complete_command() always takes the silent ETIMEDOUT path and the unit can never reach READY). This does not affect the leak path under test.
  3. Baseline run ./poc_leak 20000: kernel console flooded Warning: objcache(netgraph) exhausted on cpu0!, netgraph ENOMEMs (error=12), guest wedged (ssh dead) β€” the run never completed. Evidence: run.log (baseline section) and panic.txt-adjacent serial excerpts in DF-3514's pack.
  4. Fixed run (one-line macro fix in the guest's ng_bluetooth.h): ./poc_leak 3000 β†’ done: 3000 rounds (queued=yes), exit 0, vmstat -m netgraph count flat (7β†’7), zero exhaustion warnings. 20000-round fixed run: guest healthy throughout.

Exploit chain / impact ceiling

Unbounded kernel-memory exhaustion from attacker-controlled HCI traffic (remote Bluetooth peer, or any local user able to write to a data socket wired to the hci node's drv/acl hooks β€” ng7 data sockets carry no privilege check at attach). The leaked node refs additionally push the system into the DF-3514 rehash panic. No path to uid=0 identified (pure leak/DoS primitive; corrupted-state ceiling is the DF-3514 UAF-panic).

Fix (validated)

--- a/sys/netgraph7/bluetooth/include/ng_bluetooth.h
+++ b/sys/netgraph7/bluetooth/include/ng_bluetooth.h
@@ -208,7 +208,8 @@ (inside NG_BT_ITEMQ_DRAIN)
            NG_BT_ITEMQ_DEQUEUE((q), i);    \
            if (i == NULL)          \
                break;          \
-                           \
            NG_FREE_ITEM(i);        \
+           ng_unref_item(i, 0);        \
        }                   \
    } while (0)

plus #include <sys/refcount.h> and #include <netgraph7/netgraph2.h> where the macro is instantiated (ng_hci_misc.c) β€” see fix.diff. Validated: guest module rebuild + identical PoC β†’ zero leak, zero exhaustion, clean exits (3000 and 20000 rounds).

Classification

  • status: reproduced
  • reproduced: 1
  • impact: dos (memory exhaustion; panic via DF-3514 chain)
  • confidence: certain
  • fix_status: fixed

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.

Fix verification

not_testable
↓ fix.diffper-fix-DF-3513

Confirmed kernel references

Detail

Exploit chain

admin wires a bluetooth graph (or a real BT controller is present) -> unprivileged user writes HCI events/ACL data via ng7 data socket (no priv check at attach) -> ConCompl opens con, 2nd ACL packet flow-controls in conq, DisconCompl frees con -> drained item leaks -> repeat -> objcache exhaustion + netgraph-wide ENOMEM; leaked node refs additionally drive the DF-3514 rehash panic

Evidence (decisive lines)

run.log baseline section (objcache exhausted, guest wedged) vs fixed section (done: 3000 rounds, flat netgraph count, 0 warnings); fix.diff validated by guest module rebuild

PoC changes

full ng7 socket ABI written from scratch (no ngctl in guest): MKPEER/NAME/CONNECT wiring, data-socket binding via connect(), token-matched replies; NGM_HCI_NODE_INIT bdaddr gate bypassed in guest-local module copy because DF-3515 wedges the CC handshake (measurement scaffolding only, documented in VERDICT.md)

Verified recommended fix

NG_BT_ITEMQ_DRAIN: add ng_unref_item(i, 0) after NG_FREE_ITEM(i) (+ refcount/netgraph2 includes at the instantiation site)

Verdict

NG_BT_ITEMQ_DRAIN frees queued items with flag-only NG_FREE_ITEM, never dropping the reference taken by ng_ref_item at enqueue; every packet queued in an HCI connection's conq at destruction time permanently leaks the ng_item, its mbuf, and node/hook references. Live-verified: 20000 attack rounds (unprivileged, uid 65534) exhausted the 4096-entry netgraph item objcache ('Warning: objcache(netgraph) exhausted on cpu0!', ENOMEM error=12) and wedged the guest; the one-line ng_unref_item fix yields clean 3000/20000-round runs with flat memory and zero warnings.