Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ More details on how littlefs works can be found in [DESIGN.md](DESIGN.md) and
## Testing

The littlefs comes with a test suite designed to run on a PC using the
[emulated block device](bd/lfs_testbd.h) found in the `bd` directory.
[emulated block device](bd/lfs_emubd.h) found in the `bd` directory.
The tests assume a Linux environment and can be started with make:

``` bash
Expand Down
41 changes: 25 additions & 16 deletions lfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ static int lfs_bd_prog(lfs_t *lfs,
continue;
}

// pcache must have been flushed, either by programming and
// pcache must have been flushed, either by programming an
// entire block or manually flushing the pcache
LFS_ASSERT(pcache->block == LFS_BLOCK_NULL);

Expand Down Expand Up @@ -286,7 +286,7 @@ static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) {

// some operations on paths
static inline lfs_size_t lfs_path_namelen(const char *path) {
return strcspn(path, "/");
return (lfs_size_t)strcspn(path, "/");
}

static inline bool lfs_path_islast(const char *path) {
Expand Down Expand Up @@ -1291,6 +1291,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs,

// found a match for our fetcher?
if ((fmask & tag) == (fmask & ftag)) {
LFS_ASSERT(cb != NULL);
int res = cb(data, tag, &(struct lfs_diskoff){
dir->pair[0], off+sizeof(tag)});
if (res < 0) {
Expand Down Expand Up @@ -1501,7 +1502,7 @@ static lfs_stag_t lfs_dir_find(lfs_t *lfs, lfs_mdir_t *dir,
if (lfs_tag_type3(tag) == LFS_TYPE_DIR) {
name += strspn(name, "/");
}
lfs_size_t namelen = strcspn(name, "/");
lfs_size_t namelen = (lfs_size_t)strcspn(name, "/");

// skip '.'
if (namelen == 1 && memcmp(name, ".", 1) == 0) {
Expand All @@ -1520,7 +1521,7 @@ static lfs_stag_t lfs_dir_find(lfs_t *lfs, lfs_mdir_t *dir,
int depth = 1;
while (true) {
suffix += strspn(suffix, "/");
sufflen = strcspn(suffix, "/");
sufflen = (lfs_size_t)strcspn(suffix, "/");
if (sufflen == 0) {
break;
}
Expand Down Expand Up @@ -1761,7 +1762,7 @@ static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) {

commit->off = noff;
// perturb valid bit?
commit->ptag = ntag ^ ((0x80UL & ~eperturb) << 24);
commit->ptag = ntag ^ ((lfs_tag_t)(0x80 & ~eperturb) << 24);
// reset crc for next commit
commit->crc = 0xffffffff;

Expand Down Expand Up @@ -3244,10 +3245,12 @@ static int lfs_file_open_(lfs_t *lfs, lfs_file_t *file,
#endif

static int lfs_file_close_(lfs_t *lfs, lfs_file_t *file) {
#ifndef LFS_READONLY
int err = lfs_file_sync_(lfs, file);
#else
int err = 0;
#ifndef LFS_READONLY
// it's not safe to do anything if our file errored
if (!(file->flags & LFS_F_ERRED)) {
err = lfs_file_sync_(lfs, file);
}
#endif

// remove from list of mdirs
Expand Down Expand Up @@ -3429,18 +3432,12 @@ static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) {

#ifndef LFS_READONLY
static int lfs_file_sync_(lfs_t *lfs, lfs_file_t *file) {
if (file->flags & LFS_F_ERRED) {
// it's not safe to do anything if our file errored
return 0;
}

int err = lfs_file_flush(lfs, file);
if (err) {
file->flags |= LFS_F_ERRED;
return err;
}


if ((file->flags & LFS_F_DIRTY) &&
!lfs_pair_isnull(file->m.pair)) {
// before we commit metadata, we need sync the disk to make sure
Expand Down Expand Up @@ -3485,6 +3482,17 @@ static int lfs_file_sync_(lfs_t *lfs, lfs_file_t *file) {
file->flags &= ~LFS_F_DIRTY;
}

// mark any other file handles as dirty + desync
for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) {
if (file != f
&& f->type == LFS_TYPE_REG
&& lfs_pair_cmp(f->m.pair, file->m.pair) == 0
&& f->id == file->id) {
f->flags |= LFS_F_DUSTY;
}
}

file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY;
Comment on lines +3485 to +3495

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Only transfer LFS_F_DUSTY after this handle actually commits a new version.

A stale handle can reach this block with LFS_F_DUSTY set but LFS_F_DIRTY clear. In that case, lfs_file_sync_() currently clears its dusty bit and marks siblings dusty even though this handle still points at the orphaned CTZ chain. A later alloc scan/GC can then recycle blocks that this still-open handle may read from.

Suggested fix
 static int lfs_file_sync_(lfs_t *lfs, lfs_file_t *file) {
     int err = lfs_file_flush(lfs, file);
+    bool committed = false;
     if (err) {
         file->flags |= LFS_F_ERRED;
         return err;
     }

     if ((file->flags & LFS_F_DIRTY) &&
             !lfs_pair_isnull(file->m.pair)) {
@@
         }

         file->flags &= ~LFS_F_DIRTY;
+        committed = true;
     }

-    // mark any other file handles as dirty + desync
-    for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) {
-        if (file != f
-                && f->type == LFS_TYPE_REG
-                && lfs_pair_cmp(f->m.pair, file->m.pair) == 0
-                && f->id == file->id) {
-            f->flags |= LFS_F_DUSTY;
+    if (committed) {
+        // mark any other file handles as desynced
+        for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) {
+            if (file != f
+                    && f->type == LFS_TYPE_REG
+                    && lfs_pair_cmp(f->m.pair, file->m.pair) == 0
+                    && f->id == file->id) {
+                f->flags |= LFS_F_DUSTY;
+            }
         }
+        file->flags &= ~LFS_F_DUSTY;
     }

-    file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY;
+    file->flags &= ~LFS_F_ERRED;
     return 0;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lfs.c` around lines 3485 - 3495, The code in lfs_file_sync_ currently clears
