DLC teardown deadlock: untimeout() called under pcb->pcb_lock races the DLC timeout callback blocking on pcb_lock - DragonFly's synchronous callout_stop() turns the overlap into a permanent non-interruptible AB-BA wedge (softclock thread + task/user thread halted)
Summary
ng_btsocket_rfcomm_process_timeout() is the DLC callout callback and its first action is a BLOCKING lockmgr(&pcb->pcb_lock) - while it waits, CALLOUT_INPROG stays set. Five sites call untimeout() -> callout_stop() WHILE HOLDING pcb_lock (pcb_kill also holds session_lock, disconnect, detach, receive_sabm, receive_ua). DragonFly's callout_stop() is synchronous - it waits for the in-progress callback to return. Timer expiry inside any of those pcb_lock critical sections therefore deadlocks circularly and permanently: the triggering thread (unkillable - no PCATCH), the RFCOMM task thread (whole bluetooth RFCOMM stops), and one CPU's softclock thread. Any RFCOMM DLC with an armed timer (default 60s) whose expiry lands in a pcb_lock critical section: a remote Bluetooth peer in radio range can aim the window (answer SABM with UA at expiry; send DISC at expiry; repeatable - each connect arms a fresh timer); a local user can aim close()/shutdown(). Permanent system wedge until reboot on BT-equipped systems. Not runtime-verified (guest has no HCI hardware; connect fails before any DLC timer arms) - pack carries the full interleaving diagram and exact kern_timeout.c conditions. Fix: non-blocking callback (set TIMEDOUT without pcb_lock, move the transition to the task thread).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3476 Β· 5 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.6 KB | β raw | |
| VERDICT.md | β | 3.8 KB | β raw | |
| fix.diff | β | 2.2 KB | view raw | |
| verdict.json | β | 3.7 KB | view raw | |
| manifest.json | β | 811 B | view raw |
DF-3476 β RFCOMM DLC teardown deadlock: ng_btsocket_rfcomm_untimeout() under pcb_lock vs process_timeout() blocking on pcb_lock (synchronous callout_stop)
Where
sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c
Lock-order inversion between the pcb lockmgr lock and the callout stop-interlock:
-
Path A (timeout callback).
ng_btsocket_rfcomm_process_timeout()(:3550-3583) is the callout callback; its FIRST action islockmgr(&pcb->pcb_lock, LK_EXCLUSIVE)(:3554) β a blocking acquisition. While it waits, the callout is markedCALLOUT_INPROG(set by softclock before invoking the callback,sys/kern/kern_timeout.c:567, cleared only at :644 after the callback returns). -
Path B (timeout teardown under pcb_lock). Five call sites invoke
ng_btsocket_rfcomm_untimeout(pcb)βcallout_stop(&pcb->timo)(:3538) while holding pcb->pcb_lock: ng_btsocket_rfcomm_pcb_kill():3403-3404 (RFCOMM task thread; also holds session_lock)ng_btsocket_rfcomm_disconnect():834-835 (user thread, close/shutdown)ng_btsocket_rfcomm_detach():768-769 (user thread)ng_btsocket_rfcomm_receive_sabm():2112 (task thread, remote-driven)-
ng_btsocket_rfcomm_receive_ua():2273 (task thread, remote-driven) -
The interlock. DragonFly's
callout_stop()is synchronous (_callout_cancel_or_stop(cc, CALLOUT_STOP, 1),sys/kern/kern_timeout.c:1091-1094): when the callback is INPROG itssleep("costp")s (:910-921) until softclock clears the STOP request β which happens only after the callback returns (:643-647).
Deadlock
Timer expiry racing any pcb_lock critical section that reaches an untimeout call:
CPU X (softclock): CPU Y (task/user thread):
INPROG = 1 lockmgr(&pcb->pcb_lock, LK_EXCLUSIVE) [held]
process_timeout(): ... pcb_kill()/disconnect()/... decides
lockmgr(&pcb->pcb_lock) -> BLOCKS TIMO flag still set (callback hasn't
(waits for CPU Y) run yet, so it can't have cleared it)
ng_btsocket_rfcomm_untimeout(pcb)
callout_stop() -> sees INPROG
ssleep("costp") -> BLOCKS
(waits for callback to return)
Circular wait; both sleeps are non-interruptible (no PCATCH). Permanent
wedge of: the triggering thread (user close() hangs in D-state forever β
lksleep(&pcb->state, ..., 0, "rf_det", 0) at :785 is not catchable
either), the RFCOMM task thread (blocked on session_lock/pcb_lock β whole
bluetooth RFCOMM processing stops), and one CPU's softclock thread (its
callout wheel stops being serviced β timers on that CPU stop firing
system-wide symptoms).
Trigger
Requires an RFCOMM DLC with an armed timer (default
net.bluetooth.rfcomm.sockets.stream.timeout = 60 s) and the timer firing
inside the Β΅s-scale window in which a thread holds pcb_lock and reaches an
untimeout call. Two attacker-facing levers:
- Remote (Bluetooth peer in radio range): the peer controls frame
timing; e.g. answer our SABM with UA exactly at timeout expiry
(
receive_ua:2273 takes pcb_lock then untimeouts), or send DISC to kill the DLC at expiry. Repeatable at will β each connect attempt arms a fresh 60 s timer. - Local: close()/shutdown() of an RFCOMM socket timed against its
DLC timer (
disconnect:834-835).
The interleaving needs the timer to fire while the other side already holds pcb_lock (a window of the order of the critical-section length), so single attempts are low-probability; sustained attempts (many parallel DLCs, or a remote peer replaying UA bursts around the 60 s mark) make it a realistic persistent-wedge attack on any Bluetooth-equipped system running RFCOMM.
PoC
Not executable on the audit guest: reproducing needs an established L2CAP
session (real HCI hardware / ubt-class device); the guest has none, and
ng_btsocket_rfcomm_connect() fails at L2CAP connect before any DLC timer
is armed. status=untested for that reason; the mechanism above is proven
from source (path:line) with the exact sleep/wake conditions quoted from
kern_timeout.c.
Fix
Drop the pcb_lock across the untimeout, or take the callout interlock
BEFORE pcb_lock consistently. Minimal safe change at each site: move
ng_btsocket_rfcomm_untimeout() outside the pcb_lock critical section is
racy against process_timeout's flag updates; the robust fix is to make
ng_btsocket_rfcomm_process_timeout() NOT block on pcb_lock β e.g. have it
delegate to the RFCOMM task (which already serializes DLC processing)
instead of taking pcb_lock itself:
static void
ng_btsocket_rfcomm_process_timeout(void *xpcb)
{
ng_btsocket_rfcomm_pcb_p pcb = xpcb;
- lockmgr(&pcb->pcb_lock, LK_EXCLUSIVE);
+ /*
+ * Do NOT block on pcb_lock here: untimeout() callers hold it and
+ * callout_stop() waits for this callback to return -> AB-BA wedge.
+ * Setting the flag without the lock is safe: flags are u_int16_t,
+ * the TIMO/TIMEDOUT pair is only mutated while the callout is
+ * stopped or firing, and the task re-checks under pcb_lock.
+ */
+ if (!(pcb->flags & NG_BTSOCKET_RFCOMM_DLC_TIMO))
+ return; /* untimeout won the race */
pcb->flags &= ~NG_BTSOCKET_RFCOMM_DLC_TIMO;
pcb->flags |= NG_BTSOCKET_RFCOMM_DLC_TIMEDOUT;
... (state switch moved to the task under pcb_lock)
(full diff in fix.diff; state-machine part of the callback is relocated
into ng_btsocket_rfcomm_session_process_pcb() which already holds
pcb_lock when it checks TIMEDOUT at :1805/:1817/:1854).
DF-3476 β VERDICT
status: untested / impact: dos (if triggered; not executable on guest) / confidence: likely
Why untested
The deadlock needs an RFCOMM DLC with an armed 60 s DLC timer, which
requires an established L2CAP session over real Bluetooth hardware
(ng_hci/HCI connection). The audit guest has no Bluetooth hardware;
ng_btsocket_rfcomm_connect() fails in session_create/session_connect
(L2CAP cannot route to any remote bdaddr without an HCI node) and no DLC
timer is ever armed. Verified on the guest that
socket(PF_BLUETOOTH, SOCK_STREAM, BLUETOOTH_PROTO_RFCOMM) attaches, but
connect/listen paths cannot reach DLC CONFIGURING/CONNECTING/DISCONNECTING
states without hardware. No other userland-reachable lever arms
pcb->timo.
Why the bug is real (source trace)
ng_btsocket_rfcomm_timeout()armscallout_reset(&pcb->timo, ..., ng_btsocket_rfcomm_process_timeout, pcb)(:3522-3523).- On expiry, softclock sets
CALLOUT_INPROGbefore calling the callback and clears it only after it returns (sys/kern/kern_timeout.c:567, :630, :644). ng_btsocket_rfcomm_process_timeout()first does a blockinglockmgr(&pcb->pcb_lock, LK_EXCLUSIVE)(:3554).- Concurrent pcb_lock holders call
ng_btsocket_rfcomm_untimeout(pcb)βcallout_stop(&pcb->timo)while still holding pcb_lock β five sites: :769 (detach), :835 (disconnect), :2112 (receive_sabm), :2273 (receive_ua), :3404 (pcb_kill β also holds session_lock). callout_stop()=_callout_cancel_or_stop(cc, CALLOUT_STOP, 1)(kern_timeout.c:1091) β the1makes it synchronous: with INPROG set itssleep("costp")s until softclock processes the STOP, which happens only after the callback returns (kern_timeout.c:643-647 + update path).
β thread Y holds pcb_lock and sleeps waiting for the callback; the callback (thread X, softclock) sleeps waiting for pcb_lock. Circular, non-interruptible, permanent.
- The window is real whenever the timer fires during a pcb_lock critical
section that reaches untimeout.
pcb_kill's critical sections include L2CAP sends/msc machinery;receive_ua/receive_sabmare directly timeable by a remote Bluetooth peer;disconnect/detachare timeable by the local user. Note the TIMO flag is still set during the window (the callback clears it only after acquiring the lock at :3560), so every pcb_kill/disconnect on a timer-armed DLC takes the untimeout branch β the inversion is not masked by flags.
Consequence once triggered: triggering thread (unkillable D-state β
lksleep(..., 0, "rf_det", 0) :785 has no PCATCH), the RFCOMM task thread
(holds sessions_lock paths β whole bluetooth RFCOMM subsystem stops), and
one CPU's softclock thread all wedge permanently; system-wide timer service
on that CPU degrades. Reboot required.
Severity: Medium β remote-triggerable on Bluetooth-equipped systems with an active RFCOMM peer (unusual config for servers, default-relevant for desktops/laptops with BT), permanent DoS.
Fix
See fix.diff: keep the callback from blocking on pcb_lock (flag-only callback + state processing moved into the already-locked task path), which breaks the inversion at its root; alternatively hold no pcb_lock across untimeout at the five call sites (bigger, more invasive diff). Not kernel-build-validated on the guest (finding is untested end-to-end β no hardware β so a green build would prove little; the diff mirrors the task-side locking already used at :1805/:1817/:1854).
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
not_testableConfirmed kernel references
- sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:3554
- sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:3538
- sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:3404
- sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:835
- sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:769
- sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:2112
- sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:2273
- sys/kern/kern_timeout.c:888
- sys/kern/kern_timeout.c:910
Detail
Exploit chain
attacker (remote BT peer or local socket owner) <-> DLC timer expiry races pcb_lock critical section -> callback blocked on pcb_lock <-> callout_stop blocked on callback return -> permanent kernel wedge (unkillable user thread, RFCOMM task halted, softclock thread of one CPU halted) until reboot.
Evidence (decisive lines)
['findings/poc/DF-3476/README.md β full interleaving diagram + trigger analysis', 'findings/poc/DF-3476/VERDICT.md β why untested on guest + source trace', 'sys/kern/kern_timeout.c:567,630,644 (INPROG spans the callback)', 'sys/kern/kern_timeout.c:888-921 (callout_stop sleeps until callback completes)', 'sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:3554 (callback blocks on pcb_lock)', 'sys/netgraph7/bluetooth/socket/ng_btsocket_rfcomm.c:3404 (untimeout under pcb_lock+session_lock)']
PoC changes
n/a - no PoC written: trigger requires Bluetooth hardware (RFCOMM DLC timer can only be armed on an L2CAP-connected session; guest has no HCI device). Analysis-only evidence pack.
Verified recommended fix
Make process_timeout() non-blocking: set TIMEDOUT without taking pcb_lock and move the state transition into the RFCOMM task under pcb_lock (fix.diff); alternatively never call untimeout() while holding pcb_lock.
Verdict
ng_btsocket_rfcomm_process_timeout() (ng_btsocket_rfcomm.c:3554) blocks acquiring pcb->pcb_lock while running as a callout callback (CALLOUT_INPROG held, kern_timeout.c:567); ng_btsocket_rfcomm_untimeout() -> callout_stop() is synchronous on DragonFly (kern_timeout.c:1091, :910-921) and is invoked while HOLDING pcb_lock at five sites (:769, :835, :2112, :2273, :3404). Timer expiry inside any of those pcb_lock critical sections therefore produces a non-interruptible AB-BA circular wait: softclock thread + triggering thread (task or user close()) wedge permanently; the RFCOMM task and one CPU's callout wheel stop. Remote Bluetooth peer can aim the window (UA/SABM/DISC at DLC-timer expiry, 60s default); local user can aim close() at timer expiry. Not executable on the audit guest (no Bluetooth hardware: L2CAP session establishment fails before any DLC timer is armed - verified), hence untested; mechanism is fully source-proven with the exact sleep conditions quoted from kern_timeout.c.
No comments yet.