Commit Graph

525 Commits

Author SHA1 Message Date
Christopher Haster 6cb4e86540 Merge pull request #1194 from littlefs-project/fix-multi-whandle-corruption
Fixed data corruption with multiple write handles
2026-03-24 17:26:24 -05:00
Christopher Haster d3375f1cef Merge pull request #1180 from ChaserTlss/master
lfs: guard null callbacks in lfs_dir_fetchmatch
2026-03-24 17:26:08 -05:00
Christopher Haster 58eaa9836e Merge pull request #1161 from amubiera/size-t-warnings
Fixes for 'Implicit conversion loses integer precision' warnings.
2026-03-24 17:25:43 -05:00
Christopher Haster 488e84bb53 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
2026-03-09 19:13:43 -05:00
daimiao chen fd5e7f6253 Using LFS_ASSERT instead of an runtime check.
Remove NULL check from condition and assert callback is valid.
2026-03-09 19:09:15 -05:00
Daimiao Chen 2311cd785a Guard null callbacks in lfs_dir_fetchmatch
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.
2026-03-09 19:09:06 -05:00
Amilcar Ubiera dbe96d0326 Fixes for 'Implicit conversion loses integer precision' warnings. 2025-11-11 09:15:20 -05:00
aryeg b062c88a08 Fix comment typo and -> an 2025-10-19 08:35:05 +03:00
Christopher Haster ed127050bb Merge pull request #1137 from dschendt/dschendt-fix-dir-count
fix: compact when dir count hits 0x3ff
2025-09-29 15:10:42 -05:00
Christopher Haster ec26996631 Merge pull request #1134 from elupus/patch-2
fix: add missing return causing uninitialized reads
2025-09-29 15:10:26 -05:00
Christopher Haster 4cd2bfc2c1 Fixed inverted dir->count check logic
Curiously, the logic from 48bd2bf was incorrect, and would allow a
commit to be tried if erased _or_ dir->count was at risk of overflow.

That is clearly wrong, we should only try to commit if both conditions
are met...

Found again by dschendt
2025-09-25 15:24:33 -05:00
Christopher Haster f24ff9fb25 Moved dir->count check before commit, limited to < 0xff
This matches the logic originally implemented in 48bd2bf, which was lost
during the big no-recursion refactor 84da4c0.

Other notes:

- Checking >= 0xff matches the split logic during compaction (line
  2158):

    end - split < 0xff

- Grouping dir->erased || dir->count >= 0xff together makes it clear
  these share a common code path.

- Checking for dir->count >= 0xff early avoids committing >8-bit ids to
  disk.

  The cat may already be out-of-the bag on this one, but opening the id
  space up to the full 10-bits should probably be on a non-patch
  release.

Found by dschendt
2025-09-25 13:03:42 -05:00
David 172a186fa9 compact when dir count hits 0x3ff 2025-09-02 19:36:10 -04:00
Joakim Plate 8b75de74c9 fix: add missing return causing uninitialized reads
If lfs_bd_read fails, lfs_fcrc_fromle32 will read uninitialized memory, and hasfcrc will be set to true.

This may end up in a "working" state later due to crcs not matching. but it's hard to follow if that woud be the case.
2025-08-12 17:03:40 +02:00
Joakim Plate 11cecd079c fix: also assert inside lfs_bd_read 2025-08-12 16:29:31 +02:00
Joakim Plate 8c7b6b26e6 fix: false uninitialized read warning
Add asserts on file system reads to make sure
no positive values are returned, which would
make assumptions on error checks invalid.

This fixes clang tidy warnings on uninitialized
reads in uses of lfs_dir_get where only negative
returns are considered errors.
2025-08-12 14:56:29 +02:00
Christopher Haster 8e251dd675 Merge pull request #1110 from Ryan-CW-Code/perf_gc
perf: gc might try to populate the lookahead buffer each time
2025-06-30 11:39:17 -05:00
ryancw d5a86fd28d style: format code, limit to 80 columns. 2025-06-03 09:46:59 +08:00
ryancw 2349ac8c96 perf: gc might try to populate the lookahead buffer each time 2025-05-28 10:23:47 +08:00
ryancw 0755b00c21 refactor: value stored to 'diff' is never read 2025-05-27 20:00:29 +08:00
Christopher Haster 8c458fa6bd Merge pull request #1094 from sosthene-nitrokey/shrink-fs
Add support for shrinking a filesystem
2025-05-13 00:45:32 -05:00
Sosthène Guédon edaaaf88ea Apply review comments 2025-05-07 10:38:43 +02:00
Sosthène Guédon 7d79423972 Rename SHRINKIFCHEAP to SHRINKNONRELOCATING 2025-05-07 10:34:24 +02:00
Sosthène Guédon 9b8f802b43 fixup! Add support for shrinking a filesystem 2025-05-05 11:37:39 +02:00
Christopher Haster a3d6bec5f0 Fixed a double deorphan caused by relocation mid dir remove
Long story short: There is a specific case where removing a directory
can trigger a deorphan pass, but lfs_remove did not check for this,
would try to clean up the (already cleaned) directory orphan, and
trigger an assert:

  lfs.c:4890:assert: assert failed with false, expected eq true
      LFS_ASSERT(lfs_tag_size(lfs->gstate.tag) > 0x000 || orphans >= 0);

