Skip to content

rp2040/rp23xx: implement endpoint stall queueing and GET_STATUS responses#19452

Open
ricardgb wants to merge 1 commit into
apache:masterfrom
ricardgb:rp2040-rp23xx-stallqueue-getstatus
Open

rp2040/rp23xx: implement endpoint stall queueing and GET_STATUS responses#19452
ricardgb wants to merge 1 commit into
apache:masterfrom
ricardgb:rp2040-rp23xx-stallqueue-getstatus

Conversation

@ricardgb

Copy link
Copy Markdown
Contributor

PR 3 — rp2040/rp23xx: implement stall queueing and GET STATUS responses

Branch: rp2040-rp23xx-stallqueue-getstatus (worktree ~/nuttxspace/wt-pr-stallq)
Base: apache/nuttx master (1b6f918)
References: #19435 (bugs 5 and 6 of that report)

Summary

Two device-controller (DCD) defects in rp2040_usbdev.c / rp23xx_usbdev.c
that break standard host error recovery. Either one alone is fatal to macOS
mass-storage mounting; both are invisible to Linux hosts, whose SCSI stack
neither sends the triggering requests nor recovers with the triggering
timing.

Fix 1: stall queueing (the lost-CSW race)

epsubmit() aborted (-EBUSY) any IN request submitted while the endpoint was
halted. The mass-storage class halts bulk-IN for a failed device-to-host
command (BOT-legal) and then submits the CSW — which was silently dropped, so
after the host's Clear-Halt the endpoint NAKed forever and the host timed out
(macOS: 30 s, then a Bulk-Only-reset/device-reset spiral until it disables
the port). This is exactly the race documented for years in the
usbmsc_scsi.c header comment (David Hewson's analysis: "the subsequent
transfer for the CSW would be dropped on the floor (by the epsubmit()
function) if the end point was still stalled"). The USBMSC_STALL_RACEWAR
sleep workaround only survives hosts that clear the halt within its 100 ms
window — Linux does, macOS does not.

This PR implements the stall-queueing contract that
CONFIG_ARCH_USBDEV_STALLQUEUE documents (and that usbmsc_scsi.c has been
waiting on):

  • epsubmit(): requests submitted while an endpoint is halted are queued
    without arming the hardware (IN and OUT — arming an OUT endpoint rewrites
    the DPSRAM buffer-control word and would silently clear the STALL bit).
  • epstall(stall): cancels any in-flight IN transfer (its hardware buffer is
    disarmed by the STALL write and would otherwise never complete).
  • epstall(resume): resets the data toggle (USB 2.0 9.4.5) and re-arms the
    head of the request queue — this is what delivers the queued CSW.
  • Buffer-status IRQ: completions latched for transfers aborted by a halt are
    ignored instead of being attributed to post-halt queued requests.
  • arch/arm/Kconfig: RP2040/RP23XX now select ARCH_USBDEV_STALLQUEUE,
    which retires the RACEWAR's two 100 ms sleeps per failed command.
  • The now-unused *_abortrequest() helpers are removed.

Fix 2: GET STATUS was never answered

The USB_REQ_GETSTATUS case in *_ep0setup() validated the request but
never queued the two-byte response — for all three recipients
(device/interface/endpoint). EP0 then NAKs the host's data-IN stage forever
and every GET STATUS times out. macOS issues GetPipeStatus = GET
STATUS(endpoint) on a halted pipe ~50 ms after the stall, before Clear-Halt,
and hit this on every probe; lsusb -v's device-status query hangs on it
too. The handler now sends the response (endpoint: halt bit; device:
self-powered; interface: zeros) via epwrite(ep0, response, 2), with the
status stage armed by handle_zlp() exactly as for class-dispatched IN
transfers.

Validation

  • RP2350 (Raspberry Pi Pico 2 W) hardware, composite CDC-ACM + CDC-NCM +
    USBMSC, Linux host, raw-usbfs replay of macOS's exact commands and
    timing:
    • Pre-fix repro, deterministic: CBW (failing MODE SENSE page 0) → data-IN
      stall → wait 1 s → clear-halt → CSW read ETIMEDOUT (fix 1); all five
      GET STATUS variants ETIMEDOUT after 1.5 s (fix 2).
    • Post-fix: CSW survives the halt and is delivered after Clear-Halt with
      correct tag/status/residue across a post-stall delay sweep of
      0/30/50/80/150/500/1000 ms
      in macOS's recovery order (GetPipeStatus →
      clear-halt → GET STATUS → CSW), with the halt bit correctly reported as
      1 before and 0 after the clear; all GET STATUS variants answer in <1 ms.
    • Full macOS-equivalent BOT probe replay ×5 cycles: zero failures. No
      regressions: exact-length SCSI suite, GET MAX LUN + Bulk-Only reset
      10/10, FAT mount + full reads, CDC-NCM/ACM, warm reboots.
    • End-to-end: macOS now mounts the volume (together with the companion
      usbmsc fixes in the separate class-driver PR). The success log shows the
      expected one-time transient bulk-IN halts each cleanly recovered, and no
      EP0 timeouts/resets/port-disable.
  • RP2040: identical shared code, identical fix; build-tested
    (raspberrypi-pico:usbnsh and raspberrypi-pico:composite, the latter
    compiling usbmsc with ARCH_USBDEV_STALLQUEUE in effect). No RP2040
    hardware validation performed.

