rfcomm_ctloutput PRCO_GETOPT success path leaks one mbuf per getsockopt call β unprivileged permanent mbuf-pool exhaustion DoS (rfcomm sibling of DF-0709/5757; not the DF-0680 panic path)
Summary
Identical defect: the mbuf is freed only when getopt returns 0, so SO_RFCOMM_MTU/LM/FC_INFO getsockopts each leak one - measured linear 10007->20007->40007 across guest boots; flat at 7 across 60k calls on the patched module. Same controller-free unpriv reach. same-shape fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5758 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| bt_getopt_leak.c | trigger-source | L2CAP/RFCOMM getsockopt mbuf-leak trigger (arg 1 = rfcomm) | 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 rfcomm 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: 20007 -> 40007 (linear) | 272 B | view raw |
| run.2.log | run-log | first-guest confirmation: 10007 -> 20007 -> 40007 | 362 B | view raw |
| fix.diff | suggested-fix | GETOPT hunk: free mbuf unconditionally; copyout only on success | 494 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 across 60k calls, 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.7 KB | β raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, fix validation | 4.1 KB | β raw |
DF-5758 PoC: rfcomm_ctloutput PRCO_GETOPT success-path mbuf leak
Bug
sys/netbt/rfcomm_socket.c:116-127: in rfcomm_ctloutput(), the
PRCO_GETOPT case allocates an mbuf with m_get(M_WAITOK, MT_DATA)
(:117), fills it via rfcomm_getopt() between crit_enter()/crit_exit()
(:118-120), and copies the result to userspace with soopt_from_kbuf()
(:126) β but only the m->m_len == 0 error path (:121-125) ever frees
the mbuf. Every successful getsockopt(2) leaks exactly 1 mbuf.
For a valid option (SO_RFCOMM_MTU, SO_RFCOMM_LM, SO_RFCOMM_FC_INFO)
rfcomm_getopt() (sys/netbt/rfcomm_upper.c:535-567) returns a non-zero
length, so the success path is taken on every call.
This is the rfcomm sibling of DF-0709 (sco_socket.c) and DF-5757
(l2cap_socket.c). Pass-1 finding DF-0680 covers only the m == NULL
argument-evaluation panic on the error path; the success-path leak was
not filed.
Preconditions
- Bluetooth stack loaded:
kldload netbt.ko(admin action; netbt isoptional bluetooth, not in X86_64_GENERIC). - Then ANY local unprivileged user. No Bluetooth controller needed:
the RFCOMM socket and its DLC pcb exist right after
socket(2).
Build
cc -o bt_getopt_leak bt_getopt_leak.c
Run
./bt_getopt_leak 10000 1 # rfcomm only netstat -m | head -1 # before/after comparison (see run.sh)
Expected
mbufs in use climbs by exactly 1 per successful call
(10007 -> 20007 -> 40007 across 10k+10k+20k calls on the stock netbt.ko).
Pool max is 146632 mbufs, so ~146k calls exhaust the kernel mbuf pool
permanently. On the patched module (fix.diff) the count stays flat at 7
across 60k calls and getsockopt still returns correct data (mtu=127).
DF-5758 β rfcomm_ctloutput PRCO_GETOPT success-path mbuf leak
Verdict: REPRODUCED + FIX VALIDATED (fixed)
The leak is exactly 1 mbuf per successful
getsockopt(BTPROTO_RFCOMM, SO_RFCOMM_MTU), deterministic,
unprivileged, no Bluetooth hardware required once netbt.ko is loaded.
The authored fix.diff (shared hunk with the DF-5759 fix in the same
file) closes it completely at module level.
Mechanism (trigger β primitive β effect)
rfcomm_ctloutput() is the RFCOMM socket control-output handler
(sys/netbt/rfcomm_socket.c:92-140). PRCO_GETOPT case, lines 116-127:
case PRCO_GETOPT:
m = m_get(M_WAITOK, MT_DATA); /* :117 alloc */
crit_enter();
m->m_len = rfcomm_getopt(pcb, sopt->sopt_name, mtod(m, void *)); /* :119 */
crit_exit();
if (m->m_len == 0) { /* :121 err path */
m_freem(m); /* :122 only free */
m = NULL;
error = ENOPROTOOPT;
}
soopt_from_kbuf(sopt, mtod(m, void *), m->m_len); /* :126 copyout */
break; /* :127 m leaked */
For SO_RFCOMM_MTU, rfcomm_getopt() (sys/netbt/rfcomm_upper.c:541-543)
writes dlc->rd_mtu and returns sizeof(uint16_t) = 2 (non-zero), so the
error block is skipped on every call; soopt_from_kbuf() (which does not
take ownership) copies the value out and the mbuf is orphaned β a
permanent 1-mbuf leak per successful getsockopt.
Effect and reachability are identical to DF-5757/DF-0709: the global mbuf objcache drains monotonically; at pool exhaustion (146632 mbufs max on this guest) every kernel network allocation fails permanently until reboot.
Reachability / threat model
- Precondition: admin
kldload netbt.ko(optional-bluetooth module). - Any local user can then
socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);rfcomm_ctloutputonly needsso->so_pcb != NULL(:105), set byrfcomm_sattachat socket creation (sys/netbt/rfcomm_socket.c:301-331). No controller needed. - Verified as unprivileged uid 1001 (maxx, not in wheel).
- Impact ceiling: permanent local kernel-mbuf-exhaustion DoS. No corruption primitive β no escalation chain.
Reproduction (unpatched netbt.ko)
Two guest boots; linear across 10k / 20k batches (fresh-guest run, after the l2cap test had already left 10007 in use):
=== DF-5758 rfcomm === rfcomm: iterations=10000 ok=10000 fail=0 last_val=127 optlen=2 AFTER 10k rfcomm: 20007/146632 mbufs in use (current/max): rfcomm: iterations=20000 ok=20000 fail=0 last_val=127 optlen=2 AFTER 20k more rfcomm: 40007/146632 mbufs in use (current/max):
last_val=127 = RFCOMM_MTU_DEFAULT, returned correctly. Deltas are
exactly +10000 and +20000. (First-guest capture in run.2.log: 10007 ->
20007 -> 40007.)
Fix validation (fix.diff applied to /usr/src/sys/netbt, netbt.ko rebuilt)
=== PATCHED MODULE: DF-5758 rfcomm === rfcomm: iterations=10000 ok=10000 fail=0 last_val=127 optlen=2 AFTER 10k rfcomm: 7/146632 mbufs in use (current/max): rfcomm: iterations=50000 ok=50000 fail=0 last_val=127 optlen=2 AFTER 50k more rfcomm: 7/146632 mbufs in use (current/max):
Flat at 7 across 60k calls; correct value still returned. Module build
rc=0 under -Werror (fix_build.log); patched netbt.ko sha256
f7d2c5ad3f5a90eaea99df15faf8a58c92b886ddc0622e4c7b71abe7fd0e1d6e
(shared with DF-5757 validation β both GETOPT hunks in the same build;
the DF-5759 ssend hunk in rfcomm_socket.c was also included, validated
separately in its own pack).
The same fix hunk also removes the DF-0680 error-path NULL-deref panic (known; not re-verified here).
Rig notes
Same bt_getopt_leak.c binary as DF-5757 with argument 1 (rfcomm).
No changes needed; built first try. attempts=2 (two guest boots).
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) on a fresh vm.sh-reset guest: identical workload leaves mbufs flat at 7/146632 across 60k calls; getsockopt still returns mtu=127, optlen=2. Baseline leaked +1/call. The hunk also removes the DF-0680 error-path NULL-deref (known, not re-verified).
fix_run.log (7 -> 7 patched vs 10007 -> 40007 baseline), fix_build.log (rc=0 -Werror), fix.diff (standalone GETOPT hunk, git-apply-able)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: fresh-guest 20007 -> 40007 (+20000 for 20000 calls); run.2.log: first-guest 10007 -> 20007 -> 40007; fix_run.log: patched module 7 -> 7 across 60k calls with mtu=127/len=2 still correct; fix_build.log: netbt.ko rebuilt rc=0 under -Werror; VERDICT.md cites rfcomm_socket.c:116-127 and rfcomm_upper.c:541-543.
PoC changes
no seed existed; same fresh trigger as DF-5757 with rfcomm argument, built first try
Verified recommended fix
In rfcomm_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
rfcomm_ctloutput PRCO_GETOPT leaks exactly one mbuf per successful getsockopt(BTPROTO_RFCOMM, SO_RFCOMM_MTU): the m_get(M_WAITOK) mbuf at rfcomm_socket.c:117 is freed only on the m_len==0 error path (:122); the success path copies out via soopt_from_kbuf (:126, no ownership transfer) and breaks (:127) without freeing. Unprivileged, no BT controller needed once netbt.ko is loaded. Measured linear leak (10007->20007->40007 across 10k+20k calls, twice on two guest boots); flat at 7 across 60k calls on the fix.diff-patched netbt.ko with correct value returned. Pool max 146632 -> permanent kernel-mbuf-pool exhaustion / system-wide networking DoS after ~146k calls.
No comments yet.