The specific case being a remove commit that triggers a relocation that
creates an orphan.

This is also possible in lfs_rename, but only if you're renaming a
directory that implies a remove, which is a pretty rare operation.

---

This was probably an oversight introduced in the non-recursive commit
logic rework.

Fortunately the fix is to just check if we even have an orphan before
trying to remove it. We can rely on this instead of the file type, so
this fix shouldn't even increase the code size.

Found and root-caused by Hugh-Baoa
2025-05-03 18:13:19 -05:00
Sosthène Guédon 2105e502c5 Add support for shrinking a filesystem
This PR adds a new `lfs_fs_shrink`, which functions similarly to
`lfs_fs_grow`, but supports reducing the block count.

This functions first checks that none of the removed block are in use.
If it is the case, it will fail.
2025-04-17 10:07:37 +02:00
Christopher Haster 8ed63b27be Merge pull request #1084 from elupus/fix/packing
fix: avoid assuming struct packing
2025-03-20 01:26:11 -05:00
Christopher Haster 47e738b788 Merge pull request #1071 from RocLoong/patch-1
print lfs_file_size overflow
2025-03-20 01:25:33 -05:00
Christopher Haster 63ab1ffb65 Merge pull request #1068 from littlefs-project/fix-dir-remove-read
Fix dir iteration being broken by concurrent removes
2025-03-20 01:24:04 -05:00
Christopher Haster 61a1b0b496 Tweaked lfs_gstate_iszero for terseness 2025-03-18 02:39:28 -05:00
Joakim Plate ffafb9cbb1 fix: avoid assuming struct packing
lfs_gstate_t was assumed to be a packed array of uint32_t,
but this is not always guaranteed. Access the fields directly
instead of attempting to loop over an array of uint32_t

Fixes clang tidy warnings about use of uninitialized memory
accessed.
2025-03-14 10:03:46 +01:00
Rocloong 936919d134 LFS_TRACE: Fixed sign mismatch in lfs_file_size 2025-02-13 15:46:39 -06:00
Christopher Haster caba4f31df Fixed dir iteration being broken by concurrent removes
When removing a file, we mark all open handles as "removed" (
pair={-1,-1}) to avoid trying to later read metadata that no longer
exists. Unfortunately, this also includes open dir handles that happen
to be pointing at the removed file, causing them to return
LFS_ERR_CORRUPT on the next read.

The good news is this is _not_ actual filesystem corruption, only a
logic error in lfs_dir_read.

We actually already have logic in place to nudge the dir to the next id,
but it was unreachable with the existing logic. I suspect this worked at
one point but was broken during a refactor due to lack of testing.

---

Fortunately, all we need to do is _not_ clobber the handle if the
internal type is a dir. Then the dir-nudging logic can correctly take
over.

I've also added test_dirs_remove_read to test this and prevent another
regression, adapted from tests provided by tpwrules that identified the
original bug.

Found by tpwrules
2025-02-03 22:52:24 -06:00
Amilcar Ubiera 152d03043c Fix for "unsafe use of type bool" warning when compiling with MSVC. 2025-02-03 18:59:14 -05:00
Christopher Haster 366100b140 Fixed incorrect cache reuse when seeking from end-of-block
In v2.5, we introduced an optimization to avoid rereading data when
seeking inside the file cache. Unfortunately this used a slightly
wrong condition to check if the cache was "live", which meant seeks from
end-of-blocks could end up with invalid caches and wrong data. Not
great.

The problem is the nuance of when a file's cache is "live":

1. The file is marked as LFS_F_READING or LFS_F_WRITING.

   But we can't reuse the cache when writing, so we only care about
   LFS_F_READING.

2. file->off != lfs->cfg->block_size (end-of-block).

   This is an optimization to avoid eagerly reading blocks we may not
   actually care about.

We weren't checking for the end-of-block case, which meant if you seeked
_from_ the end of a block to a seemingly valid location in the file
cache, you could end up with an invalid cache.

Note that end-of-block may not be powers-of-two due to CTZ skip-list
pointers.

---

The fix is to check for the end-of-block case in lfs_file_seek. Note
this now matches the need-new-block logic in lfs_file_flushedread.

This logic change may also make lfs_file_seek call lfs_file_flush more
often, but only in cases where lfs_file_flush is a noop.

