Files
littlefs/tests/test_ck.toml
T
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

1173 lines
38 KiB
TOML

# Test checksum validation things
after = ['test_traversal', 'test_gc', 'test_mount']
# Test filesystem-level checksum things
# test we can detect at least fully clobbered blocks
[cases.test_ck_ckmeta_easy]
# METHOD=0 => lfsr_fs_ckmeta
# METHOD=1 => lfsr_fs_gc
# METHOD=2 => lfsr_traversal_read
# METHOD=3 => lfsr_mount
defines.METHOD = [0, 1, 2, 3]
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FILE_BUFFER_SIZE/2',
'2*FILE_BUFFER_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'8*BLOCK_SIZE',
]
if = '(SIZE*N)/BLOCK_SIZE <= 32'
code = '''
lfs_block_t i = 0;
while (true) {
// a bit hacky, but this catches infinite loops
assert(i < 2*BLOCK_COUNT);
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
// create an interesting filesystem
uint32_t prng = 42;
for (lfs_size_t i = 0; i < N; i++) {
char name[256];
sprintf(name, "squid%03x", i);
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_t file;
lfsr_file_open(&lfs, &file, name,
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
lfsr_file_close(&lfs, &file) => 0;
}
// traverse to find blocks
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, 0) => 0;
lfs_block_t k = 0;
for (lfs_block_t j = 0;; j++) {
assert(j < 2*BLOCK_COUNT);
struct lfs_tinfo tinfo;
int err = lfsr_traversal_read(&lfs, &t, &tinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
lfsr_traversal_close(&lfs, &t) => 0;
lfsr_unmount(&lfs) => 0;
goto done;
}
// this gets a bit tricky be cause we need to clobber both
// blocks in mdir pairs
if (tinfo.btype == LFS_BTYPE_MDIR
|| tinfo.btype == LFS_BTYPE_BTREE) {
if (k == i || k == i+1) {
// clobber this block
printf("clobbering 0x%x\n", tinfo.block);
uint8_t clobber_buf[BLOCK_SIZE];
memset(clobber_buf, 0xcc, BLOCK_SIZE);
CFG->erase(CFG, tinfo.block) => 0;
CFG->prog(CFG, tinfo.block, 0,
clobber_buf, BLOCK_SIZE) => 0;
if (tinfo.btype != LFS_BTYPE_MDIR || k == i+1) {
i += (tinfo.btype == LFS_BTYPE_MDIR) ? 2 : 1;
lfsr_traversal_close(&lfs, &t) => 0;
goto clobbered;
}
}
k += 1;
}
}
clobbered:;
// find clobbered blocks with lfsr_fs_ckmeta
if (METHOD == 0) {
lfsr_fs_ckmeta(&lfs) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_fs_gc
} else if (METHOD == 1) {
lfsr_fs_gc(&lfs, -1, LFS_GC_CKMETA) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_traversal_read
} else if (METHOD == 2) {
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, LFS_T_CKMETA) => 0;
for (lfs_block_t i = 0;; i++) {
// a bit hacky, but this catches infinite loops
LFS_ASSERT(i < 2*BLOCK_COUNT);
struct lfs_tinfo tinfo;
int err = lfsr_traversal_read(&lfs, &t, &tinfo);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
break;
}
}
lfsr_traversal_close(&lfs, &t) => 0;
// find clobbered blocks with lfsr_mount
} else if (METHOD == 3) {
lfsr_unmount(&lfs) => 0;
lfsr_mount(&lfs,
LFS_M_RDWR
| LFS_M_CKMETA,
CFG) => LFS_ERR_CORRUPT;
} else {
assert(false);
}
if (METHOD != 3) {
lfsr_unmount(&lfs) => 0;
}
}
done:;
'''
[cases.test_ck_ckdata_easy]
# METHOD=0 => lfsr_fs_ckdata
# METHOD=1 => lfsr_fs_gc
# METHOD=2 => lfsr_traversal_read
# METHOD=3 => lfsr_mount
defines.METHOD = [0, 1, 2, 3]
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FILE_BUFFER_SIZE/2',
'2*FILE_BUFFER_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'8*BLOCK_SIZE',
]
if = '(SIZE*N)/BLOCK_SIZE <= 32'
code = '''
lfs_block_t i = 0;
while (true) {
// a bit hacky, but this catches infinite loops
assert(i < 2*BLOCK_COUNT);
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
// create an interesting filesystem
uint32_t prng = 42;
for (lfs_size_t i = 0; i < N; i++) {
char name[256];
sprintf(name, "squid%03x", i);
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfsr_file_t file;
lfsr_file_open(&lfs, &file, name,
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
lfsr_file_write(&lfs, &file, wbuf, SIZE) => SIZE;
lfsr_file_close(&lfs, &file) => 0;
}
// traverse to find blocks
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, 0) => 0;
lfs_block_t k = 0;
for (lfs_block_t j = 0;; j++) {
assert(j < 2*BLOCK_COUNT);
struct lfs_tinfo tinfo;
int err = lfsr_traversal_read(&lfs, &t, &tinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
lfsr_traversal_close(&lfs, &t) => 0;
lfsr_unmount(&lfs) => 0;
goto done;
}
// this gets a bit tricky be cause we need to clobber both
// blocks in mdir pairs
if (tinfo.btype == LFS_BTYPE_MDIR
|| tinfo.btype == LFS_BTYPE_BTREE
|| tinfo.btype == LFS_BTYPE_DATA) {
if (k == i || k == i+1) {
// clobber this block
printf("clobbering 0x%x\n", tinfo.block);
uint8_t clobber_buf[BLOCK_SIZE];
memset(clobber_buf, 0xcc, BLOCK_SIZE);
CFG->erase(CFG, tinfo.block) => 0;
CFG->prog(CFG, tinfo.block, 0,
clobber_buf, BLOCK_SIZE) => 0;
if (tinfo.btype != LFS_BTYPE_MDIR || k == i+1) {
i += (tinfo.btype == LFS_BTYPE_MDIR) ? 2 : 1;
lfsr_traversal_close(&lfs, &t) => 0;
goto clobbered;
}
}
k += 1;
}
}
clobbered:;
// find clobbered blocks with lfsr_fs_ckmeta
if (METHOD == 0) {
lfsr_fs_ckdata(&lfs) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_fs_gc
} else if (METHOD == 1) {
lfsr_fs_gc(&lfs, -1, LFS_GC_CKDATA) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_traversal_read
} else if (METHOD == 2) {
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, LFS_T_CKDATA) => 0;
for (lfs_block_t i = 0;; i++) {
// a bit hacky, but this catches infinite loops
LFS_ASSERT(i < 2*BLOCK_COUNT);
struct lfs_tinfo tinfo;
int err = lfsr_traversal_read(&lfs, &t, &tinfo);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
break;
}
}
lfsr_traversal_close(&lfs, &t) => 0;
// find clobbered blocks with lfsr_mount
} else if (METHOD == 3) {
lfsr_unmount(&lfs) => 0;
lfsr_mount(&lfs,
LFS_M_RDWR
| LFS_M_CKDATA,
CFG) => LFS_ERR_CORRUPT;
} else {
assert(false);
}
if (METHOD != 3) {
lfsr_unmount(&lfs) => 0;
}
}
done:;
'''
# Test file-level checksum things
# test we can detect at least fully clobbered blocks
[cases.test_ck_file_ckmeta_easy]
# METHOD=0 => lfsr_file_ckmeta
# METHOD=1 => lfsr_file_close+open+ckmeta
# METHOD=2 => lfsr_file_close+open
defines.METHOD = [0, 1]
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FILE_BUFFER_SIZE/2',
'2*FILE_BUFFER_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'8*BLOCK_SIZE',
]
code = '''
for (lfs_block_t i = 0;; i++) {
// a bit hacky, but this catches infinite loops
assert(i < 2*BLOCK_COUNT);
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
// create an interesting file
uint32_t prng = 42;
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "octopus",
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;
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;
// traverse to find blocks
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, 0) => 0;
lfs_block_t k = 0;
for (lfs_block_t j = 0;; j++) {
assert(j < 2*BLOCK_COUNT);
struct lfs_tinfo tinfo;
int err = lfsr_traversal_read(&lfs, &t, &tinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
lfsr_traversal_close(&lfs, &t) => 0;
lfsr_file_close(&lfs, &file) => 0;
lfsr_unmount(&lfs) => 0;
goto done;
}
if (tinfo.btype == LFS_BTYPE_BTREE) {
if (k == i) {
// clobber this block
printf("clobbering 0x%x\n", tinfo.block);
uint8_t clobber_buf[BLOCK_SIZE];
memset(clobber_buf, 0xcc, BLOCK_SIZE);
CFG->erase(CFG, tinfo.block) => 0;
CFG->prog(CFG, tinfo.block, 0,
clobber_buf, BLOCK_SIZE) => 0;
lfsr_traversal_close(&lfs, &t) => 0;
goto clobbered;
}
k += 1;
}
}
clobbered:;
// find clobbered blocks with lfsr_file_ckmeta
if (METHOD == 0) {
lfsr_file_ckmeta(&lfs, &file) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_file_close+open+ckmeta
} else if (METHOD == 1) {
lfsr_file_close(&lfs, &file) => 0;
lfsr_file_open(&lfs, &file, "octopus", LFS_O_RDONLY) => 0;
lfsr_file_ckmeta(&lfs, &file) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_file_close+open
} else if (METHOD == 2) {
lfsr_file_open(&lfs, &file, "octopus",
LFS_O_RDONLY | LFS_O_CKMETA) => LFS_ERR_CORRUPT;
} else {
assert(false);
}
if (METHOD != 2) {
lfsr_file_close(&lfs, &file) => 0;
}
lfsr_unmount(&lfs) => 0;
}
done:;
'''
# test we can detect at least fully clobbered blocks
[cases.test_ck_file_ckdata_easy]
# METHOD=0 => lfsr_file_ckdata
# METHOD=1 => lfsr_file_close+open+ckdata
# METHOD=2 => lfsr_file_close+open
defines.METHOD = [0, 1]
defines.N = [1, 2, 4, 8, 16, 32, 64]
defines.SIZE = [
'0',
'FILE_BUFFER_SIZE/2',
'2*FILE_BUFFER_SIZE',
'BLOCK_SIZE/2',
'BLOCK_SIZE',
'2*BLOCK_SIZE',
'8*BLOCK_SIZE',
]
code = '''
for (lfs_block_t i = 0;; i++) {
// a bit hacky, but this catches infinite loops
assert(i < 2*BLOCK_COUNT);
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
// create an interesting file
uint32_t prng = 42;
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "octopus",
LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0;
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;
// traverse to find blocks
lfsr_traversal_t t;
lfsr_traversal_open(&lfs, &t, 0) => 0;
lfs_block_t k = 0;
for (lfs_block_t j = 0;; j++) {
assert(j < 2*BLOCK_COUNT);
struct lfs_tinfo tinfo;
int err = lfsr_traversal_read(&lfs, &t, &tinfo);
assert(!err || err == LFS_ERR_NOENT);
if (err == LFS_ERR_NOENT) {
lfsr_traversal_close(&lfs, &t) => 0;
lfsr_file_close(&lfs, &file) => 0;
lfsr_unmount(&lfs) => 0;
goto done;
}
if (tinfo.btype == LFS_BTYPE_BTREE
|| tinfo.btype == LFS_BTYPE_DATA) {
if (k == i) {
// clobber this block
printf("clobbering 0x%x\n", tinfo.block);
uint8_t clobber_buf[BLOCK_SIZE];
memset(clobber_buf, 0xcc, BLOCK_SIZE);
CFG->erase(CFG, tinfo.block) => 0;
CFG->prog(CFG, tinfo.block, 0,
clobber_buf, BLOCK_SIZE) => 0;
lfsr_traversal_close(&lfs, &t) => 0;
goto clobbered;
}
k += 1;
}
}
clobbered:;
// find clobbered blocks with lfsr_file_ckmeta
if (METHOD == 0) {
lfsr_file_ckdata(&lfs, &file) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_file_close+open+ckmeta
} else if (METHOD == 1) {
lfsr_file_close(&lfs, &file) => 0;
lfsr_file_open(&lfs, &file, "octopus", LFS_O_RDONLY) => 0;
lfsr_file_ckdata(&lfs, &file) => LFS_ERR_CORRUPT;
// find clobbered blocks with lfsr_file_close+open
} else if (METHOD == 2) {
lfsr_file_close(&lfs, &file) => 0;
lfsr_file_open(&lfs, &file, "octopus",
LFS_O_RDONLY | LFS_O_CKDATA) => LFS_ERR_CORRUPT;
} else {
assert(false);
}
if (METHOD != 2) {
lfsr_file_close(&lfs, &file) => 0;
}
lfsr_unmount(&lfs) => 0;
}
done:;
'''
# Some simple ckprog tests
#
# We test these much more aggressively in test_badblocks
# test every single-bit error in block 0/1
[cases.test_ck_ckprogs_mroot]
defines.BADBLOCK = [0, 1]
defines.BADBIT = -1
defines.BADBLOCK_BEHAVIOR = 'LFS_EMUBD_BADBLOCK_PROGFLIP'
# this should stay inlined
defines.SIZE = 'BLOCK_SIZE/16'
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;
// mark our badbit as bad
lfs_emubd_markbadbit(CFG, BADBLOCK, badbit) => 0;
printf("--- badblock: 0x%x.%x, badbit: 0x%x (0x%x+%x) ---\n",
(lfs_size_t)BADBLOCK, badbit/8, badbit, badbit/8, badbit%8);
// formatting the filesystem may already find the bit error
lfs_t lfs;
int err = lfsr_format(&lfs, CFG);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
goto corrupt;
}
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKPROGS, CFG) => 0;
{
// create a file
lfsr_file_t file;
err = lfsr_file_open(&lfs, &file, "stygiomedusa",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
uint32_t prng = 42;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfs_ssize_t res = lfsr_file_write(&lfs, &file, wbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
err = lfsr_file_close(&lfs, &file);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
// if we made it here without erroring we should be able 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_CKPROGS, CFG) => 0;
}
lfsr_file_open(&lfs, &file, "stygiomedusa", LFS_O_RDONLY) => 0;
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_ckprogs_data]
defines.BADBIT = -1
defines.BADBLOCK_BEHAVIOR = 'LFS_EMUBD_BADBLOCK_PROGFLIP'
# this should create a single block file
defines.SIZE = 'BLOCK_SIZE'
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, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKPROGS, CFG) => 0;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "stygiomedusa",
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;
// mark our badbit as bad
lfs_emubd_markbadbit(CFG, badblock, badbit) => 0;
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, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKPROGS, CFG) => 0;
{
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "stygiomedusa",
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);
}
lfs_ssize_t res = lfsr_file_write(&lfs, &file, wbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
int err = lfsr_file_close(&lfs, &file);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
// if we made it here without erroring we should be able 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_CKPROGS, CFG) => 0;
}
lfsr_file_open(&lfs, &file, "stygiomedusa", LFS_O_RDONLY) => 0;
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;
// reset badbit
lfs_emubd_markgood(CFG, badblock) => 0;
}
'''
# test every single-bit error in a file's btree node
[cases.test_ck_ckprogs_btree]
defines.BADBIT = -1
defines.BADBLOCK_BEHAVIOR = 'LFS_EMUBD_BADBLOCK_PROGFLIP'
# 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'
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, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKPROGS, CFG) => 0;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "stygiomedusa",
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;
// mark our badbit as bad
lfs_emubd_markbadbit(CFG, badblock, badbit) => 0;
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, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR | LFS_M_CKPROGS, CFG) => 0;
{
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "stygiomedusa",
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);
}
lfs_ssize_t res = lfsr_file_write(&lfs, &file, wbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
int err = lfsr_file_close(&lfs, &file);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
// if we made it here without erroring we should be able 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_CKPROGS, CFG) => 0;
}
lfsr_file_open(&lfs, &file, "stygiomedusa", LFS_O_RDONLY) => 0;
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;
// reset badbit
lfs_emubd_markgood(CFG, badblock) => 0;
}
'''
# 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
# ckreads can't actually guarantee single-bit error-detection since
# the bit flip may alter the leb128 encoded size field and find a new,
# erronous, parity bit.
#
# 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.
#
# So for now these tests are sort of in stasis, limited to testing
# metastability in areas we know we can detect (revision counts, raw
# data blocks, etc). Maybe future features will make them more useful.
#
# test every single-bit error in block 0/1
[cases.test_ck_ckreads_mroot]
defines.BADBLOCK = [0, 1]
defines.BADBIT = -1
defines.BADBLOCK_BEHAVIOR = [
'LFS_EMUBD_BADBLOCK_PROGFLIP',
'LFS_EMUBD_BADBLOCK_READFLIP',
]
# this should stay inlined
defines.SIZE = 'BLOCK_SIZE/16'
code = '''
// test all bad bits in the mroot
for (lfs_size_t i = 0;
// we can't detect metastable tags, so limit read-flips
// to our revision count
i < ((BADBIT == -1) ? 8*4 : 1);
i++) {
lfs_size_t badbit = (BADBIT == -1) ? i : BADBIT;
// reset the bd prng every run for reproducibility
lfs_emubd_seed(CFG, 42) => 0;
// mark our badbit as bad
lfs_emubd_markbadbit(CFG, BADBLOCK, badbit) => 0;
printf("--- badblock: 0x%x.%x, badbit: 0x%x (0x%x+%x) ---\n",
(lfs_size_t)BADBLOCK, badbit/8, badbit, badbit/8, badbit%8);
// With metastability, basically any filesystem operation can
// return LFS_ERR_CORRUPT. This is ok, what we're really testing
// for is no internal/external asserts failing.
// format
lfs_t lfs;
int err = lfsr_format(&lfs, CFG);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
goto corrupt;
}
err = lfsr_mount(&lfs, LFS_M_RDWR, CFG);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
goto corrupt;
}
{
// create a file
lfsr_file_t file;
err = lfsr_file_open(&lfs, &file, "bathykorus",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL);
assert(!err || err == LFS_ERR_CORRUPT);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
uint32_t prng = 42;
uint8_t wbuf[SIZE];
for (lfs_size_t j = 0; j < SIZE; j++) {
wbuf[j] = 'a' + (TEST_PRNG(&prng) % 26);
}
lfs_ssize_t res = lfsr_file_write(&lfs, &file, wbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
err = lfsr_file_close(&lfs, &file);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
// try to read our file
for (int remount = 0; remount < 2; remount++) {
// remount?
if (remount) {
lfsr_unmount(&lfs) => 0;
err = lfsr_mount(&lfs, LFS_M_RDWR, CFG);
if (err == LFS_ERR_CORRUPT) {
goto corrupt;
}
}
// yes reads can fail here
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",
// which is not great but we can't solve this with ckreads
// alone
|| err == LFS_ERR_NOENT);
if (err == LFS_ERR_CORRUPT || err == LFS_ERR_NOENT) {
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;
corrupt:;
// reset badbit
lfs_emubd_markgood(CFG, BADBLOCK) => 0;
}
'''
# test every single-bit error in a file's data block
[cases.test_ck_ckreads_data]
defines.BADBIT = -1
defines.BADBLOCK_BEHAVIOR = [
'LFS_EMUBD_BADBLOCK_PROGFLIP',
'LFS_EMUBD_BADBLOCK_READFLIP',
]
# this should create a single block file
defines.SIZE = 'BLOCK_SIZE'
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, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "bathykorus",
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;
// reset the bd prng every run for reproducibility
lfs_emubd_seed(CFG, 42) => 0;
// mark our badbit as bad
lfs_emubd_markbadbit(CFG, badblock, badbit) => 0;
printf("--- badblock: 0x%x.%x, badbit: 0x%x (0x%x+%x) ---\n",
badblock, badbit/8, badbit, badbit/8, badbit%8);
// With metastability, basically any filesystem operation can
// return LFS_ERR_CORRUPT. This is ok, what we're really testing
// for is no internal/external asserts failing.
// format
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
{
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "bathykorus",
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);
}
lfs_ssize_t res = lfsr_file_write(&lfs, &file, wbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
int err = lfsr_file_close(&lfs, &file);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
// 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, CFG) => 0;
}
// yes reads can fail here
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",
// which is not great but we can't solve this with ckreads
// alone
|| err == LFS_ERR_NOENT);
if (err == LFS_ERR_CORRUPT || err == LFS_ERR_NOENT) {
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_ckreads_btree]
defines.BADBIT = -1
defines.BADBLOCK_BEHAVIOR = [
'LFS_EMUBD_BADBLOCK_PROGFLIP',
'LFS_EMUBD_BADBLOCK_READFLIP',
]
# 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'
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, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "bathykorus",
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;
// we can't detect metastable tags, so limit read-flips
// to our revision count
i < ((BADBIT == -1) ? 8*4 : 1);
i++) {
lfs_size_t badbit = (BADBIT == -1) ? i : BADBIT;
// reset the bd prng every run for reproducibility
lfs_emubd_seed(CFG, 42) => 0;
// mark our badbit as bad
lfs_emubd_markbadbit(CFG, badblock, badbit) => 0;
printf("--- badblock: 0x%x.%x, badbit: 0x%x (0x%x+%x) ---\n",
badblock, badbit/8, badbit, badbit/8, badbit%8);
// With metastability, basically any filesystem operation can
// return LFS_ERR_CORRUPT. This is ok, what we're really testing
// for is no internal/external asserts failing.
// format
lfs_t lfs;
lfsr_format(&lfs, CFG) => 0;
lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0;
{
// create a file
lfsr_file_t file;
lfsr_file_open(&lfs, &file, "bathykorus",
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);
}
lfs_ssize_t res = lfsr_file_write(&lfs, &file, wbuf, SIZE);
assert(res == SIZE || res == LFS_ERR_CORRUPT);
if (res == LFS_ERR_CORRUPT) {
lfsr_file_close(&lfs, &file) => 0;
goto corrupt_mounted;
}
int err = lfsr_file_close(&lfs, &file);
if (err == LFS_ERR_CORRUPT) {
goto corrupt_mounted;
}
// 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, CFG) => 0;
}
// yes reads can fail here
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",
// which is not great but we can't solve this with ckreads
// alone
|| err == LFS_ERR_NOENT);
if (err == LFS_ERR_CORRUPT || err == LFS_ERR_NOENT) {
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;
}
'''