ng_l2cap_lp_receive trusts the ACL payload-length claim: zero-byte fragments inflate m_pkthdr.len up to 65539 over ~8 real bytes -> lying ACL header on TX -> driver heap over-read disclosed over the air (DF-0508 sink, distinct llpi reassembly entry)
Summary
The ACL header length field (u_int16_t, attacker-controlled) is never checked against the bytes actually present: m_cat does not update m_pkthdr.len, and :702 adds the CLAIM. A truthful START fragment plus zero-payload FRAGMENTs whose claims sum to the L2CAP length complete a 'packet' with m_pkthdr.len = L+4 over ~8 real bytes; the consistency check passes by construction; the echo path re-uses the lying mbuf and lp_send writes the lie into the outgoing ACL header (:548). VERIFIED on the DF-3524-fixed kernel, stock ng7 module ('lie' mode): START(8 real/claim 8/L2CAP 65523) + FRAGMENT(0 real/claim 65519) -> ECHO_RSP with ACL header length=65527 over 9 real bytes = 65519 phantom bytes exactly as constructed; module fix.diff (claim-vs-present check) suppresses the lie while truthful echo round-trips. Unauth radio-range BT peer or local unpriv on an l2cap:hci-wired data socket: driver TX reads 'length' bytes off the chain (DF-0508's verified usbd_copy_in sink) -> up to ~65KB adjacent kernel heap per request. Dormant on stock only because DF-3524 wedges L2CAP first (fix-un-gates-next-bug).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3525 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| l2cap_con.c | β | 12.6 KB | view raw | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 90 B | view raw | |
| run.log | β | 2.3 KB | view raw | |
| build.log | β | 59 B | view raw | |
| fix.diff | β | 920 B | view raw | |
| fix_run.log | β | 449 B | view raw | |
| fix_build.log | β | 125 B | view raw | |
| env.txt | β | 406 B | view raw | |
| VERDICT.md | β | 3.3 KB | β raw |
DF-3525 VERDICT β reproduced (on the DF-3524-un-gated kernel); fix validated
Status: REPRODUCED. Impact: leak β kernel-heap over-read at the
Bluetooth driver TX boundary (DF-0508's verified ng_ubt
usbd_m_copy_in sink): an unauthenticated radio-range peer (or any
local user able to write to a data socket wired to an l2cap node's hci
hook β no privilege check) can make the stack transmit up to ~65 KB of
adjacent kernel heap per request, repeatable. Fix validated at module
level.
Root cause (certain)
ng_l2cap_lp_receive (ng_l2cap_llpi.c:600-732) never validates the ACL
header's length field (u_int16_t, fully attacker-controlled) against
the bytes actually present in the mbuf:
:690 con->rx_pkt_len = le16toh(l2cap_hdr->length) + sizeof(hdr); :701 m_cat(con->rx_pkt, m); / DF m_cat does NOT touch m_pkthdr.len / :702 con->rx_pkt->m_pkthdr.len += length; / the CLAIM is added / :711 con->rx_pkt_len -= length;
Construction (l2cap_con.c, mode lie): START fragment carrying 8 real
bytes (L2CAP hdr length=65523 + cmd hdr) with a truthful claim of 8;
then ONE FRAGMENT carrying ZERO payload bytes claiming 65519. Claims
sum to L+4 exactly -> rx_pkt_len == 0 -> "complete" packet with
m_pkthdr.len = 65527 and 8 real bytes. ng_l2cap_receive's
consistency check (hdr->length == m_pkthdr.len - 4) passes by
construction. ng_l2cap_process_echo_req (evnt.c:969-1003) re-uses
the lying mbuf and ng_l2cap_lp_send (:510,:548) writes the lie into
the outgoing ACL header.
In-guest observable (no radio): the ECHO_RSP ACL packet received on the
forged hci hook carries ACL length = 65527 with 9 real payload bytes
(= 65519 phantom bytes, exactly the constructed lie). On real hardware
the driver TX reads the phantom bytes off the end of the 12-byte chain
-> heap disclosure over the air (DF-0508 sink; DF-0508 itself proved
this sink for a sibling pkthdr.len lie in the config-echo path).
The data path (ng_l2cap_l2ca_receive, ulpi.c:882-957) forwards lying
mbufs upstream with no actual-length check either (same primitive,
different sink).
Gating (documented)
On the STOCK kernel no con can reach CON_OPEN (DF-3524 wedge), so this entry is dormant until DF-3515/DF-3524 are fixed. The demo therefore ran on kernel #1 (kern_timeout fix) with the STOCK ng_l2cap module. This is the standard "fix un-gates the next bug" pattern; both findings ship together.
Fix validation (module level)
if (length > m->m_pkthdr.len) { error = EMSGSIZE; goto drop; } after
the length field is parsed (fix.diff). Rebuilt ng_l2cap.ko in-guest:
./l2cap_con lie -> ECHO_RSP NOT RECEIVED (lie suppressed)
./l2cap_con con -> ECHO_RSP received, exit 0 (truthful path intact)
Kernel log confirms the truthful flow (length=12 == actual).
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
fixedBaseline (un-gated kernel, stock module): lie propagated (ACL 65527 vs 9 bytes). Patched module: lie suppressed (ECHO_RSP not received), truthful echo still round-trips (17 bytes, exit 0).
fix_run.log, run.log
Confirmed kernel references
Detail
Exploit chain
remote BT peer (or local unpriv user on a data socket wired to l2cap:hci): truthful START (L2CAP len=65523, 8 bytes) + FRAGMENT claiming 65519 with zero payload -> completed 65527-byte 'packet' over 8 real bytes -> ECHO_REQ echoes it -> outgoing ACL header claims 65527 -> driver TX OOB read past the 12-byte chain -> kernel heap bytes transmitted over the air to the attacker (DF-0508 sink).
Evidence (decisive lines)
run.log (lie mode: 'outgoing ACL header length field = 65527, real payload bytes 9, LIE PROPAGATED'; dmesg shows the 65519-claim fragment accepted; control truthful echo works), fix_run.log (lie suppressed after fix.diff)
PoC changes
cmd length must equal L-4 (pkthdr.len after the two 4-byte m_adj's); NODE_UP sets pkt_size=65535 so lp_send emits a single ACL packet carrying the whole lie.
Verified recommended fix
ng_l2cap_llpi.c: reject the fragment when length > m->m_pkthdr.len after the ACL header is stripped (fix.diff).
Verdict
ng_l2cap_lp_receive() never validates the attacker-controlled ACL payload-length field against the bytes actually present in the mbuf: con->rx_pkt->m_pkthdr.len += length (llpi.c:702) adds the CLAIM (DF m_cat does not touch pkthdr.len), so a truthful START fragment plus zero-byte FRAGMENTs whose claims sum to the L2CAP length complete a 'packet' with m_pkthdr.len up to 65539 over ~8 real bytes; ng_l2cap_receive's consistency check passes by construction and the echo path propagates the lie into the outgoing ACL header (observed in-guest: ACL length field 65527 with 9 real bytes = 65519 phantom bytes, exactly the constructed lie). On real Bluetooth hardware the driver TX reads length bytes off the chain (DF-0508's verified ng_ubt usbd_m_copy_in sink) -> up to ~65KB of adjacent kernel heap disclosed to an unauthenticated radio-range peer per request, repeatable; the data path (ulpi.c:882) forwards lying mbufs upstream unvalidated as well. Gated on the stock kernel by DF-3524 (no con can reach CON_OPEN) - reproduced on the DF-3524-fixed kernel with the STOCK ng_l2cap module. Module-level fix validated: claim check suppresses the lie, truthful traffic unaffected.
No comments yet.