LittleFS: File Open Err: -14

LittleFS Corruption Issue Report – nRF5340 (External QSPI Flash)


1. Introduction

This document outlines a filesystem corruption issue encountered while using LittleFS on an external QSPI flash (64 MiB) with the nRF5340 platform (Zephyr RTOS). The issue occurs during high-volume file creation within a single directory and results in filesystem corruption and, in some cases, complete flash erasure upon reboot.


2. Error Description

Observed Logs

<err> littlefs: lfs.c:1374: Corrupted dir pair at {0x1, 0x0}
<err> fs: file open error (-14)
File Open Err: -14

Interpretation

  • Error code -14 indicates filesystem inconsistency.

  • “Corrupted dir pair” suggests failure in LittleFS metadata integrity (directory structure).


3. Hardware Configuration

Component Specification
MCU nRF5340
External Flash MX25L51245G
Interface QSPI
Capacity 64 MiB (67,108,864 bytes)

4. Device Tree Configuration

4.1 LittleFS Mount Configuration

fstab {
    compatible = "zephyr,fstab";

    lfs1: lfs1 {
        compatible = "zephyr,fstab,littlefs";
        mount-point = "/external";
        partition = <&external_partition>;

        read-size = <16>;
        prog-size = <16>;
        cache-size = <64>;
        lookahead-size = <32>;
        block-cycles = <0>;
    };
};

4.2 QSPI Flash Configuration

&qspi {
    pinctrl-0 = <&qspi_default>;
    pinctrl-1 = <&qspi_sleep>;
    pinctrl-names = "default", "sleep";

    mx25l512: mx25l51245g@0 {
        compatible = "nordic,qspi-nor";
        reg = <0>;

        writeoc = "pp4io";
        readoc = "read4io";
        sck-frequency = <8000000>;
        jedec-id = [c2 20 1A];

        size = <0x20000000>; // 64 MiB

        has-dpd;
        t-enter-dpd = <10000>;
        t-exit-dpd = <30000>;

        external_partition: partition {
            label = "external";
        };
    };
};

5. Project Configuration (prj.conf)

CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_SETTINGS=y
CONFIG_IMG_MANAGER=y
CONFIG_STREAM_FLASH=y
CONFIG_NET_BUF=y
CONFIG_ZCBOR=y
CONFIG_JSON_LIBRARY=y

CONFIG_THREAD_NAME=y
CONFIG_MAIN_STACK_SIZE=8192
CONFIG_HEAP_MEM_POOL_SIZE=20480
CONFIG_MBEDTLS_HEAP_SIZE=16384
CONFIG_FS_LITTLEFS_FC_HEAP_SIZE=8192

CONFIG_BT_RX_STACK_SIZE=12288
CONFIG_BT_HCI_TX_STACK_SIZE=8192
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=14288

6. Observed Behavior

6.1 Scenario A – Single Directory File Creation

  • ~2500–3000 files written in a single directory

  • Each file size ≈ 1.1 KB

  • Write operation appears successful

  • Upon reboot:

    • Filesystem becomes corrupted

    • Entire external flash is erased/reset


6.2 Scenario B – Mixed File Operations

  • Combination of:

    • File creation (Method 1)

    • File update with seek (Method 2)

  • Failure occurs around ~1500 files

  • Same corruption error observed


7. File Write Implementations


7.1 Method 1 – File Creation

int __fs_write_bytes_to_file(struct fs_mount_t *fs_mount_point,
                            const char *abs_path,
                            const char *data,
                            int len,
                            int *write_count)
{
    int rc;
    struct fs_file_t file;

    file.mp = fs_external_mount_point;
    file.flags = FS_O_CREATE;

    char fname[MAX_PATH_LEN];

    fs_file_t_init(&file);
    snprintf(fname, sizeof(fname), "%s/%s",
             fs_mount_point->mnt_point, abs_path);

    rc = fs_open(&file, fname, FS_O_CREATE | FS_O_WRITE);

    if (rc < 0) {
        printf("fs_open error (%d)", rc);
        return rc;
    }

    if (rc != 0) {
        printf("fs_seek error (%d)", rc);
        return rc;
    }

    rc = fs_write(&file, data, len);

    fs_sync(&file);
    fs_close(&file);

    printf("File write > %s, size = %d, result = %d\n",
           fname, len, rc);

    *write_count = rc;
    return rc < 0;
}

7.2 Method 2 – File Update with Seek

