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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5760 Β· 13 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 calls rfcomm_dlc_newconn(rs, pn.dlci) with no direction validation (contrast rfcomm_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_dlci and inserted on the session.

Reproduced behaviour (run.log, deterministic, first shot)

  1. 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).
  2. 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), stamped rd_dlci = 7, inserted it: POISON-CONFIRMED.
  3. Legit SABM(6) still opens DLCI 6 β†’ the same channel 3 now hosts TWO DLCs, one of them bogus (DUP-CHANNEL).
  4. Every subsequent local rfcomm_connect(ch 3) hits rfcomm_upper.c:181-182 (rfcomm_dlc_lookup(rs, 7) != NULL) β†’ EBUSY forever (LOCAL-CONNECT ... -> 16).
  5. 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 = 0 matches 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 (uptime in 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-hoisted rs->rs_l2cap load 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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.
↓ fix.diffper-fix-DF-5760

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.