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

rfcomm_detach()'s callout_active()/RFCOMM_DLC_DETACH deferred-free machinery is dead code under DragonFly's callout implementation β€” the documented callout-vs-detach lifetime 'safety net' never engages (misleading guard over the DF-0724 race)

Summary

Every path to the callout_active() test first ran callout_stop(), which on DFly clears CALLOUT_ACTIVE unconditionally at entry and drains in-progress callbacks: RFCOMM_DLC_DETACH can never be set and the timeout's release branch is dead. Not directly exploitable (the unconditional kfree is safe precisely BECAUSE of the drain) but the guard misleadingly appears to handle the DLC timer/teardown race - the genuine race is DF-0724, and anyone relying on the DETACH flag builds on dead code. Rig proves it live (pending=1 active=1 before, both 0 after dlc_close); delete-dead-machinery fix validated. Same dead-deferral class as DF-5739/5563.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5748 Β· 15 files
FileTypeDescriptionSize
harness5748.c β€” 11.8 KB view raw
l2cap_stub.c β€” 3.5 KB view raw
Makefile β€” 511 B ↓ download
build.sh β€” 90 B view raw
run.sh β€” 167 B view raw
build.log β€” 10.3 KB view raw
fix_build.log β€” 973 B view raw
setup.log β€” 393 B view raw
run.log β€” 2.1 KB view raw
fix_run.log β€” 2.9 KB view raw
env.txt β€” 452 B view raw
fix.diff β€” 950 B view raw
VERDICT.md β€” 4.7 KB ↓ raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 4.5 KB view raw
VERDICT.md
↓ download raw

DF-5748 β€” VERDICT

Status: reproduced (rig, verbatim production code) Β· Impact: none (dead code / misleading lifetime guard) Β· Confidence: certain

What was run

KLD rig rfcomm_up5748.ko: verbatim rfcomm_session.c / rfcomm_dlc.c / rfcomm_upper.c (md5s in setup.log) + stub L2CAP; remote-peer TS 07.10 frames injected with valid FCS through the exact vector the real L2CAP layer uses (btproto->input). Trigger sysctl hw.df5748_fire=1:

  1. rfcomm_attach/rfcomm_rcvd/rfcomm_connect two DLCs (channels 3, 4 β†’ DLCI 6, 8) β€” one shared session, initiator;
  2. L2CAP connected callback β†’ SABM(0); injected UA(0) β†’ session OPEN β†’ production rfcomm_dlc_connect() sends PN for both DLCs;
  3. injected PN responses (mtu 127, CFC) β†’ production path sends SABM(dlci); injected UA(dlci) β†’ production rfcomm_dlc_open() (MSC sent, state OPEN); injected MSC responses stop the mcc timers;
  4. rfcomm_disconnect() both β†’ state WAIT_DISCONNECT, DISC sent, 20 s ack timer armed by rfcomm_upper.c:252;
  5. production rfcomm_dlc_close() on A (the exact call rfcomm_detach() makes at rfcomm_upper.c:278-279);
  6. full production rfcomm_detach() on B;
  7. 22 s sleep β†’ session expiry callout fires β†’ rfcomm_session_free().

Observed (baseline, run.log)

[DF-5748] dlc A state=6 (6=OPEN), dlc B state=6 (6=OPEN)
[DF-5748] disconnect(A) -> 0; state=7 (7=WAIT_DISCONNECT)
[DF-5748] disconnect(B) -> 0; state=7
[DF-5748] A timer: pending=1 active=1   (ARMED by rfcomm_disconnect:252)
[DF-5748] A after rfcomm_dlc_close(): pending=0 active=0
[DF-5748] DEAD: callout_active() == 0 after rfcomm_dlc_close(); the RFCOMM_DLC_DETACH branch at rfcomm_upper.c:293 is UNREACHABLE
[DF-5748] rfcomm_detach(B) returned; B freed via immediate kfree (DETACH flag never taken)
=== DF-5748 harness done (no crash) ===

The decisive chain of facts, all from production calls:

  • rfcomm_disconnect() arms the DLC timer (pending=1 active=1) β€” so the callout machinery is live;
  • rfcomm_dlc_close() β€” which rfcomm_detach() always runs first for a non-CLOSED DLC (rfcomm_upper.c:278-279) β€” leaves callout_active() == 0, because its callout_stop() (rfcomm_dlc.c:164) clears CALLOUT_ACTIVE unconditionally at entry (sys/kern/kern_timeout.c:869) and drains any in-progress callback (kern_timeout.c:888-921, sync STOP);
  • no callout_reset(&dlc->rd_timeout, ...) site is reachable with rd_state == RFCOMM_DLC_CLOSED (all five require non-CLOSED states).

β‡’ the if (callout_active(...)) test at rfcomm_upper.c:293 can only see 0, β‡’ RFCOMM_DLC_DETACH is never set, β‡’ the kfree() branch in rfcomm_dlc_timeout() (rfcomm_dlc.c:203-204) is unreachable dead code. The full production rfcomm_detach() on B completed via the immediate-kfree path; the session self-expired 20 s later with no crash.

