Repository navigation
supervisor: keep CIRCUITPY writable after a USB bus reset - #11745
Conversation
A bus reset doesn't call tud_umount_cb(), so the blockdev lock that tud_msc_is_writable_cb() took was never released. The next tud_mount_cb() cleared locked[] anyway, blockdev_lock() then failed on the stale flag, and the host saw a write-protected CIRCUITPY until a hard reset. Release a lock that USB MSC still holds when USB is configured again. Fixes adafruit#10149. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dhalbert
left a comment
There was a problem hiding this comment.
Thanks!
usb_msc_umount()has a related problem, which this PR doesn't change: it releases every LUN's lock, including one held by Python or a workflow. On a board with VBUS sensing, an unplug while Python has CIRCUITPY remounted writable would clear Python's lock. Checkinglocked[i]there too would fix that. I can't test it on this board, which has no VBUS sensing, so I'd suggest it as a follow-up.
I would like to add this as part of this PR. If your LLM can suggest a board and a test procedure, I would test a new commit that fixes this.
I've gotten a suggested fix and test procedure from Claude, I'll read through it and test it myself later today, and update this PR when i have verified and tested it :) |
usb_msc_umount() unlocked every LUN's blockdev, including a lock held by a file that Python has open for writing. If the board was unplugged (with VBUS sensing) or the host sent SET_CONFIGURATION(0) while such a file was open, the host got write access when it came back, while Python was still writing. Release only the locks that USB MSC holds, with the helper that the mount path now uses too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I reviewed and tested the fix from Claude and commited the fix: A more precise version of the scenario: since #11530, a file that Python opens for writing holds the filesystem lock until it is closed. Board: any board that shows CIRCUITPY, with a Linux host. Linux can send SET_CONFIGURATION(0) through sysfs, so no unplug or VBUS sensing is needed. For a real unplug, a Feather nRF52840 Express or a Feather STM32F405 Express running on a LiPo works, since both detect VBUS loss. Test procedure (files attached):
Tested on a Retro Arcade on macOS, using IOKit's
The same steps on Linux (a Retro Arcade, |
|
Sorry, this fell off my radar, but I'll look again! |
|
Thanks for persevering on this. I ran your fix through Claude and came up with two suggestions. There was a lot of testing: it's not easy to set up the test conditions, as you found. Patch files for the two code changes are attached. I didn't want to push commits before you had a chance to try these changes. Could you try the patches and re-test? Thanks a lot. 1. Simpler fix for the bus resetIn Releasing the lock on a bus reset and taking it again later leaves a window between The 2. The eject path has the same problemThe eject branch of TestingI tested on a Metro M4 Express:
On Linux, the desktop's "Eject" doesn't send START STOP UNIT. PatchesDownload both attached files and run 0001-supervisor-keep-USB-MSC-locks-across-a-bus-reset.patch |
A bus reset calls tud_mount_cb() without tud_umount_cb(). Keep locked[] instead of releasing the lock: tud_msc_is_writable_cb() then sees that USB MSC already holds it and doesn't lock again. Releasing it and locking again later leaves a window in which filesystem_lock() can take the lock, so the host sees a write-protected drive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eject branch of tud_msc_start_stop_cb() called blockdev_unlock() even when USB MSC didn't hold the lock. If Python had a file open for writing, an eject released Python's lock: storage.remount() then succeeded, the drive went back to the host as writable, and the host and Python could both write to the filesystem. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No worries about it falling off your radar, and thanks for the patches! I agree with both changes, and they're now on the branch as your commits. I re-tested on a Retro Arcade (STM32F412, no VBUS sensing) with a Debian 13 host. The three builds differ only in
The second row is the window you described. Linux doesn't read write-protect again after a device reset, and it sent no writability query, so with my version nobody held the lock until the host's next write. For the eject rows, With all four commits, the firmware is the same size as without the PR. I've updated the PR description to match.
|
dhalbert
left a comment
There was a problem hiding this comment.
Thanks for retesting the new patches. We can say this is done!
I found this while porting CircuitPython to the ELECFREAKS Retro Arcade, a MakeCode Arcade handheld with an STM32F412. The port itself and a few more fixes will follow as separate PRs. All of them have been tested on that hardware.
Problem
When the host resets the USB bus without the board seeing an unplug, CIRCUITPY comes back write-protected. It stays that way until a hard reset: remounting and soft reloads don't help. Hosts reset the bus like this after sleep, when a powered hub keeps the board powered while the computer restarts (#10149), and when a VM attaches the device.
tud_msc_is_writable_cb()takes the blockdev lock on the first writable query and records it inlocked[lun]. The lock is released byusb_msc_umount()or an eject. But TinyUSB only callstud_umount_cb()on an unplug or SET_CONFIGURATION(0). A bus reset only resets the class drivers. The SET_CONFIGURATION that ends the next enumeration callstud_mount_cb(), and_usb_msc_uneject()then clearedlocked[]whileMP_BLOCKDEV_FLAG_LOCKEDstayed set. From then on,blockdev_lock()fails and every writable query returns false.The lock tracking came in with #8699, which fits #10149's report that 9.0.0-alpha.6 was good and 9.0.0-beta.0 bad.
Fix
locked[lun]records that USB MSC holds the blockdev lock. The four commits make every USB MSC path follow that record:_usb_msc_uneject()no longer clearslocked[]. If USB MSC held the lock before the reset, it still holds it, and the nexttud_msc_is_writable_cb()sees that and doesn't lock again. Commit 1 released the lock and let the next writable query take it again; commit 3, from the review, keeps it instead, so Python or a workflow can't take the drive in between.usb_msc_umount()releases only the locks that USB MSC holds, so a file that Python has open for writing keeps its lock.tud_msc_start_stop_cb()releases the lock only if USB MSC holds it.Locks held by Python, the web workflow or BLE are never released by USB MSC.
Testing
ELECFREAKS Retro Arcade (STM32F412, no VBUS sensing) on macOS. IOKit's
USBDeviceReEnumerate()resets the bus with VBUS on.locked[0]back at 0 while the blockdev flag stayed set.storage.remount("/", readonly=False)is still refused afterwards, so USB holds the lock again as it should.SetConfiguration(0), then a re-enumeration) and on Linux (sysfsbConfigurationValue0, then 1):usbreset),bConfigurationValue0 → 1, and eject (sg_start --readonly --eject), with and without a file open for writing. The table is in the review discussion. Without the PR, the bus-reset and file-open cases fail; with it, all of them pass.Not tested on other ports. The web workflow and BLE file transfer aren't tested either: the Retro Arcade has neither. Trying #10149's steps on an RP2040 board would be welcome.
How to run the test
Files:
msc_reset_test.pyandusb_reenumerate.c, also attached at the end of this description. You need a board running CircuitPython with CIRCUITPY mounted, and Python 3; no packages are needed. The script writes a test file to CIRCUITPY, makes the host reset the board's USB connection without unplugging it, and writes again.macOS (tested):
VID PIDare the board's IDs in hex, for example./usb_reenumerate 239a 80cffor a Meowbit. They are also in the board'smpconfigboard.mk, asUSB_VIDandUSB_PID.usb_reenumeratecalls IOKit'sUSBDeviceReEnumerate().Linux (tested on Debian 13):
usbresetis part of usbutils.Windows (tested on Windows 11, with and without this PR): there is no simple reset command. Use #10149's steps instead:
py msc_reset_test.py --check.py msc_reset_test.py --check.Another way: run
py msc_reset_test.pywith no options. When it asks, put the PC to sleep and wake it again, then press Enter. A sleep/wake only sometimes resets the bus, so it may take a few tries.Output without this PR (Windows 11, PowerShell, CIRCUITPY on
F:):Output with this PR (same set-up):
These steps also work on macOS and Linux.
Output with this PR (macOS, Retro Arcade):
Output without it:
After a FAIL, the drive stays read-only until the board is hard-reset.
The same size on the Retro Arcade as without this PR.
Trade-offs and Alternatives
None that I know of. USB MSC now releases only the locks it holds, and keeps them across a bus reset.
Generative AI
I used generative AI tools when creating this PR: Claude Code helped with the code and the testing. I have reviewed the code and tested it by hand on the hardware, and I'm responsible for the code and this description.
🤖 Generated with Claude Code
msc_reset_test.py
usb_reenumerate.c