diff --git a/lfs.c b/lfs.c index 0bb570c..93a124d 100644 --- a/lfs.c +++ b/lfs.c @@ -29,7 +29,8 @@ static int lfs_bd_read(lfs_t *lfs, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { uint8_t *data = buffer; - if ((off+size > lfs->cfg->block_size) || (block == LFS_BLOCK_NULL)) { + if (block >= lfs->cfg->block_count || + off+size > lfs->cfg->block_size) { return LFS_ERR_CORRUPT; } @@ -172,7 +173,7 @@ static int lfs_bd_prog(lfs_t *lfs, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { const uint8_t *data = buffer; - LFS_ASSERT(block != LFS_BLOCK_NULL); + LFS_ASSERT(block == LFS_BLOCK_INLINE || block < lfs->cfg->block_count); LFS_ASSERT(off + size <= lfs->cfg->block_size); while (size > 0) { @@ -747,6 +748,12 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, // scanning the entire directory lfs_stag_t besttag = -1; + // if either block address is invalid we return LFS_ERR_CORRUPT here, + // otherwise later writes to the pair could fail + if (pair[0] >= lfs->cfg->block_count || pair[1] >= lfs->cfg->block_count) { + return LFS_ERR_CORRUPT; + } + // find the block with the most recent revision uint32_t revs[2] = {0, 0}; int r = 0; @@ -2196,7 +2203,6 @@ static int lfs_ctz_find(lfs_t *lfs, return err; } - LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count); current -= 1 << skip; } @@ -2216,7 +2222,6 @@ static int lfs_ctz_extend(lfs_t *lfs, if (err) { return err; } - LFS_ASSERT(nblock >= 2 && nblock <= lfs->cfg->block_count); { err = lfs_bd_erase(lfs, nblock); @@ -2289,8 +2294,6 @@ static int lfs_ctz_extend(lfs_t *lfs, return err; } } - - LFS_ASSERT(nhead >= 2 && nhead <= lfs->cfg->block_count); } *block = nblock; @@ -3661,7 +3664,15 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { // scan directory blocks for superblock and any global updates lfs_mdir_t dir = {.tail = {0, 1}}; + lfs_block_t cycle = 0; while (!lfs_pair_isnull(dir.tail)) { + if (cycle >= lfs->cfg->block_count/2) { + // loop detected + err = LFS_ERR_CORRUPT; + goto cleanup; + } + cycle += 1; + // fetch next block in tail list lfs_stag_t tag = lfs_dir_fetchmatch(lfs, &dir, dir.tail, LFS_MKTAG(0x7ff, 0x3ff, 0), @@ -3803,7 +3814,14 @@ int lfs_fs_traverseraw(lfs_t *lfs, } #endif + lfs_block_t cycle = 0; while (!lfs_pair_isnull(dir.tail)) { + if (cycle >= lfs->cfg->block_count/2) { + // loop detected + return LFS_ERR_CORRUPT; + } + cycle += 1; + for (int i = 0; i < 2; i++) { int err = cb(data, dir.tail[i]); if (err) { @@ -3887,7 +3905,14 @@ static int lfs_fs_pred(lfs_t *lfs, // iterate over all directory directory entries pdir->tail[0] = 0; pdir->tail[1] = 1; + lfs_block_t cycle = 0; while (!lfs_pair_isnull(pdir->tail)) { + if (cycle >= lfs->cfg->block_count/2) { + // loop detected + return LFS_ERR_CORRUPT; + } + cycle += 1; + if (lfs_pair_cmp(pdir->tail, pair) == 0) { return 0; } @@ -3930,7 +3955,14 @@ static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t pair[2], // use fetchmatch with callback to find pairs parent->tail[0] = 0; parent->tail[1] = 1; + lfs_block_t cycle = 0; while (!lfs_pair_isnull(parent->tail)) { + if (cycle >= lfs->cfg->block_count/2) { + // loop detected + return LFS_ERR_CORRUPT; + } + cycle += 1; + lfs_stag_t tag = lfs_dir_fetchmatch(lfs, parent, parent->tail, LFS_MKTAG(0x7ff, 0, 0x3ff), LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 0, 8), diff --git a/tests/test_evil.toml b/tests/test_evil.toml new file mode 100644 index 0000000..920d3a0 --- /dev/null +++ b/tests/test_evil.toml @@ -0,0 +1,288 @@ +# Tests for recovering from conditions which shouldn't normally +# happen during normal operation of littlefs + +# invalid pointer tests (outside of block_count) + +[[case]] # invalid tail-pointer test +define.TAIL_TYPE = ['LFS_TYPE_HARDTAIL', 'LFS_TYPE_SOFTTAIL'] +define.INVALSET = [0x3, 0x1, 0x2] +in = "lfs.c" +code = ''' + // create littlefs + lfs_format(&lfs, &cfg) => 0; + + // change tail-pointer to invalid pointers + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + lfs_dir_commit(&lfs, &mdir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_HARDTAIL, 0x3ff, 8), + (lfs_block_t[2]){ + (INVALSET & 0x1) ? 0xcccccccc : 0, + (INVALSET & 0x2) ? 0xcccccccc : 0}})) => 0; + lfs_deinit(&lfs) => 0; + + // test that mount fails gracefully + lfs_mount(&lfs, &cfg) => LFS_ERR_CORRUPT; +''' + +[[case]] # invalid dir pointer test +define.INVALSET = [0x3, 0x1, 0x2] +in = "lfs.c" +code = ''' + // create littlefs + lfs_format(&lfs, &cfg) => 0; + // make a dir + lfs_mount(&lfs, &cfg) => 0; + lfs_mkdir(&lfs, "dir_here") => 0; + lfs_unmount(&lfs) => 0; + + // change the dir pointer to be invalid + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + // make sure id 1 == our directory + lfs_dir_get(&lfs, &mdir, + LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_NAME, 1, strlen("dir_here")), buffer) + => LFS_MKTAG(LFS_TYPE_DIR, 1, strlen("dir_here")); + assert(memcmp((char*)buffer, "dir_here", strlen("dir_here")) == 0); + // change dir pointer + lfs_dir_commit(&lfs, &mdir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 1, 8), + (lfs_block_t[2]){ + (INVALSET & 0x1) ? 0xcccccccc : 0, + (INVALSET & 0x2) ? 0xcccccccc : 0}})) => 0; + lfs_deinit(&lfs) => 0; + + // test that accessing our bad dir fails, note there's a number + // of ways to access the dir, some can fail, but some don't + lfs_mount(&lfs, &cfg) => 0; + lfs_stat(&lfs, "dir_here", &info) => 0; + assert(strcmp(info.name, "dir_here") == 0); + assert(info.type == LFS_TYPE_DIR); + + lfs_dir_open(&lfs, &dir, "dir_here") => LFS_ERR_CORRUPT; + lfs_stat(&lfs, "dir_here/file_here", &info) => LFS_ERR_CORRUPT; + lfs_dir_open(&lfs, &dir, "dir_here/dir_here") => LFS_ERR_CORRUPT; + lfs_file_open(&lfs, &file, "dir_here/file_here", + LFS_O_RDONLY) => LFS_ERR_CORRUPT; + lfs_file_open(&lfs, &file, "dir_here/file_here", + LFS_O_WRONLY | LFS_O_CREAT) => LFS_ERR_CORRUPT; + lfs_unmount(&lfs) => 0; +''' + +[[case]] # invalid file pointer test +in = "lfs.c" +define.SIZE = [10, 1000, 100000] # faked file size +code = ''' + // create littlefs + lfs_format(&lfs, &cfg) => 0; + // make a file + lfs_mount(&lfs, &cfg) => 0; + lfs_file_open(&lfs, &file, "file_here", + LFS_O_WRONLY | LFS_O_CREAT) => 0; + lfs_file_close(&lfs, &file) => 0; + lfs_unmount(&lfs) => 0; + + // change the file pointer to be invalid + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + // make sure id 1 == our file + lfs_dir_get(&lfs, &mdir, + LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_NAME, 1, strlen("file_here")), buffer) + => LFS_MKTAG(LFS_TYPE_REG, 1, strlen("file_here")); + assert(memcmp((char*)buffer, "file_here", strlen("file_here")) == 0); + // change file pointer + lfs_dir_commit(&lfs, &mdir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_CTZSTRUCT, 1, sizeof(struct lfs_ctz)), + &(struct lfs_ctz){0xcccccccc, lfs_tole32(SIZE)}})) => 0; + lfs_deinit(&lfs) => 0; + + // test that accessing our bad file fails, note there's a number + // of ways to access the dir, some can fail, but some don't + lfs_mount(&lfs, &cfg) => 0; + lfs_stat(&lfs, "file_here", &info) => 0; + assert(strcmp(info.name, "file_here") == 0); + assert(info.type == LFS_TYPE_REG); + assert(info.size == SIZE); + + lfs_file_open(&lfs, &file, "file_here", LFS_O_RDONLY) => 0; + lfs_file_read(&lfs, &file, buffer, SIZE) => LFS_ERR_CORRUPT; + lfs_file_close(&lfs, &file) => 0; + + // any allocs that traverse CTZ must unfortunately must fail + if (SIZE > 2*LFS_BLOCK_SIZE) { + lfs_mkdir(&lfs, "dir_here") => LFS_ERR_CORRUPT; + } + lfs_unmount(&lfs) => 0; +''' + +[[case]] # invalid pointer in CTZ skip-list test +define.SIZE = ['2*LFS_BLOCK_SIZE', '3*LFS_BLOCK_SIZE', '4*LFS_BLOCK_SIZE'] +in = "lfs.c" +code = ''' + // create littlefs + lfs_format(&lfs, &cfg) => 0; + // make a file + lfs_mount(&lfs, &cfg) => 0; + lfs_file_open(&lfs, &file, "file_here", + LFS_O_WRONLY | LFS_O_CREAT) => 0; + for (int i = 0; i < SIZE; i++) { + char c = 'c'; + lfs_file_write(&lfs, &file, &c, 1) => 1; + } + lfs_file_close(&lfs, &file) => 0; + lfs_unmount(&lfs) => 0; + // change pointer in CTZ skip-list to be invalid + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + // make sure id 1 == our file and get our CTZ structure + lfs_dir_get(&lfs, &mdir, + LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_NAME, 1, strlen("file_here")), buffer) + => LFS_MKTAG(LFS_TYPE_REG, 1, strlen("file_here")); + assert(memcmp((char*)buffer, "file_here", strlen("file_here")) == 0); + struct lfs_ctz ctz; + lfs_dir_get(&lfs, &mdir, + LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, 1, sizeof(struct lfs_ctz)), &ctz) + => LFS_MKTAG(LFS_TYPE_CTZSTRUCT, 1, sizeof(struct lfs_ctz)); + lfs_ctz_fromle32(&ctz); + // rewrite block to contain bad pointer + uint8_t bbuffer[LFS_BLOCK_SIZE]; + cfg.read(&cfg, ctz.head, 0, bbuffer, LFS_BLOCK_SIZE) => 0; + uint32_t bad = lfs_tole32(0xcccccccc); + memcpy(&bbuffer[0], &bad, sizeof(bad)); + memcpy(&bbuffer[4], &bad, sizeof(bad)); + cfg.erase(&cfg, ctz.head) => 0; + cfg.prog(&cfg, ctz.head, 0, bbuffer, LFS_BLOCK_SIZE) => 0; + lfs_deinit(&lfs) => 0; + + // test that accessing our bad file fails, note there's a number + // of ways to access the dir, some can fail, but some don't + lfs_mount(&lfs, &cfg) => 0; + lfs_stat(&lfs, "file_here", &info) => 0; + assert(strcmp(info.name, "file_here") == 0); + assert(info.type == LFS_TYPE_REG); + assert(info.size == SIZE); + + lfs_file_open(&lfs, &file, "file_here", LFS_O_RDONLY) => 0; + lfs_file_read(&lfs, &file, buffer, SIZE) => LFS_ERR_CORRUPT; + lfs_file_close(&lfs, &file) => 0; + + // any allocs that traverse CTZ must unfortunately must fail + if (SIZE > 2*LFS_BLOCK_SIZE) { + lfs_mkdir(&lfs, "dir_here") => LFS_ERR_CORRUPT; + } + lfs_unmount(&lfs) => 0; +''' + + +[[case]] # invalid gstate pointer +define.INVALSET = [0x3, 0x1, 0x2] +in = "lfs.c" +code = ''' + // create littlefs + lfs_format(&lfs, &cfg) => 0; + + // create an invalid gstate + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + lfs_fs_prepmove(&lfs, 1, (lfs_block_t [2]){ + (INVALSET & 0x1) ? 0xcccccccc : 0, + (INVALSET & 0x2) ? 0xcccccccc : 0}); + lfs_dir_commit(&lfs, &mdir, NULL, 0) => 0; + lfs_deinit(&lfs) => 0; + + // test that mount fails gracefully + // mount may not fail, but our first alloc should fail when + // we try to fix the gstate + lfs_mount(&lfs, &cfg) => 0; + lfs_mkdir(&lfs, "should_fail") => LFS_ERR_CORRUPT; + lfs_unmount(&lfs) => 0; +''' + +# cycle detection/recovery tests + +[[case]] # metadata-pair threaded-list loop test +in = "lfs.c" +code = ''' + // create littlefs + lfs_format(&lfs, &cfg) => 0; + + // change tail-pointer to point to ourself + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + lfs_dir_commit(&lfs, &mdir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_HARDTAIL, 0x3ff, 8), + (lfs_block_t[2]){0, 1}})) => 0; + lfs_deinit(&lfs) => 0; + + // test that mount fails gracefully + lfs_mount(&lfs, &cfg) => LFS_ERR_CORRUPT; +''' + +[[case]] # metadata-pair threaded-list 2-length loop test +in = "lfs.c" +code = ''' + // create littlefs with child dir + lfs_format(&lfs, &cfg) => 0; + lfs_mount(&lfs, &cfg) => 0; + lfs_mkdir(&lfs, "child") => 0; + lfs_unmount(&lfs) => 0; + + // find child + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_block_t pair[2]; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + lfs_dir_get(&lfs, &mdir, + LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 1, sizeof(pair)), pair) + => LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 1, sizeof(pair)); + lfs_pair_fromle32(pair); + // change tail-pointer to point to root + lfs_dir_fetch(&lfs, &mdir, pair) => 0; + lfs_dir_commit(&lfs, &mdir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_HARDTAIL, 0x3ff, 8), + (lfs_block_t[2]){0, 1}})) => 0; + lfs_deinit(&lfs) => 0; + + // test that mount fails gracefully + lfs_mount(&lfs, &cfg) => LFS_ERR_CORRUPT; +''' + +[[case]] # metadata-pair threaded-list 1-length child loop test +in = "lfs.c" +code = ''' + // create littlefs with child dir + lfs_format(&lfs, &cfg) => 0; + lfs_mount(&lfs, &cfg) => 0; + lfs_mkdir(&lfs, "child") => 0; + lfs_unmount(&lfs) => 0; + + // find child + lfs_init(&lfs, &cfg) => 0; + lfs_mdir_t mdir; + lfs_block_t pair[2]; + lfs_dir_fetch(&lfs, &mdir, (lfs_block_t[2]){0, 1}) => 0; + lfs_dir_get(&lfs, &mdir, + LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 1, sizeof(pair)), pair) + => LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 1, sizeof(pair)); + lfs_pair_fromle32(pair); + // change tail-pointer to point to ourself + lfs_dir_fetch(&lfs, &mdir, pair) => 0; + lfs_dir_commit(&lfs, &mdir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_HARDTAIL, 0x3ff, 8), pair})) => 0; + lfs_deinit(&lfs) => 0; + + // test that mount fails gracefully + lfs_mount(&lfs, &cfg) => LFS_ERR_CORRUPT; +'''