โฌข DragonFlyBSD Kernel Audit
DF-4369 / race_proof.txt
โ† back to finding โ†“ download raw
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.