Full failure chronology, host traces, and the silicon A/B evidence are in
#19435.

Impact

Any class driver that stalls an endpoint and expects queued requests to
survive (mass storage is the canonical case). GET STATUS affects every
configuration — any host or tool that sends it got a control timeout.
With ARCH_USBDEV_STALLQUEUE selected, usbmsc also stops sleeping 200 ms per
failed command (RACEWAR retired).

AI disclosure

This investigation and the fixes were performed by an AI agent (Claude Code),
operated and directed by the submitter, and reviewed by the submitter before
posting.

Two device-controller defects that break standard host error recovery,
found while root-causing why macOS never mounts a NuttX mass-storage
function (full analysis and host traces in apache#19435):

1. epsubmit() aborted (-EBUSY) any IN request submitted while the
   endpoint was halted.  The mass storage class halts bulk-IN for a
   failed device-to-host command (BOT-legal) and then submits the CSW;
   the CSW was dropped on the floor, so after the host's Clear-Halt the
   endpoint NAKed forever and the host timed out (macOS: 30 s, then a
   Bulk-Only-reset/device-reset spiral until it disables the port).
   This is exactly the race documented for years in the usbmsc_scsi.c
   header (David Hewson's analysis); the USBMSC_STALL_RACEWAR sleep
   workaround only survives hosts that clear the halt within 100 ms --
   Linux does, macOS does not, which is why Linux testing never saw it.

   Implement the stall-queueing contract instead: requests submitted
   while an endpoint is halted are queued without arming the hardware
   (arming rewrites the buffer control word and would silently clear
   the STALL bit); a halt terminates any in-flight IN transfer (its
   hardware buffer is disarmed by the STALL write and would never
   complete); clearing the halt resets the data toggle and re-arms the
   head of the queue; stale buffer completions latched for transfers
   aborted by a halt are ignored.  RP2040/RP23XX now select
   ARCH_USBDEV_STALLQUEUE, which also retires the RACEWAR's two 100 ms
   sleeps per failed command.

2. The USB_REQ_GETSTATUS handler in ep0setup() validated the request
   but never queued the two-byte response, for all three recipients
   (device/interface/endpoint), so EP0 NAKed the host's data stage
   forever and every GET STATUS timed out.  macOS issues GetPipeStatus
   = GET STATUS(endpoint) on a halted pipe before running Bulk-Only
   reset recovery and hit this on every probe; lsusb -v's device-status
   query hangs on it as well.  Send the response: endpoint recipient
   reports the halt bit, device recipient reports self-powered,
   interface reports zeros; the status stage is armed by handle_zlp()
   exactly as for class-dispatched IN transfers.

Validated on RP2350 silicon (Raspberry Pi Pico 2 W, composite
CDC-ACM + CDC-NCM + USBMSC): pre-fix, a raw-usbfs replay of macOS's
sequence and timing reproduced both defects deterministically (CSW read
ETIMEDOUT after a delayed clear-halt; all GET STATUS variants
ETIMEDOUT).  Post-fix: the CSW survives the halt and is delivered after
Clear-Halt with correct tag/status/residue across a post-stall delay
sweep of 0-1000 ms; all GET STATUS variants answer immediately with
correct halt reporting; no regressions in the exact-length SCSI suite,
Bulk-Only reset, FAT mount and full reads, CDC-NCM/ACM, or warm
reboots; and macOS now mounts the volume (together with the companion
usbmsc fixes).  The rp2040 driver shares the code and receives the
identical fix (build-tested).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ricard Rosson <ricard@groundbits.com>
@github-actions github-actions Bot added Arch: arm Issues related to ARM (32-bit) architecture Size: M The size of the change in this PR is medium labels Jul 15, 2026
@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: arm Issues related to ARM (32-bit) architecture Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant