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
This commit is contained in:
Christopher Haster
2026-03-09 17:59:58 -05:00
parent adad0fbbcf
commit 488e84bb53
3 changed files with 208 additions and 16 deletions
+183
View File
@@ -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]