LFS_F_DUSTY and marks sibling handles dusty unconditionally; change this so the
sibling-marking loop and the clearing of this handle's LFS_F_DUSTY only happen
when the handle actually committed a new version (i.e. when file->flags has
LFS_F_DIRTY). Concretely, in lfs_file_sync_ check if (file->flags & LFS_F_DIRTY)
before running the for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; ...) loop and
before clearing the LFS_F_DUSTY bit from file->flags; leave LFS_F_DUSTY
untouched if the handle was not dirty to avoid dropping its stale-dusty state.
Ensure you still preserve other flag updates (e.g. LFS_F_ERRED) as appropriate.

return 0;
}
#endif
Expand Down Expand Up @@ -3692,7 +3700,7 @@ static lfs_ssize_t lfs_file_write_(lfs_t *lfs, lfs_file_t *file,
return nsize;
}

file->flags &= ~LFS_F_ERRED;
file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Do not clear LFS_F_DUSTY on first write.

If a stale handle starts writing, it still depends on the old CTZ chain until lfs_file_flush() has copied the untouched tail and lfs_file_sync_() has committed the replacement. Clearing LFS_F_DUSTY here lets traversal stop protecting the old chain too early, so GC/allocation can reuse blocks before the flush finishes.

Suggested fix
-    file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY;
+    file->flags &= ~LFS_F_ERRED;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY;
file->flags &= ~LFS_F_ERRED;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lfs.c` at line 3703, The code currently clears both LFS_F_ERRED and
LFS_F_DUSTY at the first write (file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY;),
which prematurely removes the "dusty" protection; change it to only clear the
error flag here (leave LFS_F_DUSTY set) and ensure LFS_F_DUSTY is cleared later
only after the tail has been copied and committed (i.e., in lfs_file_flush() /
lfs_file_sync_() completion paths).

return nsize;
}
#endif
Expand Down Expand Up @@ -4771,7 +4779,8 @@ int lfs_fs_traverse_(lfs_t *lfs,
continue;
}

if ((f->flags & LFS_F_DIRTY) && !(f->flags & LFS_F_INLINE)) {
if (((f->flags & LFS_F_DIRTY) || (f->flags & LFS_F_DUSTY))
&& !(f->flags & LFS_F_INLINE)) {
int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache,
f->ctz.head, f->ctz.size, cb, data);
if (err) {
Expand Down
11 changes: 6 additions & 5 deletions lfs.h
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,15 @@ enum lfs_open_flags {

// internally used flags
#ifndef LFS_READONLY
LFS_F_DIRTY = 0x010000, // File does not match storage
LFS_F_WRITING = 0x020000, // File has been written since last flush
LFS_F_DIRTY = 0x00010000, // File does not match storage due to write
LFS_F_DUSTY = 0x00020000, // File does not match storage due to desync
LFS_F_WRITING = 0x00040000, // File has been written since last flush
#endif
LFS_F_READING = 0x040000, // File has been read since last flush
LFS_F_READING = 0x00080000, // File has been read since last flush
#ifndef LFS_READONLY
LFS_F_ERRED = 0x080000, // An error occurred during write
LFS_F_ERRED = 0x00100000, // An error occurred during write
#endif
LFS_F_INLINE = 0x100000, // Currently inlined in directory entry
LFS_F_INLINE = 0x01000000, // Currently inlined in directory entry
};

// File seek flags
Expand Down
183 changes: 183 additions & 0 deletions tests/test_alloc.toml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,189 @@ code = '''
}
'''

# multiple handle allocation test
#
# this tests that multiple open handles to the same file don't clobber
# each other
[cases.test_alloc_multihandle]
defines.FILES = 2
defines.SIZE = '(((BLOCK_SIZE-8)*(BLOCK_COUNT-6)) / FILES)'
defines.GC = [false, true]
defines.COMPACT_THRESH = ['-1', '0', 'BLOCK_SIZE/2']
defines.INFER_BC = [false, true]
defines.SYNC = [false, true]
code = '''
const char *names[] = {"eggs", "spinach"};
lfs_file_t files[FILES];

lfs_t lfs;
lfs_format(&lfs, cfg) => 0;
struct lfs_config cfg_ = *cfg;
if (INFER_BC) {
cfg_.block_count = 0;
}
lfs_mount(&lfs, &cfg_) => 0;
lfs_mkdir(&lfs, "breakfast") => 0;

// write one file
char path[1024];
sprintf(path, "breakfast/quiche");
lfs_file_open(&lfs, &files[0], path,
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;
if (GC) {
lfs_fs_gc(&lfs) => 0;
}
size_t size = strlen(names[0]);
for (lfs_size_t i = 0; i < SIZE; i += size) {
lfs_file_write(&lfs, &files[0], names[0], size) => size;
}
// sync?
if (SYNC) {
lfs_file_sync(&lfs, &files[0]) => 0;
}

// write the other file
sprintf(path, "breakfast/quiche");
lfs_file_open(&lfs, &files[1], path,
LFS_O_RDWR | LFS_O_CREAT | LFS_O_TRUNC) => 0;
if (GC) {
lfs_fs_gc(&lfs) => 0;
}
size = strlen(names[1]);
for (lfs_size_t i = 0; i < SIZE; i += size) {
lfs_file_write(&lfs, &files[1], names[1], size) => size;
}
// sync?
if (SYNC) {
lfs_file_sync(&lfs, &files[1]) => 0;
}

// try to read from both
for (int n = 0; n < FILES; n++) {
lfs_file_rewind(&lfs, &files[n]) => 0;
size_t size = strlen(names[n]);
for (lfs_size_t i = 0; i < SIZE; i += size) {
uint8_t buffer[1024];
lfs_file_read(&lfs, &files[n], buffer, size) => size;
assert(memcmp(buffer, names[n], size) == 0);
}
}
for (int n = 0; n < FILES; n++) {
lfs_file_close(&lfs, &files[n]) => 0;
}
lfs_unmount(&lfs) => 0;

// check after remounting
lfs_mount(&lfs, &cfg_) => 0;
{
// last one wins
int n = FILES-1;
char path[1024];
sprintf(path, "breakfast/quiche");
lfs_file_t file;
lfs_file_open(&lfs, &file, path, LFS_O_RDONLY) => 0;
size_t size = strlen(names[n]);
for (lfs_size_t i = 0; i < SIZE; i += size) {
uint8_t buffer[1024];
lfs_file_read(&lfs, &file, buffer, size) => size;
assert(memcmp(buffer, names[n], size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
}
lfs_unmount(&lfs) => 0;
'''

# multiple handle allocation reuse test
[cases.test_alloc_multihandle_reuse]
defines.FILES = 2
defines.SIZE = '(((BLOCK_SIZE-8)*(BLOCK_COUNT-6)) / (FILES+1))'
defines.CYCLES = [1, 10]
defines.INFER_BC = [false, true]
defines.SYNC = [false, true]
Comment on lines +346 to +351

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the missing GC test define.

Lines 371 and 388 branch on GC, but this case never declares defines.GC. That makes the generated test invalid before it can exercise the reuse path.

Suggested fix
 [cases.test_alloc_multihandle_reuse]
 defines.FILES = 2
 defines.SIZE = '(((BLOCK_SIZE-8)*(BLOCK_COUNT-6)) / (FILES+1))'
 defines.CYCLES = [1, 10]
 defines.INFER_BC = [false, true]
+defines.GC = [false, true]
 defines.SYNC = [false, true]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[cases.test_alloc_multihandle_reuse]
defines.FILES = 2
defines.SIZE = '(((BLOCK_SIZE-8)*(BLOCK_COUNT-6)) / (FILES+1))'
defines.CYCLES = [1, 10]
defines.INFER_BC = [false, true]
defines.SYNC = [false, true]
[cases.test_alloc_multihandle_reuse]
defines.FILES = 2
defines.SIZE = '(((BLOCK_SIZE-8)*(BLOCK_COUNT-6)) / (FILES+1))'
defines.CYCLES = [1, 10]
defines.INFER_BC = [false, true]
defines.GC = [false, true]
defines.SYNC = [false, true]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_alloc.toml` around lines 346 - 351, The test case
[cases.test_alloc_multihandle_reuse] is missing the defines.GC entry which other
branches (lines referencing GC) depend on; add a defines.GC define (e.g.,
defines.GC = [false, true]) to the block so the generated test includes both GC
variations and can exercise the reuse path referenced by the GC branches.

