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

Stale-callout race: callout_stop cannot cancel an in-progress bttimeout, which then fires BDR/hard-reset against a healthy reused CCB

Summary

bccb->timer is a non-lock-aided callout; callout_stop 'does not interlock against a callout that is in-progress'. If bttimeout has fired and is blocked acquiring bt->lock while btdone completes and frees the CCB (callout_stop a no-op against it), the freed BCCB can be re-grabbed and re-armed for a new command before the stale bttimeout acquires the lock. The stale handler sees BCCB_ACTIVE, treats the new command as timed out, freezes the SIM queue, stops every pending timer, and - outbox busy with the new command - escalates directly to a hard bus reset on a healthy bus plus misreported CAM_CMD_TIMEOUT completions. The sili DF-4027 / amr DF-4060 lifecycle-race family. Generation-marker handshake fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4101 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.8 KB ↓ raw
VERDICT.md β€” 3.5 KB ↓ raw
dfbt_rig.c β€” 26.9 KB view raw
dfbt_ctl.c β€” 8.0 KB view raw
Makefile β€” 127 B ↓ download
push_rig.sh β€” 975 B view raw
run1.sh β€” 3.9 KB view raw
run.log β€” 11.6 KB view raw
env.txt β€” 249 B view raw
fix.diff β€” 6.0 KB view raw
manifest.json β€” 747 B view raw
verdict.json β€” 3.2 KB view raw

DF-4101 β€” Stale bttimeout fires BDR/hard-reset on a recycled CCB

Verdict: REPRODUCED (deterministic choreography of the exact race)

Rig class as DF-2429 (see DF-2429/README.md).

Build

./push_rig.sh

Run

sh run1.sh       # DF-4101 is the third section

Choreography (mirrors the production race window)

  1. queue io ccb-A (healthy, 60 s timeout) β†’ ACTIVE (flags=0x1).
  2. hold β€” the ioctl thread takes bt->lock (the lock bttimeout acquires at bt.c:2143).
  3. racefire 3 β€” a kthread calls the production bttimeout(bccb) and blocks acquiring the lock: this is the in-progress callout invocation that callout_stop at bt.c:1449 cannot cancel.
  4. Under the lock: comp 3 completes ccb-A (BMBI_OK; the callout_stop inside btdone is a no-op against the blocked handler) and queue io ccb-B β€” production btgetccb hands out the same BCCB (bccb_kva identical), which btaction marks ACTIVE again with a fresh 60 s timer.
  5. rel β€” the stale bttimeout acquires the lock and proceeds against ccb-B.

