diff --git a/lfs.c b/lfs.c index 27986d9a..d07fb922 100644 --- a/lfs.c +++ b/lfs.c @@ -13024,6 +13024,43 @@ int lfsr_fs_mkconsistent(lfs_t *lfs) { return 0; } +// check the filesystem for metadata errors +int lfsr_fs_ckmeta(lfs_t *lfs) { + // we leave this up to lfsr_mtree_gc + lfsr_traversal_t t = LFSR_TRAVERSAL(LFS_T_CKMETA); + while (true) { + int err = lfsr_mtree_gc(lfs, &t, + NULL, NULL); + if (err) { + if (err == LFS_ERR_NOENT) { + break; + } + return err; + } + } + + return 0; +} + +// check the filesystem for metadata + data errors +int lfsr_fs_ckdata(lfs_t *lfs) { + // we leave this up to lfsr_mtree_gc + lfsr_traversal_t t = LFSR_TRAVERSAL(LFS_T_CKMETA | LFS_T_CKDATA); + while (true) { + int err = lfsr_mtree_gc(lfs, &t, + NULL, NULL); + if (err) { + if (err == LFS_ERR_NOENT) { + break; + } + return err; + } + } + + return 0; +} + +// perform any pending janitorial work int lfsr_fs_gc(lfs_t *lfs, uint32_t flags) { // some flags don't make sense when only traversing the mtree LFS_ASSERT(!lfsr_t_ismtreeonly(flags) || !lfsr_t_islookahead(flags)); diff --git a/lfs.h b/lfs.h index 8460100a..6be979e5 100644 --- a/lfs.h +++ b/lfs.h @@ -1165,7 +1165,23 @@ int lfsr_fs_mkconsistent(lfs_t *lfs); #endif #ifndef LFS_READONLY -// Attempt any janitorial work that may be pending. +// Check the filesystem for metadata errors +// +// Returns LFS_ERR_CORRUPT if a checksum mismatch is found, or a negative +// error code on failure. +int lfsr_fs_ckmeta(lfs_t *lfs); +#endif + +#ifndef LFS_READONLY +// Check the filesystem for metadata + data errors +// +// Returns LFS_ERR_CORRUPT if a checksum mismatch is found, or a negative +// error code on failure. +int lfsr_fs_ckdata(lfs_t *lfs); +#endif + +#ifndef LFS_READONLY +// Perform any janitorial work that may be pending. // // The exact janitorial work depends on the provided flags. Note multiple // calls may be required to complete all janitorial work. diff --git a/tests/test_gc.toml b/tests/test_gc.toml index 7b13f1e3..3f423ce6 100644 --- a/tests/test_gc.toml +++ b/tests/test_gc.toml @@ -779,6 +779,99 @@ code = ''' lfsr_unmount(&lfs) => 0; ''' +# test that an explicit lfsr_fs_mkconsistent call also works, this calls +# the same logic internally +[cases.test_gc_mkconsistent_explicit] +defines.SIZE = 'FILE_BUFFER_SIZE/2' +# <=2 => grm-able +# >2 => requires orphans +defines.ORPHANS = [1, 2, 3, 100] +code = ''' + lfs_t lfs; + lfsr_format(&lfs, CFG) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + + uint32_t prng = 42; + + // create two files + lfsr_file_t file1; + lfsr_file_open(&lfs, &file1, "cuttlefish", + LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0; + uint8_t wbuf1[SIZE]; + for (lfs_size_t j = 0; j < SIZE; j++) { + wbuf1[j] = 'a' + (TEST_PRNG(&prng) % 26); + } + lfsr_file_write(&lfs, &file1, wbuf1, SIZE) => SIZE; + lfsr_file_sync(&lfs, &file1) => 0; + + lfsr_file_t file2; + lfsr_file_open(&lfs, &file2, "octopus", + LFS_O_RDWR | LFS_O_CREAT | LFS_O_EXCL) => 0; + uint8_t wbuf2[SIZE]; + for (lfs_size_t j = 0; j < SIZE; j++) { + wbuf2[j] = 'a' + (TEST_PRNG(&prng) % 26); + } + lfsr_file_write(&lfs, &file2, wbuf2, SIZE) => SIZE; + lfsr_file_sync(&lfs, &file2) => 0; + + // create this many orphaned files + // + // anytime we close a not-yet-created desync file, we create an + // orphan, but note we need these to be different files, and we need + // to close them after all open calls, otherwise we just end up with + // one orphan (littlefs is eager to clean up orphans) + // + lfsr_file_t orphans[ORPHANS]; + for (lfs_size_t i = 0; i < ORPHANS; i++) { + char name[256]; + sprintf(name, "jellyfish%03x", i); + lfsr_file_open(&lfs, &orphans[i], name, + LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL | LFS_O_DESYNC) => 0; + } + for (lfs_size_t i = 0; i < ORPHANS; i++) { + lfsr_file_close(&lfs, &orphans[i]) => 0; + } + + // expect dirty initial state or else our test doesn't work + struct lfs_fsinfo fsinfo; + lfsr_fs_stat(&lfs, &fsinfo) => 0; + assert(fsinfo.flags & LFS_I_INCONSISTENT); + assert(lfs.omdirs != &lfs.gc.o.o); + + // call lfsr_fs_mkconsistent + lfsr_fs_mkconsistent(&lfs) => 0; + + // we should have made progress + lfsr_fs_stat(&lfs, &fsinfo) => 0; + assert(!(fsinfo.flags & LFS_I_INCONSISTENT)); + + // check we can still read the files + for (int remount = 0; remount < 2; remount++) { + // remount? + if (remount) { + lfsr_file_close(&lfs, &file1) => 0; + lfsr_file_close(&lfs, &file2) => 0; + lfsr_unmount(&lfs) => 0; + lfsr_mount(&lfs, LFS_M_RDWR, CFG) => 0; + lfsr_file_open(&lfs, &file1, "cuttlefish", LFS_O_RDONLY) => 0; + lfsr_file_open(&lfs, &file2, "octopus", LFS_O_RDONLY) => 0; + } + + lfsr_file_rewind(&lfs, &file1) => 0; + uint8_t rbuf[SIZE]; + lfsr_file_read(&lfs, &file1, rbuf, SIZE) => SIZE; + assert(memcmp(rbuf, wbuf1, SIZE) == 0); + + lfsr_file_rewind(&lfs, &file2) => 0; + lfsr_file_read(&lfs, &file2, rbuf, SIZE) => SIZE; + assert(memcmp(rbuf, wbuf2, SIZE) == 0); + } + + lfsr_file_close(&lfs, &file1) => 0; + lfsr_file_close(&lfs, &file2) => 0; + lfsr_unmount(&lfs) => 0; +''' + # test that mkconsistent dirtying still works with the GC API [cases.test_gc_mkconsistent_mutation] defines.GC_STEPS = 1 @@ -1329,6 +1422,183 @@ code = ''' done:; ''' +# test that our explicit functions (lfsr_fs_ckmeta/ckdata) work as well, +# these call the same logic internally +[cases.test_gc_ckmeta_explicit] +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:; + // lfsr_fs_ckmeta should find the clobbered block + lfsr_fs_ckmeta(&lfs) => LFS_ERR_CORRUPT; + + lfsr_unmount(&lfs) => 0; + } +done:; +''' + +[cases.test_gc_ckdata_explicit] +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:; + // lfsr_fs_ckdata should find the clobbered block + lfsr_fs_ckdata(&lfs) => LFS_ERR_CORRUPT; + + lfsr_unmount(&lfs) => 0; + } +done:; +''' + # pseudo-fuzz test that dirtying still works with the GC API [cases.test_gc_mutation]