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

umass CBI command-phase error path leaks the in-flight CCB when the device detaches during teardown (sense-fetch fallback is a silent no-op) β€” unkillable hung process + leaked kernel CCB

Summary

The CBI error branch completes via callback(STATUS_CMD_UNKNOWN) entering the sense-fetch machine instead of completing directly; on USB_ERR_CANCELLED during unsetup (xfer array cleared before drain) the NULL-xfer fallback calls umass_cancel_ccb which no-ops because sc_transfer.ccb was already NULLed - the CCB is never xpt_done'd: victim thread sleeps forever in uninterruptible biowait (kill -9 useless), kernel union ccb leaks. BBB immune. Evil CBI device (class 08/subclass 06/protocol 00, $2 MCU can self-unplug by dropping D+) + physical disconnect while a victim READ hangs. Rig-verified down to the final interleaving (both cable-pull and device_disconnect emulations take the AutoSense-Failed detour) but QEMU eagerly cancels in-flight transfers so the exact pending-into-unsetup window is unreachable - honest not_reproduced (emulation limitation, not refutation); path fully source-traced. Complete-directly-on-teardown fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5298 Β· 6 files
FileTypeDescriptionSize
pyevilmsc.py rig evil USB device emulator, CBI mode (cbi R3 0 = hang 4th READ(10) ADSC forever) 12.3 KB view raw
guest-pull-cable.sh trigger guest-side cable pull (usbconfig power_off) + dd observation 343 B view raw
evilmsc-cbi.log runtime-log hang + power_off variant trace 10.2 KB view raw
evilmsc-cbi-disconnect.log runtime-log usbredir device_disconnect variant trace 10.2 KB view raw
README.md analysis full path trace and why the rig cannot close the last interleaving 4.5 KB ↓ raw
VERDICT.md verdict narrative 4.4 KB ↓ raw
README.md analysis full path trace and why the rig cannot close the last interleaving
↓ download raw

DF-5298 β€” umass CBI: CCB leaked when the device detaches while a command is in the control phase

What

For CBI (Control/Bulk/Interrupt) wire protocol, the SCSI command travels as a class ADSC control transfer. umass_t_cbi_command_callback()'s error branch (sys/bus/u4b/storage/umass.c:1864-1882) bypasses umass_cancel_ccb() and does its own completion:

default:            /* Error */
    if ((error == USB_ERR_STALLED) ||
        (sc->sc_transfer.callback == &umass_cam_cb)) {
        sc->sc_transfer.ccb = NULL;            /* umass.c:1873 */
        (sc->sc_transfer.callback)
            (sc, ccb, sc->sc_transfer.data_len,       /* umass.c:1874-1876 */
            STATUS_CMD_UNKNOWN);
    } else { ... umass_tr_error(xfer, error); ... }

umass_cam_cb(STATUS_CMD_UNKNOWN) answers by starting the autosense fetch (umass_command_start, umass.c:2607-2611). If this happens while the driver is being torn down β€” i.e. the CANCELLED callback fires from inside usbd_transfer_unsetup() (usb_transfer.c:1367-1440 clears sc->sc_xfer[] at line 1405 before draining the in-flight transfer at line 1410) β€” then umass_command_start() finds sc->sc_xfer[sc->sc_last_xfer_index] == NULL and falls back to umass_cancel_ccb() (umass.c:1626-1630), which is a no-op because the CCB pointer was already NULLed at line 1873. The CCB is never xpt_done()d and never freed: the issuing thread (e.g. an unkillable biowait/getccb waiter reading the disk) sleeps forever and a kernel CCB leaks.

Contrast with the BBB protocol: every BBB error path funnels through umass_tr_error β†’ umass_cancel_ccb, which completes the CCB exactly once; BBB detach cannot leak. The CBI command-phase branch is the odd one out β€” it completes the CCB optimistically by assuming a sense fetch is still possible.

Trigger (threat model)

