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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0709 Β· 16 files
FileTypeDescriptionSize
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
README.md readme original PoC readme
↓ download 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.

VERDICT.md verdict full narrative: mechanism, reachability, fix validation
↓ download raw

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 module netbt.ko (sys/conf/files: netbt/* are optional bluetooth). It is not auto-loaded.
  • Precondition (realistic): an administrator runs kldload netbt.ko to 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 call getsockopt.
  • Verified: the unprivileged user maxx (uid 1001, not in wheel) can socket(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> (defines SO_SCO_MTU) β€” the original PoC used SO_SCO_MTU without including the header that defines it.
  • Added stdlib.h (for atol) and made the iteration count a CLI argument (default 50000) so the leak can be measured precisely against netstat -m rather than requiring a separate watch terminal.
  • The PoC prints iterations/getsockopt_ok/getsockopt_fail/last_mtu so the leak rate is unambiguous.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. 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
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (kernel binary unchanged -- netbt is a loadable MODULE, not in GENERIC; fix validated by rebuilding only netbt.ko: cd /usr/src/sys/netbt && make, rc=0, -Werror; patched netbt.ko sha256 b63c52807a38d44689b87c6ea5242ba73445df4c791af4f0f65bf9cb7037f307 loaded in place of the original sha 7a0de92f...)

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 (defines SO_SCO_MTU, which the original used without the header -> compile error), added stdlib.h, and made the iteration count a CLI arg (default 50000) printing iterations/ok/fail/last_mtu so the leak rate is unambiguous against netstat -m instead of needing a separate 'watch' terminal. Added build.sh, run.sh, VERDICT.md, fix.diff, and manifest.json.

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.