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

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)

No comments yet.

PoC verification

Evidence pack

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

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

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_ctloutput only needs so->so_pcb != NULL (:105), set by rfcomm_sattach at 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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

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.