littlefs Without a Heap: Budget Mount Buffers and Per-File Caches
To use littlefs without heap allocation, provide the mount’s read, program, and lookahead buffers, then give every simultaneously open file its own cache and persistent file configuration. Bound the number of open files explicitly. A static mount buffer alone does not make the complete storage path allocation-free.
This article targets the upstream littlefs v2.11.2 C API. An operating-system filesystem wrapper can add its own allocations, handle tables, locks, or bounce buffers; qualify those separately. The deliverable is a RAM ownership table and exhaustion test plan, rather than a persistence or transaction design.
1. Inventory the long-lived objects
The v2.11.2 header allows caller-supplied read and program buffers of cache_size bytes and a lookahead buffer of lookahead_size bytes. It also requires the filesystem object and mount configuration to remain allocated while mounted. A file opened with lfs_file_opencfg() retains its configuration for the open-file lifetime.
| Owner | Memory to reserve | Lifetime |
|---|---|---|
| Mounted volume | lfs_t, mount configuration, driver context, read cache, program cache, lookahead bitmap. |
From initialization through completed unmount and any required teardown. |
| Each live file slot | lfs_file_t, lfs_file_config, and one file cache. |
From opening until the handle has been closed and released by the wrapper. |
| Custom attributes, if used | Attribute descriptors and referenced buffers. | For every operation that can use the retained file configuration. |
| Integration layer | Locks, request objects, DMA-safe buffers, driver state, thread stacks, and wrapper bookkeeping. | According to the actual integration contract. |
Give these objects named owners. Avoid a helper that constructs a configuration on its stack, opens a file, and returns the handle while the configuration disappears. Likewise, a pointer to a temporary cache is still temporary even when the pointer itself is stored in a global structure.

2. Calculate the cache portion separately
Let C be cache_size, L be lookahead_size, and F be the maximum number of simultaneously open files. For a conventional read/write mount, the caller-provided cache and bitmap reservation is 2C + L + F×C bytes. This excludes filesystem objects, file configurations, attributes, alignment padding, wrapper resources, stacks, and the application payload buffers.
The configuration comments explicitly account for a read cache, a program cache, and an additional cache per file. Check the target compiler’s object sizes and linker map rather than copying structure sizes from a different architecture. Align buffers as required by the integration; littlefs’s byte-buffer assumptions do not remove a controller’s DMA constraints.
For a hypothetical C of 256 bytes, L of 32 bytes, and F of three files, the cache reservation is 1,312 bytes. That is a subtotal, not the complete storage RAM budget. A fourth open file needs another owned slot and cache or a clean admission failure. It must not silently borrow a cache from one of the three active files.
3. Make allocation policy observable
Use the explicit-buffer file-opening API and inspect the actual build definitions. In v2.11.2, LFS_NO_MALLOC removes the normal lfs_file_open() declaration path; the intended alternative is lfs_file_opencfg() with a supplied buffer. Missing buffers otherwise reach the allocation helper. See the mount and file-open implementation.
Do not interpret the macro name as proof that all allocation is impossible. The v2.11.2 utility implementation gives a custom LFS_MALLOC definition precedence over the default allocation branch. Review custom hooks and the linked wrapper, then instrument allocation entry points in a test build. The acceptance condition should state the interval being checked, such as “no heap calls from storage initialization through repeated open/read/write/close cycles.”
If the product permits allocation during startup but prohibits it later, describe that narrower policy honestly. It requires different evidence from a system with no heap implementation at all.
4. Implement bounded file admission
Maintain a pool whose slots contain the handle, file configuration, and uniquely owned cache. Reserve a slot before starting an open operation. If the pool is full, return a documented resource-exhaustion result without disturbing existing handles. If opening fails, release only the slot that the wrapper reserved and follow the API’s failure cleanup rules.
Serialize pool management and filesystem access according to the integration’s concurrency model. A lock does not make cache aliasing safe: two open file objects can retain different cache state even if their calls happen one after another. Reuse memory only after the previous owner’s lifetime ends. Keep a separate configuration in each slot. Do not reuse one mutable global configuration by changing its buffer pointer between opens while earlier files still retain it.
5. Run ownership and exhaustion tests
- Mount with all required buffers supplied and capture any allocator calls from the complete storage stack.
- Open F different files, alternate operations across them, and verify their independent contents and positions.
- Attempt the F+1 open and verify a bounded failure, unchanged existing handles, and no fallback allocation.
- Close one handle, reuse its slot, and check that stale references cannot operate on the new file.
- Inject an open failure and confirm the pool returns to its prior available capacity.
- Use a test build with buffer guards or a memory checker to detect overwrite, aliasing, and lifetime mistakes.
- Repeat mount/unmount and error cleanup cycles to detect slot leaks or accidental reuse of active memory.
Deliberately missing-buffer tests belong in a controlled fixture, with the expected failure matched to the pinned build and custom allocation hooks. Do not assume that every operating-system wrapper exposes upstream littlefs errors unchanged. These tests are proposed here; no target execution is claimed.
Read the failure as a resource problem
A mount that works but the first file open fails can indicate an unprovided file cache. Failure only after several opens suggests pool exhaustion or a leak. Data changing when another file is accessed suggests ownership or buffer aliasing and deserves immediate investigation before blaming flash corruption.
Release the no-heap claim with a linker-map budget, ownership table, allocation trace, and maximum-open-file test. Recheck it whenever adding a wrapper, custom attribute, driver feature, or new concurrent user. The useful guarantee is a bounded, correctly owned resource model that the complete integration actually follows.