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

Missing arglen check in L2CA_Ping path causes kernel heap OOB read leaked to remote Bluetooth peer

Field Value
ID DF-0625
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
CWE CWE-125 Out-of-bounds Read
File sys/netgraph7/bluetooth/l2cap/ng_l2cap_main.c (dispatch); root cause in ng_l2cap_ulpi.c
Lines 438-439 (dispatch); ulpi.c:1298-1367 (handler)
Area netgraph7/bluetooth (L2CAP echo/ping)
Confidence likely
Discovered 2026-07-02
Reported pending

Summary

ng_l2cap_upper_rcvmsg (main.c:439) dispatches NGM_L2CAP_L2CA_PING to ng_l2cap_l2ca_ping_req (ulpi.c:1298), which validates arglen >= sizeof(ng_l2cap_l2ca_ping_ip) and caps echo_size <= NG_L2CAP_MAX_ECHO_SIZE, but never checks that arglen actually contains sizeof(ip)+echo_size bytes. The function then passes msg->data+sizeof(ip) and ip->echo_size to _ng_l2cap_echo_req, which calls m_copyback to copy echo_size bytes from that pointer. If echo_size exceeds the actual trailing data in the ng_mesg allocation, bcopy reads adjacent kernel heap and the result is transmitted as an L2CAP Echo Request to the remote Bluetooth peer β€” a kernel heap info leak to an unauthenticated remote endpoint.

Root cause

ng_l2cap_l2ca_ping_req (ulpi.c:1298) performs two independent checks:

  • ulpi.c:1306: if (msg->header.arglen < sizeof(*ip)) β€” ensures the 8-byte ng_l2cap_l2ca_ping_ip header fits.
  • ulpi.c:1316: if (ip->echo_size > NG_L2CAP_MAX_ECHO_SIZE) β€” caps echo_size at 65531.

But it never validates arglen >= sizeof(*ip) + ip->echo_size. At ulpi.c:1357-1358:

_ng_l2cap_echo_req(cmd->aux, cmd->ident, msg->data + sizeof(*ip), ip->echo_size);

The macro (ng_l2cap_cmds.h:338-340) calls m_copyback((_m), sizeof(*c), (_size), (_data)) where _data = msg->data+8 and _size = ip->echo_size. m_copyback β†’ _m_copyback2 (uipc_mbuf.c:2361-2386) performs bcopy(cp, mtod(m,...)+off, mlen) reading up to min(echo_size, M_TRAILINGSPACE(m)) β‰ˆ 248 bytes from the source pointer. Since msg was allocated as sizeof(struct ng_mesg)+arglen (ng_message.h:81), and arglen can be as small as sizeof(*ip)=8, the source pointer msg->data+8 may point past the allocation entirely. Every byte read beyond arglen-8 is out-of-bounds kernel heap.

The resulting mbuf is then queued via ng_l2cap_link_cmd+ng_l2cap_lp_deliver (ulpi.c:1366-1367) and ultimately transmitted as L2CAP signaling data on the air.

Threat model & preconditions

  • Attacker position: local user who can send netgraph control messages to the l2cap node's l2c or ctl hook (typically requiring root or bluetooth-group membership, e.g. via l2ping or BTPROTO_L2CAP socket).
  • Trigger: craft a PING message with arglen=8 (just the ip struct, no echo data) and echo_size=N (e.g. 200). The kernel reads N bytes of adjacent heap and sends them as L2CAP Echo Request payload to whatever bdaddr the attacker specified in ip->bdaddr.
  • The attacker controls the remote Bluetooth device and captures the echo payload, recovering kernel heap contents. The remote peer need not be paired or authenticated β€” L2CAP signaling (CID 0x0001) operates before any security-mode authentication.
  • Impact: local-to-remote kernel heap info leak (C:H, scope change S:C). Leaked bytes may contain freed ng_mesg objects, credential structures, or other sensitive data recycled from prior kernel allocations.

Add an exact-match arglen validation immediately after the echo_size cap check, so that echo_size must match the actual trailing data in the message:

--- a/sys/netgraph7/bluetooth/l2cap/ng_l2cap_ulpi.c
+++ b/sys/netgraph7/bluetooth/l2cap/ng_l2cap_ulpi.c
@@ -1322,6 +1322,14 @@ ng_l2cap_l2ca_ping_req(ng_l2cap_p l2cap, struct ng_mesg *msg)
        goto out;
    }

+   /*
+    * Verify that the message actually contains the claimed echo data.
+    * Without this check, m_copyback in _ng_l2cap_echo_req would read
+    * past the end of msg->data into adjacent kernel heap.
+    */
+   if (msg->header.arglen != sizeof(*ip) + ip->echo_size) {
+       NG_L2CAP_ALERT(
+"%s: %s - L2CA_Ping echo_size does not match message size, "
+"arglen=%d, expected=%zd\n",
+           __func__, NG_NODE_NAME(l2cap->node),
+           msg->header.arglen, sizeof(*ip) + ip->echo_size);
+       error = EMSGSIZE;
+       goto out;
+   }
+
    /* Check if we have connection to the unit */
    con = ng_l2cap_con_by_addr(l2cap, &ip->bdaddr);

This mirrors the exact-match pattern already used by ng_l2cap_l2ca_get_info_req (ulpi.c:1435: if (msg->header.arglen != sizeof(*ip))).

