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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5747 Β· 15 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (same guest, rig-level rebuild of the verbatim sources)

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.