Skip to content

fs: serialize littlefs access with a recursive mutex#2449

Draft
faisal-shah wants to merge 1 commit into
InfiniTimeOrg:mainfrom
faisal-shah:fs-littlefs-mutex
Draft

fs: serialize littlefs access with a recursive mutex#2449
faisal-shah wants to merge 1 commit into
InfiniTimeOrg:mainfrom
faisal-shah:fs-littlefs-mutex

Conversation

@faisal-shah

Copy link
Copy Markdown

Problem

littlefs is not thread-safe, but Controllers::FS is called from three different FreeRTOS tasks with no serialization:

  • BLE host task: FSService runs full littlefs I/O (open/read/write/delete/mkdir/rename) directly inside GATT access callbacks (FSService.cpp).
  • Display task: settings and alarm saves (SettingDisplay.cpp, Alarm.cpp via SaveSettings()/SaveAlarm()), watchface resource loading.
  • SystemTask: fs.Init() and controller loads at boot.

The SpiMaster semaphore only serializes individual SPI bus transactions. The shared lfs_t state (block caches, open-file mlist, lookahead buffer) is unprotected, so e.g. a companion app uploading resources over the BLE FS service while the user flips a setting has two tasks inside littlefs at once, which can corrupt filesystem state.

Fix

Add a FreeRTOS recursive mutex to FS, taken in every public method, and expose a RAII FS::Lock so callers can hold the lock across multi-call sequences. The recursive type is what makes the RAII guard composable: a caller holding FS::Lock still goes through the locked public methods.

Holding the lock across a sequence matters wherever an open file handle must not race a Rename or Delete of the same file: lfs_dir_commit invalidates such handles in the mlist (lfs.c, mlist update on LFS_TYPE_DELETE), so an unlocked reader can otherwise end up on reclaimed blocks.

Existing call sites need no changes.

Cost

One recursive mutex (created in the constructor) and one handle pointer. No API change. Tasks that never overlapped are unaffected; overlapping tasks now briefly block instead of interleaving inside littlefs.

Testing

  • Compiles for the target on top of current main (arm-gcc 10.3, no size surprises: +~900 B text).
  • Exercised in InfiniSim with concurrent filesystem access from multiple threads under AddressSanitizer (200 wake/sleep cycles with continuous parallel FS traffic), no findings. Note InfiniSim's FreeRTOS shim needs xSemaphoreCreateRecursiveMutex/TakeRecursive/GiveRecursive implementations to build this; I have that as a small companion change for InfiniSim and can PR it there if this direction is acceptable.

Marked draft for feedback on the approach; happy to adjust (e.g. per-method granularity or a different guard shape) if maintainers prefer.

littlefs is not thread-safe, yet FS is called from three different
FreeRTOS tasks: the SystemTask (fs.Init, controller loads at boot), the
display task (settings and alarm saves from the settings screens,
watchface resource loading) and the BLE host task (FSService performs
full littlefs I/O inside GATT access callbacks, and DFU writes during a
BLE file transfer can run while the user navigates settings screens).
Nothing serializes these calls today. The SpiMaster semaphore only
serializes individual bus transactions; the shared lfs_t state (caches,
open-file list, lookahead) is unprotected, so a BLE filesystem transfer
racing a settings save can corrupt filesystem state.

Take a recursive mutex in every public FS method and expose a RAII
FS::Lock so callers can hold the lock across multi-call sequences. The
recursive type matters: a caller holding FS::Lock still goes through the
public methods. Holding the lock across a sequence is needed wherever an
open file handle must not race a Rename or Delete of the same file,
since lfs_dir_commit invalidates such handles in the mlist.

Cost: one FreeRTOS recursive mutex and one pointer, no API change.
Callers that never overlapped are unaffected; overlapping callers now
briefly wait instead of interleaving inside littlefs.
@github-actions

Copy link
Copy Markdown

Build size and comparison to main:

Section Size Difference
text 385792B 560B
data 944B 0B
bss 22648B 8B

Run in InfiniEmu

@faisal-shah

Copy link
Copy Markdown
Author

The build-simulator failure is the expected cross-repo coupling: that CI job clones InfiniSim main, whose FreeRTOS shim does not implement the recursive mutex API yet. Companion PR: InfiniTimeOrg/InfiniSim#199 (additive, safe to merge independently). Once it lands, re-running the job here goes green; I verified the combined build locally in the same configuration the CI job uses.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant