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

rfcomm_ssend violates the pru_send mbuf-ownership contract: m_copym-failure (ENOMEM) path leaks the caller's entire mbuf chain; l2cap_ssend/sco_ssend comply, rfcomm does not

Summary

DFly's sosend clears its top pointer unconditionally after so_pru_send, so handlers must consume m on every path - the siblings comply, rfcomm's ENOMEM branch returns without freeing: the full caller record leaks permanently. KLD rig with a real socreate'd RFCOMM socket and a drained pool: so_pru_send->ENOMEM and exactly +1025 mbufs per run x2; one-line m_freem fix validated (drains 153484, still ENOMEM, 7->7). Userland reach additionally needs a connected socket (controller) but composes with the sibling leaks.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5759 Β· 15 files
FileTypeDescriptionSize
bt_ssend_leak_kld.c trigger-source production-code KLD rig: socreate + 1025-mbuf chain + pool drain + so_pru_send 3.8 KB view raw
kld.Makefile build-source KLD Makefile (SYSDIR=/usr/src/sys, device_if/bus_if) 116 B ↓ download
build.sh build-script in-guest KLD build 108 B view raw
run.sh run-script netstat before/after + kldload + dmesg 148 B view raw
build.log build-log final -Werror KLD compile + link, rc=0 906 B view raw
build.2.log build-log iteration history (nested-externs fix, MHLEN rig fix) 339 B view raw
rig_attempt1_panic.txt notes attempt-1 rig-bug panic trace (malformed chain; NOT a kernel finding) 500 B view raw
run.log run-log baseline run 1: +1025 (40007->41032) with full dmesg trace 267 B view raw
run.2.log run-log baseline run 2: +1025 again (41032->42057) 261 B view raw
fix.diff suggested-fix one-line m_freem(m) in the ENOMEM else-branch 250 B view raw
fix_build.log build-log patched netbt.ko module build (rc=0, -Werror) 18.5 KB view raw
fix_run.log run-log patched-module run: 7 -> 7, ENOMEM preserved 507 B view raw
env.txt environment uname, kern.version, module shas 656 B view raw
README.md readme build/run/expected 2.2 KB ↓ raw
VERDICT.md verdict full narrative: contract citation, rig design, repro, fix validation 5.1 KB ↓ raw
README.md readme build/run/expected
↓ download raw

DF-5759 PoC: rfcomm_ssend violates the pru_send mbuf-ownership contract

Bug

sys/netbt/rfcomm_socket.c:426-451: rfcomm_ssend() (the pru_send handler for RFCOMM sockets) takes ownership of the caller's mbuf chain β€” the generic sosend() clears its top pointer unconditionally after so_pru_send() returns (sys/kern/uipc_socket.c:924-930; comment at :1051: "sent or freed in lower layer"), so the handler must consume m on every path. l2cap_ssend and sco_ssend honor this (their out: paths m_freem(m)); rfcomm_ssend does not:

    m0 = m_copym(m, 0, M_COPYALL, M_NOWAIT);
    if (m0) {
        sbappendstream(&so->so_snd.sb, m);
        error = rfcomm_send(pcb, m0);
    } else {
        error = ENOMEM;          /* <-- m (caller's chain) leaked */
    }

When m_copym(M_NOWAIT) fails (mbuf pool exhausted), the handler replies ENOMEM and drops m on the floor: a permanent kernel mbuf-chain leak of the full record being sent.

Preconditions / reachability

  • netbt.ko loaded (admin action). A send on an RFCOMM socket requires SS_ISCONNECTED (sosend + PR_CONNREQUIRED), i.e. in normal operation a Bluetooth controller and an established session β€” so from pure userland on a headless guest this path is not reachable.
  • The defect is in the socket-adapter layer and is exercised identically by any caller of so_pru_send(); this pack proves it with a production-code KLD rig (bt_ssend_leak_kld.c) that drives the REAL so_pru_send β†’ rfcomm_ssend path with a real socket, with no modifications to sys/.

Build (in-guest, against /usr/src/sys)

make                  # Makefile included in this pack (needs /usr/src/sys)

Run (root)

netstat -m | head -1                  # before
kldload /tmp/btssend/btssend.ko       # payload runs at MOD_LOAD
dmesg | grep btssend                  # trace
netstat -m | head -1                  # after: +1025 mbufs (leaked chain)