code = '''
const char *names[] = {"eggs", "spinach"};
lfs_file_t files[FILES];

lfs_t lfs;
lfs_format(&lfs, cfg) => 0;
struct lfs_config cfg_ = *cfg;
if (INFER_BC) {
cfg_.block_count = 0;
}

lfs_mount(&lfs, &cfg_) => 0;
lfs_mkdir(&lfs, "breakfast") => 0;

// write one file
char path[1024];
sprintf(path, "breakfast/quiche");
lfs_file_open(&lfs, &files[0], path,
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;
if (GC) {
lfs_fs_gc(&lfs) => 0;
}
size_t size = strlen(names[0]);
for (lfs_size_t i = 0; i < SIZE; i += size) {
lfs_file_write(&lfs, &files[0], names[0], size) => size;
}
// sync?
if (SYNC) {
lfs_file_sync(&lfs, &files[0]) => 0;
}

for (int c = 0; c < CYCLES; c++) {
// write the other file
sprintf(path, "breakfast/quiche");
lfs_file_open(&lfs, &files[1], path,
LFS_O_RDWR | LFS_O_CREAT | LFS_O_TRUNC) => 0;
if (GC) {
lfs_fs_gc(&lfs) => 0;
}
size = strlen(names[1]);
for (lfs_size_t i = 0; i < SIZE; i += size) {
lfs_file_write(&lfs, &files[1], names[1], size) => size;
}
// sync?
if (SYNC) {
lfs_file_sync(&lfs, &files[1]) => 0;
}

// try to read from both
for (int n = 0; n < FILES; n++) {
lfs_file_rewind(&lfs, &files[n]) => 0;
size_t size = strlen(names[n]);
for (lfs_size_t i = 0; i < SIZE; i += size) {
uint8_t buffer[1024];
lfs_file_read(&lfs, &files[n], buffer, size) => size;
assert(memcmp(buffer, names[n], size) == 0);
}
}

lfs_file_close(&lfs, &files[1]) => 0;
}
lfs_file_close(&lfs, &files[0]) => 0;
lfs_unmount(&lfs) => 0;

// check after remounting
lfs_mount(&lfs, &cfg_) => 0;
{
// last one wins
int n = (SYNC) ? FILES-1 : 0;
char path[1024];
sprintf(path, "breakfast/quiche");
lfs_file_t file;
lfs_file_open(&lfs, &file, path, LFS_O_RDONLY) => 0;
size_t size = strlen(names[n]);
for (int i = 0; i < SIZE; i += size) {
uint8_t buffer[1024];
lfs_file_read(&lfs, &file, buffer, size) => size;
assert(memcmp(buffer, names[n], size) == 0);
}
lfs_file_close(&lfs, &file) => 0;
}
lfs_unmount(&lfs) => 0;
'''

# exhaustion test
[cases.test_alloc_exhaustion]
defines.INFER_BC = [false, true]
Expand Down
Loading