int8_t fs_write_current_ble_session_command_guids_data_on_file(uint8_t *data_to_write,
                                                               uint8_t len)
{
    int8_t rc;
    struct fs_file_t file;
    fs_file_t_init(&file);

    char fname[MAX_PATH_LEN];

    snprintf(fname, sizeof(fname), "%s/%s",
             fs_external_mount_point->mnt_point,
             TRANSACTIONS_BLE_SESSION_DATA_FILE);

    rc = fs_open(&file, fname, FS_O_CREATE | FS_O_WRITE);

    if (rc < 0) {
        return -1;
    }

    int32_t location_index =
        g_current_ble_session_transaction_counter * len;

    rc = fs_seek(&file, location_index, FS_SEEK_SET);
    if (rc != 0) {
        return rc;
    }

    rc = fs_write(&file, data_to_write, len);

    if (rc != len) {
        // Log partial write
    }

    fs_close(&file);

    g_current_ble_session_transaction_counter++;

    if (g_current_ble_session_transaction_counter >=
        MAX_CURRENT_SESSION_TRANSACTIONS_COUNT) {
        g_current_ble_session_transaction_counter = 0;
    }

    return rc;
}

8. Key Observations

  • Filesystem corruption occurs under high file count in a single directory

  • Failure threshold:

    • ~3000 files (single operation)

    • ~1500 files (mixed operations)

  • Starts giving -14 Error on runtime and File corruption manifests:

    • On reboot

    • During subsequent file operations

  • Entire flash partition may be erased after corruption


9. Summary

Parameter Value
Flash Size 64 MiB
File Size ~1.1 KB
Max Files (Observed) 1500–3000
Failure Type Directory corruption
Error Code -14
Trigger Condition High file count + ops

10. Conclusion

The issue is reproducible under conditions involving:

  • High file count within a single directory

  • Frequent file creation and modification

  • Combined write patterns across multiple files

