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)
PoC verification
Evidence pack
findings/poc/DF-5759 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 REALso_pru_sendβrfcomm_ssendpath 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).
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
fixednetbt.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)
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
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.
No comments yet.