RFCOMM PN-command direction-validation hole in rfcomm_dlc_newconn: peer fabricates DLCs in the local stack's DLCI space, permanently locking local outbound connects (EBUSY)
Summary
The PN-command path creates a new DLC without validating the DLCI direction bit (recv_sabm enforces exactly this check; the PN path forgot it): a peer plants DLCs (each with a sonewconn'd socket child) on the responder-direction DLCI numbers the local stack must use for outbound - every subsequent connect() returns EBUSY until session death. Rig on verbatim production sources: POISON-CONFIRMED, LOCAL-CONNECT -> 16, 28/29 channels locked; direction-check fix validated (DM, connect -> 0). Pure logic bug, remote-adjacent peer.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5760 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | β | 16.4 KB | view raw | |
| l2cap_stub.c | β | 3.3 KB | view raw | |
| Makefile | β | 511 B | β download | |
| build.sh | β | 85 B | view raw | |
| run.sh | β | 152 B | view raw | |
| setup.log | β | 393 B | view raw | |
| build.log | β | 4.5 KB | view raw | |
| run.log | β | 8.1 KB | view raw | |
| run.fix.log | β | 60.2 KB | view raw | |
| fix.diff | β | 846 B | view raw | |
| env.txt | β | 312 B | view raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| verdict.json | β | 4.5 KB | view raw |
DF-5760 VERDICT
Status: reproduced (rig, production code verbatim; deterministic; guest up) Impact: dos β remote-peer-triggered permanent lockout of local RFCOMM outbound connections + rogue DLC/socket allocation in the wrong DLCI space.
What was run
KLD rig rfcomm_pn_harness (see README): production rfcomm_session.c,
rfcomm_dlc.c, rfcomm_upper.c copied byte-identical from
/usr/src/sys/netbt (md5s in setup.log), compiled against a stub L2CAP
layer. Frames from the "remote peer" are injected through the production
entry point rfcomm_session_input() β the same callback the real L2CAP
layer invokes. Upper-layer callbacks stand in for the socket layer
(rfcomm_sock_newconn() = sonewconn + return so->so_pcb).
Root cause (path:line)
sys/netbt/rfcomm_session.c:1322-1331βrfcomm_session_recv_mcc_pn()PN-command path callsrfcomm_dlc_newconn(rs, pn.dlci)with no direction validation (contrastrfcomm_session_recv_sabm()at rfcomm_session.c:738-742, which does check).sys/netbt/rfcomm_dlc.c:88βchan = RFCOMM_CHANNEL(dlci)and the listener matching at rfcomm_dlc.c:92-111 consider only the channel, never the direction bit.sys/netbt/rfcomm_dlc.c:128-140β the fabricated DLC is stamped with the peer-chosen (wrong-direction)rd_dlciand inserted on the session.
Reproduced behaviour (run.log, deterministic, first shot)
- Local service listening on channels 1..30; responder session opened
by the peer (SABM(0)); baseline local
rfcomm_connect(ch 4)accepted (DLCI 9 β confirming local DLCIs are the odd/responder direction). - One PN command with DLCI 7 (channel 3, responder direction = OUR
direction) β production
rfcomm_dlc_newconn()matched the channel-3 listener, called its newconn (socket child created in production), stampedrd_dlci = 7, inserted it:POISON-CONFIRMED. - Legit SABM(6) still opens DLCI 6 β the same channel 3 now hosts TWO
DLCs, one of them bogus (
DUP-CHANNEL). - Every subsequent local
rfcomm_connect(ch 3)hitsrfcomm_upper.c:181-182(rfcomm_dlc_lookup(rs, 7) != NULL) β EBUSY forever (LOCAL-CONNECT ... -> 16). - Sweeping the poison over all 30 channels: 28/29 channels refuse local
connect with EBUSY (
POISON-SWEEP) β the peer can lock the local RFCOMM client out of every server channel. (The 29th, ch16, is blocked by the separate channel-mask bug DF-5762:16 & 0x2f = 0matches no listener. Every other channel β₯ 17 was still poisoned β via the wrong listener, see DF-5762.)
Each planted DLC allocates a socket (sonewconn) + DLC in production;
they linger in RFCOMM_DLC_WAIT_CONNECT until the session dies
(recv_sabm rejects the wrong-direction SABM that would complete them).
Threat
The remote peer of any established RFCOMM session (Bluetooth adjacency, L2CAP PSM 3): permanent DoS of local outbound RFCOMM connections to that peer, plus unkillable rogue half-open DLC/socket pairs. No memory corruption (the planted DLCs are correctly refcounted by the socket layer); severity Low/Medium boundary β filed LowβMedium given the whole-channel lockout. Precondition: netbt loaded + RFCOMM session between attacker and victim service.
Fix validation
fix.diff adds the missing direction check to recv_mcc_pn (identical
in form to recv_sabm's). Applied inside the guest's /usr/src
copy; rig rebuilt from the patched verbatim sources; same harness run:
- PN(DLCI 7) now answered with DM, no DLC created
(
DUP-CHANNEL ... planted DLCI 7 (state -1),newconn calls: 2). LOCAL-CONNECT: rfcomm_connect(ch 3) -> 0β accepted.POISON-SWEEP: 1/29(the single EBUSY is ch4's own legitimate baseline DLC from step 1).- Guest alive (
uptimein transcript).
Rig notes / deviations
- The first rig iteration panicked in harness code: the DF-5733 stub's
l2cap_sockaddr()returned a fixed bdaddr so each of the 30 listeners created its own listening session and exhausted the stub's channel table (rfcomm_session_alloc()β NULL, compiler-hoistedrs->rs_l2capload faulted). Fixed by making the stub report BDADDR_ANY (the natural "bind any" setup) so listeners share one session, exactly as production does when binding ANY. - Two rig runs were lost to a stale module load (KLD unload is EBUSY); reboot between runs is required β noted in run.sh.
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
fixedfix.diff (direction check in recv_mcc_pn, DM on violation) applied to the guest's /usr/src/sys/netbt copy; rig rebuilt from the patched verbatim sources; same harness and identical frames: PN(DLCI 7) answered with DM, no DLC created ('newconn calls: 2'), local rfcomm_connect(ch 3) -> 0 (accepted, previously EBUSY), sweep poison fully rejected, guest alive.
run.fix.log lines: 'DUP-CHANNEL: ... peer-planted DLCI 7 (state -1)', 'LOCAL-CONNECT: rfcomm_connect(ch 3) -> 0 (EBUSY = 16) (unexpected)', 'POISON-SWEEP: 1/29' (the 1 is ch4's own baseline DLC); uptime captured in transcript; same-run MASK-CHECK also validates the DF-5762 mask fix.
Confirmed kernel references
Detail
Exploit chain
Bluetooth-adjacent peer with an established RFCOMM session to the victim service: (1) send PN command with DLCI = MKDLCI(local-direction, ch) for each channel 1..30 -> each creates a rogue DLC via the listening socket's newconn (socket allocation) stuck in WAIT_CONNECT; (2) any local application connect() to those channels now gets EBUSY forever; (3) rogue DLCs linger until session teardown. No memory corruption; ceiling is permanent local-outbound lockout + resource poisoning.
Evidence (decisive lines)
run.log: 'POISON-CONFIRMED: peer-planted DLCI 7 exists on session (state 2)', 'LOCAL-CONNECT: rfcomm_connect(ch 3) -> 16 (EBUSY = 16) <== locked out by peer', 'POISON-SWEEP: 28/29 poisoned channels refuse local connect with EBUSY', 'RESULT: PEER CAN LOCK THE LOCAL RFCOMM CLIENT OUT OF ALL CHANNELS'; run.fix.log: poison rejected (DLCI 7 state -1), connect -> 0 accepted, newconn calls: 2; setup.log md5s prove verbatim production sources.
PoC changes
Adapted the DF-5733 rig (l2cap_stub + inject helpers) to a listener scenario: stub l2cap_sockaddr returns BDADDR_ANY so 30 listeners share one listening session (first iteration's fixed 0x11 bdaddr exhausted the stub channel table and crashed in harness code - fixed); listener newconn callback returns fresh rfcomm_attach'd child DLCs (sonewconn stand-in); added per-channel PN sweep, fresh probe DLC per channel, listener-identity tags (upper = channel number), and a MASK-CHECK probe shared with DF-5762.
Verified recommended fix
Add the direction-bit validation to rfcomm_session_recv_mcc_pn()'s PN-command path, mirroring rfcomm_session_recv_sabm() (see fix.diff).
Verdict
Reproduced deterministically with a KLD rig compiling the verbatim production rfcomm_session.c/rfcomm_dlc.c/rfcomm_upper.c against a stubbed L2CAP layer: one PN command with a responder-direction DLCI (7 = channel 3, our direction on a responder session) is accepted by the production recv_mcc_pn -> rfcomm_dlc_newconn path (no direction check, channel-only matching), fabricating a DLC+socket child in the local stack's own DLCI space; every subsequent local rfcomm_connect() to that channel returns EBUSY (rfcomm_upper.c:181-182) until the session dies. Sweeping 30 channels locks the local RFCOMM client out of every server channel (28/29 in run.log; the 29th is blocked by the DF-5762 mask bug). recv_sabm() has the exact missing check (rfcomm_session.c:738-742). Guest stays up: pure validation/logic bug, remote-peer-triggerable DoS plus rogue half-open DLC/socket allocation.
No comments yet.