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)
PoC verification
Evidence pack
findings/poc/DF-5298 Β· 6 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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:
- usbredir
device_disconnect(evil self-unplug emulation): QEMU'susb-redireagerly cancels all in-flight transfers when the redirect peer disconnects, so the ADSC errors (USB_RET_IOERROR) beforeumass_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_ccbcompletes the CCB β the victimddexits withInput/output errorafter exactly the hang delay (1.207 s; seeevilmsc-cbi-disconnect.log). - guest-side
power_off(cable pull emulation) with the ADSC hung forever: same outcome β QEMU's UHCI cancels the pending transfer at port-off;ddexits with I/O error (seeevilmsc-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.
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
- CBI commands travel as ADSC control transfers; the in-flight command
keeps
sc->sc_transfer.ccbset andsc->sc_transfer.callback == &umass_cam_cb. - On ANY error β including
USB_ERR_CANCELLEDβumass_t_cbi_command_callback()takes the branch atumass.c:1871-1876((error == USB_ERR_STALLED) || (callback == &umass_cam_cb)), NULLs the CCB pointer, and callsumass_cam_cb(..., STATUS_CMD_UNKNOWN). umass_cam_cb(UNKNOWN)starts the autosense fetch (umass_command_start, umass.c:2607-2611).- During device teardown,
usbd_transfer_unsetup()(usb_transfer.c:1378-1439) clearssc->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. umass_command_start()then hits its NULL fallback (umass.c:1626-1630) βumass_cancel_ccb()β which is a no-op because step 2 already NULLedsc->sc_transfer.ccb.- The CCB is never
xpt_done()d: the issuing thread sleeps forever (uninterruptiblebiowaitfor disk I/O) and theunion ccbis 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 beforeumass_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_ccbcompleted the CCB, and dd exited withInput/output errorafter 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.
Recommended fix
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_testablefix 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
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.
No comments yet.