Commit Graph

29 Commits

Author SHA1 Message Date
Christopher Haster bac2464b8f Renamed lfs->cfg->shrub_size -> lfs->cfg->inline_size
While I think shrub_size is probably the more correct name at a
technical level, inline_size is probably more what users expect and
doesn't require a deeper understanding of filesystem details.

The only risk is that users may think inline_size has no effect on large
files, when in fact it still controls how much of the btree root can be
inlined.

There's also the point that sticking with inline_size maintains
compatibility with both the upstream version and any future version that
has other file representations.

May revisit this, but renaming to lfs->cfg->inline_size for now.
2025-02-11 02:50:38 -06:00
Christopher Haster 6cd29bede2 Dropped lfs->cfg->inline_size
Now that we no longer have bmoss files, inline_size and shrub_size are
effectively the same thing.

We weren't using this, so no code change, but it does save a word of
ctx:

           code          stack          ctx
  before: 36280           2576          640
  after:  36280 (+0.0%)   2576 (+0.0%)  636 (-0.6%)
2025-02-11 02:50:38 -06:00
Christopher Haster 109bd4e0ab Added lfsr_fs_cksum
This just exposes the gcksum to the user, but exposing the gcksum allows
the user to store it externally for an extra layer of protection against
filesystem corruption.

As far as I'm aware this is the only real way to protect against global
rollback issues, which is a problem for any filesystem with logs (aka
any powerloss-resilient filesystem).

This required a comically small amount of code:

           code          stack          ctx
  before: 38492           2624          640
  after:  38500 (+0.0%)   2624 (+0.0%)  640 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster 47438a8c46 Fixed test_ck_spam*'s open file bshrub/btree issues
- In lfsr_mtree_traverse, we traverse open file bshrubs/btrees before
  we validate the gcksum, which means bugs/asserts can slip through
  before we have a chance to detect something is wrong.

  To work around this, I've added an explicit mdir cksum check right
  before we start traversing an open mdir's bshrubs/btrees. If an open
  mdir doesn't match the on-disk state, the on-disk state must contain
  an error (or the RAM, but that's a different story and wayyy out of
  scope).

  It might be better to rearrange lfsr_mtree_traverse to check gcksums
  first, but this will require another look at our traversal clobbering
  logic.

- For a similar reason, ckfetches can't detect open bshrub/btree
  corruption as is. As its name suggests, ckfetches only checks fetches,
  so any corruption after we've fetched bshrubs/btrees in lfsr_file_open
  will go undetected.

  Fortunately this just means we need a full ckmeta-scan in
  test_ck_spam* tests that keep open files.

  In real use, full ckmeta-scans should be preferred anyways. Limiting
  these scans to mtreeonly was just an attempt to better stress btree
  ckfetches.

  At least we're still testing ckmeta+mtreeonly+ckfetches in
  test_ck_spam_dir_fuzz and test_ck_spam_file_fuzz.

This gets the test_ck_spam* tests running under all of the current
interesting ck-modes.

Code changes:

           code          stack          ctx
  before: 38560           2640          644
  after   38572 (+0.0%)   2640 (+0.0%)  644 (+0.0%)
2025-02-08 14:53:47 -06:00
Christopher Haster cae8b08dc9 Reworked test_ck_spam* tests to rely on gcksums
Now that gcksums are working and we can detect rollback issues, it's
worth revisiting our most aggressive bit-error tests.

Unfortunately, I think due to focusing on ckprogs, these were a bit less
ready-to-go than I had hoped. We still have the read-hole, so the sort
of errors we can expect to detect is a bit limited.

Still, managed to come up with some schemes that I think are
interesting:

- ckprogs - Limited to catching bit-errors during progs, but these tests
  work great.

- ckdata - Limited to manual bit-errors, but can detect both metdata +
  data errors.

- ckmeta+ckfetches - Limited to manual bit-errors, ckmeta detects
  mtree errors, while ckfetches detects btree + data errors.

- ckmeta+ckdatacksums - Limited to manual bit-errors, ckmeta detects
  metadata errors, while ckdatacksums detects data errors.

To make testing manual bit-errors a bit easier, and to avoid
reimplementing the bit randomizer in emubd, I added
LFS_EMUBD_BADBLOCK_MANUAL and lfs_emubd_flip to let the tests manually
control when bits flip.

---

Unfortunately open files are proving to be an issue for these tests,
since we don't really expect corrupted metadata after lfsr_file_open (
assuming no read-hole).

For now I've limited these new ck-modes to the tests without open files,
but we should probably revisit this.
2025-02-08 14:53:47 -06:00
Christopher Haster 57e9c3b706 Check gcksum during traversals, harder ckmeta/ckdata tests
This adds a check that the on-disk gcksum matches the in-RAM gcksum
in lfsr_mtree_traverse, so ckmeta/ckdata scans should now be able to
at least detect global-rollback issues that occur while mounted.

This also moves the LFS_I_CKMETA/CKDATA flag clearing logic from
lfsr_mtree_gc -> lfsr_mtree_traverse. There's no reason to not clear
these flags if we've made a successful traversal. We weren't actually
calling lfsr_mtree_traverse with the right flags for this to matter, but
it does let us drop an explicit flag clear in lfsr_fs_ck.

---

These changes were a part of adding the harder versions of our ckmeta/
ckdata tests, where we flip individual bits instead of clobbering the
entire block. These are more realistic errors and stress our gcksum
system.

Recalculating the gcksum required another gcksum copy in
lfsr_traversal_t, which adds a bit of code and ctx to our incremental-gc
build:

                   code          stack          ctx
  default before: 38428           2640          644
  default after:  38560 (+0.3%)   2640 (+0.0%)  644 (+0.0%)

  gc before:      38484           2640          788
  gc after:       38616 (+0.3%)   2640 (+0.0%)  792 (+0.5%)

