Added support for handling corrupted blocks

This provides a limited form of wear leveling. While wear is
not actually balanced across blocks, the filesystem can recover
from corrupted blocks and extend the lifetime of a device nearly
as much as dynamic wear leveling.

For use-cases where wear is important, it would be better to use
a full form of dynamic wear-leveling at the block level. (or
consider a logging filesystem).

Corrupted block handling was simply added on top of the existing
logic in place for the filesystem, so it's a bit more noodly than
it may have to be, but it gets the work done.
This commit is contained in:
Christopher Haster
2017-05-14 12:01:45 -05:00
parent b35d761196
commit fd1da602d7
8 changed files with 634 additions and 311 deletions

View File

@@ -32,7 +32,7 @@ size: $(OBJ)
.SUFFIXES: .SUFFIXES:
test: test_format test_dirs test_files test_seek test_parallel \ test: test_format test_dirs test_files test_seek test_parallel \
test_alloc test_paths test_orphan test_alloc test_paths test_orphan test_corrupt
test_%: tests/test_%.sh test_%: tests/test_%.sh
./$< ./$<

View File

@@ -144,13 +144,24 @@ int lfs_emubd_prog(const struct lfs_config *cfg, lfs_block_t block,
return -errno; return -errno;
} }
err = fseek(f, off, SEEK_SET);
if (err) {
return -errno;
}
uint8_t dat;
res = fread(&dat, 1, 1, f);
if (res < 1) {
return -errno;
}
err = fclose(f); err = fclose(f);
if (err) { if (err) {
return -errno; return -errno;
} }
emu->stats.prog_count += 1; emu->stats.prog_count += 1;
return 0; return (dat != data[0]) ? LFS_ERR_CORRUPT : 0;
} }
int lfs_emubd_erase(const struct lfs_config *cfg, lfs_block_t block) { int lfs_emubd_erase(const struct lfs_config *cfg, lfs_block_t block) {

598
lfs.c
View File

@@ -191,6 +191,16 @@ static int lfs_cmp(lfs_t *lfs, lfs_block_t block,
} }
/// Internal operations predeclared here ///
int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data);
static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir);
static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2],
lfs_dir_t *parent, lfs_entry_t *entry);
static int lfs_relocate(lfs_t *lfs,
const lfs_block_t oldpair[2], const lfs_block_t newpair[2]);
int lfs_deorphan(lfs_t *lfs);
/// Block allocator /// /// Block allocator ///
static int lfs_alloc_lookahead(void *p, lfs_block_t block) { static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
lfs_t *lfs = p; lfs_t *lfs = p;
@@ -203,11 +213,27 @@ static int lfs_alloc_lookahead(void *p, lfs_block_t block) {
return 0; return 0;
} }
static int lfs_alloc_scan(lfs_t *lfs, lfs_block_t *block) { static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) {
lfs_block_t end = lfs->free.start + lfs->cfg->block_count; // deorphan if we haven't yet, only needed once after poweron
if (!lfs->deorphaned) {
int err = lfs_deorphan(lfs);
if (err) {
return err;
}
}
while (true) { while (true) {
while (lfs->free.off < lfs->cfg->lookahead) { while (true) {
// check if we have looked at all blocks since last ack
if (lfs->free.start + lfs->free.off == lfs->free.end) {
LFS_WARN("No more free space %d", lfs->free.end);
return LFS_ERR_NOSPC;
}
if (lfs->free.off >= lfs->cfg->lookahead) {
break;
}
lfs_block_t off = lfs->free.off; lfs_block_t off = lfs->free.off;
lfs->free.off += 1; lfs->free.off += 1;
@@ -218,12 +244,8 @@ static int lfs_alloc_scan(lfs_t *lfs, lfs_block_t *block) {
} }
} }
// could not find block
lfs->free.start += lfs->cfg->lookahead; lfs->free.start += lfs->cfg->lookahead;
lfs->free.off = 0; lfs->free.off = 0;
if (lfs_scmp(lfs->free.start, end) > 0) {
return LFS_ERR_NOSPC;
}
// find mask of free blocks from tree // find mask of free blocks from tree
memset(lfs->free.lookahead, 0, lfs->cfg->lookahead/8); memset(lfs->free.lookahead, 0, lfs->cfg->lookahead/8);
@@ -234,27 +256,8 @@ static int lfs_alloc_scan(lfs_t *lfs, lfs_block_t *block) {
} }
} }
static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { static void lfs_alloc_ack(lfs_t *lfs) {
// try to scan for free block lfs->free.end = lfs->free.start + lfs->free.off + lfs->cfg->block_count;
int err = lfs_alloc_scan(lfs, block);
if (err != LFS_ERR_NOSPC) {
return err;
}
// still can't allocate a block? check for orphans
err = lfs_deorphan(lfs);
if (err) {
return err;
}
// scan again or die trying
err = lfs_alloc_scan(lfs, block);
if (err) {
LFS_WARN("No more free space%s", "");
return err;
}
return 0;
} }
@@ -276,6 +279,13 @@ static inline int lfs_paircmp(
paira[0] == pairb[1] || paira[1] == pairb[0]); paira[0] == pairb[1] || paira[1] == pairb[0]);
} }
static inline bool lfs_pairsync(
const lfs_block_t paira[2],
const lfs_block_t pairb[2]) {
return (paira[0] == pairb[0] && paira[1] == pairb[1]) ||
(paira[0] == pairb[1] && paira[1] == pairb[0]);
}
static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) { static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) {
// allocate pair of dir blocks // allocate pair of dir blocks
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
@@ -285,11 +295,6 @@ static int lfs_dir_alloc(lfs_t *lfs, lfs_dir_t *dir) {
} }
} }
// we couldn't find unique blocks, we're out of space
if (dir->pair[0] == dir->pair[1]) {
return LFS_ERR_NOSPC;
}
// rather than clobbering one of the blocks we just pretend // rather than clobbering one of the blocks we just pretend
// the revision may be valid // the revision may be valid
int err = lfs_read(lfs, dir->pair[0], 0, &dir->d.rev, 4); int err = lfs_read(lfs, dir->pair[0], 0, &dir->d.rev, 4);
@@ -360,13 +365,30 @@ static int lfs_dir_fetch(lfs_t *lfs,
return 0; return 0;
} }
struct lfs_region {
lfs_off_t oldoff;
lfs_size_t oldlen;
const void *newdata;
lfs_size_t newlen;
};
static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir, static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
const lfs_entry_t *entry, const void *data) { const struct lfs_region *regions, int count) {
dir->d.rev += 1; dir->d.rev += 1;
lfs_pairswap(dir->pair); lfs_pairswap(dir->pair);
for (int i = 0; i < count; i++) {
dir->d.size += regions[i].newlen - regions[i].oldlen;
}
const lfs_block_t oldpair[2] = {dir->pair[0], dir->pair[1]};
bool relocated = false;
while (true) {
int err = lfs_erase(lfs, dir->pair[0]); int err = lfs_erase(lfs, dir->pair[0]);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
@@ -374,125 +396,118 @@ static int lfs_dir_commit(lfs_t *lfs, lfs_dir_t *dir,
crc = lfs_crc(crc, &dir->d, sizeof(dir->d)); crc = lfs_crc(crc, &dir->d, sizeof(dir->d));
err = lfs_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d)); err = lfs_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d));
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
lfs_off_t off = sizeof(dir->d); int i = 0;
lfs_size_t size = 0x7fffffff & dir->d.size; lfs_off_t oldoff = sizeof(dir->d);
while (off < size) { lfs_off_t newoff = sizeof(dir->d);
if (entry && off == entry->off) { lfs_size_t newsize = 0x7fffffff & dir->d.size;
crc = lfs_crc(crc, &entry->d, sizeof(entry->d)); while (newoff < newsize) {
if (i < count && regions[i].oldoff == oldoff) {
crc = lfs_crc(crc, regions[i].newdata, regions[i].newlen);
int err = lfs_prog(lfs, dir->pair[0], int err = lfs_prog(lfs, dir->pair[0],
off, &entry->d, sizeof(entry->d)); newoff, regions[i].newdata, regions[i].newlen);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
off += sizeof(entry->d);
if (data) { oldoff += regions[i].oldlen;
crc = lfs_crc(crc, data, entry->d.len - sizeof(entry->d)); newoff += regions[i].newlen;
int err = lfs_prog(lfs, dir->pair[0], i += 1;
off, data, entry->d.len - sizeof(entry->d));
if (err) {
return err;
}
off += entry->d.len - sizeof(entry->d);
}
} else { } else {
uint8_t data; uint8_t data;
int err = lfs_read(lfs, dir->pair[1], off, &data, 1); int err = lfs_read(lfs, oldpair[1], oldoff, &data, 1);
if (err) { if (err) {
return err; return err;
} }
crc = lfs_crc(crc, &data, 1); crc = lfs_crc(crc, &data, 1);
err = lfs_prog(lfs, dir->pair[0], off, &data, 1); err = lfs_prog(lfs, dir->pair[0], newoff, &data, 1);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
off += 1; oldoff += 1;
newoff += 1;
} }
} }
while (off < lfs->cfg->block_size-4) { while (newoff < lfs->cfg->block_size-4) {
uint8_t data = 0xff; uint8_t data = 0xff;
crc = lfs_crc(crc, &data, 1); crc = lfs_crc(crc, &data, 1);
err = lfs_prog(lfs, dir->pair[0], off, &data, 1); err = lfs_prog(lfs, dir->pair[0], newoff, &data, 1);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
off += 1; newoff += 1;
} }
err = lfs_prog(lfs, dir->pair[0], lfs->cfg->block_size-4, &crc, 4); err = lfs_prog(lfs, dir->pair[0], lfs->cfg->block_size-4, &crc, 4);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
return lfs_sync(lfs); err = lfs_sync(lfs);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
// successful commit
if (relocated) {
LFS_DEBUG("Relocating %d %d to %d %d",
oldpair[0], oldpair[1], dir->pair[0], dir->pair[1]);
return lfs_relocate(lfs, oldpair, dir->pair);
}
return 0;
relocate:
LFS_DEBUG("Bad block at %d", dir->pair[0]);
// drop caches and prepare to relocate block
relocated = true;
lfs->pcache.block = 0xffffffff;
// can't relocate superblock, filesystem is now frozen
if (lfs_paircmp(oldpair, (const lfs_block_t[2]){0, 1}) == 0) {
LFS_WARN("Superblock %d has become unwritable", oldpair[0]);
return LFS_ERR_CORRUPT;
}
// relocate half of pair
err = lfs_alloc(lfs, &dir->pair[0]);
if (err) {
return err;
}
}
} }
static int lfs_dir_shift(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { static int lfs_dir_update(lfs_t *lfs, lfs_dir_t *dir,
dir->d.rev += 1; const lfs_entry_t *entry, const void *data) {
dir->d.size -= entry->d.len; return lfs_dir_commit(lfs, dir, (struct lfs_region[]){
lfs_pairswap(dir->pair); {entry->off, sizeof(entry->d), &entry->d, sizeof(entry->d)},
{entry->off+sizeof(entry->d), entry->d.len-sizeof(entry->d),
int err = lfs_erase(lfs, dir->pair[0]); data, entry->d.len-sizeof(entry->d)}
if (err) { }, data ? 2 : 1);
return err;
}
uint32_t crc = 0xffffffff;
crc = lfs_crc(crc, &dir->d, sizeof(dir->d));
err = lfs_prog(lfs, dir->pair[0], 0, &dir->d, sizeof(dir->d));
if (err) {
return err;
}
lfs_off_t woff = sizeof(dir->d);
lfs_off_t roff = sizeof(dir->d);
lfs_size_t size = 0x7fffffff & dir->d.size;
while (woff < size) {
if (roff == entry->off) {
roff += entry->d.len;
} else {
uint8_t data;
int err = lfs_read(lfs, dir->pair[1], roff, &data, 1);
if (err) {
return err;
}
crc = lfs_crc(crc, &data, 1);
err = lfs_prog(lfs, dir->pair[0], woff, &data, 1);
if (err) {
return err;
}
woff += 1;
roff += 1;
}
}
while (woff < lfs->cfg->block_size-4) {
uint8_t data = 0xff;
crc = lfs_crc(crc, &data, 1);
err = lfs_prog(lfs, dir->pair[0], woff, &data, 1);
if (err) {
return err;
}
woff += 1;
}
err = lfs_prog(lfs, dir->pair[0], lfs->cfg->block_size-4, &crc, 4);
if (err) {
return err;
}
return lfs_sync(lfs);
} }
static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir, static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
@@ -501,8 +516,10 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
while (true) { while (true) {
if (dir->d.size + entry->d.len <= lfs->cfg->block_size - 4) { if (dir->d.size + entry->d.len <= lfs->cfg->block_size - 4) {
entry->off = dir->d.size; entry->off = dir->d.size;
dir->d.size += entry->d.len; return lfs_dir_commit(lfs, dir, (struct lfs_region[]){
return lfs_dir_commit(lfs, dir, entry, data); {entry->off, 0, &entry->d, sizeof(entry->d)},
{entry->off, 0, data, entry->d.len - sizeof(entry->d)}
}, 2);
} }
// we need to allocate a new dir block // we need to allocate a new dir block
@@ -513,18 +530,13 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
return err; return err;
} }
// our allocator doesn't track blocks before being appended,
// so even if we found some blocks, they may not be unique
if (entry->d.type == LFS_TYPE_DIR &&
lfs_paircmp(entry->d.u.dir, newdir.pair) == 0) {
return LFS_ERR_NOSPC;
}
newdir.d.tail[0] = dir->d.tail[0]; newdir.d.tail[0] = dir->d.tail[0];
newdir.d.tail[1] = dir->d.tail[1]; newdir.d.tail[1] = dir->d.tail[1];
entry->off = newdir.d.size; entry->off = newdir.d.size;
newdir.d.size += entry->d.len; err = lfs_dir_commit(lfs, &newdir, (struct lfs_region[]){
err = lfs_dir_commit(lfs, &newdir, entry, data); {entry->off, 0, &entry->d, sizeof(entry->d)},
{entry->off, 0, data, entry->d.len - sizeof(entry->d)}
}, 2);
if (err) { if (err) {
return err; return err;
} }
@@ -532,7 +544,7 @@ static int lfs_dir_append(lfs_t *lfs, lfs_dir_t *dir,
dir->d.size |= 0x80000000; dir->d.size |= 0x80000000;
dir->d.tail[0] = newdir.pair[0]; dir->d.tail[0] = newdir.pair[0];
dir->d.tail[1] = newdir.pair[1]; dir->d.tail[1] = newdir.pair[1];
return lfs_dir_commit(lfs, dir, NULL, NULL); return lfs_dir_commit(lfs, dir, NULL, 0);
} }
int err = lfs_dir_fetch(lfs, dir, dir->d.tail); int err = lfs_dir_fetch(lfs, dir, dir->d.tail);
@@ -546,33 +558,29 @@ static int lfs_dir_remove(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
// either shift out the one entry or remove the whole dir block // either shift out the one entry or remove the whole dir block
if (dir->d.size == sizeof(dir->d)) { if (dir->d.size == sizeof(dir->d)) {
lfs_dir_t pdir; lfs_dir_t pdir;
int err = lfs_dir_fetch(lfs, &pdir, lfs->root); int res = lfs_pred(lfs, dir->pair, &pdir);
if (err) { if (res < 0) {
return err; return res;
}
while (lfs_paircmp(pdir.d.tail, dir->pair) != 0) {
int err = lfs_dir_fetch(lfs, &pdir, pdir.d.tail);
if (err) {
return err;
}
} }
if (!(pdir.d.size & 0x80000000)) { if (!(pdir.d.size & 0x80000000)) {
return lfs_dir_shift(lfs, dir, entry); return lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, entry->d.len, NULL, 0},
}, 1);
} else { } else {
pdir.d.tail[0] = dir->d.tail[0]; pdir.d.tail[0] = dir->d.tail[0];
pdir.d.tail[1] = dir->d.tail[1]; pdir.d.tail[1] = dir->d.tail[1];
return lfs_dir_commit(lfs, &pdir, NULL, NULL); return lfs_dir_commit(lfs, dir, NULL, 0);
} }
} else { } else {
return lfs_dir_shift(lfs, dir, entry); return lfs_dir_commit(lfs, dir, (struct lfs_region[]){
{entry->off, entry->d.len, NULL, 0},
}, 1);
} }
} }
static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) { static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
while (true) { while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)) {
if (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)) {
if (!(0x80000000 & dir->d.size)) { if (!(0x80000000 & dir->d.size)) {
entry->off = dir->off; entry->off = dir->off;
return LFS_ERR_NOENT; return LFS_ERR_NOENT;
@@ -585,7 +593,6 @@ static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
dir->off = sizeof(dir->d); dir->off = sizeof(dir->d);
dir->pos += sizeof(dir->d); dir->pos += sizeof(dir->d);
continue;
} }
int err = lfs_read(lfs, dir->pair[0], dir->off, int err = lfs_read(lfs, dir->pair[0], dir->off,
@@ -596,12 +603,8 @@ static int lfs_dir_next(lfs_t *lfs, lfs_dir_t *dir, lfs_entry_t *entry) {
dir->off += entry->d.len; dir->off += entry->d.len;
dir->pos += entry->d.len; dir->pos += entry->d.len;
if ((0xff & entry->d.type) == LFS_TYPE_REG ||
(0xff & entry->d.type) == LFS_TYPE_DIR) {
entry->off = dir->off - entry->d.len; entry->off = dir->off - entry->d.len;
return 0; return 0;
}
}
} }
static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir, static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
@@ -653,7 +656,9 @@ static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
return err; return err;
} }
if (entry->d.len - sizeof(entry->d) != pathlen) { if (((0xff & entry->d.type) != LFS_TYPE_REG &&
(0xff & entry->d.type) != LFS_TYPE_DIR) ||
entry->d.len - sizeof(entry->d) != pathlen) {
continue; continue;
} }
@@ -663,7 +668,7 @@ static int lfs_dir_find(lfs_t *lfs, lfs_dir_t *dir,
return ret; return ret;
} }
// Found match // found match
if (ret == true) { if (ret == true) {
break; break;
} }
@@ -707,7 +712,9 @@ int lfs_mkdir(lfs_t *lfs, const char *path) {
return err ? err : LFS_ERR_EXISTS; return err ? err : LFS_ERR_EXISTS;
} }
// Build up new directory // build up new directory
lfs_alloc_ack(lfs);
lfs_dir_t dir; lfs_dir_t dir;
err = lfs_dir_alloc(lfs, &dir); err = lfs_dir_alloc(lfs, &dir);
if (err) { if (err) {
@@ -716,7 +723,7 @@ int lfs_mkdir(lfs_t *lfs, const char *path) {
dir.d.tail[0] = cwd.d.tail[0]; dir.d.tail[0] = cwd.d.tail[0];
dir.d.tail[1] = cwd.d.tail[1]; dir.d.tail[1] = cwd.d.tail[1];
err = lfs_dir_commit(lfs, &dir, NULL, NULL); err = lfs_dir_commit(lfs, &dir, NULL, 0);
if (err) { if (err) {
return err; return err;
} }
@@ -729,7 +736,13 @@ int lfs_mkdir(lfs_t *lfs, const char *path) {
cwd.d.tail[0] = dir.pair[0]; cwd.d.tail[0] = dir.pair[0];
cwd.d.tail[1] = dir.pair[1]; cwd.d.tail[1] = dir.pair[1];
return lfs_dir_append(lfs, &cwd, &entry, path); err = lfs_dir_append(lfs, &cwd, &entry, path);
if (err) {
return err;
}
lfs_alloc_ack(lfs);
return 0;
} }
int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) { int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) {
@@ -773,7 +786,7 @@ int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) {
} }
int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) { int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) {
// Do nothing, dir is always synchronized // do nothing, dir is always synchronized
return 0; return 0;
} }
@@ -794,17 +807,24 @@ int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) {
} }
lfs_entry_t entry; lfs_entry_t entry;
while (true) {
int err = lfs_dir_next(lfs, dir, &entry); int err = lfs_dir_next(lfs, dir, &entry);
if (err) { if (err) {
return (err == LFS_ERR_NOENT) ? 0 : err; return (err == LFS_ERR_NOENT) ? 0 : err;
} }
if ((0xff & entry.d.type) == LFS_TYPE_REG ||
(0xff & entry.d.type) == LFS_TYPE_DIR) {
break;
}
}
info->type = entry.d.type & 0xff; info->type = entry.d.type & 0xff;
if (info->type == LFS_TYPE_REG) { if (info->type == LFS_TYPE_REG) {
info->size = entry.d.u.file.size; info->size = entry.d.u.file.size;
} }
err = lfs_read(lfs, dir->pair[0], entry.off + sizeof(entry.d), int err = lfs_read(lfs, dir->pair[0], entry.off + sizeof(entry.d),
info->name, entry.d.len - sizeof(entry.d)); info->name, entry.d.len - sizeof(entry.d));
if (err) { if (err) {
return err; return err;
@@ -906,6 +926,7 @@ static int lfs_index_extend(lfs_t *lfs,
lfs_cache_t *rcache, lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_cache_t *pcache,
lfs_block_t head, lfs_size_t size, lfs_block_t head, lfs_size_t size,
lfs_off_t *block, lfs_block_t *off) { lfs_off_t *block, lfs_block_t *off) {
while (true) {
// go ahead and grab a block // go ahead and grab a block
int err = lfs_alloc(lfs, block); int err = lfs_alloc(lfs, block);
if (err) { if (err) {
@@ -914,6 +935,9 @@ static int lfs_index_extend(lfs_t *lfs,
err = lfs_erase(lfs, *block); err = lfs_erase(lfs, *block);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
@@ -937,6 +961,9 @@ static int lfs_index_extend(lfs_t *lfs,
err = lfs_cache_prog(lfs, pcache, *block, i, &data, 1); err = lfs_cache_prog(lfs, pcache, *block, i, &data, 1);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
} }
@@ -953,6 +980,9 @@ static int lfs_index_extend(lfs_t *lfs,
for (lfs_off_t i = 0; i < skips; i++) { for (lfs_off_t i = 0; i < skips; i++) {
int err = lfs_cache_prog(lfs, pcache, *block, 4*i, &head, 4); int err = lfs_cache_prog(lfs, pcache, *block, 4*i, &head, 4);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
@@ -966,6 +996,13 @@ static int lfs_index_extend(lfs_t *lfs,
*off = 4*skips; *off = 4*skips;
return 0; return 0;
relocate:
LFS_DEBUG("Bad block at %d", *block);
// just clear cache and try a new block
pcache->block = 0xffffffff;
}
} }
static int lfs_index_traverse(lfs_t *lfs, static int lfs_index_traverse(lfs_t *lfs,
@@ -1003,7 +1040,7 @@ static int lfs_index_traverse(lfs_t *lfs,
/// Top level file operations /// /// Top level file operations ///
int lfs_file_open(lfs_t *lfs, lfs_file_t *file, int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
const char *path, int flags) { const char *path, int flags) {
// Allocate entry for file if it doesn't exist // allocate entry for file if it doesn't exist
lfs_dir_t cwd; lfs_dir_t cwd;
int err = lfs_dir_fetch(lfs, &cwd, lfs->root); int err = lfs_dir_fetch(lfs, &cwd, lfs->root);
if (err) { if (err) {
@@ -1044,7 +1081,6 @@ int lfs_file_open(lfs_t *lfs, lfs_file_t *file,
file->size = entry.d.u.file.size; file->size = entry.d.u.file.size;
file->flags = flags; file->flags = flags;
file->pos = 0; file->pos = 0;
file->block = -1; // TODO rm me?
if (flags & LFS_O_TRUNC) { if (flags & LFS_O_TRUNC) {
file->head = -1; file->head = -1;
@@ -1181,7 +1217,7 @@ int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) {
entry.d.u.file.head = file->head; entry.d.u.file.head = file->head;
entry.d.u.file.size = file->size; entry.d.u.file.size = file->size;
err = lfs_dir_commit(lfs, &cwd, &entry, NULL); err = lfs_dir_update(lfs, &cwd, &entry, NULL);
if (err) { if (err) {
return err; return err;
} }
@@ -1283,6 +1319,7 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
} }
// extend file with new blocks // extend file with new blocks
lfs_alloc_ack(lfs);
int err = lfs_index_extend(lfs, &lfs->rcache, &file->cache, int err = lfs_index_extend(lfs, &lfs->rcache, &file->cache,
file->block, file->pos, file->block, file->pos,
&file->block, &file->off); &file->block, &file->off);
@@ -1293,16 +1330,64 @@ lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file,
// program as much as we can in current block // 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->cfg->block_size - file->off);
while (true) {
int err = lfs_cache_prog(lfs, &file->cache, int err = lfs_cache_prog(lfs, &file->cache,
file->block, file->off, data, diff); file->block, file->off, data, diff);
if (err) { if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err; return err;
} }
break;
relocate:
LFS_DEBUG("Bad block at %d", file->block);
// just relocate what exists into new block
lfs_block_t nblock;
err = lfs_alloc(lfs, &nblock);
if (err) {
return err;
}
// either read from dirty cache or disk
for (lfs_off_t i = 0; i < file->off; i++) {
uint8_t data;
if (file->cache.block == file->block && i >= file->cache.off) {
data = file->cache.buffer[i - file->cache.off];
} else {
// just read from disk
err = lfs_read(lfs, file->block, i, &data, 1);
if (err) {
return err;
}
}
err = lfs_prog(lfs, nblock, i, &data, 1);
if (err) {
if (err == LFS_ERR_CORRUPT) {
goto relocate;
}
return err;
}
}
// copy over new state of file
memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->prog_size);
file->cache.block = lfs->pcache.block;
file->cache.off = lfs->pcache.off;
lfs->pcache.block = 0xffffffff;
file->block = nblock;
}
file->pos += diff; file->pos += diff;
file->off += diff; file->off += diff;
data += diff; data += diff;
nsize -= diff; nsize -= diff;
lfs_alloc_ack(lfs);
} }
return size; return size;
@@ -1485,7 +1570,7 @@ int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) {
newentry.d.len = sizeof(newentry.d) + strlen(newpath); newentry.d.len = sizeof(newentry.d) + strlen(newpath);
if (prevexists) { if (prevexists) {
int err = lfs_dir_commit(lfs, &newcwd, &newentry, newpath); int err = lfs_dir_update(lfs, &newcwd, &newentry, newpath);
if (err) { if (err) {
return err; return err;
} }
@@ -1574,14 +1659,17 @@ static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) {
} }
} }
// setup files as an empty list // setup default state
lfs->root[0] = 0xffffffff;
lfs->root[1] = 0xffffffff;
lfs->files = NULL; lfs->files = NULL;
lfs->deorphaned = false;
return 0; return 0;
} }
static int lfs_deinit(lfs_t *lfs) { static int lfs_deinit(lfs_t *lfs) {
// Free allocated memory // free allocated memory
if (!lfs->cfg->read_buffer) { if (!lfs->cfg->read_buffer) {
free(lfs->rcache.buffer); free(lfs->rcache.buffer);
} }
@@ -1603,26 +1691,28 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
return err; return err;
} }
// Create free lookahead // create free lookahead
memset(lfs->free.lookahead, 0, lfs->cfg->lookahead/8); memset(lfs->free.lookahead, 0, lfs->cfg->lookahead/8);
lfs->free.start = 0; lfs->free.start = 0;
lfs->free.off = 0; lfs->free.off = 0;
lfs->free.end = lfs->free.start + lfs->cfg->block_count;
// Create superblock dir // create superblock dir
lfs_alloc_ack(lfs);
lfs_dir_t superdir; lfs_dir_t superdir;
err = lfs_dir_alloc(lfs, &superdir); err = lfs_dir_alloc(lfs, &superdir);
if (err) { if (err) {
return err; return err;
} }
// Write root directory // write root directory
lfs_dir_t root; lfs_dir_t root;
err = lfs_dir_alloc(lfs, &root); err = lfs_dir_alloc(lfs, &root);
if (err) { if (err) {
return err; return err;
} }
err = lfs_dir_commit(lfs, &root, NULL, NULL); err = lfs_dir_commit(lfs, &root, NULL, 0);
if (err) { if (err) {
return err; return err;
} }
@@ -1630,7 +1720,7 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
lfs->root[0] = root.pair[0]; lfs->root[0] = root.pair[0];
lfs->root[1] = root.pair[1]; lfs->root[1] = root.pair[1];
// Write superblocks // write superblocks
lfs_superblock_t superblock = { lfs_superblock_t superblock = {
.off = sizeof(superdir.d), .off = sizeof(superdir.d),
.d.type = LFS_TYPE_SUPERBLOCK, .d.type = LFS_TYPE_SUPERBLOCK,
@@ -1643,18 +1733,24 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
}; };
superdir.d.tail[0] = root.pair[0]; superdir.d.tail[0] = root.pair[0];
superdir.d.tail[1] = root.pair[1]; superdir.d.tail[1] = root.pair[1];
superdir.d.size += sizeof(superdir.d); superdir.d.size = sizeof(superdir.d) + sizeof(superblock.d);
// write both pairs to be safe
bool valid = false;
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
// Write both pairs for extra safety, do some finagling to pretend int err = lfs_dir_commit(lfs, &superdir, (struct lfs_region[]){
// the superblock is an entry {sizeof(superdir.d), sizeof(superblock.d),
int err = lfs_dir_commit(lfs, &superdir, &superblock.d, sizeof(superblock.d)}
(const lfs_entry_t*)&superblock, }, 1);
(const struct lfs_disk_entry*)&superblock.d + 1); if (err && err != LFS_ERR_CORRUPT) {
if (err) {
LFS_ERROR("Failed to write superblock at %d", superdir.pair[0]);
return err; return err;
} }
valid = valid || !err;
}
if (!valid) {
return LFS_ERR_CORRUPT;
} }
// sanity check that fetch works // sanity check that fetch works
@@ -1663,6 +1759,7 @@ int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) {
return err; return err;
} }
lfs_alloc_ack(lfs);
return lfs_deinit(lfs); return lfs_deinit(lfs);
} }
@@ -1675,6 +1772,7 @@ int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) {
// setup free lookahead // setup free lookahead
lfs->free.start = -lfs->cfg->lookahead; lfs->free.start = -lfs->cfg->lookahead;
lfs->free.off = lfs->cfg->lookahead; lfs->free.off = lfs->cfg->lookahead;
lfs->free.end = lfs->free.start + lfs->cfg->block_count;
// load superblock // load superblock
lfs_dir_t dir; lfs_dir_t dir;
@@ -1711,6 +1809,10 @@ int lfs_unmount(lfs_t *lfs) {
/// Littlefs specific operations /// /// Littlefs specific operations ///
int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) { int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
if (lfs_pairisnull(lfs->root)) {
return 0;
}
// iterate over metadata pairs // iterate over metadata pairs
lfs_dir_t dir; lfs_dir_t dir;
lfs_entry_t entry; lfs_entry_t entry;
@@ -1777,22 +1879,49 @@ int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) {
return 0; return 0;
} }
static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2]) { static int lfs_pred(lfs_t *lfs, const lfs_block_t dir[2], lfs_dir_t *pdir) {
// iterate over all directory directory entries if (lfs_pairisnull(lfs->root)) {
lfs_dir_t parent = { return 0;
.d.tail[0] = lfs->root[0], }
.d.tail[1] = lfs->root[1],
};
while (true) { // iterate over all directory directory entries
lfs_entry_t entry; int err = lfs_dir_fetch(lfs, pdir, (const lfs_block_t[2]){0, 1});
int err = lfs_dir_fetch(lfs, &parent, parent.d.tail); if (err) {
return err;
}
while (!lfs_pairisnull(pdir->d.tail)) {
if (lfs_paircmp(pdir->d.tail, dir) == 0) {
return true;
}
int err = lfs_dir_fetch(lfs, pdir, pdir->d.tail);
if (err) {
return err;
}
}
return false;
}
static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2],
lfs_dir_t *parent, lfs_entry_t *entry) {
if (lfs_pairisnull(lfs->root)) {
return 0;
}
parent->d.tail[0] = 0;
parent->d.tail[1] = 1;
// iterate over all directory directory entries
while (!lfs_pairisnull(parent->d.tail)) {
int err = lfs_dir_fetch(lfs, parent, parent->d.tail);
if (err) { if (err) {
return err; return err;
} }
while (true) { while (true) {
int err = lfs_dir_next(lfs, &parent, &entry); int err = lfs_dir_next(lfs, parent, entry);
if (err && err != LFS_ERR_NOENT) { if (err && err != LFS_ERR_NOENT) {
return err; return err;
} }
@@ -1801,29 +1930,81 @@ static int lfs_parent(lfs_t *lfs, const lfs_block_t dir[2]) {
break; break;
} }
if ((0xf & entry.d.type) == LFS_TYPE_DIR && if (((0xf & entry->d.type) == LFS_TYPE_DIR) &&
lfs_paircmp(entry.d.u.dir, dir) == 0) { lfs_paircmp(entry->d.u.dir, dir) == 0) {
return true; return true;
} }
} }
}
if (lfs_pairisnull(parent.d.tail)) {
return false; return false;
}
}
} }
int lfs_deorphan(lfs_t *lfs) { static int lfs_relocate(lfs_t *lfs,
// iterate over all directories const lfs_block_t oldpair[2], const lfs_block_t newpair[2]) {
lfs_dir_t pdir; // find parent
lfs_dir_t cdir; lfs_dir_t parent;
lfs_entry_t entry;
int res = lfs_parent(lfs, oldpair, &parent, &entry);
if (res < 0) {
return res;
}
// skip root if (res) {
int err = lfs_dir_fetch(lfs, &pdir, lfs->root); // update disk, this creates a desync
entry.d.u.dir[0] = newpair[0];
entry.d.u.dir[1] = newpair[1];
int err = lfs_dir_update(lfs, &parent, &entry, NULL);
if (err) { if (err) {
return err; return err;
} }
// update internal root
if (lfs_paircmp(oldpair, lfs->root) == 0) {
LFS_DEBUG("Relocating root %d %d", newpair[0], newpair[1]);
lfs->root[0] = newpair[0];
lfs->root[1] = newpair[1];
}
// clean up bad block, which should now be a desync
return lfs_deorphan(lfs);
}
// find pred
res = lfs_pred(lfs, oldpair, &parent);
if (res < 0) {
return res;
}
if (res) {
// just replace bad pair, no desync can occur
parent.d.tail[0] = newpair[0];
parent.d.tail[0] = newpair[0];
return lfs_dir_commit(lfs, &parent, NULL, 0);
}
// couldn't find dir, must be new
return 0;
}
int lfs_deorphan(lfs_t *lfs) {
lfs->deorphaned = true;
if (lfs_pairisnull(lfs->root)) {
return 0;
}
lfs_dir_t pdir;
lfs_dir_t cdir;
// skip superblock
int err = lfs_dir_fetch(lfs, &pdir, (const lfs_block_t[2]){0, 1});
if (err) {
return err;
}
// iterate over all directories
while (!lfs_pairisnull(pdir.d.tail)) { while (!lfs_pairisnull(pdir.d.tail)) {
int err = lfs_dir_fetch(lfs, &cdir, pdir.d.tail); int err = lfs_dir_fetch(lfs, &cdir, pdir.d.tail);
if (err) { if (err) {
@@ -1833,19 +2014,36 @@ int lfs_deorphan(lfs_t *lfs) {
// only check head blocks // only check head blocks
if (!(0x80000000 & pdir.d.size)) { if (!(0x80000000 & pdir.d.size)) {
// check if we have a parent // check if we have a parent
int parent = lfs_parent(lfs, pdir.d.tail); lfs_dir_t parent;
if (parent < 0) { lfs_entry_t entry;
return parent; int res = lfs_parent(lfs, pdir.d.tail, &parent, &entry);
if (res < 0) {
return res;
} }
if (!parent) { if (!res) {
// we are an orphan // we are an orphan
LFS_DEBUG("Orphan %d %d", pdir.d.tail[0], pdir.d.tail[1]); LFS_DEBUG("Orphan %d %d", pdir.d.tail[0], pdir.d.tail[1]);
pdir.d.tail[0] = cdir.d.tail[0]; pdir.d.tail[0] = cdir.d.tail[0];
pdir.d.tail[1] = cdir.d.tail[1]; pdir.d.tail[1] = cdir.d.tail[1];
err = lfs_dir_commit(lfs, &pdir, NULL, NULL); err = lfs_dir_commit(lfs, &pdir, NULL, 0);
if (err) {
return err;
}
break;
}
if (!lfs_pairsync(entry.d.u.dir, pdir.d.tail)) {
// we have desynced
LFS_DEBUG("Desync %d %d", entry.d.u.dir[0], entry.d.u.dir[1]);
pdir.d.tail[0] = entry.d.u.dir[0];
pdir.d.tail[1] = entry.d.u.dir[1];
err = lfs_dir_commit(lfs, &pdir, NULL, 0);
if (err) { if (err) {
return err; return err;
} }

9
lfs.h
View File

@@ -43,7 +43,7 @@ enum lfs_error {
enum lfs_type { enum lfs_type {
LFS_TYPE_REG = 0x01, LFS_TYPE_REG = 0x01,
LFS_TYPE_DIR = 0x02, LFS_TYPE_DIR = 0x02,
LFS_TYPE_SUPERBLOCK = 0x10, LFS_TYPE_SUPERBLOCK = 0x12,
}; };
enum lfs_open_flags { enum lfs_open_flags {
@@ -193,15 +193,16 @@ typedef struct lfs_superblock {
struct lfs_disk_superblock { struct lfs_disk_superblock {
uint16_t type; uint16_t type;
uint16_t len; uint16_t len;
lfs_block_t root[2];
uint32_t version; uint32_t version;
char magic[8]; char magic[8];
uint32_t block_size; uint32_t block_size;
uint32_t block_count; uint32_t block_count;
lfs_block_t root[2];
} d; } d;
} lfs_superblock_t; } lfs_superblock_t;
typedef struct lfs_free { typedef struct lfs_free {
lfs_block_t end;
lfs_block_t start; lfs_block_t start;
lfs_block_t off; lfs_block_t off;
uint32_t *lookahead; uint32_t *lookahead;
@@ -212,8 +213,8 @@ typedef struct lfs {
const struct lfs_config *cfg; const struct lfs_config *cfg;
lfs_block_t root[2]; lfs_block_t root[2];
lfs_dir_t *scratch;
lfs_file_t *files; lfs_file_t *files;
bool deorphaned;
lfs_cache_t rcache; lfs_cache_t rcache;
lfs_cache_t pcache; lfs_cache_t pcache;
@@ -257,8 +258,8 @@ int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file);
lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file); lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file);
// miscellaneous lfs specific operations // miscellaneous lfs specific operations
int lfs_deorphan(lfs_t *lfs);
int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data); int lfs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data);
int lfs_deorphan(lfs_t *lfs);
#endif #endif

View File

@@ -13,11 +13,17 @@ void test_log(const char *s, uintmax_t v) {{
void test_assert(const char *file, unsigned line, void test_assert(const char *file, unsigned line,
const char *s, uintmax_t v, uintmax_t e) {{ const char *s, uintmax_t v, uintmax_t e) {{
static const char *last[2] = {{0, 0}}; static const char *last[6] = {{0, 0}};
if (v != e || !(last[0] == s || last[1] == s)) {{ if (v != e || !(last[0] == s || last[1] == s ||
last[2] == s || last[3] == s ||
last[4] == s || last[5] == s)) {{
test_log(s, v); test_log(s, v);
last[0] = last[1]; last[0] = last[1];
last[1] = s; last[1] = last[2];
last[2] = last[3];
last[3] = last[4];
last[4] = last[5];
last[5] = s;
}} }}
if (v != e) {{ if (v != e) {{

106
tests/test_corrupt.sh Executable file
View File

@@ -0,0 +1,106 @@
#!/bin/bash
set -eu
echo "=== Corrupt tests ==="
NAMEMULT=64
FILEMULT=1
lfs_mktree() {
tests/test.py ${1:-} << TEST
lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0;
for (int i = 1; i < 10; i++) {
for (int j = 0; j < $NAMEMULT; j++) {
buffer[j] = '0'+i;
}
buffer[$NAMEMULT] = '\0';
lfs_mkdir(&lfs, (char*)buffer) => 0;
buffer[$NAMEMULT] = '/';
for (int j = 0; j < $NAMEMULT; j++) {
buffer[j+$NAMEMULT+1] = '0'+i;
}
buffer[2*$NAMEMULT+1] = '\0';
lfs_file_open(&lfs, &file[0], (char*)buffer,
LFS_O_WRONLY | LFS_O_CREAT) => 0;
size = $NAMEMULT;
for (int j = 0; j < i*$FILEMULT; j++) {
lfs_file_write(&lfs, &file[0], buffer, size) => size;
}
lfs_file_close(&lfs, &file[0]) => 0;
}
lfs_unmount(&lfs) => 0;
TEST
}
lfs_chktree() {
tests/test.py ${1:-} << TEST
lfs_mount(&lfs, &cfg) => 0;
for (int i = 1; i < 10; i++) {
for (int j = 0; j < $NAMEMULT; j++) {
buffer[j] = '0'+i;
}
buffer[$NAMEMULT] = '\0';
lfs_stat(&lfs, (char*)buffer, &info) => 0;
info.type => LFS_TYPE_DIR;
buffer[$NAMEMULT] = '/';
for (int j = 0; j < $NAMEMULT; j++) {
buffer[j+$NAMEMULT+1] = '0'+i;
}
buffer[2*$NAMEMULT+1] = '\0';
lfs_file_open(&lfs, &file[0], (char*)buffer, LFS_O_RDONLY) => 0;
size = $NAMEMULT;
for (int j = 0; j < i*$FILEMULT; j++) {
lfs_file_read(&lfs, &file[0], rbuffer, size) => size;
memcmp(buffer, rbuffer, size) => 0;
}
lfs_file_close(&lfs, &file[0]) => 0;
}
lfs_unmount(&lfs) => 0;
TEST
}
echo "--- Sanity check ---"
rm -rf blocks
lfs_mktree
lfs_chktree
echo "--- Block corruption ---"
for i in {0..33}
do
rm -rf blocks
mkdir blocks
ln -s /dev/zero blocks/$(printf '%x' $i)
lfs_mktree
lfs_chktree
done
echo "--- Big region corruption ---"
rm -rf blocks
mkdir blocks
for i in {2..255}
do
ln -s /dev/zero blocks/$(printf '%x' $i)
done
lfs_mktree
lfs_chktree
echo "--- Alternating corruption ---"
rm -rf blocks
mkdir blocks
for i in {2..511..2}
do
ln -s /dev/zero blocks/$(printf '%x' $i)
done
lfs_mktree
lfs_chktree
echo "--- Results ---"
tests/stats.py

View File

@@ -124,6 +124,7 @@ tests/test.py << TEST
TEST TEST
echo "--- Directory remove ---" echo "--- Directory remove ---"
# TESTING HERE
tests/test.py << TEST tests/test.py << TEST
lfs_mount(&lfs, &cfg) => 0; lfs_mount(&lfs, &cfg) => 0;
lfs_remove(&lfs, "potato") => LFS_ERR_INVAL; lfs_remove(&lfs, "potato") => LFS_ERR_INVAL;

View File

@@ -10,8 +10,8 @@ tests/test.py << TEST
TEST TEST
echo "--- Invalid superblocks ---" echo "--- Invalid superblocks ---"
ln -f -s /dev/null blocks/0 ln -f -s /dev/zero blocks/0
ln -f -s /dev/null blocks/1 ln -f -s /dev/zero blocks/1
tests/test.py << TEST tests/test.py << TEST
lfs_format(&lfs, &cfg) => LFS_ERR_CORRUPT; lfs_format(&lfs, &cfg) => LFS_ERR_CORRUPT;
TEST TEST