Expected output (run.log)

  • After rel: ccb-B flags=0x7 (DEVICE_RESET set by the stale handler), timer=1 (stale handler's 2s re-arm), console CCB 0xfffff801167df620 - timed out β€” printed for a command whose own 60 s timer never expired. ccb-A meanwhile completed CAM_REQ_CMP (comp=1, last_status=0x1).
  • 2 s later: the stale handler's re-arm fires the hard-reset branch β€” vhba_hard_resets increments (1β†’2; the extra hard reset on a healthy bus), and ccb-B completes ccb_status=0xb (CAM_CMD_TIMEOUT) β€” a healthy command misreported as timed out.

The lock choreography makes the production race window deterministic; every step except the timing is genuine production code.

VERDICT.md
↓ download raw

DF-4101 VERDICT β€” stale bttimeout on a recycled CCB

Bottom line

REPRODUCED β€” the race mechanism, its preconditions and its full consequence chain, all on production code. The timing window (a bttimeout invocation entered but blocked on bt->lock) is exactly the window callout_stop (bt.c:1449) cannot close; the rig makes it deterministic by holding the lock while the stale handler runs, which is what genuine contention does.

Live trace (run.log + console)

[snap 3]  ccb-A: bccb=0xfffff801167df620 flags=0x1 timer=1   (healthy, 60s)
[rig] stale handler thread: calling production bttimeout(0xfffff801167df620),
      will block on bt->lock                                   <- in-progress callout
[comp 3]  ccb-A completed BMBI_OK (callout_stop a no-op against it)
[queue 4] ccb-B: bccb=0xfffff801167df620  <- SAME BCCB recycled by btgetccb
[rel]     lock released -> stale bttimeout proceeds on ccb-B
[snap 3]  ccb-A: comp=1 last_status=0x1 (CAM_REQ_CMP)
[snap 4]  ccb-B: flags=0x7 (DEVICE_RESET set by the STALE handler)
          timer=1 (stale handler's 2s re-arm)
          console: "CCB 0xfffff801167df620 - timed out"  (60s timer never fired)
[snap 4]  after 2s: ccb_status=0xb (CAM_CMD_TIMEOUT) β€” healthy command
          misreported; vhba_hard_resets 1 -> 2 (extra HARD_RESET(0x80)
          write on a healthy bus)

Every claim of the finding is visible: stale handler runs after the CCB was completed and freed; same BCCB recycled for a new command; stale handler sees BCCB_ACTIVE on the new command, freezes the SIM queue, arms BDR, and 2s later hard-resets a healthy bus; the new command completes CAM_CMD_TIMEOUT through no fault of its own.

Root cause (path:line)

  • bt.c:1449 β€” callout_stop(&bccb->timer) does not interlock against an in-progress bttimeout (non-lock-aided callout; the handler is entered and blocked at bt.c:2143 lockmgr(&bt->lock, ...)).
  • bt.c:2147 β€” the BCCB_ACTIVE check cannot distinguish the new command from the one that timed out (no generation handshake).

Impact

Under real contention (timeout firing while the completion path holds bt->lock), a healthy command is misreported CAM_CMD_TIMEOUT, the SIM queue is frozen, and the bus takes an unnecessary BDR followed by a hard reset 2s later β€” I/O disruption across all targets (DoS-class reliability bug, sili DF-4027 / amr DF-4060 family).

Rig fidelity note

The only synthetic element is the guarantee that the handler is mid-execution (the kthread stands in for the fired callout; in production that requires the lock to be held at fire time β€” precisely the race condition). Everything from bttimeout through btdone, btaction, btgetccb recycling and the hard reset is production code from the shipped kernel image, and the console output is the driver's own.

Fix validation

fix.diff implements the generation handshake: bt_ccb gains gen/timer_gen; btgetccb bumps gen, btexecuteccb snapshots it into timer_gen when arming, and bttimeout/btdone ignore invocations whose generation does not match. Patched bt.c+btreg.h compile clean with the kernel's own flags under -Werror (FIXED-COMPILES-CLEAN). No runtime A/B kernel build (race/DoS class, not memory corruption; not mandated).

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_testable
baseline reproduced→ patch + rebuild →patched clean

object-compile validation only (race/DoS class, kernel A/B not mandated): patched bt.c+btreg.h compile clean under -Werror with kernel headers

fix.diff; 'FIXED-COMPILES-CLEAN' with bt.fixed.o on the guest
↓ fix.diffper-fix-DF-4101

Confirmed kernel references

Detail

Exploit chain

bttimeout fires while a completion holds bt->lock -> handler entered, blocks at bt.c:2143 -> btdone completes+frees the CCB (callout_stop no-op against in-flight handler) -> btaction recycles the same BCCB for a new command -> stale handler acquires lock, sees BCCB_ACTIVE on the new command (bt.c:2147 cannot tell generations apart) -> freezes SIMq, stops all pending timers, arms BDR -> 2s later hard bus reset on the healthy bus + CAM_CMD_TIMEOUT miscompletions

Evidence (decisive lines)

run.log DF-4101 section: snap3/snap4 bccb_kva both 0xfffff801167df620; snap4 flags=0x7 timer=1 after rel with ccb-A comp=1 status=0x1; final snap4 ccb_status=0xb timer=0; console '[rig] stale handler thread: calling production bttimeout(0xfffff801167df620), will block on bt->lock' then 'CCB 0xfffff801167df620 - timed out'

PoC changes

race choreographed with explicit lock hold/release ioctls (the production timing window made deterministic); kthread stands in for the fired callout invocation

Verified recommended fix

Generation handshake: bt_ccb gains gen/timer_gen; btgetccb bumps gen, btexecuteccb snapshots it when arming the callout, bttimeout/btdone ignore generation-mismatched invocations (fix.diff, object-compile validated)

Verdict

REPRODUCED: the stale-callout race mechanism demonstrated deterministically on production code. A production bttimeout entered but blocked acquiring bt->lock (the exact in-progress invocation callout_stop at bt.c:1449 cannot cancel) survives the completion of its CCB (ccb-A, BMBI_OK -> CAM_REQ_CMP), the freed BCCB is recycled by production btgetccb for a new healthy command (identical bccb_kva captured), and once the lock is released the stale handler treats the NEW command as timed out: console 'CCB - timed out' for a command whose own 60s timer never fired, BCCB_DEVICE_RESET set, SIM queue frozen, BDR armed, and 2s later a HARD_RESET(0x80) write on the healthy bus (vhba_hard_resets 1->2) with the healthy command miscompleted CAM_CMD_TIMEOUT (0xb). The only choreographed element is the guarantee that the handler is mid-execution -- which is exactly the production contention window; all handler code is the shipped kernel's.