littlefs File Integrity Is Not an Application Transaction: Test Multi-File Power-Loss Recovery
littlefs can keep filesystem operations consistent across power loss without making two application files one transaction. If a product needs both files to describe the same configuration, publish them through one durable selection point. Test recovery at that point and between every preparation step, rather than accepting a successful mount as proof of valid application state.
Applicability: the filesystem claims below are checked against littlefs v2.11.2. The proposed design uses the native littlefs API, one filesystem instance, immutable generation files, and a serialized application writer. An RTOS filesystem wrapper, storage cache, or vendor fork needs its own contract review. This is ordinary configuration replacement, not firmware-image or schema migration.

1. Define an invariant that mounting cannot check
Consider a hypothetical dispensing controller with two files. channels contains logical channel names and their physical output assignments. program contains steps referring to those channels. A usable pair must have the same generation identifier, and the program must reference the intended channel-map digest. Each file can parse correctly while the pair is incompatible.
Suppose channels generation 23 is written and closed, followed by a reset before program generation 23 is written. A naive boot routine that opens the latest named copy of each file can combine channels 23 with program 22. Nothing about directory consistency proves that this pairing is safe to run. The failure belongs to the application’s commit protocol.
Write the boot acceptance rule first: a reader loads exactly the pair named by a valid active manifest, verifies lengths and digests, checks the shared generation and cross-file references, and only then enables operation. A digest detects mismatch or corruption within this design; it does not authenticate an attacker-controlled configuration.
2. Use the guarantees at their actual boundary
The upstream v2.11.2 README describes atomic filesystem operations such as rename and explains that file updates commit at sync or close. That does not expose a begin/commit operation spanning arbitrary application writes. Two successful closes remain two separate durability boundaries.
The tagged API header specifies return values for file operations and the block-device sync callback. Check them all. A successful write followed by a failed close is not a successful transaction. The storage integration must also honor the underlying sync requirement; an unflushed device cache breaks the assumptions behind a filesystem-level argument.
Choose the simplest representation that preserves the invariant. Combining small related values into one file may remove the multi-file problem. When separate files are needed, the following generation-manifest protocol concentrates publication into one rename. It is an engineering construction using the documented operation guarantees, not an upstream promise of multi-file transactions.
3. Prepare an immutable generation, then publish it once
- Reserve a fresh generation. Acquire application-level writer ownership. Select a generation name that does not collide with retained or abandoned files. Never overwrite the active generation’s payloads in place.
- Write channels for that generation. Check the complete byte count, synchronize or close successfully, and retain the previous active files unchanged.
- Write program for the same generation. Check its writes and final synchronization. Read back both files and verify parsing, lengths, digests, and cross-file invariants.
- Prepare a temporary manifest. Include the generation, exact filenames, lengths, and integrity fields. Write and close this file successfully. Validate the stored manifest itself.
- Publish. Rename the temporary manifest over the active manifest within the same littlefs instance. Check the result. This operation is the proposed selection boundary.
- Expose the selected generation. Notify readers only after publication is confirmed. If completion is uncertain because of a reset or storage error, resolve the durable state through the boot validation procedure.
The API allows file replacement by rename when source and destination types match. Do not generalize this to a cross-filesystem move or to a wrapper that implements replacement as delete followed by copy. Use unique temporary names and a documented retry rule so an interrupted previous attempt cannot masquerade as the new candidate.
4. Recover from the manifest, not the highest filename
At restart, mount without automatically formatting on error. Read the active manifest and validate the files it names before starting normal operation. A prepared generation with a larger number is not committed merely because it exists. Ignore such orphans for selection; they can be reclaimed later after classification.
For an interruption before publication, the expected selected pair is the previous generation. Across the atomic rename boundary, recovery should resolve to the old or new selection, with its complete payloads already available. Never permit a mixture. After publication has returned success and durability assumptions hold, expect the new selection after restart.
If the manifest or its referenced content fails validation, enter the defined recovery state and preserve evidence. Do not guess a generation by choosing whichever files parse. A product may implement an independently protected fallback selector, but that is an additional protocol requiring its own interruption tests. Keeping old payloads alone does not identify which old pair was approved.
Readers need a snapshot rule too. A task that rereads the active manifest between opening channels and program can create a mixed pair despite a correct on-disk protocol. Capture one manifest generation per logical operation and keep its payloads alive until all corresponding readers finish.
5. Turn every interruption into an observable expectation
Build a fault-injection plan on a disposable image or test unit. Interrupt after generation reservation, during each payload write, after each successful close, during manifest preparation, around rename, and during cleanup. Include failures inside block programming and erase operations through an appropriate storage test harness, not just between high-level calls.
For every restart, record the starting active generation, last acknowledged application operation, injected fault point, mount result, selected manifest, file validation results, and whether outputs remained inhibited until validation finished. Acceptance is a complete allowed old or new pair, never merely two existing files.
Repeat with nearly full storage, abandoned preparations, maximum-size payloads, write errors, sync errors, and repeated resets during recovery. A failed preparation must leave the active pair usable. If there is insufficient room to retain the current pair and prepare a new one, reject the update before removing authoritative data.
6. Make cleanup subordinate to correctness
Reclaim only generations that are neither active nor retained by readers or an explicit fallback policy. Keep cleanup restartable: interruption may leave extra files, but must not remove selected content. Serialize a new publication against cleanup so ownership cannot change halfway through a deletion decision.
The release evidence should contain the invariant, commit boundary, reader-lifetime rule, and fault-to-recovery records for the actual flash integration. No power-cut campaign or littlefs executable was run for this article. The proposed protocol must be qualified on the selected storage hardware, where caching, brownout behavior, and error reporting determine whether its durability assumptions hold.