Why it matters

Not directly exploitable β€” the unconditional kfree() is safe because callout_stop() drains. The problem is that this dead machinery is documented as the callout-vs-detach lifetime handler ("If callout is invoking we can't free the DLC so mark it and let the callout release it") when it does no such thing under DragonFly's callout implementation; the genuine race β€” unsynchronized double-entry into rfcomm_dlc_close() from the softclock thread vs the socket thread β€” is DF-0724 and is not mitigated by this flag. Anyone fixing DF-0724 by "relying on the DETACH flag" would be building on dead code. NetBSD upstream uses callout_invoking() + callout_destroy() here instead. Info severity (dead code / misleading guard; hardening + divergence).

Fix validation (fix.diff, fix_run.log, fix_build.log)

patch -p3 < fix.diff (both hunks: unconditional documented kfree in rfcomm_detach(); drop the dead release branch in rfcomm_dlc_timeout()), clean rebuild β€” zero callout_active references remain in the module β€” same trigger: identical full lifecycle, same observations, no crash (fix_run.log). Removal validated as behavior-preserving cleanup. fix_status: fixed (baseline_reproduced=1, patched_reproduced=0 β€” nothing bad exists to reproduce; lifecycle is unchanged and clean by design of the cleanup).

Bottom line

Provable dead code demonstrated against verbatim production code on the guest; the deferred-free "safety net" for the DLC timer never engages under DragonFly's callout API; cleanup fix applied and validated as behavior-preserving.

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 (two hunks) applied to the verbatim rig copies, clean-rebuilt with zero callout_active references remaining in the module, reloaded, same trigger: the full DLC lifecycle runs identically and cleanly - behavior-preserving dead-code removal validated (nothing bad to reproduce by design; baseline_reproduced=1 refers to the demonstrated dead guard, patched_reproduced=0 to the absence of any fault).

fix_build.log (both hunks applied, relink, callout_active grep count 0); fix_run.log (identical lifecycle, '=== DF-5748 harness done (no crash) ===')
↓ 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-5748] A timer: pending=1 active=1 (ARMED by rfcomm_disconnect:252)' followed by '[DF-5748] A after rfcomm_dlc_close(): pending=0 active=0' and '[DF-5748] DEAD: callout_active() == 0 after rfcomm_dlc_close(); the RFCOMM_DLC_DETACH branch at rfcomm_upper.c:293 is UNREACHABLE', then 'rfcomm_detach(B) returned; B freed via immediate kfree (DETACH flag never taken)' and '=== DF-5748 harness done (no crash) ==='; fix_run.log: identical clean lifecycle with the dead code removed; setup.log: verbatim-source md5s

PoC changes

Harness written fresh on the DF-5733 rig pattern: verbatim netbt sources + l2cap_stub.c (gained harness_last_channel()) + FCS-valid frame injectors reused; drives two DLCs (DLCI 6/8) so one demonstrates the callout state around rfcomm_dlc_close() while the other runs full production rfcomm_detach(); no production source modified for the baseline run.

Verified recommended fix

Delete the dead callout_active()/RFCOMM_DLC_DETACH machinery: unconditional documented kfree in rfcomm_detach() and drop the unreachable release branch in rfcomm_dlc_timeout() (see fix.diff); fix the real race (DF-0724) separately with proper serialization.

Verdict

Reproduced on the QEMU guest with a KLD rig compiling sys/netbt/rfcomm_session.c, rfcomm_dlc.c, rfcomm_upper.c VERBATIM against a stub L2CAP layer, driving two DLCs to RFCOMM_DLC_OPEN through the production session state machine with injected FCS-valid TS 07.10 frames. After production rfcomm_disconnect() armed the 20 s ack timer (callout_pending=1 callout_active=1), the production rfcomm_dlc_close() - the identical call rfcomm_detach() makes at rfcomm_upper.c:278-279 - left callout_active()==0: callout_stop() clears CALLOUT_ACTIVE unconditionally at entry (kern_timeout.c:869) and drains in-progress callbacks (kern_timeout.c:888-921). Since no callout_reset(&dlc->rd_timeout) site is reachable with rd_state==CLOSED, the callout_active() test at rfcomm_upper.c:293 can only ever see 0, RFCOMM_DLC_DETACH can never be set, and the kfree() branch in rfcomm_dlc_timeout() (rfcomm_dlc.c:203-204) is unreachable dead code - demonstrated live, then full production rfcomm_detach() ran the immediate-kfree path and the session expired cleanly 20 s later with no crash. Not directly exploitable (the unconditional kfree is safe precisely because callout_stop drains), but the guard is documented as the callout-vs-detach lifetime handler and does nothing - the genuine race is DF-0724, and this dead net invites fixing it the wrong way. Info severity (dead code / misleading guard / upstream divergence: NetBSD uses callout_invoking() there).