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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2539 Β· 23 files
FileTypeDescriptionSize
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 pristine ng_hci_evnt.c by trigger/gen_snippet.sh (lines 876–932, anchors + content assertions + sha256, recorded in hcipoc_hash.h, printed at boot and readable via sysctl). Because the real function is static, the verbatim copy coexists; every symbol it calls (ng_hci_con_by_handle, ng_hci_send_data) resolves to the REAL compiled-in code. Fabricated struct ng_node / ng_hci_unit_t / ng_hci_unit_con_t state 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 + attestation
  • trigger/hcipoc_guest.patch β€” documented form of the conf/files wiring
  • fix.diff β€” the validated fix (git-apply-able, -p1 from /usr/src)
  • build.sh, run.sh β€” as-run drivers
  • build.log β€” full untrimmed kernel build log (recaptured post-fix with ng_hci_evnt.c + ng_hci_poc.c forced 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.log
  • panic.txt, panic.real-malformed.txt β€” panic signatures (serial console)
  • dmesg.txt, env.txt, gen_snippet.log
  • VERDICT.md, manifest.json, verdict.json

Caveats

  • The harness prints RETURNED WITHOUT PANIC -- FAIL for 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 new invalid Number Of Completed Packets event alert is exactly the desired outcome β€” the fix validation logs must be read that way.
  • malformed/real-malformed panic the guest; the DDB-stuck or dump-then-reboot-wedged guest needs vm.sh reset (snapshot hcipoc-baseline was created mid-run and is removed at the end).
VERDICT.md
↓ download raw

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.

Fix verification

not_testable
↓ fix.diffper-fix-DF-2539

Confirmed kernel references

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.