littlefs: lfs assert from lfs_ctz_find (head >= 2 && head <= lfs->cfg->block_count) w/ 1.7.1

We’re using LFS 1.7.1 in our system and recently had an issue on a single device where an LFS_ASSERT was triggered when lfs_file_read was called. We’re wondering if this is a flaw in 1.7.1 or possibly related to something we might be doing incorrectly in our use of the lfs API?

The LFS_ASSERT occurred in lfs_ctz_find in lfs.c line 1144.

The specific assertion says: LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count);

This is the relevant code:

static int lfs_ctz_find(lfs_t *lfs,
        lfs_cache_t *rcache, const lfs_cache_t *pcache,
        lfs_block_t head, lfs_size_t size,
        lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) {
    if (size == 0) {
        *block = 0xffffffff;
        *off = 0;
        return 0;
    }

    lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size-1});
    lfs_off_t target = lfs_ctz_index(lfs, &pos);

    while (current > target) {
        lfs_size_t skip = lfs_min(
                lfs_npw2(current-target+1) - 1,
                lfs_ctz(current));

        int err = lfs_cache_read(lfs, rcache, pcache, head, 4*skip, &head, 4);
        head = lfs_fromle32(head);
        if (err) {
            return err;
        }

        LFS_ASSERT(head >= 2 && head <= lfs->cfg->block_count);
        current -= 1 << skip;
    }

    *block = head;
    *off = pos;
    return 0;
}

The git hash we are using:

SHA-1: d3a2cf48d449f259bc15a3a1058323132f3eeef7

Our configuration is:

// Statically allocated read buffer. Must be read sized.
static uint8_t read_buffer[256];

// Statically allocated program buffer. Must be program sized.
static uint8_t prog_buffer[256];

// Statically allocated lookahead buffer. Must be 1 bit per lookahead block.
static uint8_t lookahead_buffer[128];

// configuration of the filesystem is provided by this struct
static struct lfs_config m_lfsCfg = {
    // block device operations
    .read  = lfs_read,
    .prog  = lfs_prog,
    .erase = lfs_erase,
    .sync  = lfs_sync,

    // block device configuration
#define FLASH_SIZE_BYTES ((128*1024*1024)/8)
#define SERIAL_FLASH_BLOCK_ERASE_BYTES (32768)
#define SERIAL_FLASH_READ_SIZE (256)
#define SERIAL_FLASH_WRITE_SIZE (256)
    .read_size = SERIAL_FLASH_READ_SIZE,
    .prog_size = SERIAL_FLASH_WRITE_SIZE,
    .block_size = SERIAL_FLASH_BLOCK_ERASE_BYTES,
    .block_count = FLASH_SIZE_BYTES/SERIAL_FLASH_BLOCK_ERASE_BYTES,
    .lookahead = 128, // Should be a multiple of 32.

    // Statically allocated read buffer. Must be read sized.
    .read_buffer = read_buffer,

    // Statically allocated program buffer. Must be program sized.
    .prog_buffer = prog_buffer,

    // Statically allocated lookahead buffer. Must be 1 bit per lookahead block.
    .lookahead_buffer = lookahead_buffer,

    //Statically allocated buffer for files. Must be program sized.
    // If enabled, only one file may be opened at a time.
    //.file_buffer = file_buffer,
}

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Comments: 22 (8 by maintainers)

Most upvoted comments

Thank you for this – I’m looking forward to the results!

Hacker news thread brought this to my attention. I think it’s an interesting situation. It is nice to see a potential solution to this. Hope this gets somewhere.

Welp, I know it’s been a while, but I have been able to make progress on this. I have a branch here with the above forward-looking CRC proposal: https://github.com/littlefs-project/littlefs/commits/crc-rework-2

One change is it only looks-forward 1 prog_size and stores the current prog_size along with the CRC, so it should be the same cost as the current implementation. This change can also be brought in in a backwards compatible manner.

It is working as a proof-of-concept, though failing some tests at the moment (NOSPC tests are failling?). Will work on these and then bring this in.

I have made a branch in my repo: https://github.com/pjsg/littlefs/tree/test-revamp-ctz-fuzz which improves the testbd and rambd drivers and adds a test_n1.toml (which was automatically generated). The improvements add more support for power fails at bad times. It also enhances the rambd driver to be able to use an mmap’ed file as the backing buffer. This obviates the need for using the filebd.

The test_n1.toml is incredibly simple and it fails.

This failure can be fixed by adding one line to lfs.c (but this causes its own, more subtle, problems):

            // next commit not yet programmed or we're not in valid range
            if (!lfs_tag_isvalid(tag)) {
                dir->erased = (lfs_tag_type1(ptag) == LFS_TYPE_CRC &&
                        dir->off % lfs->cfg->prog_size == 0);
                dir->erased = false;    // this line fixes the test_n1 crash, but it isn't right
                break;
            } else if (off + lfs_tag_dsize(tag) > lfs->cfg->block_size) {

It turns out that I had made this “fix” to my test branch and this “fix” was causing the other errors that I had reported. I made the fix as otherwise the fuzzer got stuck with finding ever more complex ways to trigger this fault.

I’ve been fuzzing littlefs and I have a fairly simple test case. The sequence of operations is:

  open(1, "5file5.xxxxxxxxxxxx", 0x503) -> 0
  write(1, , 2007)[^ 1499 us] -> 2007
  write(1, , 2007)[^ 1411 us] -> 2007
  write(1, , 2007)[^ 1390 us] -> 2007
  write(1, , 2007)[^ 1401 us] -> 2007
  close(1) -> 0
  open(1, "1file1.xxxx", 0x503) -> 0
  mount
  open(0, "5file5.xxxxxxxxxxxx", 0x3) -> 0
  open(1, "5file5.xxxxxxxxxxxx", 0x503) -> 0
  close(1) -> 0
  open(1, "1file1.xxxx", 0x2) -> 0
  write(0, , 63) -> 63
a.out: lfs.c:2169: lfs_ctz_find: Assertion `head >= 2 && head <= lfs->cfg->block_count' failed.
  close(0)Aborted

The mount call simulates a power loss by just remounting the flash (in this case emubd) without unmounting.