DF-4369 / race_proof.txt
DF-4369 โ annotated static proof
================================
[All citations hpt27xx_osm_bsd.c unless noted]
ARM PATH (read/write CCB to hardware)
hpt_action :770 XPT_SCSI_IO
hpt_scsi_io :658 pCmd = ldm_alloc_cmds(...)
:708-715 ext = cmdext_get(vbus_ext); pCmd->priv = ext;
pCmd->done = os_cmddone; pCmd->psg = ext->psg;
SG path :730-731 callout_reset(ccb->ccb_h.timeout_ch,
HPT_OSM_TIMEOUT, hpt_timeout, pCmd);
ldm_queue_cmd(pCmd);
-- or --
busdma :737-742 bus_dmamap_load(..., hpt_io_dmamap_callback, pCmd, ...)
callback :530-531 callout_reset(..., hpt_timeout, pCmd); ldm_queue_cmd;
HPT_OSM_TIMEOUT = 20*hz [os_bsd.h:190]
PCOMMAND.priv is a plain field (not freelist link) [him.h:417]
OS_CMDEXT.vbus_ext only ever written at init [:1061]
COMPLETION PATH (interrupt / task, vbus lock held
via hpt_pci_intr :836 / hpt_do_tasks :386)
os_cmddone :421
:428 callout_stop(ccb->ccb_h.timeout_ch)
:461 bus_dmamap_unload(...)
:463 cmdext_put(ext) <- ext back on cmdext_list
:464 ldm_free_cmds(pCmd) <- pCmd back on LDM freelist
:465 xpt_done(ccb)
callout_stop() contract:
"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]
TIMEOUT PATH (softclock, NO lock)
hpt_timeout :411
:414 ext = (POS_CMDEXT)pCmd->priv <- UAF read after :464
:418 ldm_reset_vbus(ext->vbus_ext->vbus)
RECYCLERS that change priv semantics
hpt_flush_vdev :303 pCmd->priv = &done (kernel STACK, hpt_stop_tasks/
hpt_shutdown_vbus callers)
ldm internal commands (rebuild/verify, binary blob) โ priv per blob
=> interleaving
CPU0: os_cmddone enters CPU1: deadline expires,
CPU0: callout_stop (no-op vs CPU1: hpt_timeout starts
in-progress callout)
CPU0: ldm_free_cmds(pCmd)
CPU0: alloc thread recycles pCmd
CPU1: reads pCmd->priv -> &done (stack) / NULL / new ext
CPU1: ext->vbus_ext -> stack garbage / NULL-page / valid
CPU1: ldm_reset_vbus(wild) -> fatal trap
Outcome distribution: mostly benign spurious reset (freelist memory intact);
panic when the command block is recycled within the window.
DoS only โ no write primitive.
========================================================================
APPENDIX โ candidates examined and killed in this pass (negative results)
========================================================================
1. hpt_get_logical_devices count shuffle OOB (hpt27xx_osm_bsd.c:916-928,
caller hpt_stop_tasks :971-980, DEVICEID ids[32])
Feared: ldm writes TOTAL device count into pIds[0]; shuffle loop
`for (i=0;i<nMaxCount;i++) pIds[i]=pIds[i+1];` would go past ids[31]
when >31 logical devices exist (osm_max_targets = 0xff, osm.h/config).
DISASSEMBLY of the blob (x86_64-elf.hpt27xx_lib.o.uu, ioctl table at
.data+0x1b40, entry 0xff04 -> handler .text 0x7f1c0, worker 0x77330):
the worker writes at most `maxcount` ids and RETURNS min(total, maxcount)
(loop at 0x773d3: `cmp %esi,%r10d; je ret`), and the size validator at
0x7f190 requires nOutBufferSize >= count*4+4. pIds[0] <= 31 => shuffle
and the `for (i=0;i<count;i++)` walk stay inside ids[0..31]. NOT a bug.
2. cmdext exhaustion NULL-deref (HPT_ASSERT(ext) is a no-op in release,
osm.h:234; ext->ccb = ccb would NULL-deref at :710)
Cannot happen through CAM: devq openings (cam_simq_alloc(os_max_queue_comm)
:1071) == cmdext pool size (loop :1055-1069) == os_max_queue_comm, and
os_cmddone returns the ext (:463) BEFORE xpt_done (:465) releases the
opening, so in-flight CCBs (and hence ext demand) never exceed the pool.
3. Flush/ioctl lost-wakeup infinite loops (:307-311, :878-883)
Safe: `done`/`ioctl_cmnd` are checked while the caller holds the vbus
lock; completions run under the same lock (intr :836); hpt_sleep =
lksleep drops the lock atomically at sleep-enqueue (os_bsd.h:198-201).
No window between check and sleep.
4. KdPrint OOB read of short CDBs (:548-552 reads cdb[0..11] regardless of
cdb_len) โ KdPrint(x) is `do{}while(0)` in release (osm.h:233): args
never evaluated. Debug-build-only cosmetic.
5. hpt_ioctl unbounded kmalloc(nInBufferSize/nOutBufferSize) (:1281,:1290)
โ device node is root-only 0600 (make_dev UID_ROOT/S_IRUSR|S_IWUSR
:1143-1144); root self-DoS, not a finding.
6. hpt_ioctl dereferences the user HPT_IOCTL_PARAM directly (piop->Magic
:1265) instead of copyin โ unmapped a_data => kernel page fault; again
root-only (0600 node), not a finding.
7. SG path treats non-PHYS (virtual) segment lists as bus addresses
(:717-728 applies for CAM_SCATTER_VALID without CAM_SG_LIST_PHYS too)
=> DMA to user-chosen numerical "physical" addresses; reachable only
through crafted CAM CCBs (root via /dev/xpt), and DF-1563 already
covers this SG path's lack of validation. Not re-reported.
8. Shutdown worker-task UAF (TASK_ENQUEUE'd worker racing kfree(vbus_ext)
:336-364, no taskqueue drain) โ single-shot at poweroff, ta_context=NULL
mitigation covers the common case; noted, not actionable.
9. dmapool underflow at hpt_free_mem (:211-215, HPT_ASSERT(p) no-op =>
free_pages(NULL)) โ requires hpt_alloc_mem partial failure which returns
before shutdown_eh registration, making hpt_shutdown_vbus unreachable
for that vbus. Dead path.