sco_ctloutput PRCO_GETOPT success path leaks one mbuf per getsockopt call β mbuf-exhaustion DoS
Summary
sco_socket.c:111-121: m=m_get(M_WAITOK) allocated for getsockopt result. On success path (sco_getopt returns non-zero e.g. SO_SCO_MTU returns 2) mbuf used for soopt_from_kbuf :120 but NEVER freed before break :121. Inline comment "XXX There are possible memory leaks (Griffin)" :119 confirms known. Each getsockopt(BTPROTO_SCO,SO_SCO_MTU) leaks one mbuf permanently. Any local unprivileged user can exhaust mbuf pool in tight loop -> system-wide networking DoS. L2CAP counterpart avoids mbuf entirely; SETOPT path does m_freem :134. Fix: m_freem(m) after soopt_from_kbuf.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0709 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sco_mbuf_leak.c | trigger-source | Bluetooth SCO getsockopt mbuf-leak trigger (fixed includes + iters arg) | 1.7 KB | view raw |
| build.sh | build-script | cc -o sco_mbuf_leak sco_mbuf_leak.c | 166 B | view raw |
| run.sh | run-script | netstat -m before/after + 10000 getsockopt calls | 351 B | view raw |
| build.log | build-log | clean compile, BUILD_EXIT=0 | 13 B | view raw |
| run.log | run-log | decisive baseline leak measurement: 7 -> 10007 -> 20007 | 390 B | view raw |
| baseline.log | run-log | baseline #0 confirmation: +10000 mbufs / 10k calls | 183 B | view raw |
| fix_run.log | run-log | patched-module run: 7 -> 7 -> 7 (no leak) + 50k confirmation + getsockopt still works | 680 B | view raw |
| fix_build.log | build-log | netbt.ko single-fix module build output (rc=0, -Werror) | 18.9 KB | view raw |
| fix.diff | suggested-fix | git-apply-able: free GETOPT mbuf unconditionally; only copyout on success | 594 B | view raw |
| netstat_samples.txt | leak-sample | before/after mbuf counts, baseline vs patched | 964 B | view raw |
| patched_module_info.txt | environment | patched netbt.ko sha256 + kern.version (#0) | 303 B | view raw |
| env.txt | environment | uname, kern.version, cc version, netbt load state | 384 B | view raw |
| README.md | readme | original PoC readme | 567 B | β raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, fix validation | 5.9 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 |
DF-0709 PoC: sco_ctloutput PRCO_GETOPT mbuf leak
Bug
sco_socket.c:111-121: mbuf allocated by m_get() for getsockopt result is
never freed on the success path. The inline comment at line 119
(XXX There are possible memory leaks (Griffin)) flags this.
Build
cc -o sco_mbuf_leak sco_mbuf_leak.c
Run
./sco_mbuf_leak
In another terminal, watch mbufs climb:
watch -n1 'netstat -m | head'
Expected
mbufs in use climbs monotonically (~1 per iteration). Eventually the mbuf
pool is exhausted and all kernel networking fails.
DF-0709 β sco_ctloutput PRCO_GETOPT mbuf leak
Verdict: REPRODUCED + FIX VALIDATED (fixed)
The bug is real and the leak is exactly 1 mbuf per getsockopt(BTPROTO_SCO, SO_SCO_MTU) call, deterministic and reproducible. The authored fix.diff closes it completely (verified by building a single-fix netbt.ko module, loading it, and re-running the same workload β mbuf count goes flat).
Mechanism (trigger β primitive β effect)
sco_ctloutput() is the Bluetooth SCO socket control-output (getsockopt/setsockopt) handler. In the PRCO_GETOPT (getsockopt) case, sys/netbt/sco_socket.c:111-121:
case PRCO_GETOPT:
m = m_get(M_WAITOK, MT_DATA); /* :111 allocate 1 mbuf */
m->m_len = sco_getopt(pcb, sopt->sopt_name, mtod(m, uint8_t *)); /* :112 */
if (m->m_len == 0) { /* :113 error path */
m_freem(m);
m = NULL;
err = ENOPROTOOPT;
}
/* *opt = m; */
/* XXX There are possible memory leaks (Griffin) */ /* :119 the author flagged it */
soopt_from_kbuf(sopt, mtod(m, void *), m->m_len); /* :120 copy result to user */
break; /* :121 *** m is NEVER freed *** */
For SO_SCO_MTU, sco_getopt() (sys/netbt/sco_upper.c:345-347) returns sizeof(uint16_t) = 2 (non-zero), so the if (m->m_len == 0) block at :113-117 is skipped every call. Execution falls straight through :120 (soopt_from_kbuf copies the 2-byte MTU out to userspace) and break at :121. The mbuf allocated at :111 is never freed on this success path. Compare the sibling PRCO_SETOPT case at :123-135, which correctly calls m_freem(m) at :134 β GETOPT is missing the equivalent free.
The author left a self-incriminating comment at :119: /* XXX There are possible memory leaks (Griffin) */.
Effect: every successful getsockopt(SO_SCO_MTU) permanently leaks one mbuf. The mbuf pool (146632 mbufs max on this guest) is exhausted after ~146k calls, after which all kernel networking fails (observed: the guest's network stack died mid-test during a runaway loop, requiring a reset β concrete DoS).
Reachability / threat model
- The netbt Bluetooth stack is not compiled into
X86_64_GENERIC; it is a loadable modulenetbt.ko(sys/conf/files:netbt/*areoptional bluetooth). It is not auto-loaded. - Precondition (realistic): an administrator runs
kldload netbt.koto enable Bluetooth support. This is a normal admin action (you load the module because you want Bluetooth). Once loaded, the Bluetooth socket domain (AF_BLUETOOTH/BTPROTO_SCO) is open to any local user β no privilege or special device is required to create an SCO socket and callgetsockopt. - Verified: the unprivileged user
maxx(uid 1001, not in wheel) cansocket(AF_BLUETOOTH, SOCK_SEQPACKET, BTPROTO_SCO)and trigger the leak with no further setup. No Bluetooth controller hardware is needed β the socket and its PCB are created without any adapter attached. - Impact ceiling: local memory-exhaustion DoS. A pure resource leak β no memory-corruption primitive, so per Phase 6 there is no escalation chain to develop.
Reproduction (unpatched #0 baseline, netbt.ko loaded)
BEFORE(baseline): 7/146632 mbufs in use (current/max): iterations=10000 getsockopt_ok=10000 getsockopt_fail=0 last_mtu=0 AFTER 10k(baseline): 10007/146632 mbufs in use (current/max):
Delta = +10000 mbufs per 10000 calls = exactly 1 mbuf leaked per getsockopt. Confirmed across multiple runs (7β10007β20007 in an earlier run; deterministic).
Fix
fix.diff restructures the PRCO_GETOPT case so soopt_from_kbuf() runs only on success (m_len != 0) and the mbuf is freed unconditionally on the way out:
case PRCO_GETOPT:
m = m_get(M_WAITOK, MT_DATA);
m->m_len = sco_getopt(pcb, sopt->sopt_name, mtod(m, uint8_t *));
if (m->m_len == 0) {
err = ENOPROTOOPT;
} else {
soopt_from_kbuf(sopt, mtod(m, void *), m->m_len);
}
m_freem(m); /* <-- the fix: always free the GETOPT mbuf */
break;
This is a single logical change (free the GETOPT mbuf). It additionally closes a latent NULL-deref on the original error path (the old code set m = NULL then dereferenced mtod(m,...) at :120 when m_len == 0 β e.g. SO_SCO_HANDLE with no link), but that is incidental to fixing the leak correctly.
Fix validation (single-fix module build + reload)
Because netbt is a module, the fix was validated by rebuilding only netbt.ko (cd /usr/src/sys/netbt && make, rc=0, -Werror) and loading the patched module (sha256 b63c5280β¦) β the GENERIC kernel proper is unchanged (#0). Same workload on the patched module:
=== PATCHED MODULE LEAK TEST === BEFORE(patched): 7/146632 mbufs in use (current/max): iterations=10000 getsockopt_ok=10000 getsockopt_fail=0 last_mtu=0 AFTER 10k(patched): 7/146632 mbufs in use (current/max): iterations=10000 getsockopt_ok=10000 getsockopt_fail=0 last_mtu=0 AFTER 20k(patched): 7/146632 mbufs in use (current/max): === PATCHED CONFIRMATION (50k calls) === BEFORE: 8/146632 mbufs in use (current/max): iterations=50000 getsockopt_ok=50000 getsockopt_fail=0 last_mtu=0 AFTER 50k: 7/146632 mbufs in use (current/max): === functionality: getsockopt still returns valid data === getsockopt OK, len=2, mtu=0
Before/after contrast: baseline leaks +10000 mbufs / 10k calls; patched leaks 0 mbufs over 70k calls. getsockopt still functions (returns len=2, valid MTU). fix_status = fixed.
PoC changes
- Added
#include <netbt/sco.h>(definesSO_SCO_MTU) β the original PoC usedSO_SCO_MTUwithout including the header that defines it. - Added
stdlib.h(foratol) and made the iteration count a CLI argument (default 50000) so the leak can be measured precisely againstnetstat -mrather than requiring a separatewatchterminal. - The PoC prints
iterations/getsockopt_ok/getsockopt_fail/last_mtuso the leak rate is unambiguous.
Fix verification
fixedVALIDATED. The unfixed netbt.ko (sha 7a0de92f...) leaks +10000 mbufs per 10000 getsockopt calls (7 -> 10007). After applying fix.diff and rebuilding only the netbt.ko module (rc=0), the SAME workload leaks 0 mbufs: 7 -> 7 after 10000 calls, 8 -> 7 after 50000 calls, and getsockopt still returns valid data (len=2, mtu=0). The fix closes the leak deterministically.
baseline(unpatched): BEFORE 7/146632 -> AFTER 10k 10007/146632 (+10000 mbufs) | patched(sha b63c5280...): BEFORE 7/146632 -> AFTER 10k 7/146632 -> AFTER 20k 7/146632 (0); 50k confirmation 8->7 (0); getsockopt OK len=2 mtu=0
Confirmed kernel references
Detail
Exploit chain
none (pure resource-exhaustion leak; no memory-corruption primitive, so per Phase 6 no escalation chain exists to develop). Impact ceiling = local memory-exhaustion DoS: ~146k getsockopt calls drain the 146632-mbuf pool and the kernel's network stack fails (observed when a test loop ran unbounded and wedged the guest, requiring a reset).
Evidence (decisive lines)
BASELINE (unpatched netbt.ko, sha 7a0de92f...): BEFORE: 7/146632 mbufs in use / iterations=10000 getsockopt_ok=10000 / AFTER 10k: 10007/146632 mbufs in use (delta +10000 = exactly 1/call). Earlier run: 13 -> 50012 -> 100012. PATCHED (netbt.ko sha b63c5280...): BEFORE(patched): 7 / AFTER 10k: 7 / AFTER 20k: 7 (delta 0); 50k-call confirmation: 8 -> 7; getsockopt OK, len=2, mtu=0 (functionality preserved).
PoC changes
findings/poc/DF-0709/sco_mbuf_leak.c: added #include
Verified recommended fix
In sys/netbt/sco_socket.c PRCO_GETOPT case, only call soopt_from_kbuf() on success (m_len != 0) and free the mbuf unconditionally before break (m_freem(m)). This is one logical change (free the GETOPT mbuf) and incidentally also avoids a latent NULL-deref on the old m_len==0 path. The git-apply-able diff is findings/poc/DF-0709/fix.diff. Matches the finding markdown's intent; the finding had no concrete diff so this supersedes with a verified one.
Verdict
REPRODUCED + FIX VALIDATED. In sys/netbt/sco_socket.c:111-121 the PRCO_GETOPT case of sco_ctloutput allocates an mbuf via m_get(M_WAITOK, MT_DATA) at :111 and never frees it on the success path: for SO_SCO_MTU, sco_getopt (sys/netbt/sco_upper.c:345-347) returns sizeof(uint16_t)=2 (non-zero), so the if(m->m_len==0) free block at :113-117 is skipped, execution runs soopt_from_kbuf at :120 (copies the result to the user) and break at :121 -- leaving the mbuf leaked. The author flagged it with '/ XXX There are possible memory leaks (Griffin) /' at :119. The sibling PRCO_SETOPT case correctly calls m_freem(m) at :134; GETOPT is missing the equivalent free. Confirmed by measuring netstat -m: exactly +1 mbuf per getsockopt (7 -> 10007 after 10000 calls, deterministic). The runaway loop also demonstrated the DoS ceiling -- it exhausted the 146632-mbuf pool and killed the guest's network stack. Reachable by any unprivileged local user once an admin has kldload'd netbt.ko (Bluetooth is a loadable module, not in X86_64_GENERIC); maxx (uid 1001, not in wheel) triggers it with no further setup and no Bluetooth hardware.
No comments yet.