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)
PoC verification
Evidence pack
findings/poc/DF-5750 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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_testableone-line state check; not built for an Info finding (no security impact beyond routing semantics)
VERDICT.md recommended fix
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.
No comments yet.