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.
This commit is contained in:
Christopher Haster
2024-08-13 01:17:28 -05:00
parent 10feccf18c
commit 5502fe55ab
7 changed files with 539 additions and 6 deletions
+307 -5
View File
@@ -770,8 +770,6 @@ code = '''
# Some simple ckread tests
#
# We test these much more aggressively in test_badblocks
# These tests were originally intended to test all single-bit
# metastability errors with ckreads, however they quickly found that
@@ -883,7 +881,7 @@ code = '''
err = lfsr_file_open(&lfs, &file, "bathykorus", LFS_O_RDONLY);
assert(!err
|| err == LFS_ERR_CORRUPT
// metastability can also cause our fs state to "rollback",
// bit errors can also cause our fs state to "rollback",
// which is not great but we can't solve this with ckreads
// alone
|| err == LFS_ERR_NOENT);
@@ -1015,7 +1013,7 @@ code = '''
err = lfsr_file_open(&lfs, &file, "bathykorus", LFS_O_RDONLY);
assert(!err
|| err == LFS_ERR_CORRUPT
// metastability can also cause our fs state to "rollback",
// bit errors can also cause our fs state to "rollback",
// which is not great but we can't solve this with ckreads
// alone
|| err == LFS_ERR_NOENT);
@@ -1151,7 +1149,7 @@ code = '''
err = lfsr_file_open(&lfs, &file, "bathykorus", LFS_O_RDONLY);
assert(!err
|| err == LFS_ERR_CORRUPT
// metastability can also cause our fs state to "rollback",
// bit errors can also cause our fs state to "rollback",
// which is not great but we can't solve this with ckreads
// alone
|| err == LFS_ERR_NOENT);
@@ -1177,3 +1175,307 @@ code = '''
lfs_emubd_markgood(CFG, badblock) => 0;
}
'''
# Some simple ckfetches tests
# test every single-bit error in block 0/1
[cases.test_ck_ckfetches_mroot]
defines.BADBLOCK = [0, 1]
defines.BADBIT = -1
# this should stay inlined
defines.SIZE = 'BLOCK_SIZE/16'
ifdef = 'LFS_CKFETCHES'
code = '''
// test all bad bits in the mroot
for (lfs_size_t i = 0;
i < ((BADBIT == -1) ? 8*BLOCK_SIZE : 1);
i++) {
lfs_size_t badbit = (BADBIT == -1) ? i : BADBIT;
printf("--- badblock: 0x%x.%x, badbit: 0x%x (0x%x+%x) ---\n",
(lfs_size_t)BADBLOCK, badbit/8, badbit, badbit/8, badbit%8);
// format
lfs_t lfs;
lfsr_format(&lfs, LFS_F_RDWR | LFS_F_CKFETCHES, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKFETCHES, CFG) => 0;
{
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "tripedalia",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint32_t prng = 42;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
lfsr_file_close(&lfs, &file) => 0;
// try to read our file
for (int remount = 0; remount < 2; remount++) {
// remount?
if (remount) {
lfsr_unmount(&lfs) => 0;
// flip our badbit
lfs_emubd_flipbit(CFG, BADBLOCK, badbit) => 0;
int err = lfsr_mount(&lfs,
LFS_M_RDWR | LFS_M_CKFETCHES, CFG);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
goto corrupt;
}
}
// yes reads can fail here
int err = lfsr_file_open(&lfs, &file,
"tripedalia", LFS_O_RDONLY);
assert(!err
// bit errors can also cause our fs state to "rollback",
// which is not great but we can't solve this with
// ckfetches alone
|| err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
goto corrupt_mounted;
}
uint8_t rbuf[SIZE];
lfsr_file_read(&lfs, &file, rbuf, SIZE) => SIZE;
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfsr_file_close(&lfs, &file) => 0;
}
}
corrupt_mounted:;
lfsr_unmount(&lfs) => 0;
corrupt:;
// reset badbit
lfs_emubd_markgood(CFG, BADBLOCK) => 0;
}
'''
# test every single-bit error in a file's data block
[cases.test_ck_ckfetches_data]
defines.BADBIT = -1
# this should create a single block file
defines.SIZE = 'BLOCK_SIZE'
ifdef = 'LFS_CKFETCHES'
code = '''
// first we need to figure out where the data block will actually
// end up, fortunately our block randomization is intentionally
// consistent
// format
lfs_t lfs;
lfsr_format(&lfs, LFS_F_RDWR | LFS_F_CKFETCHES, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKFETCHES, CFG) => 0;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "tripedalia",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint32_t prng = 42;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
lfsr_file_close(&lfs, &file) => 0;
// find the data block
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, 0) => 0;
lfs_block_t badblock;
while (true) {
struct lfs_tinfo tinfo;
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
if (tinfo.btype == LFS_BTYPE_DATA) {
badblock = tinfo.block;
break;
}
}
lfsr_traversal_close(&lfs, &t) => 0;
lfsr_unmount(&lfs) => 0;
// now test all bad bits in the data block
for (lfs_size_t i = 0;
i < ((BADBIT == -1) ? 8*BLOCK_SIZE : 1);
i++) {
lfs_size_t badbit = (BADBIT == -1) ? i : BADBIT;
printf("--- badblock: 0x%x.%x, badbit: 0x%x (0x%x+%x) ---\n",
badblock, badbit/8, badbit, badbit/8, badbit%8);
// format
lfs_t lfs;
lfsr_format(&lfs, LFS_F_RDWR | LFS_F_CKFETCHES, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKFETCHES, CFG) => 0;
{
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "tripedalia",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint32_t prng = 42;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
lfsr_file_close(&lfs, &file) => 0;
// flip our badbit
lfs_emubd_flipbit(CFG, badblock, badbit) => 0;
// try to read our file
for (int remount = 0; remount < 2; remount++) {
// remount?
if (remount) {
lfsr_unmount(&lfs) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKFETCHES, CFG) => 0;
}
// yes reads can fail here
int err = lfsr_file_open(&lfs, &file,
"tripedalia", LFS_O_RDONLY);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
uint8_t rbuf[SIZE];
lfs_ssize_t res = lfsr_file_read(&lfs, &file, rbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfsr_file_close(&lfs, &file) => 0;
}
}
corrupt_mounted:;
lfsr_unmount(&lfs) => 0;
// reset badbit
lfs_emubd_markgood(CFG, badblock) => 0;
}
'''
# test every single-bit error in a file's btree node
[cases.test_ck_ckfetches_btree]
defines.BADBIT = -1
# force the file to create a btree
defines.INLINE_SIZE = 0
defines.CRYSTAL_THRESH = -1
defines.FRAGMENT_SIZE = 'BLOCK_SIZE/8'
defines.SIZE = '2*FRAGMENT_SIZE'
ifdef = 'LFS_CKFETCHES'
code = '''
// first we need to figure out where the btree block will actually
// end up, fortunately our block randomization is intentionally
// consistent
// format
lfs_t lfs;
lfsr_format(&lfs, LFS_F_RDWR | LFS_F_CKFETCHES, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKFETCHES, CFG) => 0;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "tripedalia",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint32_t prng = 42;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
lfsr_file_close(&lfs, &file) => 0;
// find the btree block
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, 0) => 0;
lfs_block_t badblock;
while (true) {
struct lfs_tinfo tinfo;
lfsr_traversal_read(&lfs, &t, &tinfo) => 0;
if (tinfo.btype == LFS_BTYPE_BTREE) {
badblock = tinfo.block;
break;
}
}
lfsr_traversal_close(&lfs, &t) => 0;
lfsr_unmount(&lfs) => 0;
// now test all bad bits in the btree block
for (lfs_size_t i = 0;
i < ((BADBIT == -1) ? 8*BLOCK_SIZE : 1);
i++) {
lfs_size_t badbit = (BADBIT == -1) ? i : BADBIT;
printf("--- badblock: 0x%x.%x, badbit: 0x%x (0x%x+%x) ---\n",
badblock, badbit/8, badbit, badbit/8, badbit%8);
// format
lfs_t lfs;
lfsr_format(&lfs, LFS_F_RDWR | LFS_F_CKFETCHES, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKFETCHES, CFG) => 0;
{
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "tripedalia",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
uint32_t prng = 42;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
lfsr_file_close(&lfs, &file) => 0;
// flip our badbit
lfs_emubd_flipbit(CFG, badblock, badbit) => 0;
// try to read our file
for (int remount = 0; remount < 2; remount++) {
// remount?
if (remount) {
lfsr_unmount(&lfs) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKFETCHES, CFG) => 0;
}
// yes reads can fail here
int err = lfsr_file_open(&lfs, &file,
"tripedalia", LFS_O_RDONLY);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
uint8_t rbuf[SIZE];
lfs_ssize_t res = lfsr_file_read(&lfs, &file, rbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
assert(memcmp(rbuf, wbuf, SIZE) == 0);
lfsr_file_close(&lfs, &file) => 0;
}
}
corrupt_mounted:;
lfsr_unmount(&lfs) => 0;
// reset badbit
lfs_emubd_markgood(CFG, badblock) => 0;
}
'''