rfcomm_bind() has no DLC state check: a live listening (or connecting) DLC can be re-bound in place, mutating its advertised channel/PSM underneath the session (NetBSD upstream added the check; DF never took it)
Summary
bind memcpy's the caller sockaddr unconditionally: an unprivileged user can re-bind an already-LISTENING socket, changing the live listener's bt_channel/bt_psm while it stays on the listening session's DLC list - re-routing/starving inbound matching and desyncing bookkeeping. NetBSD has had the EINVAL check since before 2024; the 2008 DF import predates it. KLD rig on verbatim sources: 'bind #2 (state LISTEN) -> 0 (BUG: live listener mutated)', listener advertises the new channel; state-check fix validated (EINVAL, original preserved). Semantic only - no memory unsafety, effect confined to the caller's own sockets.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5747 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness5747.c | β | 4.8 KB | view raw | |
| l2cap_stub.c | β | 3.5 KB | view raw | |
| Makefile | β | 511 B | β download | |
| build.sh | β | 90 B | view raw | |
| run.sh | β | 130 B | view raw | |
| build.log | β | 10.3 KB | view raw | |
| fix_build.log | β | 1.5 KB | view raw | |
| setup.log | β | 393 B | view raw | |
| run.log | β | 969 B | view raw | |
| fix_run.log | β | 1.1 KB | view raw | |
| env.txt | β | 452 B | view raw | |
| fix.diff | β | 287 B | view raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| manifest.json | β | 842 B | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-5747 β VERDICT
Status: reproduced (rig, verbatim production code) Β· Impact: none (semantic bug / API hardening) Β· Confidence: certain
What was run
KLD rig rfcomm_up5747.ko: sys/netbt/rfcomm_session.c, rfcomm_dlc.c,
rfcomm_upper.c copied unmodified from the guest's /usr/src/sys/netbt
(md5 3528f6f33426dbb23216d88859ec8098 for rfcomm_upper.c, identical to the
audit tree; recorded in setup.log), compiled against a stub L2CAP layer
(l2cap_stub.c). Trigger sysctl hw.df5747_fire=1 calls only production
entry points: rfcomm_attach β rfcomm_rcvd β rfcomm_bind β
rfcomm_listen β rfcomm_bind (again, state=LISTEN) β
rfcomm_disconnect β rfcomm_detach.
Observed (baseline, run.log)
[DF-5747] bind #1 (state CLOSED) -> 0 (expect 0) [DF-5747] listen() -> 0; state=8 (8=LISTEN), channel=3 [DF-5747] bind #2 (state LISTEN) -> 0 (BUG: live listener mutated; upstream returns EINVAL) [DF-5747] listener now advertises: channel=5 psm=0x1001 state=8 [DF-5747] disconnect() on LISTEN dlc -> 22 (expect EINVAL) [DF-5747] detach() complete, dlc freed, listen session freed
The production rfcomm_bind() (rfcomm_upper.c:103-109) accepted a re-bind of
a live listening DLC, mutating its advertised channel (3 β 5) and PSM
(0x0003 β 0x1001) in place. NetBSD's rfcomm_bind_pcb() returns EINVAL
for exactly this call (state check added upstream, present through
rfcomm_upper.c 1.24, 2024). Teardown afterwards was clean β this is a pure
semantic/state-confusion bug (both rfcomm_listen()'s channel allocator at
rfcomm_upper.c:357-373 and rfcomm_dlc_newconn()'s best-match at
rfcomm_dlc.c:92-113 consume rd_laddr.bt_channel), not memory corruption.
Why it matters / threat
Unprivileged local user (RFCOMM sockets need no privilege; kernel must have
netbt compiled in). Effect confined to the caller's own sockets: re-routing
or starving inbound-connection matching for a listener, inconsistent
getsockname output, listener bookkeeping that no longer matches the
listening session. No cross-privilege path exists (a listener can only be
re-bound by its owner). Hence Info severity, defense-in-depth /
upstream-divergence class.
Fix validation (fix.diff, fix_run.log, fix_build.log)
patch -p3 < fix.diff on the verbatim copy adds the upstream state check.
Clean rebuild (make clean + MAKEOBJDIRPREFIX reset β an intermediate
rebuild without these linked a stale rfcomm_upper.o from a second objdir
and showed old behaviour; disassembly of the clean rebuild shows
cmpw $0x0,0xc(%rdi) / mov $0x16,%eax = the EINVAL path, see
fix_build.log). Same trigger:
[DF-5747] bind #2 (state LISTEN) -> 22 (EINVAL - fixed) [DF-5747] listener now advertises: channel=3 psm=0x0003 state=8
Bad behaviour gone; bind #1 (CLOSED state) still returns 0; teardown
clean. fix_status: fixed.
Bottom line
Real, upstream-confirmed API-validation gap in rfcomm_bind(); demonstrated
on the guest with verbatim production code; one-line fix validated.
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 applied to the verbatim rig copy with patch -p3, clean-rebuilt, reloaded, same trigger: bind #2 (state LISTEN) now returns 22/EINVAL and the listener keeps channel=3/psm=0x0003; bind #1 (CLOSED) still returns 0; detach/teardown clean. Bad behavior gone on patched build.
fix_run.log ('[DF-5747] bind #2 (state LISTEN) -> 22 (EINVAL - fixed)'); fix_build.log (patch output + relink + rfcomm_bind disassembly with the EINVAL path)
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log: '[DF-5747] bind #2 (state LISTEN) -> 0 (BUG: live listener mutated; upstream returns EINVAL)' and 'listener now advertises: channel=5 psm=0x1001 state=8'; fix_run.log: 'bind #2 (state LISTEN) -> 22 (EINVAL - fixed)' with channel=3 psm=0x0003 preserved; fix_build.log: objdump of patched rfcomm_bind showing cmpw rd_state/mov $0x16 (EINVAL); setup.log: verbatim-source md5s
PoC changes
Harness written fresh on the DF-5733 rig pattern (verbatim netbt sources + l2cap_stub.c + FCS-valid frame injectors were not needed here - only production upper-API calls); stub gained harness_last_channel(); no production source modified for the baseline run.
Verified recommended fix
Add the upstream NetBSD state check to rfcomm_bind(): reject with EINVAL unless dlc->rd_state == RFCOMM_DLC_CLOSED (see fix.diff).
Verdict
Reproduced on the QEMU guest with a KLD rig compiling sys/netbt/rfcomm_session.c, rfcomm_dlc.c, rfcomm_upper.c VERBATIM (md5-verified vs /usr/src/sys/netbt) against a stub L2CAP layer. Production rfcomm_bind() (rfcomm_upper.c:103-109) accepted a re-bind of a live LISTENing DLC: bind #2 in state 8 returned 0 and mutated the listener's advertised channel 3->5 and psm 0x0003->0x1001 in place. NetBSD upstream returns EINVAL for exactly this call (state check in rfcomm_bind_pcb(), still present in rfcomm_upper.c 1.24/2024); DragonFly never took the fix. Pure semantic/state-confusion bug confined to the caller's own sockets (rfcomm_listen's channel allocator and rfcomm_dlc_newconn's best-match both consume rd_laddr.bt_channel); no memory unsafety, no cross-privilege effect - hence Info severity. One-line fix validated on the guest: patched build returns 22/EINVAL for the LISTEN-state bind while the CLOSED-state bind still succeeds and teardown stays clean.
No comments yet.