The corruption originates from instability in LittleFS directory metadata under heavy usage patterns. The problem is not directly related to flash capacity but rather to filesystem behavior under scaling conditions.


  • Hi,

    LittleFS is an Arm module that is within Zephyr. Have you reported the issue in upstream Zephyr?

    Error -14:
    In Zephyr’s LittleFS glue layer, LFS_ERR_CORRUPT is mapped to -EFAULT, and EFAULT is 14 in the minimal C library, so the application sees -14 (not a generic “-EIO for corruption”).

    Relevant mapping:

    static int lfs_to_errno(int error)
    {
    	if (error >= 0) {
    		return error;
    	}
    
    	switch (error) {
    	default:
    	case LFS_ERR_IO:        /* Error during device operation */
    		return -EIO;
    	case LFS_ERR_CORRUPT:   /* Corrupted */
    		return -EFAULT;


    So your log line matches metadata validation failing inside lfs_dir_fetchmatch (in your tree the user-visible Corrupted dir pair is emitted when the directory walk does not find a valid tag and treats the directory pair as corrupt):

    	}
    
        LFS_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}",
                dir->pair[0], dir->pair[1]);
        return LFS_ERR_CORRUPT;
    }


    The pair {0x1, 0x0} is worth noting: a 0 block in a directory pair is atypical in a healthy, mounted volume (often one block is the superblock area / invalid as a mdir copy). So either the on-flash structure is really damaged, or something (driver, POR, DPD, bad sync, or wrong read/prog geometry) is returning inconsistent contents for that metadata.

    A few comments

    • 64 MiB capacity – you are not running out of space; this is a metadata / coherency class of failure, not a simple ENOSPC story.

    • LittleFS “does not support many files” in an absolute sense – v2 is designed to scale, but a single directory with thousands of small files is a stress case: more directory compaction, more tags, and more work per open/list. It can expose bugs, too-small caches, or concurrency issues faster than a sharded layout.


    • Method 1) Application and mount consistency
      You set file.mp = fs_external_mount_point but build the path with fs_mount_point->mnt_point. If those two are not the same fs_mount_t*, the VFS can open using one mount and the path from another, which is a direct route to “impossible” metadata. Those must match.

      Also, the block right after fs_open:

      On success, rc == 0, so if (rc != 0) { ... fs_seek error ... never runs; that branch looks like a copy-paste error and suggests the function was not carefully validated.
      The return return rc < 0; returns 0/1, not the negative errno—worth fixing if anything depends on the actual error code.

    • CONFIG_FS_LITTLEFS_FC_HEAP_SIZE=8192
      For heavy metadata churn (thousands of files in one directory), 8 KiB can be tight. Bumping the flash-cache heap (in line with Nordics’ guidance and your cache-size) is a cheap experiment to rule out allocation pressure during compaction.

    • Your read size / prog size is 16B. Make sure that this aligns properly to the macronix part you're using (most are 256B pages).

    Action items on you:

    1. Shard files across many subdirectories (e.g. 100–500 files per dir by hash, date, or prefix). This is the classic fix for “single huge directory” stress and makes debugging much easier.
    2. Verify exclusive FS access (no other subsystem touching the same qspi/flash region; no OTA, logging, or settings to overlapping partitions).
    3. A/B test: DPD off, higher lookahead, align prog to flash page, increase LittleFS cache/FC heap – controlled one change at a time.
    4. Fix the file.mp vs path mnt_point mismatch and clean up the Method 1 return/branch logic, then re-run the stress test.
    5. If corruption persists, collect one failing dump (raw read of the blocks reported in the pair) and post it here with the exact NCS version and west manifest SHA—this becomes either a driver issue or a LittleFS corner case with enough evidence to act on.

    Kind regards,
    Andreas

  • Hi Nordic Team,

    I am following up on my previous post. I have done an isolated stress test on a fresh LittleFS sample project to narrow down the issue.


    Environment

    • Board: nRF5340 custom PCB
    • SDK: nRF Connect SDK 3.0.2
    • External flash: MX25L51245GZ2I-08G (64MB NOR, QSPI)
    • Filesystem: LittleFS via Zephyr fstab

    What I am doing

    I am writing the same single file in a loop of 10,000 iterations — overwriting it every time. The file is always the same name and always the same size. Flash is fully erased before the test starts. Only DATA_SIZE changes between tests runs. Everything else is identical.


    Observation

    DATA_SIZE Result
    270 bytes -14 (EFAULT) at ~4000 writes
    800 bytes No error at 10000 writes
    1024 bytes No error at 10000 writes
    4000 bytes No error at 10000 writes

    Question:

    What is causing -14 (EFAULT) specifically when writing a 270 byte file repeatedly but not when writing the file with size 800, 1024 or 4000?


    &qspi {
    pinctrl-0 = <&qspi_default>;
    pinctrl-1 = <&qspi_sleep>;
    pinctrl-names = "default", "sleep";

    mx25l512: mx25l51245g@0 {
    compatible = "nordic,qspi-nor";
    reg = <0>;
    writeoc = "pp4io";
    readoc = "read4io";
    sck-frequency = <8000000>;
    jedec-id = [c2 20 1a];
    sfdp-bfp = [
    e5 20 fb ff ff ff ff 1f 44 eb 08 6b 08 3b 04 bb
    fe ff ff ff ff ff 00 ff ff ff 44 eb 0c 20 0f 52
    10 d8 00 ff d6 49 c5 00 81 df 04 e3 44 03 67 38
    30 b0 30 b0 f7 bd d5 5c 4a 9e 29 ff f0 50 f9 85
    ];
    size = <0x20000000>;
    has-dpd;
    t-enter-dpd = <10000>;
    t-exit-dpd = <30000>;
    external_partition: partition {
    label = "external";
    };
    };
    };

    fstab {
    compatible = "zephyr,fstab";
    lfs1: lfs1 {
    compatible = "zephyr,fstab,littlefs";
    mount-point = "/external";
    partition = <&external_partition>;
    read-size = <16>;
    prog-size = <256>;
    cache-size = <4096>;
    lookahead-size = <512>;
    block-cycles = <500>;
    };
    };


    Proj.conf

    CONFIG_MAIN_STACK_SIZE=10240

    CONFIG_HEAP_MEM_POOL_SIZE=20480
    CONFIG_MBEDTLS_HEAP_SIZE=18432
    CONFIG_FS_LITTLEFS_FC_HEAP_SIZE=16384

    # Debug
    CONFIG_DEBUG=y
    # Logging — use IMMEDIATE so logs aren't buffered/lost on quick exit
    CONFIG_LOG=y
    CONFIG_LOG_MODE_IMMEDIATE=y
    CONFIG_LOG_BACKEND_UART=y
    # Console/Serial
    CONFIG_SERIAL=y
    CONFIG_UART_CONSOLE=y
    CONFIG_PRINTK=y

    # Storage
    CONFIG_FLASH=y
    CONFIG_FLASH_MAP=y
    CONFIG_FILE_SYSTEM=y
    CONFIG_FILE_SYSTEM_LITTLEFS=y
    CONFIG_APP_LITTLEFS_STORAGE_FLASH=y
    CONFIG_CONSOLE=y
    CONFIG_REBOOT=y
    CONFIG_SPI=y
    CONFIG_FLASH_JESD216_API=y



    Refer the code below

    #define DATA_SIZE (270)

    int fs_write_bytes_to_file(struct fs_mount_t *fs_mount_point,
    const char *abs_path,
    const uint8_t *data,
    int len)
    {
    if (fs_mount_point == NULL || abs_path == NULL || data == NULL)
    {
    LOG_ERR("%s: NULL argument", __func__);
    return -EINVAL;
    }

    if (len <= 0)
    {
    LOG_ERR("%s: invalid length %d", __func__, len);
    return -EINVAL;
    }

    char fname[MAX_PATH_LEN];
    int path_len = snprintf(fname, sizeof(fname), "%s/%s",
    fs_mount_point->mnt_point, abs_path);

    if (path_len < 0 || path_len >= (int)sizeof(fname))
    {
    LOG_ERR("%s: path too long (%s/%s)",
    __func__, fs_mount_point->mnt_point, abs_path);
    return -ENAMETOOLONG;
    }

    struct fs_file_t file;
    fs_file_t_init(&file);

    int rc = fs_open(&file, fname, FS_O_CREATE | FS_O_RDWR);
    if (rc < 0)
    {
    LOG_ERR("%s: fs_open failed (%s) rc=%d", __func__, fname, rc);
    return rc;
    }

    rc = fs_write(&file, data, len);
    if (rc < 0)
    {
    LOG_ERR("%s: fs_write failed (%s) rc=%d", __func__, fname, rc);
    goto cleanup;
    }

    if (rc != len)
    {
    LOG_ERR("%s: partial write (%s) wrote=%d expected=%d",
    __func__, fname, rc, len);
    rc = -EIO;
    goto cleanup;
    }

    rc = fs_sync(&file);
    if (rc < 0)
    {
    LOG_ERR("%s: fs_sync failed (%s) rc=%d", __func__, fname, rc);
    goto cleanup;
    }

    rc = len;

    cleanup:
    int close_rc = fs_close(&file);
    if (close_rc < 0)
    {
    LOG_ERR("%s: fs_close failed (%s) rc=%d", __func__, fname, close_rc);
    if (rc >= 0)
    rc = close_rc;
    }

    return rc;
    }




    int fs_write_data_file(const uint8_t *data, int size)
    {
    return fs_write_bytes_to_file(fs_external_mount_point,
    LOCKER_RSA_PUB_KEY_FILE,
    data, size);
    }




    static int little_fs_stress_test1(void)
    {
    uint8_t dummy_data[DATA_SIZE];
    for (size_t i = 0; i < DATA_SIZE; i++)
    {
    dummy_data[i] = (uint8_t)(i & 0xFF);
    }

    for (int i = 0; i < 10000; i++)
    {
    LOG_INF("Writing file counter =:: %d", i);
    fs_write_data_file(dummy_data, DATA_SIZE);
    }

    return 0;
    }



    int main(void)
    {
    struct fs_statvfs sbuf;
    int rc;

    rc = littlefs_mount(fs_external_mount_point);
    if (rc < 0)
    {
    LOG_ERR("FAIL: mount external flash: %d", rc);
    return 0;
    }

    rc = fs_statvfs(fs_external_mount_point->mnt_point, &sbuf);
    if (rc < 0)
    {
    LOG_PRINTK("FAIL: statvfs: %d\n", rc);
    return 0;
    }

    LOG_PRINTK("%s: bsize=%lu frsize=%lu blocks=%lu bfree=%lu\n",
    fs_external_mount_point->mnt_point,
    sbuf.f_bsize, sbuf.f_frsize,
    sbuf.f_blocks, sbuf.f_bfree);

    fs_flash_area_erase(fs_external_mount_point);
    little_fs_stress_test1();

    return 0;
    }

  • Hi,

    1. -EFAULT from fs_* here means LFS_ERR_CORRUPT, per Zephyr’s errno mapping — not a literal invalid buffer address from your dummy_data.
    2. The 270 B vs 800 B+ split is very plausibly because 270 B is an inline file (≤ 512 B with 4 KiB blocks and your 4096 cache), while 800 B+ are non-inline CTZ files.
    3. The failure around ~4000 iterations could be interpreted as something going wrong that LittleFS classifies as corruption on the inline-heavy metadata rewrite pattern — that may be real flash/QSPI readback mismatch (verify), marginal timing, power-down (has-dpd), etc., or a structural/metadata edge case. EFAULT-14 LittleFS corruption error mapped to -EFAULT, and the size threshold is consistent with inline vs non-inline file handling given default geometry.

    To confirm on your build, check the mount log line that prints block_size and the rd ; pr ; ca ; la sizes (littlefs_init_cfg logs them): if block_size is 4096, the 512 B inline cutoff applies as above.

    Enabling CONFIG_FS_LOG_LEVEL_DBG (and LittleFS’s own logging if you use it) would narrow whether you’re hitting verify mismatch, tag/CRC issues, or directory tail/cycle detection — still without changing your application test logic.

    Kind regards,
    Andreas

Related