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)
PoC verification
Evidence pack
findings/poc/DF-3513 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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(objcachenetgraph, pool capnet.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 theng_node, keepingng_nodeselevated.
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):
Connection_Completeevent (status=0, handle=1, ACL) βcon_compl()creates an OPEN connection (ng_hci_evnt.c:479-503).- Two ACL packets via the acl hook: #1 is forwarded to the driver
(
acl_free1β0), #2 stays flow-controlled incon->conq(ng_hci_main.c:859-878). Disconnection_Completeevent (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.
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_DRAINusesNG_FREE_ITEM(i).sys/netgraph7/netgraph.h:816-820βNG_FREE_ITEMonly sets theNGQF_FREEflag (and KKASSERTs it wasn't already set). No reference is dropped; nothing consumes the flag later.- Every
NG_BT_ITEMQ_ENQUEUEsite takes a real reference first:ng_ref_item(item)atng_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_conis called from my audited files:discon_compl()(ng_hci_evnt.c:674, remote-peer-triggerable),con_compl()(ng_hci_evnt.c:501), and viang_hci_unit_clean()fromng_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
- Built the netgraph7 stack as modules in the guest (core, socket, bluetooth, hci) β the stock kernel ships none of them.
- Wrote
poc_leak.c(this dir): root wires anhcinode namedhci0with two ng7 data sockets ondrv/acl(+ control socket for graph messages), thensetuid(65534); the unprivileged loop sends ConCompl / 2ΓACL / DisconCompl events per round. NOTE: the guest'sNGM_HCI_NODE_INITbdaddr 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, socomplete_command()always takes the silent ETIMEDOUT path and the unit can never reach READY). This does not affect the leak path under test. - Baseline run
./poc_leak 20000: kernel console floodedWarning: objcache(netgraph) exhausted on cpu0!, netgraph ENOMEMs (error=12), guest wedged (ssh dead) β the run never completed. Evidence:run.log(baseline section) andpanic.txt-adjacent serial excerpts in DF-3514's pack. - Fixed run (one-line macro fix in the guest's ng_bluetooth.h):
./poc_leak 3000βdone: 3000 rounds (queued=yes), exit 0,vmstat -mnetgraph 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_testableConfirmed 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.
No comments yet.