Commit Graph

18 Commits

Author SHA1 Message Date
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