References

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0625 Β· 9 files
FileTypeDescriptionSize
args_overflow.c trigger-source documentation stub: trigger would require ng_l2cap + ng_hci + BT peer 3.9 KB view raw
build.sh build-script compiles the C stub 115 B view raw
run.sh run-script runs the stub 47 B view raw
run.log run-log stub output 971 B view raw
env.txt environment uname, kern.version 209 B view raw
fix.diff suggested-fix add exact-match arglen != sizeof(*ip)+echo_size check in ping handler 1.1 KB view raw
VERDICT.md verdict full narrative 4.5 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full narrative
↓ download raw

DF-0625 β€” VERDICT

Verdict: NOT REPRODUCED at runtime (no BT hardware). Bug CONFIRMED in source.

Mechanism (the bug IS real in source)

ng_l2cap_l2ca_ping_req (sys/netgraph7/bluetooth/l2cap/ng_l2cap_ulpi.c:1298) validates only:

1306:   if (msg->header.arglen < sizeof(*ip)) {     /* 8-byte header fits */
1316:   if (ip->echo_size > NG_L2CAP_MAX_ECHO_SIZE) { /* echo_size <= 65531 */

and then, without checking that arglen actually contains sizeof(*ip) + echo_size bytes, passes them downstream:

1357:   _ng_l2cap_echo_req(cmd->aux, cmd->ident,
1358:       msg->data + sizeof(*ip), ip->echo_size);

The macro (ng_l2cap_cmds.h:338-340) calls m_copyback((_m), sizeof(*c), (_size), (_data)) with _size = ip->echo_size and _data = msg->data + 8. The ng_mesg was allocated as sizeof(struct ng_mesg) + arglen (ng_message.h:81), so when arglen = 8 (just the header) and echo_size = 200, the read at msg->data + 8 runs ~192 bytes past the allocation into adjacent kernel heap (bcopy in _m_copyback2, uipc_mbuf.c:2361-2386). The resulting mbuf is then queued via ng_l2cap_link_cmd + ng_l2cap_lp_deliver (ulpi.c:1366-1367) and ultimately transmitted as L2CAP Echo Request payload on the air.

The sibling ng_l2cap_l2ca_get_info_req (ulpi.c:1428+) does do an exact-match check:

1439:   if (msg->header.arglen != sizeof(*ip)) {

so the fix is to mirror that pattern in the ping handler.

Why it CANNOT be triggered on this guest

The bug path at ulpi.c:1357 requires a valid BT connection (con != NULL) at :1325 or a successful ng_l2cap_lp_con_req at :1328. The latter fails fast without an HCI lower layer:

llpi.c:87:  if (l2cap->hci == NULL || NG_HOOK_NOT_VALID(l2cap->hci)) {
llpi.c:92:      return (ENOTCONN);

On the audit guest: - No Bluetooth hardware (no USB BT dongle, no virtual BT adapter). - ng_hci / ng_ubt not loaded; no HCI hook to connect to an l2cap node. - Therefore l2cap->hci == NULL, ng_l2cap_lp_con_req returns ENOTCONN, ng_l2cap_l2ca_ping_req takes the early goto out at :1333, and the buggy _ng_l2cap_echo_req call at :1357 is never reached.

Additionally, the pre-installed /boot/kernel/netgraph.ko in the audit snapshot is ABI-incompatible with a freshly-built ng_l2cap.ko (NG_ABI_VERSION mismatch surfaced as KLD ng_l2cap.ko: depends on netgraph - not available or version mismatch), so even loading the module to confirm the path requires rebuilding netgraph.ko too. The bug path is still code-confirmed.

Classification: bug CONFIRMED in source, runtime trigger requires Bluetooth hardware/HCI emulation not present on this guest. Valid hard blocker: no harness can exercise the bug path without BT hardware or an HCI lower-layer emulation, neither of which is available on this kernel.

Realistic impact ceiling

If the bug WERE reachable (any deployment that actually uses DragonFly's Bluetooth L2CAP stack β€” typically embedded systems or routers with USB BT dongles):

  • A local user with netgraph control access (root, or in a bluetooth group if configured) sends a crafted NGM_L2CAP_L2CA_PING with arglen=8, echo_size up to 65531, and bdaddr pointing at a Bluetooth peer the attacker controls.
  • The kernel reads up to echo_size bytes of adjacent kernel heap and transmits them as the Echo Request payload.
  • The attacker captures the packet at their BT peer and recovers kernel heap contents β€” freed ng_mesg objects, slab metadata, possibly credential structures recycled from prior allocations.

Severity Medium with C:H/S:C is appropriate IF BT hardware is present; on this guest, no demonstrated runtime impact.

fix.diff adds the missing exact-match arglen validation in ng_l2cap_l2ca_ping_req, mirroring the pattern already used by the sibling ng_l2cap_l2ca_get_info_req:

if (msg->header.arglen != sizeof(*ip) + ip->echo_size) {
    NG_L2CAP_ALERT(...);
    error = EMSGSIZE;
    goto out;
}

Placed immediately after the existing echo_size cap check, so the function returns EMSGSIZE for any PING whose claimed echo data does not match the actual trailing bytes.

This matches the finding markdown's proposal (same check, same placement, same error code).

Caveats

The "no BT hardware" hard blocker applies to runtime demonstration. The fix is small, surgical, and mirrors a proven pattern in a sibling function β€” low regression risk. Recommended for merge regardless of the runtime reproducibility status.

Fix verification

not_testable

n/a

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. L2CA_Ping missing arglen>=sizeof(ip)+echo_size check. No BT HW, ng_l2cap can't load.