Unfortunately we can't easily abuse the copies in lfs_t since
multiple traversals may be open at once.
2025-02-08 14:53:47 -06:00
Christopher Haster 1c5adf71b3 Implemented self-validating global-checksums (gcksums)
This was quite a puzzle.

The problem: How do we detect corrupt mdirs?

Seems like a simple question, but we can't just rely on mdir cksums. Our
mdirs are independently updateable logs, and logs have this annoying
tendency to "rollback" to previously valid states when corrupted.

Rollback issues aren't littlefs-specific, but what _is_ littlefs-
specific is that when one mdir rolls back, it can disagree with other
mdirs, resulting in wildly incorrect filesystem state.

To solve this, or at least protect against disagreeable mdirs, we need
to somehow include the state of all other mdirs in each mdir commit.

---

The first thought: Why not use gstate?

We already have a system for storing distributed state. If we add the
xor of all of our mdir cksums, we can rebuild it during mount and verify
that nothing changed:

   .--------.   .--------.   .--------.   .--------.
  .| mdir 0 |  .| mdir 1 |  .| mdir 2 |  .| mdir 3 |
  ||        |  ||        |  ||        |  ||        |
  || gdelta |  || gdelta |  || gdelta |  || gdelta |
  |'-----|--'  |'-----|--'  |'-----|--'  |'-----|--'
  '------|-'   '------|-'   '------|-'   '------|-'
  '--.------'  '--.------'  '--.------'  '--.------'
   cksum |      cksum |      cksum |      cksum |
     |   |        v   |        v   |        v   |
     '---------> xor -------> xor -------> xor -------> gcksum
         |            v            v            v         =?
         '---------> xor -------> xor -------> xor ---> gcksum

Unfortunately it's not that easy. Consider what this looks like
mathematically (g is our gcksum, c_i is an mdir cksum, d_i is a
gcksumdelta, and +/-/sum is xor):

  g = sum(c_i) = sum(d_i)

If we solve for a new gcksumdelta, d_i:

  d_i = g' - g
  d_i = g + c_i - g
  d_i = c_i

The gcksum cancels itself out! We're left with an equation that depends
only on the current mdir, which doesn't help us at all.

Next thought: What if we permute the gcksum with a function t before
distributing it over our gcksumdeltas?

   .--------.   .--------.   .--------.   .--------.
  .| mdir 0 |  .| mdir 1 |  .| mdir 2 |  .| mdir 3 |
  ||        |  ||        |  ||        |  ||        |
  || gdelta |  || gdelta |  || gdelta |  || gdelta |
  |'-----|--'  |'-----|--'  |'-----|--'  |'-----|--'
  '------|-'   '------|-'   '------|-'   '------|-'
  '--.------'  '--.------'  '--.------'  '--.------'
   cksum |      cksum |      cksum |      cksum |
     |   |        v   |        v   |        v   |
     '---------> xor -------> xor -------> xor -------> gcksum
         |            |            |            |   .--t--'
         |            |            |            |   '-> t(gcksum)
         |            v            v            v          =?
         '---------> xor -------> xor -------> xor ---> t(gcksum)

In math terms:

  t(g) = t(sum(c_i)) = sum(d_i)

