Compare commits

...

9 Commits

Author SHA1 Message Date
Christopher Haster
20b46ded5a WIP making good progress, found a solution for the cycle issue
Found solution for cycle issue for tail+branch solution, though it does
require creating an extra metadata-pair on relocate.

Need to get passing the advanced relocation tests, this is currently
failing. Also need to figure out why I added the assert on tail end, it
may be related.
2020-03-10 08:43:28 -05:00
Christopher Haster
eecb06a9dc WIP crazy new idea work in progress
passing non-reentrant tests already!
2020-03-06 20:14:27 -06:00
Christopher Haster
3ee291de59 WIP so close 2020-02-28 06:03:59 -06:00
Christopher Haster
f7bc22937a WIP branches in dir 2020-02-28 04:51:25 -06:00
Christopher Haster
26ed6dee7d WIP initial commit of branch idea to repair non-DAG trees 2020-02-26 15:50:38 -06:00
Christopher Haster
e33bef55d3 Added "evil" tests and detecion/recovery from bad pointers and infinite loops
These two features have been much requested by users, and have even had
several PRs proposed to fix these in several cases. Before this, these
error conditions usually were caught by internal asserts, however
asserts prevented users from implementing their own workarounds.

It's taken me a while to provide/accept a useful recovery mechanism
(returning LFS_ERR_CORRUPT instead of asserting) because my original thinking
was that these error conditions only occur due to bugs in the filesystem, and
these bugs should be fixed properly.

While I still think this is mostly true, the point has been made clear
that being able to recover from these conditions is definitely worth the
code cost. Hopefully this new behaviour helps the longevity of devices
even if the storage code fails.

Another, less important, reason I didn't want to accept fixes for these
situations was the lack of tests that prove the code's value. This has
been fixed with the new testing framework thanks to the additional of
"internal tests" which can call C static functions and really take
advantage of the internal information of the filesystem.
2020-02-24 08:18:28 -06:00
Chris Desjardins
cb26157880 Change assert to runtime check.
I had a system that was constantly hitting this assert, after making
this change it recovered immediately.
2020-02-23 22:18:08 -06:00
Christopher Haster
a7dfae4526 Minor tweaks to debugging scripts, fixed explode_asserts.py off-by-1
- Changed readmdir.py to print the metadata pair and revision count,
  which is useful when debugging commit issues.
- Added truncated data view to readtree.py by default. This does mean
  readtree.py must read all files on the filesystem to show the
  truncated data, hopefully this does not end up being a problem.
- Made overall representation hopefully more readable, including moving
  superblock under the root dir, userattrs under files, fixing a gstate
  rendering issue.
- Added rendering of soft-tails as dotted-arrows, hopefully this isn't
  too noisy.
- Fixed explode_asserts.py off-by-1 in #line mapping caused by a strip
  call in the assert generation eating newlines. The script matches
  line numbers between the original+modified files by emitting assert
  statements that use the same number of lines. An off-by-1 here causes
  the entire file to map lines incorrectly, which can be very annoying.
2020-02-22 23:50:03 -06:00
Christopher Haster
50fe8ae258 Renamed test_format -> test_superblocks, tweaked superblock tests
With the superblock expansion stuff, the test_format tests have grown
to test more advanced superblock-related features. This is fine but
deserves a rename so it's more clear.

Also fixed a typo that meant tests never ran with block cycles.
2020-02-22 23:35:28 -06:00
10 changed files with 1558 additions and 338 deletions

1206
lfs.c

File diff suppressed because it is too large Load Diff

10
lfs.h
View File