Expected (stock netbt.ko)

so_pru_send -> 12 (ENOMEM) in the dmesg trace and mbufs in use permanently +1025 (the 1024-mbuf payload chain + pkthdr head), after the rig has already released its 113k-490k drain mbufs and closed the socket. Second run: another exact +1025. On the fix.diff-patched netbt.ko: same ENOMEM, but mbufs stay flat (7 -> 7).

VERDICT.md verdict full narrative: contract citation, rig design, repro, fix validation
↓ download raw

DF-5759 β€” rfcomm_ssend ENOMEM path leaks the caller's mbuf chain (pru_send ownership violation)

Verdict: REPRODUCED + FIX VALIDATED (fixed)

rfcomm_ssend() fails the DragonFly pru_send mbuf-ownership contract on its m_copym(M_NOWAIT)-failure path: it returns ENOMEM without freeing, queueing, or otherwise consuming the caller's mbuf chain m. Proven on the guest with a production-code KLD rig that calls the real so_pru_send() on a real kernel-created RFCOMM socket with the global mbuf pool drained: the 1025-mbuf chain disappears permanently (+1025 mbufs in use, twice, deterministic). One m_freem(m) line in the else-branch fixes it (same ENOMEM returned, zero leak, validated on a patched netbt.ko).

The ownership contract (why this is a leak, not caller's problem)

DragonFly's generic sosend() hands the assembled record to the protocol via so_pru_send() and then unconditionally forgets it:

/* sys/kern/uipc_socket.c:924-930 (PR_ATOMIC/seqpacket path) */
error = so_pru_send(so, pru_flags, top, addr, control, td);
...
clen = 0;
control = NULL;
top = NULL;          /* regardless of error */
if (error)
        goto release;

(and the stream specialization at :1048-1053 comments: "sent or freed in lower layer"). The out:/release: frees at :939-942 only cover pre-send failures. Therefore pru_send handlers own m on all paths. The netbt siblings honor it β€” l2cap_ssend and sco_ssend both goto out into if (m) m_freem(m) on every error path (sys/netbt/l2cap_socket.c:453-455, sys/netbt/sco_socket.c:414-416). rfcomm_ssend (sys/netbt/rfcomm_socket.c:443-449) frees control, copies, and on copy-failure just returns ENOMEM: m is orphaned.

Rig (production-code harness, no sys/ modifications)

bt_ssend_leak_kld.c at MOD_LOAD: 1. socreate(AF_BLUETOOTH, &so, SOCK_STREAM, BTPROTO_RFCOMM, td) β€” the real socket creation path; rfcomm_sattach runs, so_pcb = DLC. 2. Builds a well-formed 1025-mbuf chain (pkthdr head + 1024 payload mbufs, each m_len = MHLEN so m_cat() joins rather than splats). 3. Drains the global mbuf objcache with m_get(M_NOWAIT) until it returns NULL (113k-153k mbufs held). 4. so_pru_send(so, 0, chain, NULL, NULL, td) β€” the exact production message path (lwkt msg to so_port β†’ rfcomm_ssend). 5. Releases every drained mbuf, closes the socket.

Honest rig-bug note: the first rig attempt (attempt 1) set the pkthdr head's m_len = MLEN (> MHLEN data room of a pkthdr mbuf). The malformed chain made m_copym's partial copy trip KASSERT(M_TRAILINGSPACE(m) >= 0) in m_free ("panic: overflowed mbuf") β€” a rig error, not a kernel finding (no in-kernel builder creates such chains), fixed by using MHLEN everywhere; see rig_attempt1_panic.txt. The successful runs below are with the well-formed chain.

Reproduction (unpatched netbt.ko)

Run 1 (fresh-guest baseline, after the DF-5757/5758 leak tests left 40007 in use):

mbufs BEFORE: 40007/146632 mbufs in use (current/max):
btssend: socreate ok so=0xfffff801174eea80 so_pcb=0xfffff8004f1e8490 (rfcomm_dlc)
btssend: chain=0xfffff80119b18a00 pkthdr.len=254200 (~1025 mbufs)
btssend: drained 113490 mbufs; pool now empty
btssend: so_pru_send -> 12 (ENOMEM - m_copym failed)
btssend: done. ...
mbufs AFTER: 41032/146632 mbufs in use (current/max):

Delta = +1025 β€” exactly the chain β€” permanent after the drain was released and the socket closed (soclose/rfcomm_detach ran inside the rig).

Run 2 (kldunload/kldload again): 41032 -> 42057 = +1025 again. Deterministic; so_pru_send returned error 12 (ENOMEM) both times, proving m_copym failed and the else-branch was taken.

Fix validation (fix.diff = one-line m_freem(m) in the else branch)

Patched netbt.ko (built rc=0 under -Werror, sha256 f7d2c5ad3f5a90eaea99df15faf8a58c92b886ddc0622e4c7b71abe7fd0e1d6e), same rig, fresh guest:

mbufs BEFORE: 7/146632 mbufs in use (current/max):
btssend: drained 153484 mbufs; pool now empty
btssend: so_pru_send -> 12 (ENOMEM - m_copym failed)
mbufs AFTER: 7/146632 mbufs in use (current/max):

Same ENOMEM semantics preserved; zero leak (7 β†’ 7).

Reachability / threat model

From pure userland the path additionally requires an SS_ISCONNECTED RFCOMM socket (a real Bluetooth session), because sosend gates PR_CONNREQUIRED sockets; on a machine with a controller, any local user with an established RFCOMM socket who can push the mbuf pool to exhaustion (e.g. via the sibling GETOPT leaks DF-0709/5757/5758, which need no controller) converts every blocked send into a full-record leak. The defect and its fix are in the socket adapter itself, which is why severity is Low (hardware-assisted in normal use) while the contract violation is certain (code + production-path KLD proof).

No memory-safety primitive (pure leak) β†’ no escalation chain.

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

netbt.ko rebuilt with the one-line fix (sha256 f7d2c5ad3f5a90eaea99df15faf8a58c92b886ddc0622e4c7b71abe7fd0e1d6e, -Werror clean) on a fresh vm.sh-reset guest: identical rig (drained 153484 mbufs, m_copym failed) returns the same ENOMEM(12) while mbufs stay 7 -> 7. Baseline stock module leaked +1025 per run.

fix_run.log (7 -> 7 patched vs 40007 -> 41032 -> 42057 baseline, both with so_pru_send -> 12), fix_build.log (rc=0 -Werror), fix.diff (one-line, git-apply-able)
↓ fix.diffper-fix-DF-5759

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log: baseline run 1 (dmesg trace: drained 113490, so_pru_send -> 12, mbufs 40007->41032 = +1025); run.2.log: run 2 (+1025 again, 41032->42057); fix_run.log: patched netbt.ko, drained 153484, ENOMEM, mbufs 7->7; fix.diff: one-line m_freem(m); rig_attempt1_panic.txt: honest record of the attempt-1 rig bug (malformed chain KASSERT panic, fixed by using MHLEN).

PoC changes

no seed existed; rig written fresh. Two iterations were needed: (1) -Werror nested-externs -> include ; (2) rig chain head used m_len=MLEN on a pkthdr mbuf (only MHLEN room) -> attempt-1 run panicked in m_free's KASSERT; fixed to MHLEN everywhere. Attempt-1 panic was a rig bug, not a kernel finding.

Verified recommended fix

Add m_freem(m) in the m0==NULL else-branch of rfcomm_ssend (rfcomm_socket.c:447-449), matching l2cap_ssend/sco_ssend error-path behavior (see fix.diff).

Verdict

rfcomm_ssend (rfcomm_socket.c:443-449) violates the DFly pru_send ownership contract (sosend clears top unconditionally after so_pru_send, uipc_socket.c:924-930): on the m_copym(M_NOWAIT)-failure path it replies ENOMEM without consuming the caller's mbuf chain. Proven with a production-code KLD rig (real socreate'd RFCOMM socket, real so_pru_send msg, global mbuf pool drained via m_get(M_NOWAIT)): so_pru_send returns ENOMEM(12) and exactly the 1025-mbuf chain leaks permanently, twice (40007->41032->42057). One-line m_freem(m) fix validated at module level: same ENOMEM, 7 -> 7 flat.