Evil CBI USB device (the device chooses its own descriptors β€” class 08 / subclass 06 / protocol 00): serve the CAM probe normally so the disk comes up, then, on a victim READ(10), stop answering the ADSC control transfer (the umass CBI command xfer β€” 5 s timeout β€” is now in flight, sc->sc_transfer.ccb = the victim's CCB, callback == umass_cam_cb), and physically disconnect (a $2 MCU can drop its D+ pullup β€” the classic BadUSB self-unplug). On real hardware the control transfer is still pending when uhub detaches the driver, so the CANCELLED callback fires from inside usbd_transfer_unsetup() with sc->sc_xfer[] already NULL β€” the leak path above.

Verification status (honest)

Source-proven path; not reproduced at runtime on the QEMU rig:

The pack contains a full CBI-mode evil device (pyevilmsc.py … cbi R3 0 β€” hang the 4th READ(10) ADSC forever) plus a guest-side cable pull (guest-pull-cable.sh = usbconfig -d 0.2 power_off). Two detach variants were exercised:

  1. usbredir device_disconnect (evil self-unplug emulation): QEMU's usb-redir eagerly cancels all in-flight transfers when the redirect peer disconnects, so the ADSC errors (USB_RET_IOERROR) before umass_detach() runs; the sense-fetch path then runs with the transfer array still alive, the sense fetch fails on the dead device, umass_tr_error β†’ umass_cancel_ccb completes the CCB β€” the victim dd exits with Input/output error after exactly the hang delay (1.207 s; see evilmsc-cbi-disconnect.log).
  2. guest-side power_off (cable pull emulation) with the ADSC hung forever: same outcome β€” QEMU's UHCI cancels the pending transfer at port-off; dd exits with I/O error (see evilmsc-cbi.log + guest-pull-cable.sh).

Both runs confirm the driver takes the umass_cam_cb(UNKNOWN) sense-fetch detour on CBI command-phase errors (dmesg: (da8:umass-sim0:0:0:0): AutoSense Failed), but QEMU's USB emulation never leaves the control transfer pending into usbd_transfer_unsetup() the way a real cable yank does (real HC hardware keeps the TD queue active on a vanished device; the stack only sees the disconnect via the hub port change). The leak window β€” CANCELLED delivered with sc->sc_xfer[] already NULL β€” is therefore not reachable through usbredir/UHCI emulation, and the finding is filed on the strength of the code trace with runtime evidence of every step except the final teardown interleaving.

Files

  • pyevilmsc.py β€” evil USB device emulator (BBB + CBI modes; cbi R3 0 = hang the 4th READ(10) forever).
  • guest-pull.sh β€” guest-side port power-off (cable pull).
  • evilmsc-cbi.log, evilmsc-cbi-disconnect.log β€” device-side traces of both detach variants.
VERDICT.md verdict narrative
↓ download raw

DF-5298 verdict

Status: NOT REPRODUCED at runtime (source-proven code path; the QEMU USB emulation cannot produce the required teardown interleaving). Claimed impact: local DoS β€” permanent unkillable D-state process plus a leaked kernel CCB, triggered by an evil CBI USB device. Confidence: likely (every step of the path is source-verified and most were runtime-confirmed; the final interleaving is emulation-blocked).

Why the code path is real

  1. CBI commands travel as ADSC control transfers; the in-flight command keeps sc->sc_transfer.ccb set and sc->sc_transfer.callback == &umass_cam_cb.
  2. On ANY error β€” including USB_ERR_CANCELLED β€” umass_t_cbi_command_callback() takes the branch at umass.c:1871-1876 ((error == USB_ERR_STALLED) || (callback == &umass_cam_cb)), NULLs the CCB pointer, and calls umass_cam_cb(..., STATUS_CMD_UNKNOWN).
  3. umass_cam_cb(UNKNOWN) starts the autosense fetch (umass_command_start, umass.c:2607-2611).
  4. During device teardown, usbd_transfer_unsetup() (usb_transfer.c:1378-1439) clears sc->sc_xfer[n] at line 1405 before draining the still-pending transfer at line 1410, so the CANCELLED callback runs with the xfer array already NULL.
  5. umass_command_start() then hits its NULL fallback (umass.c:1626-1630) β†’ umass_cancel_ccb() β€” which is a no-op because step 2 already NULLed sc->sc_transfer.ccb.
  6. The CCB is never xpt_done()d: the issuing thread sleeps forever (uninterruptible biowait for disk I/O) and the union ccb is never freed.

BBB is immune: every BBB error path funnels through umass_tr_error β†’ umass_cancel_ccb, which completes the CCB exactly once. The CBI command-phase branch is the only place that "completes" a CCB by assuming a further transfer is possible.

What was runtime-tested (and what it proved)

Rig: pyevilmsc.py in CBI mode (cbi R3 0) presents a CBI SCSI disk (proto 0x00, no CCI interrupt endpoint β€” legal), serves the whole CAM probe (sense answers carry the previous command's status so the probe finishes), then hangs the ADSC carrying dd's first READ(10) forever. dd (pid observed in D-state) has its CCB in flight as required by step 1. Two detach mechanisms were then exercised:

  • usbredir device_disconnect (evil-device self-unplug): QEMU's usb-redir cancels all in-flight transfers the moment the peer disconnects, so the ADSC errors before umass_detach(). The driver still took the step-2 detour (dmesg: (da8:umass-sim0:0:0:0): AutoSense Failed β€” proving the sense-fetch path ran), the fetch failed on the dead device, umass_tr_error β†’ umass_cancel_ccb completed the CCB, and dd exited with Input/output error after exactly the 1.2 s hang delay.
  • guest-side usbconfig -d 0.2 power_off (cable pull): same outcome β€” QEMU's UHCI cancels the pending transfer at port-off, dd exits with I/O error, device detaches cleanly.

Both runs confirm steps 1-3 of the chain at runtime. The missing ingredient is step 4's interleaving: on real hardware a cable yank does NOT abort the host-controller TD queue β€” the control transfer stays pending until usbd_transfer_unsetup() drains it, which is exactly when the CANCELLED callback meets a NULLed xfer array. QEMU's device model always aborts eagerly on disconnect/port-off, so the window cannot be produced on this rig. This is an emulation limitation, not a refutation β€” nothing in the two runs contradicts the code trace.

In the CBI command error branch, complete the CCB unconditionally through the umass_cancel_ccb() path (status STATUS_WIRE_FAILED) instead of entering the sense fetch when the transport is being torn down, or guard the umass_cam_cb(STATUS_CMD_UNKNOWN) call by verifying sc->sc_xfer[UMASS_T_CBI_COMMAND] != NULL first.

Fix validation

fix_status: not_tested β€” with no runtime manifestation on this rig there is no observable to validate against; the fix is argued from the code path.

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix not built: the bug itself did not manifest at runtime on this rig (emulation limitation, not a refutation of the code path), so there is no observable to validate a fix against

n/a
per-fix-DF-5298

Confirmed kernel references

Detail

Evidence (decisive lines)

evilmsc-cbi.log (hang-ADSC-forever + power_off pull: dd exits I/O error, device removed cleanly); evilmsc-cbi-disconnect.log (usbredir self-unplug: dd exits I/O error after exactly the 1.2s hang, 'AutoSense Failed' in dmesg proving the sense-fetch detour ran); pyevilmsc.py cbi R3 0 mode; guest-pull-cable.sh

PoC changes

CBI-mode evil device added to the usbredir emulator (ADSC command transport on ep0, sense answers reflecting the previous command's status so the CAM probe completes); hang-index can target the Nth READ(10); hang_ms=0 hangs forever; guest-side cable pull via usbconfig power_off

Verified recommended fix

in umass_t_cbi_command_callback's error branch, do not attempt the sense fetch when tearing down: complete via umass_cancel_ccb() semantics (or guard the umass_cam_cb(UNKNOWN) path by checking the xfer array is still alive)

Verdict

Source-proven CCB leak in the umass CBI command-phase error path: umass.c:1864-1882 completes the CCB optimistically by entering the autosense fetch (umass_cam_cb STATUS_CMD_UNKNOWN -> umass_command_start) instead of funnelling through umass_cancel_ccb; when the USB_ERR_CANCELLED callback fires from inside usbd_transfer_unsetup (usb_transfer.c:1405 clears sc->sc_xfer[] before draining), umass_command_start's NULL-xfer fallback (umass.c:1626-1630) becomes a no-op because sc->sc_transfer.ccb was already NULLed at umass.c:1873 - the CCB is never xpt_done()d and never freed (unkillable waiter + kernel memory leak). Not reproduced at runtime: QEMU's USB emulation (usb-redir device_disconnect AND guest-side port power_off) eagerly cancels in-flight transfers before umass_detach() runs, so the sense-fetch path always finds a live transfer and the CCB completes with an error - both emulated detach variants were exercised and each ended with the victim dd exiting with Input/output error (the driver's umass_cam_cb(UNKNOWN) detour itself was confirmed: dmesg 'AutoSense Failed'). A real cable yank leaves the control transfer pending into usbd_transfer_unsetup, which is the exact interleaving the QEMU rig cannot produce.