@@ -111,12 +111,14 @@ enum lfs_type {
LFS_TYPE_INLINESTRUCT = 0x201, LFS_TYPE_INLINESTRUCT = 0x201,
LFS_TYPE_SOFTTAIL = 0x600, LFS_TYPE_SOFTTAIL = 0x600,
LFS_TYPE_HARDTAIL = 0x601, LFS_TYPE_HARDTAIL = 0x601,
LFS_TYPE_BRANCH = 0x681,
LFS_TYPE_MOVESTATE = 0x7ff, LFS_TYPE_MOVESTATE = 0x7ff,
// internal chip sources // internal chip sources
LFS_FROM_NOOP = 0x000, LFS_FROM_NOOP = 0x000,
LFS_FROM_MOVE = 0x101, LFS_FROM_MOVE = 0x101,
LFS_FROM_USERATTRS = 0x102, LFS_FROM_DROP = 0x102,
LFS_FROM_USERATTRS = 0x103,
}; };
// File open flags // File open flags
@@ -310,8 +312,11 @@ typedef struct lfs_mdir {
uint32_t etag; uint32_t etag;
uint16_t count; uint16_t count;
bool erased; bool erased;
bool first; // TODO come on
bool split; bool split;
bool mustrelocate; // TODO not great either
lfs_block_t tail[2]; lfs_block_t tail[2];
lfs_block_t branch[2];
} lfs_mdir_t; } lfs_mdir_t;
// littlefs directory type // littlefs directory type
@@ -366,6 +371,9 @@ typedef struct lfs {
lfs_cache_t pcache; lfs_cache_t pcache;
lfs_block_t root[2]; lfs_block_t root[2];
lfs_block_t relocate_tail[2];
lfs_block_t relocate_end[2];
bool relocate_do_hack; // TODO fixme
struct lfs_mlist { struct lfs_mlist {
struct lfs_mlist *next; struct lfs_mlist *next;
uint16_t id; uint16_t id;

View File

@@ -166,8 +166,8 @@ def mkassert(type, comp, lh, rh, size=None):
'type': type.lower(), 'TYPE': type.upper(), 'type': type.lower(), 'TYPE': type.upper(),
'comp': comp.lower(), 'COMP': comp.upper(), 'comp': comp.lower(), 'COMP': comp.upper(),
'prefix': PREFIX.lower(), 'PREFIX': PREFIX.upper(), 'prefix': PREFIX.lower(), 'PREFIX': PREFIX.upper(),
'lh': lh.strip(), 'lh': lh.strip(' '),
'rh': rh.strip(), 'rh': rh.strip(' '),
'size': size, 'size': size,
} }
if size: if size:

View File

@@ -18,9 +18,10 @@ TAG_TYPES = {
'ctzstruct': (0x7ff, 0x202), 'ctzstruct': (0x7ff, 0x202),
'inlinestruct': (0x7ff, 0x201), 'inlinestruct': (0x7ff, 0x201),
'userattr': (0x700, 0x300), 'userattr': (0x700, 0x300),
'tail': (0x700, 0x600), 'tail': (0x700, 0x600), # TODO rename these?
'softtail': (0x7ff, 0x600), 'softtail': (0x7ff, 0x600),
'hardtail': (0x7ff, 0x601), 'hardtail': (0x7ff, 0x601),
'branch': (0x7ff, 0x681),
'gstate': (0x700, 0x700), 'gstate': (0x700, 0x700),
'movestate': (0x7ff, 0x7ff), 'movestate': (0x7ff, 0x7ff),
'crc': (0x700, 0x500), 'crc': (0x700, 0x500),
@@ -103,7 +104,7 @@ class Tag:
def mkmask(self): def mkmask(self):
return Tag( return Tag(
0x700 if self.isunique else 0x7ff, 0x780 if self.is_('tail') else 0x700 if self.isunique else 0x7ff, # TODO best way?
0x3ff if self.isattr else 0, 0x3ff if self.isattr else 0,
0) 0)
@@ -233,8 +234,8 @@ class MetadataPair:
def __lt__(self, other): def __lt__(self, other):
# corrupt blocks don't count # corrupt blocks don't count
if not self and other: if not self or not other:
return True return bool(other)
# use sequence arithmetic to avoid overflow # use sequence arithmetic to avoid overflow
return not ((other.rev - self.rev) & 0x80000000) return not ((other.rev - self.rev) & 0x80000000)
@@ -318,6 +319,14 @@ def main(args):
# find most recent pair # find most recent pair
mdir = MetadataPair(blocks) mdir = MetadataPair(blocks)
print("mdir {%s} rev %d%s%s" % (
', '.join('%#x' % b
for b in [args.block1, args.block2]
if b is not None),
mdir.rev,
' (was %s)' % ', '.join('%d' % m.rev for m in mdir.pair[1:])
if len(mdir.pair) > 1 else '',
' (corrupted!)' if not mdir else ''))
if args.all: if args.all:
mdir.dump_all(truncate=not args.no_truncate) mdir.dump_all(truncate=not args.no_truncate)
elif args.log: elif args.log:

View File

@@ -5,6 +5,7 @@ import sys
import json import json
import io import io
import itertools as it import itertools as it
import collections as c
from readmdir import Tag, MetadataPair from readmdir import Tag, MetadataPair
def popc(x): def popc(x):
@@ -13,31 +14,29 @@ def popc(x):
def ctz(x): def ctz(x):
return len(bin(x)) - len(bin(x).rstrip('0')) return len(bin(x)) - len(bin(x).rstrip('0'))
def dumpentries(args, mdir, f): def dumpentries(args, mdir, mdirs, f):
for k, id_ in enumerate(mdir.ids): for k, id_ in enumerate(mdir.ids):
name = mdir[Tag('name', id_, 0)] name = mdir[Tag('name', id_, 0)]
struct_ = mdir[Tag('struct', id_, 0)] struct_ = mdir[Tag('struct', id_, 0)]
f.write("id %d %s %s" % ( desc = "id %d %s %s" % (
id_, name.typerepr(), id_, name.typerepr(),
json.dumps(name.data.decode('utf8')))) json.dumps(name.data.decode('utf8')))
if struct_.is_('dirstruct'): if struct_.is_('dirstruct'):
f.write(" dir {%#x, %#x}" % struct.unpack( pair = struct.unpack('<II', struct_.data[:8].ljust(8, b'\xff'))
'<II', struct_.data[:8].ljust(8, b'\xff'))) desc += " dir {%#x, %#x}%s" % (
pair[0], pair[1],
'?' if frozenset(pair) not in mdirs else '')
if struct_.is_('ctzstruct'): if struct_.is_('ctzstruct'):
f.write(" ctz {%#x} size %d" % struct.unpack( desc += " ctz {%#x} size %d" % struct.unpack(
'<II', struct_.data[:8].ljust(8, b'\xff'))) '<II', struct_.data[:8].ljust(8, b'\xff'))
if struct_.is_('inlinestruct'): if struct_.is_('inlinestruct'):
f.write(" inline size %d" % struct_.size) desc += " inline size %d" % struct_.size
f.write("\n")
if args.data and struct_.is_('inlinestruct'): data = None
for i in range(0, len(struct_.data), 16): if struct_.is_('inlinestruct'):
f.write(" %08x: %-47s %-16s\n" % ( data = struct_.data
i, ' '.join('%02x' % c for c in struct_.data[i:i+16]), elif struct_.is_('ctzstruct'):
''.join(c if c >= ' ' and c <= '~' else '.'
for c in map(chr, struct_.data[i:i+16]))))
elif args.data and struct_.is_('ctzstruct'):
block, size = struct.unpack( block, size = struct.unpack(
'<II', struct_.data[:8].ljust(8, b'\xff')) '<II', struct_.data[:8].ljust(8, b'\xff'))
data = [] data = []
@@ -51,43 +50,62 @@ def dumpentries(args, mdir, f):
data.append(dat[4*(ctz(i)+1) if i != 0 else 0:]) data.append(dat[4*(ctz(i)+1) if i != 0 else 0:])
block, = struct.unpack('<I', dat[:4].ljust(4, b'\xff')) block, = struct.unpack('<I', dat[:4].ljust(4, b'\xff'))
i -= 1 i -= 1
data = bytes(it.islice( data = bytes(it.islice(
it.chain.from_iterable(reversed(data)), size)) it.chain.from_iterable(reversed(data)), size))
for i in range(0, min(len(data), 256)
if not args.no_truncate else len(data), 16): f.write("%-45s%s\n" % (desc,
"%-23s %-8s" % (
' '.join('%02x' % c for c in data[:8]),
''.join(c if c >= ' ' and c <= '~' else '.'
for c in map(chr, data[:8])))
if not args.no_truncate and len(desc) < 45
and data is not None else ""))
if name.is_('superblock') and struct_.is_('inlinestruct'):
f.write(
" block_size %d\n"
" block_count %d\n"
" name_max %d\n"
" file_max %d\n"
" attr_max %d\n" % struct.unpack(
'<IIIII', struct_.data[4:4+20].ljust(20, b'\xff')))
for tag in mdir.tags:
if tag.id==id_ and tag.is_('userattr'):
desc = "%s size %d" % (tag.typerepr(), tag.size)
f.write(" %-43s%s\n" % (desc,
"%-23s %-8s" % (
' '.join('%02x' % c for c in tag.data[:8]),
''.join(c if c >= ' ' and c <= '~' else '.'
for c in map(chr, tag.data[:8])))
if not args.no_truncate and len(desc) < 43 else ""))
if args.no_truncate:
for i in range(0, len(tag.data), 16):
f.write(" %08x: %-47s %-16s\n" % (
i, ' '.join('%02x' % c for c in tag.data[i:i+16]),
''.join(c if c >= ' ' and c <= '~' else '.'
for c in map(chr, tag.data[i:i+16]))))
if args.no_truncate and data is not None:
for i in range(0, len(data), 16):
f.write(" %08x: %-47s %-16s\n" % ( f.write(" %08x: %-47s %-16s\n" % (
i, ' '.join('%02x' % c for c in data[i:i+16]), i, ' '.join('%02x' % c for c in data[i:i+16]),
''.join(c if c >= ' ' and c <= '~' else '.' ''.join(c if c >= ' ' and c <= '~' else '.'
for c in map(chr, data[i:i+16])))) for c in map(chr, data[i:i+16]))))
for tag in mdir.tags:
if tag.id==id_ and tag.is_('userattr'):
f.write("id %d %s size %d\n" % (
id_, tag.typerepr(), tag.size))
if args.data:
for i in range(0, len(tag.data), 16):
f.write(" %-47s %-16s\n" % (
' '.join('%02x' % c for c in tag.data[i:i+16]),
''.join(c if c >= ' ' and c <= '~' else '.'
for c in map(chr, tag.data[i:i+16]))))
def main(args): def main(args):
superblock = None
gstate = b'\0\0\0\0\0\0\0\0\0\0\0\0'
mdirs = c.OrderedDict()
corrupted = []
cycle = False
with open(args.disk, 'rb') as f: with open(args.disk, 'rb') as f:
dirs = []
superblock = None
gstate = b''
mdirs = []
cycle = False
tail = (args.block1, args.block2) tail = (args.block1, args.block2)
hard = False while tail:
while True: if frozenset(tail) in mdirs:
for m in it.chain((m for d in dirs for m in d), mdirs): # cycle detected
if set(m.blocks) == set(tail): cycle = tail
# cycle detected
cycle = m.blocks
if cycle:
break break
# load mdir # load mdir
@@ -110,6 +128,13 @@ def main(args):
except KeyError: except KeyError:
mdir.tail = None mdir.tail = None
try:
mdir.branch = mdir[Tag('branch', 0, 0)]
if mdir.branch.size != 8 or mdir.branch.data == 8*b'\xff':
mdir.branch = None
except KeyError:
mdir.branch = None
# have superblock? # have superblock?
try: try:
nsuperblock = mdir[ nsuperblock = mdir[
@@ -126,106 +151,135 @@ def main(args):
except KeyError: except KeyError:
pass pass
# add to directories # corrupted?
mdirs.append(mdir) if not mdir:
if mdir.tail is None or not mdir.tail.is_('hardtail'): corrupted.append(mdir)
dirs.append(mdirs)
mdirs = []
if mdir.tail is None: # add to metadata-pairs
break mdirs[frozenset(mdir.blocks)] = mdir
tail = (struct.unpack('<II', mdir.tail.data)
if mdir.tail else None)
tail = struct.unpack('<II', mdir.tail.data) # derive paths and build directories
hard = mdir.tail.is_('hardtail') dirs = {}
rogue = {}
# find paths pending = [('/', (args.block1, args.block2))]
dirtable = {}
for dir in dirs:
dirtable[frozenset(dir[0].blocks)] = dir
pending = [("/", dirs[0])]
while pending: while pending:
path, dir = pending.pop(0) path, branch = pending.pop(0)
for mdir in dir: dir = []
while branch and frozenset(branch) in mdirs:
mdir = mdirs[frozenset(branch)]
dir.append(mdir)
for tag in mdir.tags: for tag in mdir.tags:
if tag.is_('dir'): if tag.is_('dir'):
try: try:
npath = tag.data.decode('utf8') npath = path + '/' + tag.data.decode('utf8')
npath = npath.replace('//', '/')
dirstruct = mdir[Tag('dirstruct', tag.id, 0)] dirstruct = mdir[Tag('dirstruct', tag.id, 0)]
nblocks = struct.unpack('<II', dirstruct.data) npair = struct.unpack('<II', dirstruct.data)
nmdir = dirtable[frozenset(nblocks)] pending.append((npath, npair))
pending.append(((path + '/' + npath), nmdir))
except KeyError: except KeyError:
pass pass
dir[0].path = path.replace('//', '/') branch = (struct.unpack('<II', mdir.branch.data)
if mdir.branch else None)
# dump tree if not dir:
if not args.superblock and not args.gstate and not args.mdirs: rogue[path] = branch
args.superblock = True else:
args.gstate = True dirs[path] = dir
args.mdirs = True
if args.superblock and superblock: # also find orphans
print("superblock %s v%d.%d" % ( not_orphans = {frozenset(mdir.blocks)
json.dumps(superblock[0].data.decode('utf8')), for dir in dirs.values()
struct.unpack('<H', superblock[1].data[2:2+2])[0], for mdir in dir}
struct.unpack('<H', superblock[1].data[0:0+2])[0])) orphans = []
print( for pair, mdir in mdirs.items():
" block_size %d\n" if pair not in not_orphans:
" block_count %d\n" if len(orphans) > 0 and (pair == frozenset(
" name_max %d\n" struct.unpack('<II', orphans[-1][-1].tail.data))):
" file_max %d\n" orphans[-1].append(mdir)
" attr_max %d" % struct.unpack( else:
'<IIIII', superblock[1].data[4:4+20].ljust(20, b'\xff'))) orphans.append([mdir])
if args.gstate and gstate: # print littlefs + version info
print("gstate 0x%s" % ''.join('%02x' % c for c in gstate)) version = ('?', '?')
tag = Tag(struct.unpack('<I', gstate[0:4].ljust(4, b'\xff'))[0]) if superblock:
blocks = struct.unpack('<II', gstate[4:4+8].ljust(8, b'\xff')) version = tuple(reversed(
if tag.size: struct.unpack('<HH', superblock[1].data[0:4].ljust(4, b'\xff'))))
print(" orphans %d" % tag.size) print("%-47s%s" % ("littlefs v%s.%s" % version,
if tag.type: "data (truncated, if it fits)"
print(" move dir {%#x, %#x} id %d" % ( if not any([args.no_truncate, args.tags, args.log, args.all]) else ""))
blocks[0], blocks[1], tag.id))
if args.mdirs: # print gstate
for i, dir in enumerate(dirs): badgstate = None
print("dir %s" % (json.dumps(dir[0].path) print("gstate 0x%s" % ''.join('%02x' % c for c in gstate))
if hasattr(dir[0], 'path') else '(orphan)')) tag = Tag(struct.unpack('<I', gstate[0:4].ljust(4, b'\xff'))[0])
blocks = struct.unpack('<II', gstate[4:4+8].ljust(8, b'\xff'))
if tag.size or not tag.isvalid:
print(" orphans >=%d" % max(tag.size, 1))
if tag.type:
if frozenset(blocks) not in mdirs:
badgstate = gstate
print(" move dir {%#x, %#x}%s id %d" % (
blocks[0], blocks[1],
'?' if frozenset(blocks) not in mdirs else '',
tag.id))
for j, mdir in enumerate(dir): # print dir info
print("mdir {%#x, %#x} rev %d%s" % ( for path, dir in it.chain(
mdir.blocks[0], mdir.blocks[1], mdir.rev, sorted(dirs.items()),
' (corrupted)' if not mdir else '')) zip(it.repeat(None), orphans)):
print("dir %s" % json.dumps(path) if path else "orphaned")
f = io.StringIO() for j, mdir in enumerate(dir):
if args.tags: print("mdir {%#x, %#x} rev %d (was %d)%s%s" % (
mdir.dump_tags(f, truncate=not args.no_truncate) mdir.blocks[0], mdir.blocks[1], mdir.rev, mdir.pair[1].rev,
elif args.log: ' (corrupted!)' if not mdir else '',
mdir.dump_log(f, truncate=not args.no_truncate) ' -> {%#x, %#x}' % struct.unpack('<II', mdir.tail.data)
elif args.all: if mdir.tail else ''))
mdir.dump_all(f, truncate=not args.no_truncate)
else:
dumpentries(args, mdir, f)
lines = list(filter(None, f.getvalue().split('\n'))) f = io.StringIO()
for k, line in enumerate(lines): if args.tags:
print("%s %s" % ( mdir.dump_tags(f, truncate=not args.no_truncate)
' ' if j == len(dir)-1 else elif args.log:
'v' if k == len(lines)-1 else mdir.dump_log(f, truncate=not args.no_truncate)
'|', elif args.all:
line)) mdir.dump_all(f, truncate=not args.no_truncate)
else:
dumpentries(args, mdir, mdirs, f)
lines = list(filter(None, f.getvalue().split('\n')))
for k, line in enumerate(lines):
print("%s %s" % (
' ' if j == len(dir)-1 else
'v' if k == len(lines)-1 else
'|' if path else '.',
line))
errcode = 0
for mdir in corrupted:
errcode = errcode or 1
print("*** corrupted mdir {%#x, %#x}! ***" % (
mdir.blocks[0], mdir.blocks[1]))
for path, pair in rogue.items():
errcode = errcode or 2
print("*** couldn't find dir %s {%#x, %#x}! ***" % (
json.dumps(path), pair[0], pair[1]))
if badgstate:
errcode = errcode or 3
print("*** bad gstate 0x%s! ***" %
''.join('%02x' % c for c in gstate))
if cycle: if cycle:
print("*** cycle detected! -> {%#x, %#x} ***" % (cycle[0], cycle[1])) errcode = errcode or 4
print("*** cycle detected {%#x, %#x}! ***" % (
cycle[0], cycle[1]))
if cycle: return errcode
return 2
elif not all(mdir for dir in dirs for mdir in dir):
return 1
else:
return 0;
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse
@@ -242,20 +296,12 @@ if __name__ == "__main__":
parser.add_argument('block2', nargs='?', default=1, parser.add_argument('block2', nargs='?', default=1,
type=lambda x: int(x, 0), type=lambda x: int(x, 0),
help="Optional second block address for finding the root.") help="Optional second block address for finding the root.")
parser.add_argument('-s', '--superblock', action='store_true',
help="Show contents of the superblock.")
parser.add_argument('-g', '--gstate', action='store_true',
help="Show contents of global-state.")
parser.add_argument('-m', '--mdirs', action='store_true',
help="Show contents of metadata-pairs/directories.")
parser.add_argument('-t', '--tags', action='store_true', parser.add_argument('-t', '--tags', action='store_true',
help="Show metadata tags instead of reconstructing entries.") help="Show metadata tags instead of reconstructing entries.")
parser.add_argument('-l', '--log', action='store_true', parser.add_argument('-l', '--log', action='store_true',
help="Show tags in log.") help="Show tags in log.")
parser.add_argument('-a', '--all', action='store_true', parser.add_argument('-a', '--all', action='store_true',
help="Show all tags in log, included tags in corrupted commits.") help="Show all tags in log, included tags in corrupted commits.")
parser.add_argument('-d', '--data', action='store_true',
help="Also show the raw contents of files/attrs/tags.")
parser.add_argument('-T', '--no-truncate', action='store_true', parser.add_argument('-T', '--no-truncate', action='store_true',
help="Don't truncate large amounts of data.") help="Show the full contents of files/attrs/tags.")
sys.exit(main(parser.parse_args())) sys.exit(main(parser.parse_args()))

View File

@@ -231,7 +231,7 @@ class TestCase:
ncmd.extend(['-ex', 'r']) ncmd.extend(['-ex', 'r'])
if failure.assert_: if failure.assert_:
ncmd.extend(['-ex', 'up 2']) ncmd.extend(['-ex', 'up 2'])
elif gdb == 'start': elif gdb == 'start' or isinstance(gdb, int):
ncmd.extend([ ncmd.extend([
'-ex', 'b %s:%d' % (self.suite.path, self.code_lineno), '-ex', 'b %s:%d' % (self.suite.path, self.code_lineno),
'-ex', 'r']) '-ex', 'r'])
@@ -329,7 +329,9 @@ class ReentrantTestCase(TestCase):
persist = 'noerase' persist = 'noerase'
# exact cycle we should drop into debugger? # exact cycle we should drop into debugger?
if gdb and failure and failure.cycleno == cycles: if gdb and failure and (
failure.cycleno == cycles or
(isinstance(gdb, int) and gdb == cycles)):
return super().test(gdb=gdb, persist=persist, cycles=cycles, return super().test(gdb=gdb, persist=persist, cycles=cycles,
failure=failure, **args) failure=failure, **args)
@@ -760,7 +762,8 @@ if __name__ == "__main__":
help="Store disk image in a file.") help="Store disk image in a file.")
parser.add_argument('-b', '--build', action='store_true', parser.add_argument('-b', '--build', action='store_true',
help="Only build the tests, do not execute.") help="Only build the tests, do not execute.")
parser.add_argument('-g', '--gdb', choices=['init', 'start', 'assert'], parser.add_argument('-g', '--gdb', metavar='{init,start,assert},CYCLE',
type=lambda n: n if n in {'init', 'start', 'assert'} else int(n, 0),
nargs='?', const='assert', nargs='?', const='assert',
help="Drop into gdb on test failure.") help="Drop into gdb on test failure.")
parser.add_argument('--no-internal', action='store_true', parser.add_argument('--no-internal', action='store_true',

View File

@@ -246,6 +246,8 @@ code = '''
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0; LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
lfs_file_close(&lfs, &file) => 0; lfs_file_close(&lfs, &file) => 0;
} }
// TODO rm me
lfs_mkdir(&lfs, "a") => 0;
lfs_unmount(&lfs) => 0; lfs_unmount(&lfs) => 0;
lfs_mount(&lfs, &cfg) => 0; lfs_mount(&lfs, &cfg) => 0;
@@ -256,6 +258,9 @@ code = '''
lfs_dir_read(&lfs, &dir, &info) => 1; lfs_dir_read(&lfs, &dir, &info) => 1;
assert(info.type == LFS_TYPE_DIR); assert(info.type == LFS_TYPE_DIR);
assert(strcmp(info.name, "..") == 0); assert(strcmp(info.name, "..") == 0);
lfs_dir_read(&lfs, &dir, &info) => 1;
assert(info.type == LFS_TYPE_DIR);
assert(strcmp(info.name, "a") == 0);
for (int i = 0; i < N; i++) { for (int i = 0; i < N; i++) {
sprintf(path, "file%03d", i); sprintf(path, "file%03d", i);
lfs_dir_read(&lfs, &dir, &info) => 1; lfs_dir_read(&lfs, &dir, &info) => 1;

285
tests/test_evil.toml Normal file
View File

@@ -0,0 +1,285 @@
# 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));
// 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));
// 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));
// 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;
'''

View File

@@ -148,7 +148,7 @@ code = '''
# almost every tree operation needs a relocation # almost every tree operation needs a relocation
reentrant = true reentrant = true
# TODO fix this case, caused by non-DAG trees # TODO fix this case, caused by non-DAG trees
if = '!(DEPTH == 3 && LFS_CACHE_SIZE != 64)' #if = '!(DEPTH == 3 && LFS_CACHE_SIZE != 64)'
define = [ define = [
{FILES=6, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1}, {FILES=6, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},
{FILES=26, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1}, {FILES=26, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},
@@ -210,7 +210,7 @@ code = '''
[[case]] # reentrant testing for relocations, but now with random renames! [[case]] # reentrant testing for relocations, but now with random renames!
reentrant = true reentrant = true
# TODO fix this case, caused by non-DAG trees # TODO fix this case, caused by non-DAG trees
if = '!(DEPTH == 3 && LFS_CACHE_SIZE != 64)' #if = '!(DEPTH == 3 && LFS_CACHE_SIZE != 64)'
define = [ define = [
{FILES=6, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1}, {FILES=6, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},
{FILES=26, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1}, {FILES=26, DEPTH=1, CYCLES=20, LFS_BLOCK_CYCLES=1},

View File

@@ -27,41 +27,55 @@ code = '''
''' '''
[[case]] # expanding superblock [[case]] # expanding superblock
define.BLOCK_CYCLES = [32, 33, 1] define.LFS_BLOCK_CYCLES = [32, 33, 1]
define.N = [10, 100, 1000] define.N = [10, 100, 1000]
code = ''' code = '''
lfs_format(&lfs, &cfg) => 0; lfs_format(&lfs, &cfg) => 0;
lfs_mount(&lfs, &cfg) => 0; lfs_mount(&lfs, &cfg) => 0;
for (int i = 0; i < N; i++) { for (int i = 0; i < N; i++) {
lfs_mkdir(&lfs, "dummy") => 0; lfs_file_open(&lfs, &file, "dummy",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
lfs_file_close(&lfs, &file) => 0;
lfs_stat(&lfs, "dummy", &info) => 0; lfs_stat(&lfs, "dummy", &info) => 0;
assert(strcmp(info.name, "dummy") == 0); assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
lfs_remove(&lfs, "dummy") => 0; lfs_remove(&lfs, "dummy") => 0;
} }
lfs_unmount(&lfs) => 0; lfs_unmount(&lfs) => 0;
// one last check after power-cycle // one last check after power-cycle
lfs_mount(&lfs, &cfg) => 0; lfs_mount(&lfs, &cfg) => 0;
lfs_mkdir(&lfs, "dummy") => 0; lfs_file_open(&lfs, &file, "dummy",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
lfs_file_close(&lfs, &file) => 0;
lfs_stat(&lfs, "dummy", &info) => 0; lfs_stat(&lfs, "dummy", &info) => 0;
assert(strcmp(info.name, "dummy") == 0); assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
lfs_unmount(&lfs) => 0; lfs_unmount(&lfs) => 0;
''' '''
[[case]] # expanding superblock with power cycle [[case]] # expanding superblock with power cycle
define.BLOCK_CYCLES = [32, 33, 1] define.LFS_BLOCK_CYCLES = [32, 33, 1]
define.N = [10, 100, 1000] define.N = [10, 100, 1000]
code = ''' code = '''
lfs_format(&lfs, &cfg) => 0; lfs_format(&lfs, &cfg) => 0;
for (int i = 0; i < N; i++) { for (int i = 0; i < N; i++) {
lfs_mount(&lfs, &cfg) => 0; lfs_mount(&lfs, &cfg) => 0;
// remove lingering dummy? // remove lingering dummy?
err = lfs_remove(&lfs, "dummy"); err = lfs_stat(&lfs, "dummy", &info);
assert(err == 0 || (err == LFS_ERR_NOENT && i == 0)); assert(err == 0 || (err == LFS_ERR_NOENT && i == 0));
if (!err) {
assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
lfs_remove(&lfs, "dummy") => 0;
}
lfs_mkdir(&lfs, "dummy") => 0; lfs_file_open(&lfs, &file, "dummy",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
lfs_file_close(&lfs, &file) => 0;
lfs_stat(&lfs, "dummy", &info) => 0; lfs_stat(&lfs, "dummy", &info) => 0;
assert(strcmp(info.name, "dummy") == 0); assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
lfs_unmount(&lfs) => 0; lfs_unmount(&lfs) => 0;
} }
@@ -69,11 +83,12 @@ code = '''
lfs_mount(&lfs, &cfg) => 0; lfs_mount(&lfs, &cfg) => 0;
lfs_stat(&lfs, "dummy", &info) => 0; lfs_stat(&lfs, "dummy", &info) => 0;
assert(strcmp(info.name, "dummy") == 0); assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
lfs_unmount(&lfs) => 0; lfs_unmount(&lfs) => 0;
''' '''
[[case]] # reentrant expanding superblock [[case]] # reentrant expanding superblock
define.BLOCK_CYCLES = [2, 1] define.LFS_BLOCK_CYCLES = [2, 1]
define.N = 24 define.N = 24
reentrant = true reentrant = true
code = ''' code = '''
@@ -85,12 +100,20 @@ code = '''
for (int i = 0; i < N; i++) { for (int i = 0; i < N; i++) {
// remove lingering dummy? // remove lingering dummy?
err = lfs_remove(&lfs, "dummy"); err = lfs_stat(&lfs, "dummy", &info);
assert(err == 0 || (err == LFS_ERR_NOENT && i == 0)); assert(err == 0 || (err == LFS_ERR_NOENT && i == 0));
if (!err) {
assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
lfs_remove(&lfs, "dummy") => 0;
}
lfs_mkdir(&lfs, "dummy") => 0; lfs_file_open(&lfs, &file, "dummy",
LFS_O_WRONLY | LFS_O_CREAT | LFS_O_EXCL) => 0;
lfs_file_close(&lfs, &file) => 0;
lfs_stat(&lfs, "dummy", &info) => 0; lfs_stat(&lfs, "dummy", &info) => 0;
assert(strcmp(info.name, "dummy") == 0); assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
} }
lfs_unmount(&lfs) => 0; lfs_unmount(&lfs) => 0;
@@ -99,5 +122,6 @@ code = '''
lfs_mount(&lfs, &cfg) => 0; lfs_mount(&lfs, &cfg) => 0;
lfs_stat(&lfs, "dummy", &info) => 0; lfs_stat(&lfs, "dummy", &info) => 0;
assert(strcmp(info.name, "dummy") == 0); assert(strcmp(info.name, "dummy") == 0);
assert(info.type == LFS_TYPE_REG);
lfs_unmount(&lfs) => 0; lfs_unmount(&lfs) => 0;
''' '''