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

l2cap_bind() has no channel-state check: a live LISTENING channel can be re-bound in place, mutating its advertised PSM under the sorted listener list (DF-5747 parity; no duplicate-PSM listener rejection either)

Summary

bind memcpy's unconditionally: a socket already on l2cap_listen_list can be re-bound to any PSM, silently changing which service it answers and breaking the sorted-list invariant; two sockets can also listen the same PSM (first-wins, the netbt analog of DF-3477). Reproduced from PLAIN USERLAND as the unprivileged maxx user (bind/listen/re-bind returns 0, socket still a listener with the mutated PSM). state-check fix (the upstream rfcomm shape).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5750 Β· 7 files
FileTypeDescriptionSize
README.md β€” 1.4 KB ↓ raw
VERDICT.md β€” 2.1 KB ↓ raw
bind_state.c β€” 3.2 KB view raw
run.sh β€” 142 B view raw
run.log β€” 356 B view raw
manifest.json β€” 777 B view raw
verdict.json β€” 2.0 KB view raw

DF-5750 β€” l2cap_bind() has no channel-state check (DF-5747 parity)

sys/netbt/l2cap_upper.c:106-112: l2cap_bind() memcpy's the new local address into the channel unconditionally. A channel that is already listening (on l2cap_listen_list, inserted sorted by its old PSM at l2cap_listen(), l2cap_upper.c:302-345, matched first-match at l2cap_signal.c:266-273) can be re-bound in place to any other PSM β€” silently mutating the routing key of a live listener. Upstream added the missing state check to the rfcomm sibling (rfcomm_bind, cf. DF-5747); l2cap_bind() never got one.

Reproduce

# guest, once as root: kldload netbt
cc -O -o bind_state bind_state.c
./bind_state        # as the UNPRIVILEGED user

Expected (vulnerable) β€” see run.log

bind(psm=BNEP)           = 0 (ok)
listen()                 = 0 (ok, channel now on l2cap_listen_list)
RE-bind(psm=HID_CNTL) on the LISTENING socket = 0 (ok)   <-- DF-5750
listen() again           = -1 (Invalid argument)  [still a listener]
bind(same psm on 2nd socket) = 0 (ok (no duplicate-PSM rejection))
listen() on 2nd socket   = 0 (ok)

Impact: semantic (listener routing-key mutation + no duplicate-PSM rejection β€” on hosts with a Bluetooth controller any local user can occupy/re-route service PSMs; cf. DF-3477 for the ng_btsocket analog). No memory-safety consequence demonstrated: the list stays consistent (first-match semantics only), hence severity Info.

VERDICT.md
↓ download raw

DF-5750 VERDICT

Reproduced as the unprivileged user (maxx) on DragonFly 6.5-DEVELOPMENT X86_64_GENERIC with kldload netbt (netbt is KLD-only; loading is privileged, using the sockets is not).

sys/netbt/l2cap_upper.c:106-112:

int
l2cap_bind(struct l2cap_channel *chan, struct sockaddr_bt *addr)
{
    memcpy(&chan->lc_laddr, addr, sizeof(struct sockaddr_bt));
    return 0;
}

No state check, no PSM validation (the caller l2cap_sbind sys/netbt/l2cap_socket.c:320-339 only validates bt_len/bt_family). l2cap_listen() validated the PSM at listen time (l2cap_upper.c:309-311) and inserted the channel into l2cap_listen_list ordered by that PSM (l2cap_upper.c:327-342); the re-bind replaces the PSM in place. Incoming connection requests match listeners first-match with the current PSM (sys/netbt/l2cap_signal.c:266-273), so a re-bound listener silently changes which service PSM it answers β€” and the sorted-insert invariant documented at l2cap_upper.c:321-326 no longer describes the list. Demonstrated output in run.log: re-bind of a LISTENING L2CAP socket returns 0; the socket remains a listener (listen() again β†’ EINVAL because of the L2CAP_SIGNAL_CID marker, l2cap_upper.c:306-307). Also demonstrated: two sockets can listen on the same PSM (no duplicate-listener rejection, first-listener-wins silently).

Not a memory-safety bug on its own (the LIST stays structurally consistent; only match precedence changes) β€” severity Info, parity with DF-5747 (rfcomm_bind). Fix: reject bind() unless chan->lc_state == L2CAP_CLOSED && chan->lc_lcid == L2CAP_NULL_CID (exactly the NetBSD upstream fix shape taken for rfcomm).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

one-line state check; not built for an Info finding (no security impact beyond routing semantics)

VERDICT.md recommended fix
per-fix-DF-5750

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (bind/listen/re-bind sequence, re-bind of LISTENING socket returns 0); VERDICT.md

PoC changes

written fresh; no seed

Verified recommended fix

l2cap_bind(): return EINVAL unless chan->lc_state == L2CAP_CLOSED && chan->lc_lcid == L2CAP_NULL_CID (upstream rfcomm_bind fix shape).

Verdict

REPRODUCED as unprivileged user: bind() on an already-LISTENING L2CAP socket succeeds and mutates the listener's advertised PSM in place (l2cap_upper.c:106-112 has no state check, unlike l2cap_listen/l2cap_connect discipline; the rfcomm sibling got the upstream check, DF-5747). No duplicate-PSM listener rejection either. Semantic/hardening only (listener routing-key mutation; no memory-safety consequence demonstrated) -> Info.