Keep those configs alive or forever regret it

Keep those configs alive or forever regret it

For today's mistake I was initializing LittleFs by defining its lsf_config struct inside my wrapper code init function.

static lfs_t fileSystem;

int LfsReadCallback(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size)
{
   externalFlashDriver->ReadData((block * c->block_size) + off, size, (uint8_t*) buffer);

   return 0;
}

void FileSystemDriver::Init(IExternalFlashDriver* externalFlashDrv, unsigned eraseBlockSizeBytes, unsigned eraseBlocksCount)
{
   struct lfs_config cfg = {
      ...
      .read = LfsReadCallback,
      ...
   };

   lfs_mount(&fileSystem, &cfg);

   // File system works fine here, I can read and write files
}

LfsReadCallback is used by LittleFs when it needs to read data from flash. I can use the file system just fine inside the Init function.

However, when I later try to use LittleFs from a wrapper function I run into callstack corruption errors.

int32_t FileSystemDriver::OpenFile(File *file, unsigned fileSizeBytes, const char *path)
{
   // lfs_file_opencfg eventually leads to a corrupt callstack, why?
   return lfs_file_opencfg(&fileSystem, &file->file, path, LFS_O_RDWR | LFS_O_CREAT, &file->file_config);
}

The mistake is that my cfg struct that I pass into LittleFs is local to the Init function stack memory, and will only exist for as long as we are within the Init function scope. Once we exit, the cfg struct memory is freed and used for other things while LittleFs still maintains a pointer to that location. When I was first prototyping LittleFs initialization and my wrapper code I was doing all initialization and testing within the scope of a single function, and I did not really think about the lifetime of the LittleFs configuration struct during that stage.

The solution is to move the cfg struct out of the Init scope so it will exist for as long as we need it to.

static lfs_t fileSystem;

int LfsReadCallback(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size)
{
   externalFlashDriver->ReadData((block * c->block_size) + off, size, (uint8_t*) buffer);

   return 0;
}

static struct lfs_config cfg = {
   ...
   .read = LfsReadCallback,
   ...
};

void FileSystemDriver::Init(IExternalFlashDriver* externalFlashDrv, unsigned eraseBlockSizeBytes, unsigned eraseBlocksCount)
{
   lfs_mount(&fileSystem, &cfg);
}

Once a mistake like this is made, it can be hard to pinpoint the problem. The cause of a corrupted callstack can be hard to track down, especially if it happens inside a library you do not have the sources for. The lesson here is to always consider the lifetime of configuration data, also during an early prototyping stage of development when everything may be tested within the scope of a single Init() function.