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

trm_Reselect spins forever (no exit) if reselected target/LUN does not match any DCB - interrupt-context DoS

Summary

trm_Reselect walks circular DCB list looking for DCB whose TargetID/LUN matches reselected ID. If no DCB matches (malicious/misbehaving SCSI device reselects with never-enumerated ID/LUN) while loop has no escape spins at interrupt context hanging CPU freezing all interrupt processing. DCB list circular no iteration counter no NULL/identity exit. Hardware already accepted reselection only way out bus reset.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2523 Β· 14 files
FileTypeDescriptionSize
README.md β€” 3.0 KB ↓ raw
VERDICT.md β€” 5.5 KB ↓ raw
df252x_trm_rig.c β€” 18.2 KB view raw
trm.h β€” 34.2 KB view raw
Makefile β€” 125 B ↓ download
build.sh β€” 192 B view raw
run.sh β€” 261 B view raw
build.log β€” 5.7 KB view raw
run.log β€” 5.2 KB view raw
run.patched.log β€” 1.0 KB view raw
fix.diff β€” 983 B view raw
fix_build.log β€” 5.6 MB ↓ download
env.txt β€” 710 B view raw
verdict.json β€” 4.0 KB view raw

DF-2523 β€” PoC evidence pack (verify run, 2026-10-14)

Finding: sys/dev/disk/trm/trm.c β€” trm_Reselect() walks the circular DCB list looking for the reselecting target/LUN with no exit when nothing matches (trm.c:2428-2431):

pDCB = pACB->pLinkDCB;
while (RselTarLunId != *((u_int16_t *) &pDCB->TargetID)) {
    pDCB = pDCB->pNextDCB;
}

RselTarLunId is a 13-bit register value (& 0x1FFF, trm.c:2426) while a DCB can only ever pack to (LUN<<8)|ID <= 0x070F β€” a reselection with any higher bit set (malicious or glitching SCSI device driving the reselection phase) matches nothing and the loop spins forever at interrupt context.

How it was verified

Same production-handler rig as DF-2522/24 (df252x_trm_rig.c, zero driver code): fabricated post-attach ACB with a circular 3-DCB ring (ID/LUN = 1/0, 2/0, 3/2), the TRM-S1040 register file emulated behind the kernel's own bus_space accessors, and the running kernel's own trm_Interrupt() dispatched with INT_RESELECTED.

  • control (go=4): TRMREG_SCSI_TARGETID=0x0203 matches ring[2] β€” production trm_Reselect completes (pActiveDCB=ring[2], TmpSRB armed with SRB_UNEXPECT_RESEL, MSG_ABORT in MsgOutBuf, DO_DATALATCH written) and returns;
  • DF-2523 (go=5): TRMREG_SCSI_TARGETID=0x1000 β€” matches no DCB. Dispatch runs in a dedicated kernel thread (df2523_t1) with 3s/6s/9s heartbeat callouts.

What the run showed (stock kernel)

df2523[T1]: entering production trm_Interrupt (RESELECTED,
            RselTarLunId=0x1000 matches NO DCB of the 3-DCB circular list)
df2523[HB]: *** NON-TERMINATION CONFIRMED: 3.-08038 s inside
            trm_Interrupt/trm_Reselect - the DCB ring walk
            (trm.c:2428-2431) has no exit ***
df2523[HB]: *** NON-TERMINATION CONFIRMED: 6.-07982 s ... ***
df2523[HB]: *** NON-TERMINATION CONFIRMED: 9.002041 s ... ***

The "RETURNED" marker never printed. Within ~1 minute the whole guest wedged: ssh stopped answering (connection banner timeout), the serial console stopped echoing, no kernel panic was printed, and recovery required killing QEMU (vm.sh down: "guest not answering (likely DDB on panic)"). An unkillable kernel thread spinning in production code starves DragonFly's per-CPU user scheduler β€” the same non-termination is a hard, unrecoverable lockup on real hardware where the loop runs in the interrupt handler at crit priority (clock included).

Build / run (in guest, as root) β€” run LAST, the guest does not recover

sh build.sh
kldload trm
sh run.sh          # control(4) returns; DF-2523(5) wedges the guest

Fix validation