I've also extended the test_seek tests to cover a few more boundary-read
cases and prevent a regression in the future.

Found by wjl and lrodorigo
2024-12-19 02:39:10 -06:00
Dong Wang 469c863c18 Assert on NULL IO function 2024-12-10 22:54:54 +08:00
Christopher Haster 2fcecc8894 Merge pull request #1046 from littlefs-project/fix-trailing-slashes
paths: Revisit path parsing, fix trailing slash behavior
2024-12-06 13:48:26 -06:00
Christopher Haster 83fe41b605 Merge pull request #1031 from littlefs-project/fix-enospc-issues
Fix metadata_max==prog_size commit->end calculation
2024-12-06 13:47:36 -06:00
Christopher Haster d7a911923b Merge pull request #1027 from littlefs-project/fix-seek-overflow-ub
Fix seek undefined behavior on signed integer overflow
2024-12-06 13:47:20 -06:00
Christopher Haster 2ba4280a5e Merge pull request #997 from littlefs-project/fix-trace-format-again
Fix some more LFS_TRACE format specifiers
2024-12-06 13:47:06 -06:00
Christopher Haster 999ef6656f paths: Changed CREAT with a trailing slash to return NOTDIR
- before: lfs_file_open("missing/") => LFS_ERR_ISDIR
- after:  lfs_file_open("missing/") => LFS_ERR_NOTDIR

As noted by bmcdonnell-fb, returning LFS_ERR_ISDIR here was inconsistent
with the case where the file exists:

  case                           before          after
  lfs_file_open("dir_a")      => LFS_ERR_ISDIR   LFS_ERR_ISDIR
  lfs_file_open("dir_a/")     => LFS_ERR_ISDIR   LFS_ERR_ISDIR
  lfs_file_open("reg_a/")     => LFS_ERR_NOTDIR  LFS_ERR_NOTDIR
  lfs_file_open("missing_a/") => LFS_ERR_ISDIR   LFS_ERR_NOTDIR

Note this is consistent with the behavior of lfs_stat:

  lfs_file_open("reg_a/") => LFS_ERR_NOTDIR
  lfs_stat("reg_a/")      => LFS_ERR_NOTDIR

And the only other function that can "create" files, lfs_rename:

  lfs_file_open("missing_a/")       => LFS_ERR_NOTDIR
  lfs_rename("reg_a", "missing_a/") => LFS_ERR_NOTDIR

There is some ongoing discussion about if these should return NOTDIR,
ISDIR, or INVAL, but this is at least an improvement over the
rename/open mismatch.
2024-11-25 15:40:44 -06:00
Christopher Haster 80ca1ea300 paths: Reject empty paths
Before this, the empty path ("") was treated as an alias for the root.
This was unintentional and just a side-effect of how the path parser
worked.

Now, the empty path should always result in LFS_ERR_INVAL:

- before: lfs_stat("") => 0
- after:  lfs_stat("") => LFS_ERR_INVAL
2024-11-25 15:40:15 -06:00
Christopher Haster 815f0d85a5 paths: Fixed dots followed by dotdots
Unlike normal files, dots (".") should not change the depth when
attempting to skip dotdot ("..") entries.

A weird nuance in the path parser, but at least it had a relatively easy
fix.

Added test_paths_dot_dotdots to prevent a regression.
2024-11-25 15:40:15 -06:00
Christopher Haster dc92dec6d3 paths: Reject dotdots above root
This changes the behavior of paths that attempt to navigate above root
to now return LFS_ERR_INVAL:

- before: lfs_stat("/../a") => 0
- after:  lfs_stat("/../a") => LFS_ERR_INVAL

This is a bit of an opinionated change while making other path
resolution tweaks.

In terms of POSIX-compatibility, it's a bit unclear exactly what dotdots
above the root should do.

POSIX notes:

> As a special case, in the root directory, dot-dot may refer to the
> root directory itself.

But the word choice of "may" implies it is up to the implementation.

I originally implement this as a root-loop simply because that is what
my Linux machine does, but I now think that's not the best option. Since
we're making other path-related tweaks, we might as well try to adopt
behavior that is, in my opinion, safer and less... weird...

This should also help make paths more consistent with future theoretical
openat-list APIs, where saturating at the current directory is sort of
the least expected behavior.
2024-11-25 15:40:07 -06:00
Christopher Haster a6035071be paths: Fixed/doc trailing slash/dot POSIX incompatibilities
- lfs_mkdir now accepts trailing slashes:
  - before: lfs_mkdir("a/") => LFS_ERR_NOENT
  - after:  lfs_mkdir("a/") => 0

