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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5757 Β· 12 files
FileTypeDescriptionSize
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
README.md readme build/run/expected
↓ download 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 is optional 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_ctloutput only needs so->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).

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

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 admin kldload netbt.ko to 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 1001 maxx, not in wheel).
  • No controller hardware needed: l2cap_ctloutput only requires so->so_pcb != NULL (:108), which l2cap_sattach sets 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Single-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)
↓ fix.diffper-fix-DF-5757

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.