# DF-4369 — VERDICT

**Status: not_reproduced (hardware-gated — no RocketRAID 27xx controller on the audit guest).**
**Static analysis: the race is real; confidence `likely`.**

## The bug

`sys/dev/raid/hpt27xx/hpt27xx_osm_bsd.c` arms a per-command timeout whenever
a CAM read/write is queued:

- scatter/gather fast path: `callout_reset(ccb->ccb_h.timeout_ch, HPT_OSM_TIMEOUT, hpt_timeout, pCmd)`
  — hpt27xx_osm_bsd.c:730-731
- busdma path: same call from `hpt_io_dmamap_callback` — hpt27xx_osm_bsd.c:530-531

`HPT_OSM_TIMEOUT` is `20*hz` (os_bsd.h:190).

The completion path `os_cmddone()` (hpt27xx_osm_bsd.c:421-466) runs from the
interrupt / taskqueue context (always under the vbus lock, taken by
`hpt_pci_intr()` at :836 or `hpt_do_tasks()` at :386):

```c
static void os_cmddone(PCOMMAND pCmd)
{
	POS_CMDEXT ext = (POS_CMDEXT)pCmd->priv;      /* :423 */
	union ccb *ccb = ext->ccb;
	callout_stop(ccb->ccb_h.timeout_ch);          /* :428 — NOT synchronizing */
	...
	cmdext_put(ext);                              /* :463 — ext back on free list */
	ldm_free_cmds(pCmd);                          /* :464 — pCmd back on LDM freelist */
	xpt_done(ccb);                                /* :465 */
}
```

The timeout handler runs in softclock context **without any lock**:

```c
static void hpt_timeout(void *arg)                /* :411 */
{
	PCOMMAND pCmd = (PCOMMAND)arg;
	POS_CMDEXT ext = (POS_CMDEXT)pCmd->priv;      /* :414 — UAF read if freed */
	ldm_reset_vbus((PVBUS)ext->vbus_ext->vbus);   /* :418 — wild deref if recycled */
}
```

DragonFly's `callout_stop()` explicitly does not protect against this:

> "Stops a callout if it is pending or queued, does not block.
>  This function does not interlock against a callout that is in-progress."
> — sys/kern/kern_timeout.c:1087-1088 (contract in sys/sys/callout.h:131)

So the following interleaving is legal on an SMP box:

| CPU 0 (interrupt, vbus lock held) | CPU 1 (softclock) |
|---|---|
| `os_cmddone(pCmd)` entered | 20 s deadline expires; `hpt_timeout(pCmd)` dequeued and starts running |
| `callout_stop(timeout_ch)` — returns; **cannot stop the in-progress callback** | |
| `cmdext_put(ext)` | |
| `ldm_free_cmds(pCmd)` — pCmd on the LDM freelist | |
| (new I/O on another thread) `ldm_alloc_cmds()` recycles pCmd | reads `pCmd->priv` (:414) — **stale or foreign** |
| | reads `ext->vbus_ext` (:418) |
| | `ldm_reset_vbus(<wild>)` → **fatal trap** |

`priv` is a dedicated field of `struct _COMMAND` (him.h:417), not overlaid by
freelist linkage, so after a plain free→idle it still points at the old (still
mapped, contents-intact) OS_CMDEXT and the outcome is a *benign spurious bus
reset*. The crash needs the freed PCOMMAND to be recycled before :414 runs:

- recycled as a flush command (`hpt_flush_vdev`, hpt27xx_osm_bsd.c:291-303):
  `pCmd->priv = &done` — a **kernel stack address**; `ext->vbus_ext` then
  reads stack garbage and `ldm_reset_vbus()` gets a wild pointer;
- recycled as an LDM-internal command (rebuild/verify, blob code): `priv` per
  the blob's internal convention (can be NULL → NULL-page deref).

## Why it was not reproduced

The audit guest (QEMU/KVM, DragonFly 6.5-DEVELOPMENT X86_64_GENERIC) has the
driver compiled in (`sys/config/X86_64_GENERIC:117`; `kldstat -v` shows
`86 pci/hpt27xx`) but **no HighPoint controller**: `pciconf -l` lists only
i440FX/PIIX4/bochs-VGA/virtio devices, dmesg says
`hpt27xx: no controller detected.` (hpt_final_init, hpt27xx_osm_bsd.c:1021),
there is no `/dev/hpt27xx` and no CAM sim. The racing code paths
(`hpt_scsi_io` → `callout_reset` → `os_cmddone`/`hpt_timeout`) are only
reachable with a real RocketRAID 27xx doing I/O. See `run.log`.

No amount of guest-side setup can synthesize a PCI 0x1103 SAS controller, so
this stays honestly `not_reproduced` (device absent), with the static proof
above carrying the finding.

## Field-trigger scenario (for the maintainer)

Any RocketRAID 27xx system under SMP with storage that completes a command in
the same instant its 20-second deadline expires (failing/detaching disk,
firmware stall recovering at ~20 s, heavy VM stall). Observed field symptom
would be a `Fatal double memory fault` / panic in `hpt_timeout` on storage
boxes, previously attributed to the controller.

## Exploitability

Panic/DoS only: the corrupted dereference is a read; `ldm_reset_vbus` then
chases a wild pointer. There is no attacker-controlled write primitive.
Not a privilege-escalation path.

## Fix

`fix.diff` — serialize `hpt_timeout()` with the completion path by taking the
vbus lock (it is recursive: `lockinit(..., LK_CANRECURSE)`,
hpt27xx_osm_bsd.c:1040) and re-validating `pCmd->priv` before acting. If the
command was recycled, `priv` no longer equals the captured `ext` and the reset
is skipped; if it merely completed and sits idle on the freelist, the reset is
equivalent to today's behavior but now race-free. `callout_stop_sync()` was
deliberately NOT used: `os_cmddone` runs in interrupt context and
`hpt_timeout` needs the same vbus lock, so a synchronizing stop would
deadlock. Fix validation is `not_testable` on this guest (no hardware).

## Verified-negative side work (same file, killed candidates)

See `race_proof.txt` appendix: the `hpt_get_logical_devices` count shuffle is
clamped by the blob (disassembly: the lib's get-logical-devices returns
`min(total, maxcount)`); the cmdext NULL-deref cannot happen (devq openings ==
cmdext pool size == os_max_queue_comm); the flush/ioctl sleep loops are
lost-wakeup-safe because `lksleep` drops the lock atomically and completions
run under the same lock.

## 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.
