From 75f80aabd728aafec6adc8d586be800b95229cb1 Mon Sep 17 00:00:00 2001 From: Christopher Haster Date: Thu, 10 Nov 2022 17:43:26 -0600 Subject: [PATCH] Added block_size search in lfs_mount, moved block_size/count into lfs_t This adds two new configuration options: erase_size and erase_count, allowing block_size and block_count to be loaded from the superblock during mount. For backwards compability these default to block_size and block_count if zero. --- Unfortunately this is a bit easier said than done. littlefs keeps its superblock in the metadata pair located at blocks {0,1}, which is also where the root directory lives (keep in mind small littlefs images may only have 2 blocks in total). If we mutate blocks {0,1}, we have to erase before programming, which means it's possible to have the only superblock in block 1. This presents a puzzle because how do you find block 1 if you don't know the size of a block? One solution presented here is to search for block 1 by trying different sizes until we find a superblock. This isn't great but there are some properties of this search that help: 1. If we do find a superblock, the search will never take longer than a mount with a known block_size. This is because we stop at block 1, searching at most O(block_size) bytes, and metadata fetch is already a O(block_size) operation. This means the concern is limited to how long it takes to fail when littlefs is not present on the disk. 2. We can assume the on-disk block_size is probably a factor of the total size of the disk. After a bit of digging into the math, this apparently reduces the runtime to the divisor function, d(n), which is sublinear. According to a blog post by Terence Tao this is bounded by the ridiculous O(e^O(log(n)/log(log(n)))): https://terrytao.wordpress.com/2008/09/23/the-divisor-bound This is apparently somewhere between O(sqrt(n)) and O(log(n)), but conveniently O(log(n)) on average and O(log(n)) for powers of 2. I've left it as O(d(n)) in the documentation, which might be a bit confusing, but I'm not sure how best to capture "mostly log(n)" correctly. 3. If we don't know the block_size, or don't know that block_size is aligned to the disk size, the best we can do is a O(n) search. In this case I've added a warning, so at least it's distinguishable from an infinite loop if debugging. --- lfs.c | 470 +++++++++++++++++++++++++----------- lfs.h | 53 +++- tests/test_badblocks.toml | 2 +- tests/test_evil.toml | 2 +- tests/test_superblocks.toml | 2 +- 5 files changed, 373 insertions(+), 156 deletions(-) diff --git a/lfs.c b/lfs.c index 48a10e8e..63be7fb8 100644 --- a/lfs.c +++ b/lfs.c @@ -26,6 +26,100 @@ enum { }; +/// Mapping from logical to physical erase size /// + +static int lfs_bd_rawread(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size) { + LFS_ASSERT(block < lfs->block_count); + LFS_ASSERT(off + size <= lfs->block_size); + LFS_ASSERT(size % lfs->cfg->read_size == 0); + + // adjust to physical erase size + block = (block * (lfs->block_size/lfs->erase_size)) + + (off / lfs->erase_size); + off = off % lfs->erase_size; + uint8_t *buffer_ = buffer; + + // read in erase_size chunks + while (size > 0) { + lfs_size_t delta = lfs_min(size, off + lfs->erase_size); + LFS_ASSERT(block <= lfs->erase_count); + LFS_ASSERT(off + size <= lfs->erase_size); + LFS_ASSERT(size % lfs->cfg->read_size == 0); + int err = lfs->cfg->read(lfs->cfg, block, off, buffer_, delta); + LFS_ASSERT(err <= 0); + if (err) { + return err; + } + + off += delta; + if (off == lfs->erase_size) { + block += 1; + off = 0; + } + size -= delta; + } + + return 0; +} + +#ifndef LFS_READONLY +static int lfs_bd_rawprog(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size) { + LFS_ASSERT(block < lfs->block_count); + LFS_ASSERT(off + size <= lfs->block_size); + LFS_ASSERT(size % lfs->cfg->prog_size == 0); + + // adjust to physical erase size + block = (block * (lfs->block_size/lfs->erase_size)) + + (off / lfs->erase_size); + off = off % lfs->erase_size; + uint8_t *buffer_ = buffer; + + // prog in erase_size chunks + while (size > 0) { + lfs_size_t delta = lfs_min(size, off + lfs->erase_size); + LFS_ASSERT(block <= lfs->erase_count); + LFS_ASSERT(off + size <= lfs->erase_size); + LFS_ASSERT(size % lfs->cfg->prog_size == 0); + int err = lfs->cfg->prog(lfs->cfg, block, off, buffer_, delta); + LFS_ASSERT(err <= 0); + if (err) { + return err; + } + + off += delta; + if (off == lfs->erase_size) { + block += 1; + off = 0; + } + size -= delta; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { + LFS_ASSERT(block < lfs->block_count); + + // adjust to physical erase size + block = block * (lfs->block_size/lfs->erase_size); + for (lfs_block_t i = 0; i < lfs->block_size/lfs->erase_size; i++) { + LFS_ASSERT(block + i <= lfs->erase_count); + int err = lfs->cfg->erase(lfs->cfg, block + i); + LFS_ASSERT(err <= 0); + if (err < 0) { + return err; + } + } + + return 0; +} +#endif + + /// Caching block device operations /// static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) { @@ -46,8 +140,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 (block >= lfs->cfg->block_count || - off+size > lfs->cfg->block_size) { + if (block >= lfs->block_count || + off+size > lfs->block_size) { return LFS_ERR_CORRUPT; } @@ -92,7 +186,7 @@ static int lfs_bd_read(lfs_t *lfs, size >= lfs->cfg->read_size) { // bypass cache? diff = lfs_aligndown(diff, lfs->cfg->read_size); - int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); + int err = lfs_bd_rawread(lfs, block, off, data, diff); if (err) { return err; } @@ -104,18 +198,16 @@ static int lfs_bd_read(lfs_t *lfs, } // load to cache, first condition can no longer fail - LFS_ASSERT(block < lfs->cfg->block_count); rcache->block = block; rcache->off = lfs_aligndown(off, lfs->cfg->read_size); rcache->size = lfs_min( lfs_min( lfs_alignup(off+hint, lfs->cfg->read_size), - lfs->cfg->block_size) + lfs->block_size) - rcache->off, lfs->cfg->cache_size); - int err = lfs->cfg->read(lfs->cfg, rcache->block, + int err = lfs_bd_rawread(lfs, rcache->block, rcache->off, rcache->buffer, rcache->size); - LFS_ASSERT(err <= 0); if (err) { return err; } @@ -155,11 +247,9 @@ static int lfs_bd_cmp(lfs_t *lfs, static int lfs_bd_flush(lfs_t *lfs, lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) { if (pcache->block != LFS_BLOCK_NULL && pcache->block != LFS_BLOCK_INLINE) { - LFS_ASSERT(pcache->block < lfs->cfg->block_count); lfs_size_t diff = lfs_alignup(pcache->size, lfs->cfg->prog_size); - int err = lfs->cfg->prog(lfs->cfg, pcache->block, + int err = lfs_bd_rawprog(lfs, pcache->block, pcache->off, pcache->buffer, diff); - LFS_ASSERT(err <= 0); if (err) { return err; } @@ -208,8 +298,8 @@ 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_INLINE || block < lfs->cfg->block_count); - LFS_ASSERT(off + size <= lfs->cfg->block_size); + LFS_ASSERT(block == LFS_BLOCK_INLINE || block < lfs->block_count); + LFS_ASSERT(off + size <= lfs->block_size); while (size > 0) { if (block == pcache->block && @@ -250,15 +340,6 @@ static int lfs_bd_prog(lfs_t *lfs, } #endif -#ifndef LFS_READONLY -static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { - LFS_ASSERT(block < lfs->cfg->block_count); - int err = lfs->cfg->erase(lfs->cfg, block); - LFS_ASSERT(err <= 0); - return err; -} -#endif - /// Small type-level utilities /// // operations on block pairs @@ -528,7 +609,7 @@ static int lfs_rawunmount(lfs_t *lfs); static int lfs_alloc_lookahead(void *p, lfs_block_t block) { lfs_t *lfs = (lfs_t*)p; lfs_block_t off = ((block - lfs->free.off) - + lfs->cfg->block_count) % lfs->cfg->block_count; + + lfs->block_count) % lfs->block_count; if (off < lfs->free.size) { lfs->free.buffer[off / 32] |= 1U << (off % 32); @@ -542,7 +623,7 @@ static int lfs_alloc_lookahead(void *p, lfs_block_t block) { // is to prevent blocks from being garbage collected in the middle of a // commit operation static void lfs_alloc_ack(lfs_t *lfs) { - lfs->free.ack = lfs->cfg->block_count; + lfs->free.ack = lfs->block_count; } // drop the lookahead buffer, this is done during mounting and failed @@ -563,7 +644,7 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { if (!(lfs->free.buffer[off / 32] & (1U << (off % 32)))) { // found a free block - *block = (lfs->free.off + off) % lfs->cfg->block_count; + *block = (lfs->free.off + off) % lfs->block_count; // eagerly find next off so an alloc ack can // discredit old lookahead blocks @@ -586,7 +667,7 @@ static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { } lfs->free.off = (lfs->free.off + lfs->free.size) - % lfs->cfg->block_count; + % lfs->block_count; lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size, lfs->free.ack); lfs->free.i = 0; @@ -676,7 +757,7 @@ static int lfs_dir_getread(lfs_t *lfs, const lfs_mdir_t *dir, lfs_tag_t gmask, lfs_tag_t gtag, lfs_off_t off, void *buffer, lfs_size_t size) { uint8_t *data = buffer; - if (off+size > lfs->cfg->block_size) { + if (off+size > lfs->block_size) { return LFS_ERR_CORRUPT; } @@ -998,7 +1079,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, // 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) { + if (pair[0] >= lfs->block_count || pair[1] >= lfs->block_count) { return LFS_ERR_CORRUPT; } @@ -1044,7 +1125,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, lfs_tag_t tag; off += lfs_tag_dsize(ptag); int err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, + NULL, &lfs->rcache, lfs->block_size, dir->pair[0], off, &tag, sizeof(tag)); if (err) { if (err == LFS_ERR_CORRUPT) { @@ -1063,7 +1144,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, dir->erased = (lfs_tag_type1(ptag) == LFS_TYPE_CRC && dir->off % lfs->cfg->prog_size == 0); break; - } else if (off + lfs_tag_dsize(tag) > lfs->cfg->block_size) { + } else if (off + lfs_tag_dsize(tag) > lfs->block_size) { dir->erased = false; break; } @@ -1074,7 +1155,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, // check the crc attr uint32_t dcrc; err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, + NULL, &lfs->rcache, lfs->block_size, dir->pair[0], off+sizeof(tag), &dcrc, sizeof(dcrc)); if (err) { if (err == LFS_ERR_CORRUPT) { @@ -1117,7 +1198,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, for (lfs_off_t j = sizeof(tag); j < lfs_tag_dsize(tag); j++) { uint8_t dat; err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, + NULL, &lfs->rcache, lfs->block_size, dir->pair[0], off+j, &dat, 1); if (err) { if (err == LFS_ERR_CORRUPT) { @@ -1150,7 +1231,7 @@ static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, tempsplit = (lfs_tag_chunk(tag) & 1); err = lfs_bd_read(lfs, - NULL, &lfs->rcache, lfs->cfg->block_size, + NULL, &lfs->rcache, lfs->block_size, dir->pair[0], off+sizeof(tag), &temptail, 8); if (err) { if (err == LFS_ERR_CORRUPT) { @@ -1770,7 +1851,7 @@ static int lfs_dir_compact(lfs_t *lfs, .begin = 0, .end = (lfs->cfg->metadata_max ? - lfs->cfg->metadata_max : lfs->cfg->block_size) - 8, + lfs->cfg->metadata_max : lfs->block_size) - 8, }; // erase block to write to @@ -1940,11 +2021,11 @@ static int lfs_dir_splittingcompact(lfs_t *lfs, lfs_mdir_t *dir, // if (end - split < 0xff && size <= lfs_min( - lfs->cfg->block_size - 40, + lfs->block_size - 40, lfs_alignup( (lfs->cfg->metadata_max ? lfs->cfg->metadata_max - : lfs->cfg->block_size)/2, + : lfs->block_size)/2, lfs->cfg->prog_size))) { break; } @@ -1986,7 +2067,7 @@ static int lfs_dir_splittingcompact(lfs_t *lfs, lfs_mdir_t *dir, // do we have extra space? littlefs can't reclaim this space // by itself, so expand cautiously - if ((lfs_size_t)size < lfs->cfg->block_count/2) { + if ((lfs_size_t)size < lfs->block_count/2) { LFS_DEBUG("Expanding superblock at rev %"PRIu32, dir->rev); int err = lfs_dir_split(lfs, dir, attrs, attrcount, source, begin, end); @@ -2056,7 +2137,7 @@ static int lfs_dir_relocatingcommit(lfs_t *lfs, lfs_mdir_t *dir, .begin = dir->off, .end = (lfs->cfg->metadata_max ? - lfs->cfg->metadata_max : lfs->cfg->block_size) - 8, + lfs->cfg->metadata_max : lfs->block_size) - 8, }; // traverse attrs that need to be written out @@ -2647,7 +2728,7 @@ static int lfs_dir_rawrewind(lfs_t *lfs, lfs_dir_t *dir) { /// File index list operations /// static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) { lfs_off_t size = *off; - lfs_off_t b = lfs->cfg->block_size - 2*4; + lfs_off_t b = lfs->block_size - 2*4; lfs_off_t i = size / b; if (i == 0) { return 0; @@ -2725,7 +2806,7 @@ static int lfs_ctz_extend(lfs_t *lfs, noff = noff + 1; // just copy out the last block if it is incomplete - if (noff != lfs->cfg->block_size) { + if (noff != lfs->block_size) { for (lfs_off_t i = 0; i < noff; i++) { uint8_t data; err = lfs_bd_read(lfs, @@ -3265,7 +3346,7 @@ static lfs_ssize_t lfs_file_flushedread(lfs_t *lfs, lfs_file_t *file, while (nsize > 0) { // check if we need a new block if (!(file->flags & LFS_F_READING) || - file->off == lfs->cfg->block_size) { + file->off == lfs->block_size) { if (!(file->flags & LFS_F_INLINE)) { int err = lfs_ctz_find(lfs, NULL, &file->cache, file->ctz.head, file->ctz.size, @@ -3282,10 +3363,10 @@ static lfs_ssize_t lfs_file_flushedread(lfs_t *lfs, lfs_file_t *file, } // read as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + lfs_size_t diff = lfs_min(nsize, lfs->block_size - file->off); if (file->flags & LFS_F_INLINE) { int err = lfs_dir_getread(lfs, &file->m, - NULL, &file->cache, lfs->cfg->block_size, + NULL, &file->cache, lfs->block_size, LFS_MKTAG(0xfff, 0x1ff, 0), LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), file->off, data, diff); @@ -3294,7 +3375,7 @@ static lfs_ssize_t lfs_file_flushedread(lfs_t *lfs, lfs_file_t *file, } } else { int err = lfs_bd_read(lfs, - NULL, &file->cache, lfs->cfg->block_size, + NULL, &file->cache, lfs->block_size, file->block, file->off, data, diff); if (err) { return err; @@ -3339,7 +3420,7 @@ static lfs_ssize_t lfs_file_flushedwrite(lfs_t *lfs, lfs_file_t *file, lfs_min(0x3fe, lfs_min( lfs->cfg->cache_size, (lfs->cfg->metadata_max ? - lfs->cfg->metadata_max : lfs->cfg->block_size) / 8))) { + lfs->cfg->metadata_max : lfs->block_size) / 8))) { // inline file doesn't fit anymore int err = lfs_file_outline(lfs, file); if (err) { @@ -3351,7 +3432,7 @@ static lfs_ssize_t lfs_file_flushedwrite(lfs_t *lfs, lfs_file_t *file, while (nsize > 0) { // check if we need a new block if (!(file->flags & LFS_F_WRITING) || - file->off == lfs->cfg->block_size) { + file->off == lfs->block_size) { if (!(file->flags & LFS_F_INLINE)) { if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { // find out which block we're extending from @@ -3385,7 +3466,7 @@ static lfs_ssize_t lfs_file_flushedwrite(lfs_t *lfs, lfs_file_t *file, } // program as much as we can in current block - lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + lfs_size_t diff = lfs_min(nsize, lfs->block_size - file->off); while (true) { int err = lfs_bd_prog(lfs, &file->cache, &lfs->rcache, true, file->block, file->off, data, diff); @@ -3915,20 +3996,40 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { int err = 0; // validate that the lfs-cfg sizes were initiated properly before - // performing any arithmetic logics with them + // performing any arithmetic logic with them LFS_ASSERT(lfs->cfg->read_size != 0); LFS_ASSERT(lfs->cfg->prog_size != 0); LFS_ASSERT(lfs->cfg->cache_size != 0); + LFS_ASSERT(lfs->cfg->erase_size != 0 || lfs->cfg->block_size != 0); + LFS_ASSERT(lfs->cfg->erase_count != 0 || lfs->cfg->block_count != 0); - // check that block size is a multiple of cache size is a multiple - // of prog and read sizes + // check that cache_size is a multiple of prog_size and read_size LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->read_size == 0); LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->prog_size == 0); - LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->cache_size == 0); + + // setup erase_size/count, these can be zero for backwards compatibility + lfs->erase_size = lfs->cfg->erase_size; + lfs->erase_count = lfs->cfg->erase_count; + lfs->block_size = lfs->cfg->block_size; + lfs->block_count = lfs->cfg->block_count; + if (!lfs->erase_size) { + lfs->erase_size = lfs->block_size; + } + if (!lfs->erase_count) { + lfs->erase_count = lfs->block_count; + } + + // check that block_size is a multiple of erase_size is a mulitiple of + // cache_size, this implies everything is a multiple of read_size and + // prog_size + LFS_ASSERT(lfs->erase_size % lfs->cfg->cache_size == 0); + if (lfs->block_size) { + LFS_ASSERT(lfs->block_size % lfs->erase_size == 0); + } // check that the block size is large enough to fit ctz pointers - LFS_ASSERT(4*lfs_npw2(0xffffffff / (lfs->cfg->block_size-2*4)) - <= lfs->cfg->block_size); + LFS_ASSERT(4*lfs_npw2(0xffffffff / (lfs->block_size-2*4)) + <= lfs->block_size); // block_cycles = 0 is no longer supported. // @@ -3998,7 +4099,7 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { lfs->attr_max = LFS_ATTR_MAX; } - LFS_ASSERT(lfs->cfg->metadata_max <= lfs->cfg->block_size); + LFS_ASSERT(lfs->cfg->metadata_max <= lfs->block_size); // setup default state lfs->root[0] = LFS_BLOCK_NULL; @@ -4045,11 +4146,19 @@ static int lfs_rawformat(lfs_t *lfs, const struct lfs_config *cfg) { return err; } + // if block_size/block_count not specified, assume equal to erase blocks + if (!lfs->block_size) { + lfs->block_size = lfs->erase_size; + } + if (!lfs->block_count) { + lfs->block_count = lfs->erase_count; + } + // create free lookahead memset(lfs->free.buffer, 0, lfs->cfg->lookahead_size); lfs->free.off = 0; lfs->free.size = lfs_min(8*lfs->cfg->lookahead_size, - lfs->cfg->block_count); + lfs->block_count); lfs->free.i = 0; lfs_alloc_ack(lfs); @@ -4063,8 +4172,8 @@ static int lfs_rawformat(lfs_t *lfs, const struct lfs_config *cfg) { // write one superblock lfs_superblock_t superblock = { .version = LFS_DISK_VERSION, - .block_size = lfs->cfg->block_size, - .block_count = lfs->cfg->block_count, + .block_size = lfs->block_size, + .block_count = lfs->block_count, .name_max = lfs->name_max, .file_max = lfs->file_max, .attr_max = lfs->attr_max, @@ -4108,111 +4217,180 @@ static int lfs_rawmount(lfs_t *lfs, const struct lfs_config *cfg) { return err; } - // 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; + // if block_size is unknown we need to search for it + lfs_size_t block_size_limit = lfs->block_size; + if (!lfs->block_size) { + lfs->block_size = lfs->erase_size; + // make sure this doesn't overflow + lfs_size_t limit = lfs->block_count + ? lfs->block_count/2 + : lfs->erase_count/2; + if (limit > ((lfs_size_t)-1) / lfs->block_size) { + block_size_limit = ((lfs_size_t)-1); + } else { + block_size_limit = limit * lfs->block_size; } - cycle += 1; + } - // fetch next block in tail list - lfs_stag_t tag = lfs_dir_fetchmatch(lfs, &dir, dir.tail, - LFS_MKTAG(0x7ff, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), - NULL, - lfs_dir_find_match, &(struct lfs_dir_find_match){ - lfs, "littlefs", 8}); - if (tag < 0) { - err = tag; - goto cleanup; - } + // search for the correct block_size + while (true) { + // setup block_size/count so underlying operations work + lfs->block_count = lfs->erase_count + / (lfs->block_size/lfs->erase_size); - // has superblock? - if (tag && !lfs_tag_isdelete(tag)) { - // update root - lfs->root[0] = dir.pair[0]; - lfs->root[1] = dir.pair[1]; + // 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->block_count/2) { + // loop detected + err = LFS_ERR_CORRUPT; + goto cleanup; + } + cycle += 1; - // grab superblock - lfs_superblock_t superblock; - tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x7ff, 0x3ff, 0), - LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), - &superblock); + // fetch next block in tail list + lfs_stag_t tag = lfs_dir_fetchmatch(lfs, &dir, dir.tail, + LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), + NULL, + lfs_dir_find_match, &(struct lfs_dir_find_match){ + lfs, "littlefs", 8}); if (tag < 0) { + if (tag == LFS_ERR_CORRUPT) { + // maybe our block_size is wrong + goto next_block_size; + } err = tag; goto cleanup; } - lfs_superblock_fromle32(&superblock); - // check version - uint16_t major_version = (0xffff & (superblock.version >> 16)); - uint16_t minor_version = (0xffff & (superblock.version >> 0)); - if ((major_version != LFS_DISK_VERSION_MAJOR || - minor_version > LFS_DISK_VERSION_MINOR)) { - LFS_ERROR("Invalid version v%"PRIu16".%"PRIu16, - major_version, minor_version); - err = LFS_ERR_INVAL; - goto cleanup; - } + // has superblock? + if (tag && !lfs_tag_isdelete(tag)) { + // grab superblock + lfs_superblock_t superblock; + tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock); + if (tag < 0) { + if (tag == LFS_ERR_CORRUPT) { + // maybe our block_size is wrong + goto next_block_size; + } + err = tag; + goto cleanup; + } + lfs_superblock_fromle32(&superblock); - // check superblock configuration - if (superblock.name_max) { - if (superblock.name_max > lfs->name_max) { - LFS_ERROR("Unsupported name_max (%"PRIu32" > %"PRIu32")", - superblock.name_max, lfs->name_max); + // we may not be done, first check the stored block_size, it + // it's different we need to remount in case we found an + // outdated superblock + if (superblock.block_size != lfs->block_size) { + if (lfs->cfg->block_size + || superblock.block_size % lfs->erase_size != 0 + || superblock.block_size < lfs->block_size) { + LFS_ERROR("Invalid block size %"PRIu32, + superblock.block_size); + err = LFS_ERR_INVAL; + goto cleanup; + } + + // remount with correct block_size + lfs->block_size = superblock.block_size; + goto next_mount; + } + + if (superblock.block_count != lfs->block_count) { + if ((lfs->cfg->block_size && lfs->cfg->block_count) + || superblock.block_count > lfs->block_count) { + LFS_ERROR("Invalid block count %"PRIu32, + superblock.block_count); + err = LFS_ERR_INVAL; + goto cleanup; + } + + lfs->block_count = superblock.block_count; + } + + // check version + uint16_t major_version = (0xffff & (superblock.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.version >> 0)); + if ((major_version != LFS_DISK_VERSION_MAJOR || + minor_version > LFS_DISK_VERSION_MINOR)) { + LFS_ERROR("Invalid version v%"PRIu16".%"PRIu16, + major_version, minor_version); err = LFS_ERR_INVAL; goto cleanup; } - lfs->name_max = superblock.name_max; - } + // check superblock configuration + if (superblock.name_max) { + if (superblock.name_max > lfs->name_max) { + LFS_ERROR("Unsupported name_max %"PRIu32, + superblock.name_max); + err = LFS_ERR_INVAL; + goto cleanup; + } - if (superblock.file_max) { - if (superblock.file_max > lfs->file_max) { - LFS_ERROR("Unsupported file_max (%"PRIu32" > %"PRIu32")", - superblock.file_max, lfs->file_max); - err = LFS_ERR_INVAL; - goto cleanup; + lfs->name_max = superblock.name_max; } - lfs->file_max = superblock.file_max; - } + if (superblock.file_max) { + if (superblock.file_max > lfs->file_max) { + LFS_ERROR("Unsupported file_max %"PRIu32, + superblock.file_max); + err = LFS_ERR_INVAL; + goto cleanup; + } - if (superblock.attr_max) { - if (superblock.attr_max > lfs->attr_max) { - LFS_ERROR("Unsupported attr_max (%"PRIu32" > %"PRIu32")", - superblock.attr_max, lfs->attr_max); - err = LFS_ERR_INVAL; - goto cleanup; + lfs->file_max = superblock.file_max; } - lfs->attr_max = superblock.attr_max; + if (superblock.attr_max) { + if (superblock.attr_max > lfs->attr_max) { + LFS_ERROR("Unsupported attr_max %"PRIu32, + superblock.attr_max); + err = LFS_ERR_INVAL; + goto cleanup; + } + + lfs->attr_max = superblock.attr_max; + } + + // update root + lfs->root[0] = dir.pair[0]; + lfs->root[1] = dir.pair[1]; } - if (superblock.block_count != lfs->cfg->block_count) { - LFS_ERROR("Invalid block count (%"PRIu32" != %"PRIu32")", - superblock.block_count, lfs->cfg->block_count); - err = LFS_ERR_INVAL; + // has gstate? + err = lfs_dir_getgstate(lfs, &dir, &lfs->gstate); + if (err) { goto cleanup; } - if (superblock.block_size != lfs->cfg->block_size) { - LFS_ERROR("Invalid block size (%"PRIu32" != %"PRIu32")", - superblock.block_size, lfs->cfg->block_size); - err = LFS_ERR_INVAL; - goto cleanup; - } + // we found a valid superblock, set block_size_limit so block_size + // will no longer change + block_size_limit = lfs->block_size; } - // has gstate? - err = lfs_dir_getgstate(lfs, &dir, &lfs->gstate); - if (err) { + break; + +next_block_size: + lfs->block_size += lfs->erase_size; + if (lfs->block_size > block_size_limit) { + err = LFS_ERR_INVAL; goto cleanup; } + + // if block_count is set, skip block_sizes that aren't a factor, + // this brings our search down from O(n) to O(d(n)), and + // O(log(n)) for powers of 2 + if (lfs->cfg->block_count && lfs->cfg->block_count + % (lfs->block_size/lfs->erase_size) != 0) { + goto next_block_size; + } + +next_mount:; } // found superblock? @@ -4233,7 +4411,7 @@ static int lfs_rawmount(lfs_t *lfs, const struct lfs_config *cfg) { // setup free lookahead, to distribute allocations uniformly across // boots, we start the allocator at a random location - lfs->free.off = lfs->seed % lfs->cfg->block_count; + lfs->free.off = lfs->seed % lfs->block_count; lfs_alloc_drop(lfs); return 0; @@ -4270,7 +4448,7 @@ int lfs_fs_rawtraverse(lfs_t *lfs, lfs_block_t cycle = 0; while (!lfs_pair_isnull(dir.tail)) { - if (cycle >= lfs->cfg->block_count/2) { + if (cycle >= lfs->block_count/2) { // loop detected return LFS_ERR_CORRUPT; } @@ -4355,7 +4533,7 @@ static int lfs_fs_pred(lfs_t *lfs, pdir->tail[1] = 1; lfs_block_t cycle = 0; while (!lfs_pair_isnull(pdir->tail)) { - if (cycle >= lfs->cfg->block_count/2) { + if (cycle >= lfs->block_count/2) { // loop detected return LFS_ERR_CORRUPT; } @@ -4392,7 +4570,7 @@ static int lfs_fs_parent_match(void *data, lfs_block_t child[2]; int err = lfs_bd_read(lfs, - &lfs->pcache, &lfs->rcache, lfs->cfg->block_size, + &lfs->pcache, &lfs->rcache, lfs->block_size, disk->block, disk->off, &child, sizeof(child)); if (err) { return err; @@ -4411,7 +4589,7 @@ static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t pair[2], parent->tail[1] = 1; lfs_block_t cycle = 0; while (!lfs_pair_isnull(parent->tail)) { - if (cycle >= lfs->cfg->block_count/2) { + if (cycle >= lfs->block_count/2) { // loop detected return LFS_ERR_CORRUPT; } @@ -4828,7 +5006,7 @@ static int lfs1_dir_fetch(lfs_t *lfs, } if ((0x7fffffff & test.size) < sizeof(test)+4 || - (0x7fffffff & test.size) > lfs->cfg->block_size) { + (0x7fffffff & test.size) > lfs->block_size) { continue; } @@ -5262,8 +5440,8 @@ static int lfs_rawmigrate(lfs_t *lfs, const struct lfs_config *cfg) { lfs_superblock_t superblock = { .version = LFS_DISK_VERSION, - .block_size = lfs->cfg->block_size, - .block_count = lfs->cfg->block_count, + .block_size = lfs->block_size, + .block_count = lfs->block_count, .name_max = lfs->name_max, .file_max = lfs->file_max, .attr_max = lfs->attr_max, @@ -5324,6 +5502,7 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { LFS_TRACE("lfs_format(%p, %p {.context=%p, " ".read=%p, .prog=%p, .erase=%p, .sync=%p, " ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".erase_size=%"PRIu32", .erase_count=%"PRIu32", " ".block_size=%"PRIu32", .block_count=%"PRIu32", " ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", " ".lookahead_size=%"PRIu32", .read_buffer=%p, " @@ -5333,8 +5512,9 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { (void*)lfs, (void*)cfg, cfg->context, (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, + cfg->read_size, cfg->prog_size, cfg->erase_size, cfg->erase_count, + cfg->block_size, cfg->block_count, cfg->block_cycles, + cfg->cache_size, cfg->lookahead_size, cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, cfg->name_max, cfg->file_max, cfg->attr_max); @@ -5354,6 +5534,7 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { LFS_TRACE("lfs_mount(%p, %p {.context=%p, " ".read=%p, .prog=%p, .erase=%p, .sync=%p, " ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".erase_size=%"PRIu32", .erase_count=%"PRIu32", " ".block_size=%"PRIu32", .block_count=%"PRIu32", " ".block_cycles=%"PRIu32", .cache_size=%"PRIu32", " ".lookahead_size=%"PRIu32", .read_buffer=%p, " @@ -5363,8 +5544,9 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { (void*)lfs, (void*)cfg, cfg->context, (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, - cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, - cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, + cfg->read_size, cfg->prog_size, cfg->erase_size, cfg->erase_count, + cfg->block_size, cfg->block_count, cfg->block_cycles, + cfg->cache_size, cfg->lookahead_size, cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, cfg->name_max, cfg->file_max, cfg->attr_max); diff --git a/lfs.h b/lfs.h index ab534526..d5927bca 100644 --- a/lfs.h +++ b/lfs.h @@ -189,21 +189,52 @@ struct lfs_config { int (*unlock)(const struct lfs_config *c); #endif - // Minimum size of a block read in bytes. All read operations will be a - // multiple of this value. + // Minimum size of a read operation in bytes. All read operations + // will be a multiple of this value. lfs_size_t read_size; - // Minimum size of a block program in bytes. All program operations will be - // a multiple of this value. + // Minimum size of a program operation in bytes. All program operations + // will be a multiple of this value. lfs_size_t prog_size; - // Size of an erasable block in bytes. This does not impact ram consumption - // and may be larger than the physical erase size. However, non-inlined - // files take up at minimum one block. Must be a multiple of the read and - // program sizes. + // Minimum size of an erase operation in bytes. All erase operations + // will be a multiple of this value. This must be a multiple of the read + // and program sizes. + // + // If zero, the block_size is used as the erase_size. This is mostly for + // backwards compatibility. + lfs_size_t erase_size; + + // Number of erase blocks on the device. + // + // If zero, the block_count is used as the erase_count. This is mostly for + // backwards compatibility. + lfs_size_t erase_count; + + // Size of a logical block in bytes. This does not impact RAM consumption + // and may be a multiple of the physical erase_size. + // + // Note, non-inlined files take up at minimum one logical block, and + // directories take up at minimum two logical blocks. + // + // If zero, littlefs attempts to find the superblock and use the the + // block_size stored there. This requires searching different block_sizes. + // If a superblock is found this takes no longer than mounting with a known + // block_size, but it can take time to fail if a superblock is not found: + // + // - O(block_size) if a superblock is found + // - O(d(block_count)) if block_count is non-zero + // - O(log(block_count)) if block_count is a power of 2 + // - O(erase_count) if block_count is zero lfs_size_t block_size; - // Number of erasable blocks on the device. + // Number of logical blocks on the device. + // + // If zero, littlefs uses the block_count stored in the superblock. + // + // If non-zero and block_size is zero, littlefs will assume block_size + // is a factor of erase_size*block_count to speed up mount when no + // superblock is found. lfs_size_t block_count; // Number of erase cycles before littlefs evicts metadata logs and moves @@ -408,6 +439,10 @@ typedef struct lfs { } free; const struct lfs_config *cfg; + lfs_size_t erase_size; + lfs_size_t erase_count; + lfs_size_t block_size; + lfs_size_t block_count; lfs_size_t name_max; lfs_size_t file_max; lfs_size_t attr_max; diff --git a/tests/test_badblocks.toml b/tests/test_badblocks.toml index b50b3933..5035b931 100644 --- a/tests/test_badblocks.toml +++ b/tests/test_badblocks.toml @@ -256,5 +256,5 @@ code = ''' lfs_t lfs; lfs_format(&lfs, cfg) => LFS_ERR_NOSPC; - lfs_mount(&lfs, cfg) => LFS_ERR_CORRUPT; + lfs_mount(&lfs, cfg) => LFS_ERR_INVAL; ''' diff --git a/tests/test_evil.toml b/tests/test_evil.toml index 4acd5ef0..1dde8650 100644 --- a/tests/test_evil.toml +++ b/tests/test_evil.toml @@ -24,7 +24,7 @@ code = ''' lfs_deinit(&lfs) => 0; // test that mount fails gracefully - lfs_mount(&lfs, cfg) => LFS_ERR_CORRUPT; + lfs_mount(&lfs, cfg) => LFS_ERR_INVAL; ''' [cases.test_evil_invalid_dir_pointer] diff --git a/tests/test_superblocks.toml b/tests/test_superblocks.toml index 689bbcd2..60bda767 100644 --- a/tests/test_superblocks.toml +++ b/tests/test_superblocks.toml @@ -31,7 +31,7 @@ code = ''' [cases.test_superblocks_invalid_mount] code = ''' lfs_t lfs; - lfs_mount(&lfs, cfg) => LFS_ERR_CORRUPT; + lfs_mount(&lfs, cfg) => LFS_ERR_INVAL; ''' # expanding superblock