l2cap_ctloutput PRCO_GETOPT success path leaks one mbuf per getsockopt call β unprivileged permanent mbuf-pool exhaustion DoS (l2cap sibling of DF-0709; not the DF-0682 panic path)
Summary
The mbuf is freed only on the m_len==0 error path: for any valid option every successful getsockopt leaks exactly one mbuf - measured 7->10007 per 10000 calls on two guest boots (deterministic +1/call); pool max 146632 = permanent kernel-wide ENOBUFS after ~146k calls by ANY local user once netbt.ko is loaded (no controller, no privilege needed - SOCK_SEQPACKET/BTPROTO_L2CAP sockets are free). free-unconditionally fix validated (flat at 7 across 10k, value still returned).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5757 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| bt_getopt_leak.c | trigger-source | L2CAP/RFCOMM getsockopt mbuf-leak trigger (arg 0 = l2cap) | 1.9 KB | view raw |
| build.sh | build-script | cc -o bt_getopt_leak bt_getopt_leak.c | 65 B | view raw |
| run.sh | run-script | netstat -m before/after + 10000 l2cap getsockopt calls | 89 B | view raw |
| build.log | build-log | clean compile, rc=0 | 83 B | view raw |
| run.log | run-log | fresh-guest decisive leak measurement: 7 -> 10007 | 217 B | view raw |
| run.2.log | run-log | first-guest confirmation: 7 -> 10007 | 229 B | view raw |
| fix.diff | suggested-fix | free GETOPT mbuf unconditionally; copyout only on success | 478 B | view raw |
| fix_build.log | build-log | netbt.ko single-fix module build (rc=0, -Werror) | 18.5 KB | view raw |
| fix_run.log | run-log | patched-module run: 7 -> 7 (no leak), correct opt value | 479 B | view raw |
| env.txt | environment | uname, kern.version, cc, module shas, netstat baseline | 656 B | view raw |
| README.md | readme | build/run/expected | 1.8 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, fix validation | 4.7 KB | β raw |
DF-5757 PoC: l2cap_ctloutput PRCO_GETOPT success-path mbuf leak
Bug
sys/netbt/l2cap_socket.c:119-132: in l2cap_ctloutput(), the
PRCO_GETOPT case allocates an mbuf with m_get(M_NOWAIT, MT_DATA)
(:120), fills it via l2cap_getopt() (:125), and copies the result to
userspace with soopt_from_kbuf() (:131) β but only the m->m_len == 0
error path (:126-130) ever frees the mbuf. On every successful
getsockopt(2) the mbuf is leaked: exactly 1 mbuf per call.
For a valid option (SO_L2CAP_IMTU, SO_L2CAP_OMTU, SO_L2CAP_FLUSH,
SO_L2CAP_LM, SO_L2CAP_IQOS/OQOS) l2cap_getopt()
(sys/netbt/l2cap_upper.c:521-555) returns a non-zero length, so the
success path is taken on every call.
This is the l2cap sibling of DF-0709 (sco_socket.c, same defect shape).
The pass-1 finding DF-0682 covers only the m == NULL argument-evaluation
panic on the error path of this same block; the success-path leak was
not filed.
Preconditions
- Bluetooth stack loaded:
kldload netbt.ko(admin action; netbt isoptional bluetooth, not in X86_64_GENERIC β sys/conf/files:1613). - Then ANY local unprivileged user. No Bluetooth controller needed: the
socket and its pcb exist right after
socket(2);l2cap_ctloutputonly needsso->so_pcb != NULL(:108).
Build
cc -o bt_getopt_leak bt_getopt_leak.c
Run
./bt_getopt_leak 10000 0 # l2cap only netstat -m | head -1 # before/after comparison (see run.sh)
Expected
mbufs in use climbs by exactly 1 per successful call
(7 -> 10007 after 10000 calls on the stock netbt.ko). Pool max is
146632 mbufs on this guest, so ~146k calls exhaust the kernel mbuf pool
and every network allocation in the system fails (permanent, the leaked
mbufs are never freed). On the patched module (fix.diff) the count stays
flat and getsockopt still returns correct data (mtu=672, len=2).
DF-5757 β l2cap_ctloutput PRCO_GETOPT success-path mbuf leak
Verdict: REPRODUCED + FIX VALIDATED (fixed)
The bug is real and the leak is exactly 1 mbuf per successful
getsockopt(BTPROTO_L2CAP, SO_L2CAP_IMTU), deterministic, unprivileged,
no Bluetooth hardware required. The authored fix.diff closes it
completely (module-level fix validation: netbt.ko rebuilt with only this
fix, leak count goes flat, getsockopt still returns correct data).
Mechanism (trigger β primitive β effect)
l2cap_ctloutput() is the L2CAP socket control-output handler
(sys/netbt/l2cap_socket.c:95-144). PRCO_GETOPT case, lines 119-132:
case PRCO_GETOPT:
m = m_get(M_NOWAIT, MT_DATA); /* :120 allocate 1 mbuf */
if (m == NULL) { error = ENOMEM; break; }
m->m_len = l2cap_getopt(pcb, sopt->sopt_name, mtod(m, void *)); /* :125 */
if (m->m_len == 0) { /* :126 error path */
m_freem(m); /* :127 only free here */
m = NULL;
error = ENOPROTOOPT;
}
soopt_from_kbuf(sopt, mtod(m, void *), m->m_len); /* :131 copyout */
break; /* :132 m NEVER freed */
For SO_L2CAP_IMTU, l2cap_getopt() (sys/netbt/l2cap_upper.c:526-528)
writes chan->lc_imtu and returns sizeof(uint16_t) = 2 (non-zero), so
the m->m_len == 0 block is skipped every call. soopt_from_kbuf()
copies the 2-byte value out to userspace (it does not take ownership of
the buffer), and break exits with the mbuf allocated at :120 still
live and unreferenced by anything β a permanent 1-mbuf leak per call.
m_get charges the mbuf to the global mbuf objcache
(sys/kern/uipc_mbuf.c:1009-1044); nothing ever frees it, so
netstat -m "mbufs in use" grows monotonically by 1 per call. With the
guest's pool max of 146632 mbufs, ~146k getsockopt calls exhaust the
pool; from that point every m_get(M_NOWAIT) in the kernel fails
(ENOBUFS) until reboot β a permanent, unprivileged, system-wide
networking/memory DoS. (Not driven to exhaustion in this run to keep the
guest usable; the linear leak to pool-max is deterministic arithmetic.
The sco sibling DF-0709 did drive it to exhaustion and killed networking.)
Reachability / threat model
- netbt is
optional bluetooth(sys/conf/files:1613) β a KLD module, not in X86_64_GENERIC. Precondition: an adminkldload netbt.koto enable Bluetooth support (normal admin action; DF-0709 established the same precondition). Once loaded, AF_BLUETOOTH/BTPROTO_L2CAP sockets are creatable by ANY local user (socket(2)needs no privilege; verified as uid 1001maxx, not in wheel). - No controller hardware needed:
l2cap_ctloutputonly requiresso->so_pcb != NULL(:108), whichl2cap_sattachsets at socket creation (sys/netbt/l2cap_socket.c:295-317). - Impact ceiling: local kernel-memory-exhaustion DoS. Pure resource leak, no corruption primitive β no escalation chain (Phase V rule).
Reproduction (unpatched netbt.ko, sha of stock module = guest /boot/kernel/netbt.ko)
Two independent runs (first boot, and fresh vm.sh reset with-src guest):
=== DF-5757 l2cap (fresh guest) === BEFORE: 7/146632 mbufs in use (current/max): l2cap: iterations=10000 ok=10000 fail=0 last_val=672 optlen=2 AFTER 10k l2cap: 10007/146632 mbufs in use (current/max):
Delta = +10000 mbufs / 10000 calls, last_val=672 = L2CAP_MTU_DEFAULT
returned correctly. (run.log, run.2.log; the first-guest capture in
run.2.log shows the same 7 -> 10007.)
Fix validation (fix.diff applied to /usr/src/sys/netbt, netbt.ko rebuilt)
=== PATCHED MODULE: DF-5757 l2cap === BEFORE: 7/146632 mbufs in use (current/max): l2cap: iterations=10000 ok=10000 fail=0 last_val=672 optlen=2 AFTER 10k l2cap: 7/146632 mbufs in use (current/max):
Flat (7 β 7), correct value still returned. Build: cd /usr/src/sys/netbt
&& make rc=0 under -Werror (fix_build.log), patched netbt.ko sha256
f7d2c5ad3f5a90eaea99df15faf8a58c92b886ddc0622e4c7b71abe7fd0e1d6e.
Note: the same fix hunk also removes the m = NULL +
soopt_from_kbuf(sopt, mtod(m, ...), m->m_len) NULL-deref panic on the
error path (pass-1 DF-0682); that behavior is known and was not
re-verified separately here.
Rig notes
bt_getopt_leak.c was written fresh for this run (no seed existed); the
only fix-up vs the first compile attempt was none β it built and ran
first try. attempts=2 (two full guest runs).
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
fixedSingle-fix netbt.ko (sha256 f7d2c5ad3f5a90eaea99df15faf8a58c92b886ddc0622e4c7b71abe7fd0e1d6e, -Werror clean) loaded on a fresh vm.sh-reset guest: identical 10000-call workload leaves mbufs flat at 7/146632 and getsockopt still returns mtu=672, optlen=2. Baseline on stock module leaked +10000. The same hunk also removes the DF-0682 error-path NULL-deref (not separately re-verified).
fix_run.log (7 -> 7 patched vs 7 -> 10007 baseline), fix_build.log (rc=0 -Werror module build), fix.diff (git-apply-able unified diff)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: fresh-guest 7 -> 10007 mbufs per 10k l2cap getsockopt; run.2.log: first-guest 7 -> 10007; fix_run.log: patched module 7 -> 7 across 10k calls with correct value returned; fix_build.log: netbt.ko rebuilt rc=0 under -Werror; VERDICT.md mechanism section cites l2cap_socket.c:119-132 and l2cap_upper.c:526-528.
PoC changes
no seed existed; trigger written fresh (bt_getopt_leak.c, arg 0=l2cap), built and ran first try
Verified recommended fix
In l2cap_ctloutput PRCO_GETOPT, call soopt_from_kbuf() only when m->m_len != 0 and m_freem(m) the mbuf unconditionally before break (see fix.diff).
Verdict
l2cap_ctloutput PRCO_GETOPT leaks exactly one mbuf per successful getsockopt(BTPROTO_L2CAP, SO_L2CAP_IMTU): the m_get(M_NOWAIT) mbuf at l2cap_socket.c:120 is freed only on the m_len==0 error path (:127); the success path copies out via soopt_from_kbuf (:131, which does not take ownership) and breaks (:132) without freeing. Unprivileged, no BT controller needed once netbt.ko is loaded (admin action). Measured 7 -> 10007 mbufs in use per 10000 calls on the stock module, twice on two guest boots; flat (7 -> 7) on the fix.diff-patched netbt.ko with getsockopt still returning mtu=672/len=2. Pool max 146632 -> permanent kernel-mbuf-pool exhaustion / system-wide networking DoS after ~146k calls.
No comments yet.