- lfs_stat, lfs_getattr, etc, now reject trailing slashes if the file is
  not a directory:
  - before: lfs_stat("reg_a/") => 0
  - after:  lfs_stat("reg_a/") => LFS_ERR_NOTDIR

  Note trailing slashes are accepted if the file is a directory:
  - before: lfs_stat("dir_a/") => 0
  - after:  lfs_stat("dir_a/") => 0

- lfs_file_open now returns LFS_ERR_NOTDIR if the file exists but the
  path contains trailing slashes:
  - before: lfs_file_open("reg_a/") => LFS_ERR_NOENT
  - after:  lfs_file_open("reg_a/") => LFS_ERR_NOTDIR

To make these work, the internal lfs_dir_find API required some
interesting changes:

- lfs_dir_find no longer sets id=0x3ff on not finding a parent entry in
  the path. Instead, lfs_path_islast can be used to determine if the
  modified path references a parent entry or child entry based on the
  remainder of the path string.

  Note this is only necessary for functions that create new entries
  (lfs_mkdir, lfs_rename, lfs_file_open).

- Trailing slashes mean we can no longer rely on the modified path being
  NULL-terminated. lfs_path_namelen provides an alternative to strlen
  that stops at slash or NULL.

- lfs_path_isdir also tells you if the modified path must reference a
  dir (contains trailing slashes). I considered handling this entirely
  in lfs_dir_find, but the behavior of entry-creating functions is too
  nuanced.

  At least lfs_dir_find returns LFS_ERR_NOTDIR if the file exists on
  disk.

Like strlen, lfs_path_namelen/islast/isdir are all O(n) where n is the
name length. This isn't great, but if you're using filenames large
enough for this to actually matter... uh... open an issue on GitHub and
we might improve this in the future.

---

There are a couple POSIX incompatibilities that I think are not
worth fixing:

- Root modifications return EINVAL instead of EBUSY:
  - littlefs: remove("/") => EINVAL
  - POSIX:    remove("/") => EBUSY
  Reason: This would be the only use of EBUSY in the system.

- We accept modifications of directories with trailing dots:
  - littlefs: remove("a/.") => 0
  - POSIX:    remove("a/.") => EBUSY
  Reason: Not worth implementing.

- We do not check for existence of directories followed by dotdots:
  - littlefs: stat("a/missing/..") => 0
  - POSIX:    stat("a/missing/..") => ENOENT
  Reason: Difficult to implement non-recursively.

- We accept modifications of directories with trailing dotdots:
  - littlefs: rename("a/b/..", "c") => 0
  - POSIX:    rename("a/b/..", "c") => EBUSY
  Reason: Not worth implementing.

These are at least now documented in tests/test_paths.toml, which isn't
the greatest location, but it's at least something until a better
document is created.

Note that these don't really belong in SPEC.md because path parsing is
a function of the driver and has no impact on disk.
2024-11-25 15:39:29 -06:00
Christopher Haster ea431bd6ae Added some checks that metadata_max makes sense
Like the read/prog/block_size checks, these are just asserts. If these
invariants are broken the filesystem will break in surprising ways.
2024-10-04 13:45:57 -05:00
Christopher Haster 2d62d2f4c9 Fixed metadata_max==prog_size commit->end calculation
The inconsistency here between the use of block_size vs metadata_max was
suspicious. Turns out there's a bug when metadata_max == prog_size.

We correctly use metadata_max for the block_size/2 check, but we weren't
using it for the block_size-40 check. The second check seems unnecessary
after the first, but it protects against running out of space in a
commit for commit-related metadata (checksums, tail pointers, etc) when
we can't program half-blocks.

Turns out this is also needed when limiting metadata_max to a single
prog, otherwise we risk erroring with LFS_ERR_NOSPC early.

Found by ajheck, dpkristensen, NLLK, and likely others.
2024-10-04 13:45:43 -05:00
wdfk-prog a2c2e49e6b Write the detect cycles function as a function to optimize code 2024-10-04 10:37:25 +08:00
Christopher Haster abaec45652 Fixed seek undefined behavior on signed integer overflow
In the previous implementation of lfs_file_seek, we calculated the new
offset using signed arithmetic before checking for possible
overflow/underflow conditions. This results in undefined behavior in C.

Fortunately for us, littlefs is now limited to 31-bit file sizes for API
reasons, so we don't have to be too clever here. Doing the arithmetic
with unsigned integers and just checking if we're in a valid range
afterwards should work.

Found by m-kostrzewa and lucic71
2024-09-24 14:01:20 -05:00
Christopher Haster ac207586ba Fixed some more LFS_TRACE format specifiers
- block_cycles is signed and should use PRId32
- flags is signed (which is a bit weird) and should be cast for %x

Unfortunately exactly what PRI* expands to is dependant on both the
compiler and the underlying architecture, so I don't think it's possible
for us to catch these mistakes with CI...

Found by stefano-zanotti
2024-06-25 16:08:00 -05:00