Unbounded loop in num_compl_pkts causes kernel panic from malformed Number Of Completed Packets event
Summary
num_compl_pkts loops ep->num_con_handles times attacker-controlled u_int8_t reading 4 bytes per iteration via m_copydata with no check mbuf contains enough data. Malformed Number_Of_Completed_Packets event with num_con_handles larger than payload drives m_copydata past mbuf chain panicking kernel. Event can be emitted by controller at any time not only during active connections. Malicious USB BT dongle or compromised firmware. [VERIFIED pass2: reproduced deterministically - malformed event with num_con_handles 255 vs 1 pair panics in m_copydata KASSERT length greater than size of mbuf chain with num_compl_pkts in the backtrace, both via the extracted function and the REAL ng_hci_process_event dispatch; control bit-identical; escalation closed with facts - walk faults at m_next NULL never reads adjacent heap, values feed clamped counters only; root-configured netgraph7 stack needed, stock kernels ship none; payload-length guard fix-validated; sibling inquiry_result same class noted for follow-up]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2539 Β· 23 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.6 KB | β raw | |
| VERDICT.md | β | 8.0 KB | β raw | |
| trigger/ng_hci_poc.c | β | 9.1 KB | view raw | |
| trigger/gen_snippet.sh | β | 2.0 KB | view raw | |
| trigger/hcipoc_guest.patch | β | 667 B | view raw | |
| build.sh | β | 456 B | view raw | |
| run.sh | β | 271 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| build.log | β | 5.6 MB | β download | |
| gen_snippet.log | β | 313 B | view raw | |
| run.control.log | β | 920 B | view raw | |
| run.real-control.log | β | 628 B | view raw | |
| run.malformed.log | β | 711 B | view raw | |
| run.real-malformed.log | β | 568 B | view raw | |
| fix.real-control.log | β | 407 B | view raw | |
| fix.real-malformed.log | β | 494 B | view raw | |
| panic.txt | β | 662 B | view raw | |
| panic.real-malformed.txt | β | 535 B | view raw | |
| panic.malformed.raw.txt | β | 2.6 KB | view raw | |
| dmesg.txt | β | 1.3 KB | view raw | |
| env.txt | β | 515 B | view raw | |
| manifest.json | β | 3.5 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-2539 β ng_hci num_compl_pkts unvalidated num_con_handles loop (kernel panic)
sys/netgraph7/bluetooth/hci/ng_hci_evnt.c lines 876β932 β num_compl_pkts()
loops ep->num_con_handles (controller-controlled u_int8_t) times, consuming
4 bytes per iteration with m_copydata()/m_adj(), and never checks that the
event mbuf chain actually holds 4 * num_con_handles bytes. A malformed
Number Of Completed Packets HCI event walks the loop past the end of the mbuf
chain and panics the kernel inside m_copydata()
(sys/kern/uipc_mbuf.c:1671).
Verification rig (verbatim interposition, kernel-compiled)
Stock kernels ship NO netgraph (checked on the guest: no /dev/ng*, no
NETGRAPH options in sys/config/X86_64_GENERIC), and runtime kldload
GPFs on this guest, so the harness is compiled INTO a custom kernel:
options NETGRAPH7,options NETGRAPH7_BLUETOOTH,options NETGRAPH7_BLUETOOTH_HCIβ the REAL netgraph7 ng_hci stack is linked in (ng_hci_evnt.c,ng_hci_misc.c, ... all pristine).device hcipoc+sys/netgraph7/bluetooth/hci/ng_hci_poc.cβ a harness whose function under test is extracted byte-for-byte from the pristineng_hci_evnt.cbytrigger/gen_snippet.sh(lines 876β932, anchors + content assertions + sha256, recorded inhcipoc_hash.h, printed at boot and readable via sysctl). Because the real function isstatic, the verbatim copy coexists; every symbol it calls (ng_hci_con_by_handle,ng_hci_send_data) resolves to the REAL compiled-in code. Fabricatedstruct ng_node/ng_hci_unit_t/ng_hci_unit_con_tstate uses the REAL types.
Triggers (sysctl, root, mirroring the controller-initiated event):
| mode | what runs | expectation |
|---|---|---|
control |
verbatim num_compl_pkts, num_con_handles=2, 2 pairs |
PASS: rc=0, con.pending 5β3, buffer.acl_free 1β3, invalid-handle alert for 0x999 |
real-control |
REAL ng_hci_process_event, well-formed event |
identical PASS (dispatch path proven) |
malformed |
verbatim copy, num_con_handles=255, 1 pair | PANIC m_copydata: length > size of mbuf chain |
real-malformed |
REAL ng_hci_process_event, num_con_handles=255, 1 pair |
PANIC same |
Reproduce
# host side (repo root): guest must be UP
cd findings/poc/DF-2539
scp -F dfbsd-qemu/config trigger/ng_hci_poc.c trigger/gen_snippet.sh dfbsd:/tmp/
dfbsd-qemu/vm.sh run_root 'cp /tmp/ng_hci_poc.c /tmp/gen_snippet.sh /usr/src/sys/netgraph7/bluetooth/hci/ && sh /usr/src/sys/netgraph7/bluetooth/hci/gen_snippet.sh'
# wire conf/files + KERNCONF (see build.sh) then:
dfbsd-qemu/vm.sh run_root 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_HCIPOC' # buildkernel is blocked by bk_tools on this snapshot
dfbsd-qemu/vm.sh run_root 'cd /usr/src && make installkernel KERNCONF=X86_64_HCIPOC DESTKERNNAME=kernel'
dfbsd-qemu/vm.sh run_root 'shutdown -r now'
# after reboot:
./run.sh control && ./run.sh real-control # no panic
./run.sh malformed # panics; see panic.txt
# reboot, then:
dfbsd-qemu/vm.sh run_root 'sysctl kern.hcipoc=real-malformed' # panics; panic.real-malformed.txt
Fix validation: apply fix.diff in the guest /usr/src (never on the host
sys/), rebuild + install + reboot, then re-run
real-control (must PASS bit-identically) and real-malformed
(must be dropped, no panic). See fix.real-control.log /
fix.real-malformed.log.
Expected decisive output
Baseline (vulnerable) β malformed and real-malformed:
panic: m_copydata: length > size of mbuf chain Trace ... num_compl_pkts.constprop.3() at num_compl_pkts.constprop.3+0xac
(panic.txt, panic.real-malformed.txt; serial console, INVARIANTS kernel.)
Fixed kernel β real-malformed:
num_compl_pkts: hcipoc0 - invalid Number Of Completed Packets event, num_con_handles=255, payload=4 bytes
guest stays up; con.pending/acl_free untouched (event rejected before any
state change). real-control still PASS with identical values.
Files
trigger/ng_hci_poc.cβ harness (as-run, frozen; sha256 see verdict.json)trigger/gen_snippet.shβ verbatim extraction + attestationtrigger/hcipoc_guest.patchβ documented form of the conf/files wiringfix.diffβ the validated fix (git-apply-able,-p1from/usr/src)build.sh,run.shβ as-run driversbuild.logβ full untrimmed kernel build log (recaptured post-fix withng_hci_evnt.c+ng_hci_poc.cforced to recompile; the original baseline-build log was lost with/tmp(tmpfs) across the panic reboot β the panic and test logs below are from the serial console, not tmpfs)run.control.log,run.real-control.log,run.malformed.log,run.real-malformed.log,fix.real-control.log,fix.real-malformed.logpanic.txt,panic.real-malformed.txtβ panic signatures (serial console)dmesg.txt,env.txt,gen_snippet.logVERDICT.md,manifest.json,verdict.json
Caveats
- The harness prints
RETURNED WITHOUT PANIC -- FAILfor the malformed modes. That verdict string is BASELINE-relative (no panic = the bug did not reproduce). On the FIXED kernel, "returned, no panic" plus the newinvalid Number Of Completed Packets eventalert is exactly the desired outcome β the fix validation logs must be read that way. malformed/real-malformedpanic the guest; the DDB-stuck or dump-then-reboot-wedged guest needsvm.sh reset(snapshothcipoc-baselinewas created mid-run and is removed at the end).
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):
- The function under test,
num_compl_pkts()(ng_hci_evnt.c:876β932), is extracted byte-for-byte from the pristine source bytrigger/gen_snippet.sh(anchor + content assertions, sha256 attested at boot and via sysctl): *ng_hci_evnt.csha25614d26fb7c9610763fef6ea78383119a882217a1f04336a4d79618fc1af9ae2a4* verbatim 57-line extraction sha256ed5ab16ea4b248af10ad4cbdbf420a02acf37d53509f9a35d739296111dfafde* gen_snippet provedng_hci_evnt.c:891isfor (; ep->num_con_handles > 0; ep->num_con_handles --) { - Everything the verbatim copy calls is the REAL compiled-in netgraph7
ng_hci code (
ng_hci_con_by_handle,ng_hci_send_datafrom the linked pristine objects). Fabricated state uses the REALstruct ng_node/ng_hci_unit_t/ng_hci_unit_con_ttypes. - 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.pending5β3,buffer.acl_free1β3, expectedinvalid 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 -= papplied,NG_HCI_BUFF_ACL_FREEapplied, unknown handle ignored.real-control(REALng_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): PANICpanic: 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(REALng_hci_process_eventβ REAL staticnum_compl_pkts, event[04 13 05 FF 34 12 02 00]): PANIC, same signature, crash dump written, reboot path then wedged insend_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 boundarym_pkthdr.len == 4*nstill 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_freeuntouched (event rejected before any state mutation) (fix.real-malformed.log). The harness lineRETURNED WITHOUT PANIC -- FAILis 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_copydataonly followsm_next; at chain endm_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 mutatecon->pending(clamped β₯ 0) andbuffer.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_hcidrvhook β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 onhdr->event) βcase NG_HCI_EVENT_NUM_COMPL_PKTS 0x13(ng_hci_evnt.c:171) βnum_compl_pkts()(ng_hci_evnt.c:878). Notehdr->length(ng_hci_evnt.c:97β104) is never validated either β the length field is decorative for this path;num_con_handlesalone 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_HCIPOCINVARIANTS kernel built from the guest's pristine/usr/srcsnapshot (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). buildkernelwas blocked bybk_toolson this snapshot ("use nativekernel");make -j6 nativekernel KERNCONF=X86_64_HCIPOC+make installkernel ... DESTKERNNAME=kernelwas used (build.sh).- The original baseline full-build log was lost with
/tmp(tmpfs) across a panic reboot;build.logis 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.
Fix verification
not_testableConfirmed kernel references
- sys/netgraph7/bluetooth/hci/ng_hci_evnt.c:878
- sys/netgraph7/bluetooth/hci/ng_hci_evnt.c:891
- sys/netgraph7/bluetooth/hci/ng_hci_evnt.c:893
- sys/netgraph7/bluetooth/hci/ng_hci_evnt.c:898
- sys/kern/uipc_mbuf.c:1671
- sys/netgraph7/bluetooth/hci/ng_hci_main.c:691
- sys/netgraph7/bluetooth/hci/ng_hci_main.c:749
- sys/netgraph7/bluetooth/include/ng_hci.h:1571
Detail
Evidence (decisive lines)
panic.txt (verbatim copy panic: m_copydata: length > size of mbuf chain, num_compl_pkts frame in trace); panic.real-malformed.txt (same panic via REAL ng_hci_process_event, dump taken, guest dead); run.control.log + run.real-control.log (PASS: rc=0 pending=3 acl_free=3, invalid connection handle=2457); fix.real-malformed.log (invalid Number Of Completed Packets event, num_con_handles=255, payload=4 bytes - dropped, guest up); fix.real-control.log (bit-identical PASS on fixed kernel); gen_snippet.log (sha256 attestation of the 57-line verbatim extraction, proof ng_hci_evnt.c:891 is the loop head); build.log (full untrimmed build, -Werror clean); dmesg.txt (boot attestation + all test lines)
PoC changes
Authored from scratch (no seed pack): kernel-compiled sysctl harness because runtime kldload GPFs on this guest and stock kernels ship no netgraph; sha256-gated verbatim extraction of ng_hci_evnt.c lines 876-932; fabricated state uses REAL ng_hci types with REAL linked-in ng_hci_con_by_handle/ng_hci_send_data; added real-dispatch modes to prove the full drv-hook path; fix.diff hunk counts regenerated after the first draft failed patch(1) validation
Verified recommended fix
In num_compl_pkts, after m_adj of the event parameters header, drop the event with EINVAL unless m_pkthdr.len >= num_con_handles * 4 (see fix.diff); apply the same guard to inquiry_result.
Verdict
Reproduced on both the verbatim-extracted function and the REAL ng_hci_process_event dispatch path: a Number Of Completed Packets event with num_con_handles=255 but a single (handle,pkt) pair drives the unvalidated loop at ng_hci_evnt.c:891 past the end of the event mbuf chain and panics the kernel in m_copydata (INVARIANTS KASSERT length > size of mbuf chain; NULL deref on non-INVARIANTS). Well-formed control events behave exactly per real struct semantics (pending/buffer counters applied, unknown handle alert). fix.diff validated A/B: well-formed events bit-identical, malformed dropped with EINVAL before any state change, no panic. Impact ceiling is kernel DoS: the chain walk faults at m_next==NULL and never reads adjacent heap as data; the values read only feed clamped counters and log lines - no leak, no write primitive, no uid0 route. Precondition: root-configured netgraph7 ng_hci stack (stock kernel ships no netgraph); attacker is malicious or compromised BT controller firmware / dongle, or root injecting on the drv hook. Medium severity stands.
No comments yet.