fix: serialize SdFat FsFile close through HalStorage mutex (#2135)
SdFat's SdSpiCard tracks SPI bus state with an unsynchronized m_spiActive bool. When two tasks call into SdFat concurrently they can confuse that state machine, ending with one task calling SPIClass::endTransaction() against a paramLock the other task holds. That trips FreeRTOS's xTaskPriorityDisinherit assert (tasks.c:5156, pxTCB == pxCurrentTCBs[0]) and panics the system. HalStorage already serialized every explicit method call via storageMutex, but HalFile's destructor was `= default`, which let the underlying SdFat FsFile destructor run close() outside any lock (DESTRUCTOR_CLOSES_FILE=1). Any task that destructed a HalFile while another task was mid-SD-op would race the unsynchronized state. Move the locking discipline into HalFile::Impl::~Impl: an explicit close() under StorageLock, then the FsFile member destructor's redundant close() is a no-op. HalFile's special members can stay = default. Switch storageMutex to xSemaphoreCreateRecursiveMutex so openFileForRead and openFileForWrite can hold the lock while assigning to a HalFile& out-param whose prior Impl needs locked teardown. Priority inheritance still applies to recursive mutexes. Also documented the no-bypass rule in CLAUDE.md: never call SdFat / SdSpiCard / FsBaseFile / SDCardManager directly, never define HAL_STORAGE_IMPL outside HalStorage.cpp. Addresses my admittedly synthetic repro for #2047 Did you use AI tools to help write this code? partial
This commit is contained in:
@@ -161,6 +161,12 @@ if (Storage.openFileForRead("MODULE", "/path/to/file.bin", file)) {
|
||||
|
||||
**Usage**: See example above. Uses `FsFile` (SdFat), NOT Arduino `File`. Do NOT add `file.close()` for local variables (see DESTRUCTOR_CLOSES_FILE above).
|
||||
|
||||
**SdFat is not thread-safe; all SD access MUST go through HalStorage**:
|
||||
- SdFat's `SdSpiCard` tracks SPI bus state with an unsynchronized `m_spiActive` bool. Two tasks calling SdFat concurrently can confuse that state machine and end with one task calling `SPIClass::endTransaction()` against a paramLock the *other* task is holding. That trips FreeRTOS's `xTaskPriorityDisinherit` assert (`tasks.c:5156, pxTCB == pxCurrentTCBs[0]`) and panics the system. See SdFat issue #518.
|
||||
- `HalStorage` serializes everything via `storageMutex`. Downstream code includes `<HalStorage.h>`, which transparently `using FsFile = HalFile;`; every method call (read, write, seek, close) takes the mutex. `HalFile`'s destructor also takes the mutex before letting the underlying SdFat `FsFile` close.
|
||||
- **Never** call into `SdFat` / `SdSpiCard` / `FsBaseFile` / `SDCardManager` directly. **Never** define `HAL_STORAGE_IMPL` outside `HalStorage.cpp`; that disables the `FsFile -> HalFile` typedef and you'll get a raw SdFat handle that bypasses the mutex.
|
||||
- If you're storing a raw `FsFile` in a place that won't transitively include `<HalStorage.h>` (rare), include the header explicitly so the typedef applies.
|
||||
|
||||
---
|
||||
|
||||
## Coding Standards
|
||||
|
||||
Reference in New Issue
Block a user