fix.diff bounds the walk by pACB->DeviceCnt (the driver's own ring bookkeeping) and, when the reselection matches no DCB, drops the connection (STOPDMAXFER + DO_CLRFIFO|DO_HWRESELECT) and returns instead of spinning. Validated on the rebuilt kernel: go=5 returns immediately (df2523[T1]: ... RETURNED), no heartbeat fires, the guest stays healthy; the matching control still completes exactly as before.

VERDICT.md
↓ download raw

DF-2523 VERDICT β€” REPRODUCED (production handler, stock module; guest wedged)

Status: reproduced (DoS) β€” the production trm_Reselect() DCB ring walk (sys/dev/disk/trm/trm.c:2428-2431) has no exit when the reselecting target/LUN matches no DCB. Dispatching the running kernel's own trm_Interrupt() with INT_RESELECTED and an unmatchable RselTarLunId never returned: heartbeat callouts confirmed the thread inside the loop at 3 s, 6 s and 9 s, and the guest subsequently wedged completely (ssh dead, serial console dead, no panic printed; recovery required killing QEMU). The bounded-walk fix returns immediately on a rebuilt module.

Rig (production code)

Same production-handler rig as DF-2522/24: real /boot/kernel/trm.ko loaded, fabricated post-attach ACB with a circular 3-DCB ring (ID/LUN = 1/0, 2/0, 3/2 β€” DeviceCnt=3, production ring bookkeeping), TRM-S1040 register file emulated behind the kernel's own bus_space accessors, dispatch of the module's real trm_Interrupt() (0xffffffff826001d40).

  • Control (go=4): TRMREG_SCSI_TARGETID = 0x0203 packs exactly as *(u_int16_t*)&DCB->TargetID for ring[2] (TargetID=3, LUN=2 β€” the comparison at trm.c:2428 reads TargetID | LUN<<8). Production trm_Reselect found it, armed the unexpected-reselection abort (pActiveDCB=ring[2], pActiveSRB=&TmpSRB, TmpSRB.SRBState=SRB_UNEXPECT_RESEL, MSG_ABORT in MsgOutBuf, DO_DATALATCH written) and returned β€” behavioral proof the production code executed.
  • DF-2523 (go=5): TRMREG_SCSI_TARGETID = 0x1000. The driver masks the register with 0x1FFF (trm.c:2426) but a DCB can only pack to (LUN<<8)|ID ≀ 0x070F (16 targets, 8 LUNs), so bits 11-12 are unmatchable by construction β€” a reselection driven by a malicious or glitching SCSI device (the chip's hardware reselect is enabled, DO_HWRESELECT) enters a loop with no exit: the list is circular, there is no iteration bound, no counter, no NULL/identity exit (trm.c:2428-2431).

What the run showed (stock module, run.log)

Dispatch ran in a dedicated kernel thread (df2523_t1) with 3 s heartbeat callouts:

df2523[T1]: entering production trm_Interrupt (RESELECTED,
            RselTarLunId=0x1000 matches NO DCB of the 3-DCB circular list)
df2523[HB]: *** NON-TERMINATION CONFIRMED: 3.-08038 s inside
            trm_Interrupt/trm_Reselect ***   (Γ—3: 3 s, 6 s, 9 s)

The RETURNED marker never printed. Within roughly a minute the whole guest wedged: further ssh commands produced "Connection timed out during banner exchange", vm.sh status went down, and shutdown reported "guest not answering (likely DDB on panic); killing qemu" β€” an unkillable kernel thread spinning in production driver code starved DragonFly's per-CPU user scheduler until userland (sshd, getty, the serial console) stopped responding. No kernel panic was printed β€” the machine was simply dead.

On real hardware the same loop runs in the driver's interrupt handler (registered via bus_setup_intr, trm_attach), i.e. at interrupt/crit priority: the loop hard-locks the CPU (clock interrupts included) the moment it is entered β€” strictly worse than the rig's thread-context demonstration. The only recovery is the SCSI bus reset that can never happen because the CPU that would issue it is the one spinning.

Fix validation (fix.diff, run.patched.log, fix_build.log)

The fix bounds the walk by the driver's own ring bookkeeping (pACB->DeviceCnt) and, when the reselection matches no DCB, drops the connection (STOPDMAXFER + DO_CLRFIFO|DO_HWRESELECT β€” the recovery the driver's pDCB==NULL exception branch already uses) and returns. It deliberately does NOT loop on DO_RSTSCSI (that would add a second unbounded register poll). Validated on the rebuilt module (same fix.all.diff kernel/module build as DF-2522/24; this scenario touches only the reselect hunk):

df2523[T1]: entering production trm_Interrupt (RESELECTED,
            RselTarLunId=0x1000 matches NO DCB of the 3-DCB circular list)
df2523[T1]: trm_Interrupt RETURNED (unreachable on vulnerable kernel)

No heartbeat fired, uptime answered, guest stayed healthy; the matching control (go=4) behaved identically to stock (pActiveDCB=ring[2], TmpSRB abort path).

Things checked and killed

  • "The chip would never deliver such a value": the register is 13 bits and the driver masks 0x1FFF while DCB encodings cap at 0x070F β€” any stray high bit (device-controlled reselection bytes) yields an unmatchable value. Premise stands.
  • My rig's spin could be a rig artifact: ruled out by the control dispatch returning through the identical production path with only the register value different (match vs no-match is the sole differential).
  • Wedge attribution: heartbeat non-termination precedes the wedge; nothing else runs in T1. The wedge is the OS-level consequence of the non-terminating production loop.

Bottom line

Real bug, demonstrated through the running kernel's own driver code: non-termination proven at 3/6/9 s + total guest lockup requiring QEMU kill; bounded-walk fix returns immediately, verified on a rebuilt module. Gate: machines with Tekram DC395/TRM-S1040 HBAs and a misbehaving/malicious SCSI target (or bus glitch).

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

Rebuilt the driver module from the patched source (same nativekernel build as DF-2522, full log fix_build.log) and re-ran the identical scenario against the patched module's own trm_Interrupt: the unmatchable reselection (0x1000) returns immediately - 'df2523[T1]: trm_Interrupt RETURNED', no heartbeat markers, guest healthy (uptime answered); the matching control behaves exactly as stock (pActiveDCB=ring[2], TmpSRB abort path armed).

run.patched.log: 'df2523[T1]: ... RETURNED (unreachable on vulnerable kernel)' with zero HB markers, vs run.log stock: three NON-TERMINATION markers + total guest wedge requiring QEMU kill.
↓ fix.diffper-fix-DF-2523

Confirmed kernel references

Detail

Exploit chain

DoS primitive only (non-terminating interrupt-context loop => unrecoverable machine lockup); no escalation path.

Evidence (decisive lines)

run.log (full serial): df2523[T1] dispatch lines + three 'df2523[HB]: *** NON-TERMINATION CONFIRMED ... no exit ***' markers at 3/6/9s, no RETURNED marker, guest subsequently unresponsive (vm.sh down reported 'guest not answering (likely DDB on panic); killing qemu'); run.patched.log: 'df2523[T1]: trm_Interrupt RETURNED (unreachable on vulnerable kernel)' with no heartbeat, guest healthy.

PoC changes

No seed PoC existed (empty pack). Built from scratch: production-handler dispatch rig (kldload real trm.ko; trm_Interrupt resolved at kldstat base + nm offset 0x1d40); scenario 5 dispatches in a dedicated kernel thread (df2523_t1) with 3s/6s/9s heartbeat callouts so non-termination is provable before the wedge; matching-ID control (go=4) proves the same production path returns.

Verified recommended fix

Bound the reselect DCB walk by pACB->DeviceCnt and bail out with bus recovery (STOPDMAXFER + DO_CLRFIFO|DO_HWRESELECT) when no DCB matches.

Verdict

Reproduced with the production handler: dispatched the real trm_Interrupt() of kldload'd /boot/kernel/trm.ko with INT_RESELECTED and TRMREG_SCSI_TARGETID=0x1000 (the driver masks the register with 0x1FFF at trm.c:2426, but a DCB can only pack to (LUN<<8)|ID <= 0x070F - any reselection with stray high bits is unmatchable by construction). The production DCB ring walk at trm.c:2428-2431 (circular list, no bound, no escape) never returned: a dedicated kernel thread sat inside it through 3s/6s/9s heartbeat checkpoints, and the guest then wedged completely - ssh stopped answering, the serial console went dead, no panic was printed, and recovery required killing QEMU. On real hardware the loop runs in the interrupt handler at crit priority and hard-locks the CPU outright. Control dispatch (matching ID 0x0203) returns through the identical production path, isolating the no-match loop as the sole differential. The bounded-walk fix (DeviceCnt-bounded search + drop-connection recovery on miss) returns immediately on a rebuilt module.