In order for this to work, t needs to be non-linear. If t is linear, the
same thing happens:

  d_i = t(g') - t(g)
  d_i = t(g + c_i) - t(g)
  d_i = t(g) + t(c_i) - t(g)
  d_i = t(c_i)

This was quite funny/frustrating (funnistrating?) during development,
because it means a lot of seemingly obvious functions don't work!

- t(g) = g              - Doesn't work
- t(g) = crc32c(g)      - Doesn't work because crc32cs are linear
- t(g) = g^2 in GF(2^n) - g^2 is linear in GF(2^n)!?

Fortunately, powers coprime with 2 finally give us a non-linear function
in GF(2^n), so t(g) = g^3 works:

  d_i = g'^3 - g^3
  d_i = (g + c_i)^3 - g^3
  d_i = (g^2 + gc_i + gc_i + c_i^2)(g + c_i) - g^3
  d_i = (g^2 + c_i^2)(g + c_i) - g^3
  d_i = g^3 + gc_i^2 + g^2c_i + c_i^3 - g^3
  d_i = gc_i^2 + g^2c_i + c_i^3

---

Bleh, now we need to implement finite-field operations? Well, not
entirely!

Note that our algorithm never uses division. This means we don't need a
full finite-field (+, -, *, /), but can get away with a finite-ring (+,
-, *). And conveniently for us, our crc32c polynomial defines a ring
epimorphic to a 31-bit finite-field.

All we need to do is define crc32c multiplication as polynomial
multiplication mod our crc32c polynomial:

  crc32cmul(a, b) = pmod(pmul(a, b), P)

And since crc32c is more-or-less just pmod(x, P), this lets us take
advantage of any crc32c hardware/tables that may be available.

---

Bunch of notes:

- Our 2^n-bit crc-ring maps to a 2^n-1-bit finite-field because our crc
  polynomial is defined as P(x) = Q(x)(x + 1), where Q(x) is a 2^n-1-bit
  irreducible polynomial.

  This is a common crc construction as it provides optimal odd-bit/2-bit
  error detection, so it shouldn't be too difficult to adapt to other
  crc sizes.

- t(g) = g^3 is not the only function that works, but it turns out to be
  a pretty good one:

  - 3 and 2^(2^n-1)-1 are coprime, which means our function t(g) = g^3
    provides a one-to-one mapping in the underlying fields of all crc
    rings of size 2^(2^n).

    We know 3 and 2^(2^n-1)-1 are coprime because 2^(2^n-1)-1 =
    2^(2^n)-1 (a Fermat number) - 2^(2^n-1) (a power-of-2), and 3
    divides Fermat numbers >=3 (A023394) and is not 2.

  - Our delta, when viewed as a polynomial in g: d(g) = gc^2 + g^2c +
    c^3, has degree 2, which implies there are at most 2 solutions or
    1-bit of information loss in the underlying field.

    This is optimal since the original definition already had 2
    solutions before we even chose a function:

      d(g) = t(g + c) - t(g)
      d(g) = t(g + c) - t((g + c) - c)
      d(g) = t((g + c) + c) - t(g + c)
      d(g) = d(g + c)

  Though note the mapping of our crc-ring to the underlying field
  already represents 1-bit of information loss.

- If you're using a cryptographic hash or other non-crc, you should
  probably just use an equal sized finite-field.

  Though note changing from a 2^n-1-bit field to a 2^n-bit field does
  change the math a bit, with t(g) = g^7 being a better non-linear
  function:

  - 7 is the smallest odd-number coprime with 2^n-1, a Fermat number,
    which makes t(g) = g^7 a one-to-one mapping.

    3 humorously divides all 2^n-1 Fermat numbers.

  - Expanding delta with t(g) = g^7 gives us a 6 degree polynomial,
    which implies at most 6 solutions or ~3-bits of information loss.

    This isn't actually the best you can do, some exhaustive searching
    over small fields (<=2^16) suggests t(g) = g^(2^(n-1)-1) _might_ be
    optimal, but that's a heck of a lot more multiplications.

- Because our crc32cs preserve parity/are epimorphic to parity bits,
  addition (xor) and multiplication (crc32cmul) also preserve parity,
  which can be used to show our entire gcksum system preserves parity.

  This is quite neat, and means we are guaranteed to detect any odd
  number of bit-errors across the entire filesystem.

- Another idea was to use two different addition operations: xor and
  overflowing addition (or mod a prime).

  This probably would have worked, but lacks the rigor of the above
  solution.

- You might think an RS-like construction would help here, where g =
  sum(c_ia^i), but this suffers from the same problem:

    d_i = g' - g
    d_i = g + c_ia^i - g
    d_i = c_ia^i

  Nothing here depends on anything outside of the current mdir.

- Another question is should we be using an RS-like construction anyways
  to include location information in our gcksum?

  Maybe in another system, but I don't think it's necessary in littlefs.

  While our mdir are independently updateable, they aren't _entirely_
  independent. The location of each mdir is stored in either the mtree
  or a parent mdir, so it always gets mixed into the gcksum somewhere.

  The only exception being the mrootanchor which is always at the fixed
  blocks 0x{0,1}.

- This does _not_ catch "global-rollback" issues, where the most recent
  commit in the entire filesystem is corrupted, revealing an older, but
  still valid, filesystem state.

  But as far as I am aware this is just a fundamental limitation of
  powerloss-resilient filesystems, short of doing destructive
  operations.

  At the very least, exposing the gcksum would allow the user to store
  it externally and prevent this issue.

---

Implementation details:

- Our gcksumdelta depends on the rbyd's cksum, so there's a catch-22 if
  we include it in the rbyd itself.

  We can avoid this by including it in the commit tags (actually the
  separate canonical cksum makes this easier than it would have been
  earlier), but this does mean LFSR_TAG_GCKSUMDELTA is not an
  LFSR_TAG_GDELTA subtype. Unfortunate but not a dealbreaker.

- Reading/writing the gcksumdelta gets a bit annoying with it not being
  in the rbyd. For now I've extended the low-level lfsr_rbyd_fetch_/
  lfsr_rbyd_appendcksum_ to accept an optional gcksumdelta pointer,
  which is a bit awkward, but I don't know of a better solution.

- Unlike the grm, _every_ mdir commit involves the gcksum, which means
  we either need to propagate the gcksumdelta up the mroot chain
  correctly, or somehow keep track of partially flushed gcksumdeltas.

  To make this work I modified the low-level lfsr_mdir_commit__
  functions to accept start_rid=-2 to indicate when gcksumdeltas should
  be flushed.

  It's a bit of a hack, but I think it might make sense to extend this
  to all gdeltas eventually.

The gcksum cost both code and RAM, but I think it's well worth it for
removing an entire category of filesystem corruption:

           code          stack          ctx
  before: 37796           2608          620
  after:  38428 (+1.7%)   2640 (+1.2%)  644 (+3.9%)
2025-02-08 14:53:30 -06:00
Christopher Haster 9c9a23e27b gc: Renamed lfsr_gc -> lfsr_fs_gc, keep lfsr_fs_unck in non-gc
- lfsr_gc -> lfsr_fs_gc
- lfsr_gc_unck -> lfsr_fs_unck

lfsr_fs_unck is surprisingly still useful in non-gc builds, since we
still have ckmeta/ckdata state. These flags can still be queried with
lfsr_fs_stat and cleared with lfsr_fs_ckmeta/ckdata/lfsr_traversal_t, so
it seems useful to keep this function around.

It's also a relatively cheap function.

Though this does mean it deserves a rename. Dropping the gc prefix
hopefully makes it clearer this function is not entirely gc-specific.

And since we no longer have lfsr_gc_setflags/setsteps, it makes sense to
rename lfsr_gc back to lfsr_fs_gc, to be consistent with the other
filesystem-wide utilities.

Code changes, apparently lfsr_fs_unck costs 12 bytes:

                   code          stack          ctx
  default before: 37792           2608          620
  default after:  37804 (+0.0%)   2608 (+0.0%)  620 (+0.0%)

  gc before:      37938           2608          768
  gc after:       37940 (+0.0%)   2608 (+0.0%)  768 (+0.0%)
2025-01-28 14:41:45 -06:00
Christopher Haster 1b3054db89 gc: Moved incremental gc behind ifdef LFS_GC
Incremental gc, being stateful and not gc-able (ironic), was always
going to need to be conditionally compilable.

This moves incremental gc behind the LFS_GC define, so that we can focus
on the "default" costs. This cuts lfs_t in nearly half!

  lfs_t with LFS_GC:   308
  lfs_t without LFS_C: 168 (-45.5%)

This does save less code than one might expect though. We still need
most of the internal traversal/gc logic for things like block allocation
and orphan cleanup, so most of the savings is limited to the RAM storing
the incremental state:

                          code          stack          ctx
  before:                37916           2608          768
  after with LFS_CFG:    37944 (+0.1%)   2608 (+0.0%)  768 (+0.0%)
  after without LFS_CFG: 37796 (-0.3%)   2608 (+0.0%)  620 (-19.3%)

On the flip side, this does mean most of the incremental gc
functionality is still availables in the lfsr_traversal_t APIs.

Applications with more advanced gc use-cases may actually benefit from
_not_ enabling the incremental gc APIs, and instead use the
lfsr_traversal_t APIs directly.
2025-01-28 14:41:45 -06:00
Christopher Haster 5d756fe698 gc: Tweaked lfsr_gc API to be more stateful
Before:

  int lfsr_fs_gc(lfs_t *lfs, lfs_soff_t steps, uint32_t flags);

After:

  int lfsr_gc(lfs_t *lfs);
  int lfsr_gc_setflags(lfs_t *lfs, uint32_t flags);
  int lfsr_gc_setsteps(lfs_t *lfs, lfs_soff_t steps);

---

The interesting thing about the lfsr_gc API is that the caller will
often be very different from whoever configures the system. One example
being an OS calling lfsr_gc in a background loop, while leaving
configuration up to the user.

The idea here, is instead of forcing the OS to come up with its own
stateful system to pass flags to lfsr_gc, we just embed this state in
littlefs directly. The whole point of lfsr_gc is that it's a stateful
system anyways.

Unfortunately this state does require a bit more logic to maintain,
which adds code/ctx cost:

           code          stack          ctx
  before: 37812           2608          752
  after:  37916 (+0.3%)   2608 (+0.0%)  768 (+2.1%)
2025-01-28 14:41:45 -06:00
Christopher Haster 1d21355707 Renamed ckcksums -> ckdatacksums
To clarify this only checks data reads, and to makes space for future
theoretical ck-operations:

- ckmetaredund - likely
- ckdataredund - unlikely, expensive
- ckmetacksums - unlikely, expensive
- ckdatacksums - implemented

This also tweaks the relevant mount/format/info flags a bit:

  LFS_M_CKPROGS       0x00100000 Check progs by reading back progged data
  LFS_M_CKFETCHES     0x00200000 Check block checksums before first use
  LFS_M_CKPARITY      0x00400000 Check metadata tag parity bits
  LFS_M_CKMETAREDUND+ 0x01000000 Check metadata redund blocks on reads
  LFS_M_CKDATAREDUND* 0x02000000 Check data redund blocks on reads
  LFS_M_CKMETACKSUMS* 0x04000000 Check metadata checksums on reads
  LFS_M_CKDATACKSUMS  0x08000000 Check data checksums on reads

  +Planned
  *Hypothetical

No code changes.
2025-01-28 14:41:45 -06:00
Christopher Haster 7edb3b231f Limited ckcksums to check data cksums
So... Long store short, checking metadata cksums is just intractably
slow.

But data cksums?

Yes checking data cksums is still O(b^2), but unlike metadata lookups,
which involve many small backwards reads, data reads are very easy to
cache. So instead of O(b^2), it's more like O(b^2/c), where c is your
rcache size.

Still O(b^2) when c << b, but I'm not sure that's avoidable without
adding more cksums.

At the very least, if you have enough RAM, c == b reduces this to O(b),
which is nice for "large" systems that want hardened reads without a
performance loss.

---

But why bother checking data cksums if we still have a read-hole with
metadata cksums?

Well, while considering the problem in the context of future features, I
noticed something _really interesting_:

- ckredund + metadata - reasonable ✓
- ckredund + data     - impractical ✗, parity fanout + O(f+r) is bad
- ckcksums + metadata - impractical ✗, small reads + O(b^2) is bad
- ckcksums + data     - reasonable ✓, assuming enough rcache

The current planned design for data redundancy makes it also intractably
slow to check every read, since it would require xoring all blocks that
contribute to the relevant parity block, but this isn't a problem for
metadata redundancy.

So while neither ckredund nor ckcksums can tractably close the read-hole
on their own, it looks like together they will be able to cover
everything without completely sacrificing performance. Neat!

Of course this isn't possible if ckcksums/ckredund imply checking both
metadata and data, so they need to be split apart.

And I don't really see a point in keeping the intractable variants
around in the codebase.

---

Dropping metadata ckcksums also means we can get rid of the ugly
lfsr_bd_ckrbydprefix and lfsr_bd_ckrbydsuffix functions, which were
basically duplicating all of lfsr_rbyd_fetch. That was quite a wart!

This saves a nice chunk of code when ckcksums is enabled:

                    code          stack          ctx
  default before:  38128           2624          752
  default after:   38128 (+0.0%)   2624 (+0.0%)  752 (+0.0%)

  ckparity before: 39724           3048          764
  ckparity after:  39700 (-0.1%)   3048 (+0.0%)  760 (-0.5%)

  ckcksums before: 40612           3184          772
  ckcksums after:  39396 (-3.0%)   3096 (-2.8%)  760 (-1.6%)
2025-01-28 14:41:45 -06:00
Christopher Haster 18190054d9 Trying to better use uncreat/zombie/orphan terms in tests
Renamed a bunch of tests:

- test_forphans_create_* -> test_forphans_uncreat_*
- test_forphans_cleanup_opened -> test_forphans_cleanup_open
- test_forphans_cleanup_orphaned -> test_forphans_cleanup_uncreat
- test_forphans_orphanzombie_fuzz -> test_forphans_uz_fuzz
- test_forphans_orphanzombiedir_fuzz -> test_forphans_uzd_fuzz
- test_*_oz_fuzz -> test_*_uz_fuzz
- test_*_ozd_fuzz -> test_*_uzd_fuzz
- test_traversal_*_orphan_* -> test_traversal_*_uncreat_*
- test_traversal_*_orphaned -> test_traversal_*_uncreat
- test_attrs_fattr_orphan -> test_attrs_fattr_uncreat

And renamed a number of variables and things.
2025-01-28 14:41:45 -06:00
Christopher Haster 2f11fa71f4 Implemented ckcksums
Since we already need all the machinery to track ck info for ckparity, I
figured we might as well implement a full ckcksums option as well.

Ckcksums closes the checksum-read-hole by reading enough data to check a
relevant checksum on ever read, even if this ends up being significantly
more data than the initial request. This should always detect detectable
bit-errors, even if they occur between consecutive reads.

If this sounds naive, that's because it is. Performance will be awful.

To be clear, ckcksums should probably never be used in production. I
can't think of a use case that isn't better handled by either ECC in the
block device or the future-planned ckredund feature. Just look at the
runtime complexities:

                  small-reads  rbyd-lookup  rbyd-compaction
  ckcksums:            O(b^2)   O(b log b)     O(b^2 log b)
  ckredund*: O(log_b(n) + xb)     O(log b)       O(b log b)
  eccbd*:                O(b)     O(log b)       O(b log b)

  * theoretical

We've already seen that O(b^2) compactions turns a performance problem
into a tractability problem, so I think O(b^2 log b) compactions will be
a bit too much for most applications.

We can already seen this in our test_ck_ckcksums_* tests (which do pass
by the way!). Compare to test_ck_ckprogs_*, which is basically the same
set of tests:

  test_ck_ckprogs_*:   6.08s
  test_ck_ckcksums_*: 64.88s

Or consider test_rbyd with/without ckcksums:

  test_rbyd:           12.21s
  test_rbyd+ckcksums: 389.94s

Still, ckcksums is an interesting proof-of-concept, and does manage to
close the checksum-read-hole.

---

Like ckprogs/ckfetches/ckparity/etc, ckcksums is an opt-in feature,
requiring both 1. defining LFS_CKCKSUMS and 2. passing LFS_M_CKCKSUMS at
mount time.

Like ckparity, ckcksums requires a significant code and stack increase
to track ck info in lfsr_data_t:

                 code          stack
  before:       36416           2616
  yes-ckcksums: 38872 (+6.7%)   3176 (+21.4%)
  no-ckcksums:  36416 (+0.0%)   2616 (+0.0%)

It's interesting to note how this compares to all of the current
ck-modes, though each has their own set of tradeoffs:

                 code          stack
  default:      36416           2616
  ckprogs:      36468 (+0.1%)   2616 (+0.0%)
  ckfetches:    36666 (+0.7%)   2648 (+1.2%)
  ckparity:     37996 (+4.3%)   3040 (+16.2%)
  ckcksums:     38872 (+6.7%)   3176 (+21.4%)

---

Note that even though ckcksums is opt-in, it may still be worth removing
from the codebase in the future, for a couple reasons:

- Every feature, even if unused, adds developer/maintenance burden.

- Ck info is particularly messy with how it interacts with all
  lfsr_data_t APIs. Though getting rid of ck info would also require
  getting rid of ckparity.

- It's possible for a user to see ckcksums in the codebase,
  misunderstand its tradeoffs, enable it, and get the impression that
  littlefs itself is just unusably slow.
2024-08-20 00:32:00 -05:00
Christopher Haster 4515f4811a ckparity: Increased bit-error tests to first 6 bytes
It's only the 7th byte (first leb128) that can fail to detect single-bit
errors. This is slightly more interesting since we actually test the
parity of a tag, and not just the revision count.
2024-08-20 00:32:00 -05:00
Christopher Haster 2d121c8d19 Relegated ckreads -> ckparity
Ckparity is pretty flawed in littlefs, for several reasons. The biggest
one being that we can't even reliably detect single-bit errors.

But! It can still provide an extra layer of safety in a system where you
don't care about the extra code/stack cost.

And, for ckreads, performance cost...

Performance isn't a big problem for parity-checking. We can assume
metadata tags are going to relatively small (and can be controlled by
fragment_size). But for data checksums, ckreads risks O(b^2) when
performing many small reads, which can be a bit of a problem.

And since ckreads doesn't really prove anything interesting about the
system anymore, it makes sense to unbundle these two checks, rename
ckreads -> ckparity, and limit it to only checking parity bits.

This way, you can enable ckparity for a bit of extra safety, with a
code/stack cost hit, but without sacrificing performance.

---

I was hoping more code/stack savings, but since we still need to track
parity context in lfsr_data_t, and still need to intercept bd_read/cmp/
cpy calls that reference metadata, we end up needing to keep most of
the ck circuitry around:

                    code          stack
  default before:  36464           2672
  default after:   36464 (+0.0%)   2672 (+0.0%)

                    code          stack
  ckparity before: 38036           3080
  ckparity after:  38024 (-0.0%)   3080 (+0.0%)

We even end up still tracking checksum context for bptrs! Maybe we
should just go ahead and add ckcksums as a joke...
2024-08-20 00:30:29 -05:00
Christopher Haster ba09513e7d Fixed mdir-relocate-pcache corruption, test_ck_spam_* bitflips
Ckprogs does not suffer from rollback issues! I was too quick to assume
this was the case in test_ck_spam_* (I blame ckfetches), but it just
turned out that the more aggressive bit flip tests found an actual bug!

The bug in question is caused by bit-errors being introduced in multiple
blocks during mdir relocation.

When relocating, we make the false assumption that if
lfsr_mdir_compact__ returns success, the intermediary compaction has
successfully been written to disk. But this is not true until we
write the rest of the commit and flush the pcache. If the remaining
commit fails due to a bit-error, the pcache can end up corrupt and the
intermediary compaction lost.

But why do we care about the intermediary compaction at all after
corruption? Why do we keep updating the mdir every attempted relocation?

We already mark all relevant mdirs as unerased (eoff=-1) in the
top-level lfsr_mdir_commit, so as far as I can tell the only reason for
updating the mdir on error is to propagate mdir.rbyd.weight=0 when the
mdir is empty (LFS_ERR_NOENT).

But this is a bit stupid. Relying on mdir state across function
boundaries on error is incredibly fragile. If instead we consider the
mdir clobbered on any error and move all the implicit mdir.rbyd.weight=0
stuff up into lfsr_mdir_commit, this whole category of problems goes
away.

So yeah, that's what we do now:

- lfsr_mdir_commit__ failed => mdir clobbered
- lfsr_mdir_compact__ failed => mdir clobbered
- lfsr_mdir_commit_ failed => mdir preserved, marked unerased
- lfsr_mdir_commit failed => mdir preserved, marked unerased

---

Curiously, all of these changes ended up with a net-zero cost:

           code          stack
  before: 36432           2672
  after:  36432 (+0.0%)   2672 (+0.0%)
2024-08-20 00:28:55 -05:00
Christopher Haster a53151df1f Renamed high-level spam tests to include *_spam_*
These are our current set of general-purpose high-level tests that can
be turned to when needing to test a wide range of filesystem operations.

They were getting a bit hard to keep track of without a consistent
prefix, especially since no individual test suite can actually use all
of them at the same time.

Now, finding these tests is as simple as: ./scripts/test.py -L *_spam_*

I also renamed a couple because their names were starting to get
ridiculous. I mean just look at
test_badblocks_alternating_spam_orphanzombiedir_fuzz...

- *_spam_orphanzombie_fuzz    -> *_spam_oz_fuzz
- *_spam_orphanzombiedir_fuzz -> *_spam_ozd_fuzz
- *_spam_file_pl_fuzz         -> *_spam_f_pl_fuzz
- *_spam_filedir_pl_fuzz      -> *_spam_fd_pl_fuzz

Here are all of the current spam tests and contexts we use them in:

                traversal               badblocks   relocations
                |     gc    ck    grow  |     powerloss   exhaustion
  dir_many      y     y           y     y     y     y
  dir_fuzz      y     y     y     y     y           y     y
  file_many     y     y           y     y     y     y
  file_fuzz     y     y     y     y     y           y     y
  fwrite_fuzz   y     y     y           y                 y
  oz_fuzz       y     y     y     y     y           y     y
  ozd_fuzz      y     y     y     y     y           y     y
  f_pl_fuzz                       y           y     y
  fd_pl_fuzz                      y           y     y
2024-08-20 00:28:55 -05:00
Christopher Haster 4fa2864f30 Replaced test_ck_every_* with more interesting error-spam tests
Instead of testing every block (which test_badblocks_every already
does) with a single random bit-error, the new test_ck_spam tests
continuously throw bit-errors at the filesystem until it fails.

This should reveal much more interesting failures than flipping a single
bit in the entire device, while also taking less testing time. And we
still have test_badblocks_every to make sure no specific problem blocks
(except the mrootanchor) are missed.

This makes test_ck_spam more similar to test_exhaustion than
test_badblocks_every.

All this being said, these tests are still sort of in stasis until
rollback protection gets sorted out. So we're not actually testing
anything interesting yet...

I've also reverted the test_badblocks -> test_ck dependency, since we
want to keep the longer-running tests near the end of the queue.
2024-08-20 00:28:55 -05:00
Christopher Haster 73015909a1 Added high-level every-block error tests to test_ck
These are basically the same as our test_badblock tests, except we
accept LFS_ERR_CORRUPT. This lets us test more checking modes that may
not enable recovery (ckreads, ckfetches, etc).

Well, in theory, at least. The lack of rollback protection gets in the
way of both ckreads and ckfetches, so we're currently only testing
ckprogs, which isn't much of an improvement. At least this gets the
scaffolding in place...

This also inverts the test_ck -> test_badblocks dependency. Now that
these both have exhaustive tests, we might as well limit test_badblocks
to simple erroring erases/progs and let test_ck check the ck checks.
2024-08-20 00:28:55 -05:00
Christopher Haster e7a150ea36 Rearranged jellyfish in tests to better match ck-modes 2024-08-20 00:28:52 -05:00
Christopher Haster 5502fe55ab Implemented ckfetches
Ckfetches implements what might be your first idea on how to check
checksums in a filesystem: Check each block/mdir on first access
(fetch) to make sure the data is sound.

Unfortunately, there are two problems with this approach, both which
come from the fact that blocks are big and can't fit in RAM:

1. We still have a checksum-read hole.

   We can't keep a whole block around in RAM, so reads after a fetch may
   need to reread from disk, at which point new bit-errors may slip in
   undetected.

   This is especially problematic for traversing our rbyds, which
   involves a lot of small reads in a block.

2. Ckfetches may have a surprisingly negative performance impact.

   Consider the case of reading a large file with a bunch of small
   reads. Because we don't cache blocks, each read may need a btree
   lookup, and a full block fetch. On paper this can quickly end up
   O(b^2), which is not great.

   Though this is helped by the file buffer. It will be interesting to
   benchmark and see if this theoretical O(b^2) translates to poor
   performance in practice.

   Note ckreads has this same performance issue.

Still, despite these problems, ckfetches may be useful for cases where
you just want an extra layer of safety, or don't care about the tiny
chance an error is introduced between a fetch an subsequent read.

---

Like ckprogs/ckreads, ckfetches is an opt-in feature, and requires both
1. defining LFS_CKFETCHES, and 2. passing LFS_M_CKFETCHES during mount.

This is a bit of a quick implementation to get testing in place, so the
code cost is probably higher than strictly necessary. If we can refactor
the code internally to avoid all the duplicate lfsr_rbyd_fetchck/
lfsr_bptr_ck calls, we can probably bring this down a bit:

                  code          stack
  before:        36428           2680
  yes-ckfetches: 36848 (+1.2%)   2680 (+0.0%)
  no-ckfetches:  36428 (+0.0%)   2680 (+0.0%)

Oh, and also added lfs_emubd_flipbit to allow tests to manually flip
bits themselves. LFS_EMUBD_BADBLOCK_PROGFLIP is quick to find the above
mentioned checksum-read hole.

This could be done manually with read+erase+prog, but no reason to make
it harder than it needs to be.
2024-08-16 01:04:26 -05:00
Christopher Haster 10feccf18c Moved ckprogs behind LFS_CKPROGS ifdef
So just like ckreads, ckprogs is now opt-in, requiring both 1. defining
LFS_CKPROGS at compile-time, and 2. passing the LFS_M_CKPROGS flag
during lfsr_mount.

_Unlike_ ckreads, ckprogs is actually a very lightweight feature. So the
difference between compiling with/without ckprogs is really quite small:

                code          stack
  before:      36480           2680
  yes-ckprogs: 36480 (+0.0%)   2680 (+0.0%)
  no-ckprogs:  36428 (-0.1%)   2680 (+0.0%)

It's almost not worth putting behind an ifdef if not for consistency
with ckreads.
2024-08-16 01:04:24 -05:00
Christopher Haster acad3a3143 Added format flags to lfsr_format
This is mainly to solve the weird check-hole where passing CKPROGS/
CKREADS as mount flags has no effect on lfsr_format (I mean, it'd be a
bit silly if it did somehow):

  LFS_F_RDWR              0  // Format the filesystem as read and write
  LFS_F_CKPROGS  0x00000010  // Check progs by reading back progged data
  LFS_F_CKREADS  0x00000020  // Check reads via parity bits/checksums

This makes lfsr_format a more cumbersome interface, but I don't know if
this is necessarily a bad thing. There's always risk of data loss when
calling lfsr_format, so maybe it should be a pain to call.

At the very least, format flags may be useful in the future for
enabling/disabling format-time things such as the planned block-map,
parity-tree, etc. Though it's unclear if such significant settings
should be format flags or somehow encoded as fields in our config
struct.

---

The LFS_F_* format flags of course ended up conflicting with our
internal LFS_F_* flags, so I renamed most of the internal flags to match
the closest flag set they participate in:

- LFS_F_TYPE        -> LFS_O_TYPE
- LFS_F_UNFLUSH     -> LFS_O_UNFLUSH
- LFS_F_UNSYNC      -> LFS_O_UNSYNC
- LFS_F_ORPHAN      -> LFS_O_ORPHAN
- LFS_F_ZOMBIE      -> LFS_O_ZOMBIE

- LFS_F_ORPHANS     -> LFS_I_ORPHANS
- LFS_F_UNCOMPACTED -> LFS_I_UNCOMPACTED

- LFS_F_TSTATE      -> LFS_T_TSTATE
- LFS_F_BTYPE       -> LFS_T_BTYPE
- LFS_F_DIRTY       -> LFS_T_DIRTY
- LFS_F_MUTATED     -> LFS_T_MUTATED

This may make it a bit less clear which flags are a part of the public
API, vs intended only for internal use, but at the very least our asserts
in format/mount/open/etc should catch most of these mistakes.

---

Code cost ended up being pretty minimal. Actually negative. This is the
second time we're _adding_ a feature that somehow saves code, though the
reality for this one is we're really just pushing constants up into the
user's stack frame. Still, it's a good indication the cost of format
flags is small:

           code          stack
  before: 36452           2680
  after:  36448 (-0.0%)   2680 (+0.0%)
2024-08-16 01:04:13 -05:00
Christopher Haster 6e2af5bf80 Carved out ckreads, disabled at compile-time by default
This moves all ckread-related logic behind the new opt-in compile-time
LFS_CKREADS flag. So in order to use ckreads you need to 1. define
LFS_CKREADS at compile time, and 2. pass LFS_M_CKREADS during
lfsr_mount.

This was always the plan since, even if ckreads worked perfectly, it
adds a significant amount of baggage (stack mostly) to track the
ck context of all reads.

---

This is the first non-trivial opt-in define in littlefs, so more test
framework features!

test.py and build.py now support the optional ifdef attribute, which
makes it easy to indicate a test suite/case should not be compiled when
a feature is missing.

Also interesting to note is the addition of LFS_IFDEF_CKREADS, which
solves several issues (and general ugliness) related to #ifdefs in
expression. For example:

  // does not compile :( (can't embed ifdefs in macros)
  LFS_ASSERT(flags == (
          LFS_M_CKPROGS
              #ifdef LFS_CKREADS
              | LFS_M_CKREADS
              #endif
              ))

  // does compile :)
  LFS_ASSERT(flags == (
          LFS_M_CKPROGS
              | LFS_IFDEF_CKREADS(LFS_M_CKREADS, 0)));

---

This brings us way back down to our pre-ckread levels of code/stack:

                   code          stack
  before-ckreads: 36352           2672
  ckreads:        38060 (+4.7%)   3056 (+14.4%)
  after-ckreads:  36428 (+0.2%)   2680 (+0.3%)

Unfortunately, we do end up with a bit more code cost than where we
started. Mainly due to code moving around to support the ckread
infrastructure:

                   code          stack
  lfsr_bd_readtag:  +52 (+23.2%)    +8 (+10.0%)
  lfsr_rbyd_fetch:  +36 (+5.0%)     +8 (+6.2%, cold)
  lfs_toleb128:     -12 (-25.0%)    -4 (-20.0%, cold)
  total:            +76 (+0.2%)     +8 (+0.3%)

But oh well. Note that some of these changes are good even without
ckreads, such as only parsing the last ecksum tag.
2024-08-16 01:04:03 -05:00
Christopher Haster 185f209dbf Moved ckreads behind the LFS_M_CKREADS flag
Added some code, though we don't _really_ care:

           code          stack
  before: 37872           3048
  after:  38060 (+0.5%)   3056 (+0.3%)

Also interesting to note the difference in testing time, this highlights
_some_ of the performance cost of ckreads:

  with ckreads:    1135.92s
  without ckreads:  821.24s
2024-08-16 01:04:00 -05:00
Christopher Haster 458fe16f38 Extended emubd to test metastability, added ckprog/ckread tests
Metastability is a rather nasty error condition where successive reads
to a memory location may return different values, either due to bus
issues or a failed prog. It's a tricky error condition to detect, and
one that ckreads was, in theory, supposed to help with.

To help test metastability (and other single-bit errors), emubd gained
several new features:

- LFS_EMUBD_BADBLOCK_PROGFLIP    - Prog flips a bit
- LFS_EMUBD_BADBLOCK_READFLIP    - Read flips a bit sometimes
- LFS_EMUBD_POWERLOSS_METASTABLE - Reads may flip a bit

These only affect a single bit in a given block, but by randomizing
which bit during every erase (and exhaustive bit testing in test_ck) we
should still see some fairly interesting bit-error patterns over time.

It's a bit difficult to test with more than a single bit error because
you can quickly find checksum/parity collisions when fuzz testing. But
there may be other interesting error patterns to look at in the future?

Also the erase_cycles implementation got a bit of a rework since it was
lopsided previously (progs/reads would always error before erases). And
since I was messing with emubd's internals I added lfs_emubd_markbad/
markgood and a few other convenience functions that seem useful:

- lfs_emubd_seed - Manually set the prng, needed in test_ck actually
- lfs_emubd_markbad - Mark block as bad, same as wear=-1
- lfs_emubd_markgood - Mark block as good, same as wear=0
- lfs_emubd_badbit - Get which big failed
- lfs_emubd_setbadbit - Set which bit will fail
- lfs_emubd_randomizebadbit - Randomize bad bit on erase
- lfs_emubd_markbadbit - Mark bit as bad, same as setbadbit+markbad

---

The intention of this new metastability emulation was to extend test_ck
to test ckreads/ckprogs. This went... interestingly.

The good news, the new emulation and tests worked quite well. They were
able to quite quickly show that ckreads is fundamentally not able to
detect all single-bit errors in our current design.

The problem boils down to the fact that the location of our parity bits
depends on the tag's leb128-encoded size. If a bit flip changes this
size field, we end up with a new parity bit, which 50/50 may or may not
detect the error.

For example, one bit flip:

  40 0c 00 12 80 0d ff ff
  '----.----' ^--------------------.
       '- altble 0xc w0 -18 parity=1

  40 0c 80 12 80 0d ff ff
  '-------.-------' ^----------------------.
          '- altble 0xc w2304 -1664 parity=1

This doesn't make ckreads _completely_ useless, just mostly useless. We
can still use it to check parity bits, but without a systematic proof.

But there's enough problems with ckreads: performance, RAM, code, etc,
that I think it may just be an interesting proof-of-concept and not
something users should actually use. Checking reads in the bd-layer
solves all of these problems...

---

At the very least ckprogs gets better testing, thanks to new tests in
test_ck and the addition of LFS_EMUBD_BADBLOCK_PROGFLIP in
test_badblocks.

The extra testing also found a ckprog/ckread hole in that we don't
ckprog/ckread during lfsr_format! I fixed this by making lfsr_format
always use ckprogs/ckreads if available, but maybe lfsr_format should
take its own set of flags?

Funnily enough this had no impact on code size since it probably just
changed the constant in a constant pool:

          code           stack
  before: 37872           3048
  after:  37872 (+0.0%)   3048 (+0.0%)
2024-08-16 01:03:57 -05:00
Christopher Haster d79e4ae455 Added LFS_O_CKMETA/CKDATA flags
These flags just call lfsr_file_ckmeta/ckdata under the hood, but make
it very easy to check metadata/data when opening a file. As an extra
plus they implicitly close the file on failure, so might make cleanup
easier.

Of course, everything has a cost:

           code          stack
  before: 36368           2664
  after:  36424 (+0.2%)   2664 (+0.0%)

These also ruin my previous "you don't pay for what you don't call"
assertion, since runtime flags unfortunately always pull in code.

We should add a compile-time switch for these evntually.
2024-07-27 00:47:45 -05:00
Christopher Haster e2c238c30d Added lfsr_file_ckmeta/ckdata
These are basically the same as lfsr_fs_ckmeta/ckdata but limited to a
single file. They may be useful when you need to validate a file but
don't want to bother validating the entire filesystem:

  // Check a file for metadata errors
  int lfsr_file_ckmeta(lfs_t *lfs, lfsr_file_t *file);

  // Check a file for metadata + data errors
  int lfsr_file_ckdata(lfs_t *lfs, lfsr_file_t *file);

I've also added test_ck to test these and added some more
lfsr_fs_ckmeta/ckdata tests there. These currently just test simple
full-block clobbering, but we should eventually test more interesting
error patterns.

Unfortunately lfsr_file_ckmeta/ckdata can't reuse the internal
lfsr_mtree_traverse in quite the same way lfsr_fs_ckmeta/ckdata can, so
they're actually a bit more expensive. Though keep in mind with
link-time gc you won't pay the cost unless you call these functions:

           code          stack
  before: 36024           2696
  after:  36368 (+1.0%)   2664 (-1.2%)

Oh, and the multiple calls to lfsr_btree/bshrub_traverse apparently
uninlined it out of lfsr_mtree_traverse, saving the stack cost in the
stack hot-path... Yay?
2024-07-27 00:47:45 -05:00