From b062c88a08869399773dae8f4aa6c24bff4737cd Mon Sep 17 00:00:00 2001 From: aryeg Date: Sun, 19 Oct 2025 08:35:05 +0300 Subject: [PATCH 1/6] Fix comment typo and -> an --- lfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lfs.c b/lfs.c index da4bfca4..f4c5af36 100644 --- a/lfs.c +++ b/lfs.c @@ -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); From dbe96d0326166331dcee9ff15b3b1b935ce44a02 Mon Sep 17 00:00:00 2001 From: Amilcar Ubiera Date: Mon, 27 Oct 2025 09:51:54 -0400 Subject: [PATCH 2/6] Fixes for 'Implicit conversion loses integer precision' warnings. --- lfs.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lfs.c b/lfs.c index da4bfca4..aca67c3f 100644 --- a/lfs.c +++ b/lfs.c @@ -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) { @@ -1501,7 +1501,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) { @@ -1520,7 +1520,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; } @@ -1761,7 +1761,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; From 74f32c83ac58dfa89afce84f44044e9f9f1078c3 Mon Sep 17 00:00:00 2001 From: Daniel Lamana Date: Tue, 25 Nov 2025 12:07:09 +0100 Subject: [PATCH 3/6] Fix broken link on README.md for emu device --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 95db2a0e..0f278ff1 100644 --- a/README.md +++ b/README.md @@ -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 From 2311cd785ae875965a77d848d3f6ee29258fa0fe Mon Sep 17 00:00:00 2001 From: Daimiao Chen Date: Thu, 22 Jan 2026 11:04:59 -0500 Subject: [PATCH 4/6] Guard null callbacks in lfs_dir_fetchmatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lfs_dir_fetch relies on an impossible tag match to avoid calling a NULL callback. Add an explicit cb != NULL check in the match path so future refactors don’t risk a NULL function-pointer call. --- lfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lfs.c b/lfs.c index da4bfca4..fce9d3bf 100644 --- a/lfs.c +++ b/lfs.c @@ -1290,7 +1290,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, } // found a match for our fetcher? - if ((fmask & tag) == (fmask & ftag)) { + if ((fmask & tag) == (fmask & ftag) && (cb != NULL)) { int res = cb(data, tag, &(struct lfs_diskoff){ dir->pair[0], off+sizeof(tag)}); if (res < 0) { From fd5e7f62538d727653a50178dbd578bb33ac0ab6 Mon Sep 17 00:00:00 2001 From: daimiao chen Date: Thu, 5 Mar 2026 16:56:03 -0500 Subject: [PATCH 5/6] Using LFS_ASSERT instead of an runtime check. Remove NULL check from condition and assert callback is valid. --- lfs.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lfs.c b/lfs.c index fce9d3bf..ecedcd8f 100644 --- a/lfs.c +++ b/lfs.c @@ -1290,7 +1290,8 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, } // found a match for our fetcher? - if ((fmask & tag) == (fmask & ftag) && (cb != NULL)) { + 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) { From 488e84bb538c20a62b2976e9d631f81bf3fc51ec Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Mon, 9 Mar 2026 17:59:58 -0500 Subject: [PATCH 6/6] Fixed data corruption with multiple write handles Multiple write handles in littlefs has always been a bit confusing (hopefully improving in littlefs3), but I didn't realize it could lead to corrupted data. The problem, as noted by Ictogan1, is that syncs to related file handles ignores the LFS_F_DIRTY flag. If you open a file twice, and write to one handle, littlefs doesn't realize the other handle it out-of-date. This may not seem like a problem, but then littlefs is happy to reallocate those (still referenced) blocks, leading to data corruption: open(a, "quiche.txt") write(a) sync(a) // syncs a's contents open(b, "quiche.txt") truncate(b) sync(b) // syncs b's contents write(b) // may allocate from a rewind(a) read(a) // potentially corrupted --- What we want is to set LFS_F_DIRTY in all other file handles during lfs_file_sync, but doing so would force those file handles to sync during close. That would be even more confusing (not to mention backwards incompatible). In theory setting both LFS_F_DIRTY + LFS_F_ERRED could work, but that would prevent implicit syncs of writes that haven't actually errored: open(a, "quiche.txt") write(a) open(b, "quiche.txt") write(b) close(b) // syncs b's contents close(a) // should sync a's contents So, instead, as a somewhat clunky workaround, a new flag: LFS_F_DUSTY, which indicates a file does not match storage, but should not be synced during close. --- It's worth noting this is already fixed in littlefs3, which includes a more rigorous, and hopefully easier to use sync model. But in the meantime, this should at least prevent the loss of data. Added test_alloc_multihandle and test_alloc_multihandle_reuse to prevent a regression, test_alloc_multihandle_reuse does reproduce the bug. Found and reproduced by Ictogan1 --- lfs.c | 30 ++++--- lfs.h | 11 +-- tests/test_alloc.toml | 183 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 16 deletions(-) diff --git a/lfs.c b/lfs.c index da4bfca4..78eab28d 100644 --- a/lfs.c +++ b/lfs.c @@ -3244,10 +3244,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 @@ -3429,18 +3431,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 @@ -3485,6 +3481,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; return 0; } #endif @@ -3692,7 +3699,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; return nsize; } #endif @@ -4771,7 +4778,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) { diff --git a/lfs.h b/lfs.h index 215309c5..8fbd5b47 100644 --- a/lfs.h +++ b/lfs.h @@ -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 diff --git a/tests/test_alloc.toml b/tests/test_alloc.toml index 338c75d5..7165f8fd 100644 --- a/tests/test_alloc.toml +++ b/tests/test_alloc.toml @@ -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] +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]