From 1f81ddb1eddc18d2a85c0e6bdd959da105fbf6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vegard=20W=C3=A6rp?= Date: Wed, 4 Nov 2026 13:45:05 +0100 Subject: [PATCH 1/4] supervisor: keep CIRCUITPY writable after a USB bus reset 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 #10149. Co-Authored-By: Claude Opus 5.5 --- supervisor/shared/usb/usb_msc_flash.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/supervisor/shared/usb/usb_msc_flash.c b/supervisor/shared/usb/usb_msc_flash.c index 01b0eae2e10..1c1378dfd7a 100644 --- a/supervisor/shared/usb/usb_msc_flash.c +++ b/supervisor/shared/usb/usb_msc_flash.c @@ -205,7 +205,15 @@ static fs_user_mount_t *get_vfs(int lun) { static void _usb_msc_uneject(void) { for (uint8_t i = 0; i < LUN_COUNT; i++) { ejected[i] = false; - locked[i] = false; + // A bus reset doesn't call tud_umount_cb(), so a lock from before it may still be held. + // Release it, or blockdev_lock() fails and the host sees a write-protected drive. + if (locked[i]) { + fs_user_mount_t *vfs = get_vfs(i); + if (vfs != NULL) { + blockdev_unlock((supervisor_vfs_t *)vfs); + } + locked[i] = false; + } } } From 789384d72abb54e5fc2e644c4fa9a454b4100085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vegard=20W=C3=A6rp?= Date: Wed, 4 Nov 2026 19:41:33 +0100 Subject: [PATCH 2/4] supervisor: release only USB MSC's own locks in usb_msc_umount() 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 --- supervisor/shared/usb/usb_msc_flash.c | 35 ++++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/supervisor/shared/usb/usb_msc_flash.c b/supervisor/shared/usb/usb_msc_flash.c index 1c1378dfd7a..ae3e19e6f3b 100644 --- a/supervisor/shared/usb/usb_msc_flash.c +++ b/supervisor/shared/usb/usb_msc_flash.c @@ -202,18 +202,26 @@ static fs_user_mount_t *get_vfs(int lun) { return NULL; } +// Release the locks that USB MSC holds. Locks held by others (Python, workflows) are not touched. +static void release_msc_locks(void) { + for (uint8_t i = 0; i < LUN_COUNT; i++) { + if (!locked[i]) { + continue; + } + fs_user_mount_t *vfs = get_vfs(i); + if (vfs != NULL) { + blockdev_unlock((supervisor_vfs_t *)vfs); + } + locked[i] = false; + } +} + static void _usb_msc_uneject(void) { + // A bus reset doesn't call tud_umount_cb(), so a lock from before it may still be held. + // Release it, or blockdev_lock() fails and the host sees a write-protected drive. + release_msc_locks(); for (uint8_t i = 0; i < LUN_COUNT; i++) { ejected[i] = false; - // A bus reset doesn't call tud_umount_cb(), so a lock from before it may still be held. - // Release it, or blockdev_lock() fails and the host sees a write-protected drive. - if (locked[i]) { - fs_user_mount_t *vfs = get_vfs(i); - if (vfs != NULL) { - blockdev_unlock((supervisor_vfs_t *)vfs); - } - locked[i] = false; - } } } @@ -222,14 +230,7 @@ void usb_msc_mount(void) { } void usb_msc_umount(void) { - for (uint8_t i = 0; i < LUN_COUNT; i++) { - fs_user_mount_t *vfs = get_vfs(i); - if (vfs == NULL) { - continue; - } - blockdev_unlock((supervisor_vfs_t *)vfs); - locked[i] = false; - } + release_msc_locks(); } // Release the USB MSC lock on CIRCUITPY (LUN 0), if USB MSC holds it. From 9c52a565cf68281bf76e4d5a91cdad8ae6421a2d Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 11 Nov 2026 19:14:13 -0500 Subject: [PATCH 3/4] supervisor: keep USB MSC locks across a bus reset 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 --- supervisor/shared/usb/usb_msc_flash.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/supervisor/shared/usb/usb_msc_flash.c b/supervisor/shared/usb/usb_msc_flash.c index ae3e19e6f3b..914ba55e10b 100644 --- a/supervisor/shared/usb/usb_msc_flash.c +++ b/supervisor/shared/usb/usb_msc_flash.c @@ -217,9 +217,8 @@ static void release_msc_locks(void) { } static void _usb_msc_uneject(void) { - // A bus reset doesn't call tud_umount_cb(), so a lock from before it may still be held. - // Release it, or blockdev_lock() fails and the host sees a write-protected drive. - release_msc_locks(); + // A bus reset doesn't call tud_umount_cb(), so USB MSC may still hold its locks. + // Keep them: tud_msc_is_writable_cb() sees locked[] and doesn't lock again. for (uint8_t i = 0; i < LUN_COUNT; i++) { ejected[i] = false; } From dd6a146d41005687720a8f2601f4a71f0ccb7bd9 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Wed, 11 Nov 2026 19:14:49 -0500 Subject: [PATCH 4/4] supervisor: release only USB MSC's own lock on eject 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 --- supervisor/shared/usb/usb_msc_flash.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/supervisor/shared/usb/usb_msc_flash.c b/supervisor/shared/usb/usb_msc_flash.c index 914ba55e10b..0bf19c2b9b1 100644 --- a/supervisor/shared/usb/usb_msc_flash.c +++ b/supervisor/shared/usb/usb_msc_flash.c @@ -501,9 +501,13 @@ bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, boo if (disk_ioctl(current_mount, CTRL_SYNC, NULL) != RES_OK) { return false; } else { - blockdev_unlock((supervisor_vfs_t *)current_mount); + // Release the lock only if USB MSC holds it. Locks held by others + // (Python, workflows) are not touched. + if (locked[lun]) { + blockdev_unlock((supervisor_vfs_t *)current_mount); + locked[lun] = false; + } ejected[lun] = true; - locked[lun] = false; } } else { // We can only load if it hasn't been ejected.