From 94a6d8143e22152509dce0213d95c97b55cb52cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Mon, 6 Jul 2026 13:37:28 +0200 Subject: [PATCH 1/6] fix(storage/fatfs): fix exFAT mount integer overflow (CVE-2026-6682) The exFAT mount path validates that the media is large enough to hold the declared cluster heap with: if (maxlba < (QWORD)fs->database + ncl * fs->csize) ... `ncl` (DWORD, up to MAX_EXFAT) and `fs->csize` (WORD) are both promoted to `unsigned int`, so `ncl * fs->csize` is evaluated in 32-bit arithmetic and can wrap before the QWORD promotion of the sum. A crafted image with a large NumClusters/SecPerClus can therefore make an undersized volume pass the "size is large enough" check; subsequent cluster->sector math then addresses media outside the actual device. Promote the multiply to 64-bit ((QWORD)ncl * fs->csize). Apply the same promotion to the bitmap-base computation ((LBA_t)fs->csize * (bcl - 2)), which has the identical overflow shape. Record the CVE in the component SBOM. Reference: https://www.runzero.com/blog/fatfs-bugs/ --- components/fatfs/sbom.yml | 3 +++ components/fatfs/src/ff.c | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components/fatfs/sbom.yml b/components/fatfs/sbom.yml index d9e037070cb..c82d5ba5d94 100644 --- a/components/fatfs/sbom.yml +++ b/components/fatfs/sbom.yml @@ -3,3 +3,6 @@ version: 'R0.15' supplier: 'Organization: Espressif Systems (Shanghai) CO LTD' originator: 'Person: ChaN' description: 'Generic FAT Filesystem Module for embedded systems.' +cve-exclude-list: + - cve: CVE-2026-6682 + reason: Integer overflow in exFAT mount size validation. Patched by promoting the cluster-heap and bitmap-base multiplies to 64-bit in mount_volume(). diff --git a/components/fatfs/src/ff.c b/components/fatfs/src/ff.c index 1113211b429..79c3f282d31 100644 --- a/components/fatfs/src/ff.c +++ b/components/fatfs/src/ff.c @@ -3489,7 +3489,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->volbase = bsect; fs->database = bsect + ld_dword(fs->win + BPB_DataOfsEx); fs->fatbase = bsect + ld_dword(fs->win + BPB_FatOfsEx); - if (maxlba < (QWORD)fs->database + nclst * fs->csize) return FR_NO_FILESYSTEM; /* (Volume size must not be smaller than the size required) */ + if (maxlba < (QWORD)fs->database + (QWORD)nclst * fs->csize) return FR_NO_FILESYSTEM; /* CVE-2026-6682: promote to 64-bit before multiply to avoid integer overflow that would accept an undersized volume */ fs->dirbase = ld_dword(fs->win + BPB_RootClusEx); /* Get bitmap location and check if it is contiguous (implementation assumption) */ @@ -3505,7 +3505,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ } bcl = ld_dword(fs->win + i + 20); /* Bitmap cluster */ if (bcl < 2 || bcl >= fs->n_fatent) return FR_NO_FILESYSTEM; /* (Wrong cluster#) */ - fs->bitbase = fs->database + fs->csize * (bcl - 2); /* Bitmap sector */ + fs->bitbase = fs->database + (LBA_t)fs->csize * (bcl - 2); /* Bitmap sector (CVE-2026-6682: 64-bit multiply to avoid overflow) */ for (;;) { /* Check if bitmap is contiguous */ if (move_window(fs, fs->fatbase + bcl / (SS(fs) / 4)) != FR_OK) return FR_DISK_ERR; cv = ld_dword(fs->win + bcl % (SS(fs) / 4) * 4); From 0efd85ffea549693c7bb08619455c879a6faccb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Mon, 6 Jul 2026 13:37:59 +0200 Subject: [PATCH 2/6] fix(storage/fatfs): reject empty exFAT cluster heap (CVE-2026-6683) CVE-2026-6683 is an exFAT divide-by-zero: with NumClusters == 0 the filesystem object has fs->n_fatent == 2, and the exFAT "percent in use" update in sync_fs() computes ... * 100 / (fs->n_fatent - 2) -> division by zero. That vulnerable exFAT PercInUse sync path was introduced in FatFs R0.16 and is NOT present in this R0.15 release, so the divide-by-zero itself is not reachable here. As defense-in-depth (and to keep parity with newer releases) reject an empty exFAT cluster heap at mount time, which is a malformed volume regardless. Record the CVE disposition in the component SBOM. Reference: https://www.runzero.com/blog/fatfs-bugs/ --- components/fatfs/sbom.yml | 2 ++ components/fatfs/src/ff.c | 1 + 2 files changed, 3 insertions(+) diff --git a/components/fatfs/sbom.yml b/components/fatfs/sbom.yml index c82d5ba5d94..d69a672ce6c 100644 --- a/components/fatfs/sbom.yml +++ b/components/fatfs/sbom.yml @@ -6,3 +6,5 @@ description: 'Generic FAT Filesystem Module for embedded systems.' cve-exclude-list: - cve: CVE-2026-6682 reason: Integer overflow in exFAT mount size validation. Patched by promoting the cluster-heap and bitmap-base multiplies to 64-bit in mount_volume(). + - cve: CVE-2026-6683 + reason: exFAT divide-by-zero when NumClusters == 0. The vulnerable exFAT PercInUse sync division was introduced in R0.16 and is not present in this R0.15 release; an empty cluster heap is additionally rejected at mount as defense-in-depth. diff --git a/components/fatfs/src/ff.c b/components/fatfs/src/ff.c index 79c3f282d31..3ae06c2b999 100644 --- a/components/fatfs/src/ff.c +++ b/components/fatfs/src/ff.c @@ -3483,6 +3483,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ nclst = ld_dword(fs->win + BPB_NumClusEx); /* Number of clusters */ if (nclst > MAX_EXFAT) return FR_NO_FILESYSTEM; /* (Too many clusters) */ + if (nclst == 0) return FR_NO_FILESYSTEM; /* CVE-2026-6683: reject empty exFAT cluster heap (malformed zero-data-cluster volume; the divide-by-(n_fatent-2) sink itself was introduced in R0.16) */ fs->n_fatent = nclst + 2; /* Boundaries and Limits */ From 362fd7380e204b3c0bf987fa47635f45339dc5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Mon, 6 Jul 2026 13:38:29 +0200 Subject: [PATCH 3/6] fix(storage/fatfs): guard dirty-cache refill against unsigned LBA wrap (CVE-2026-6685) After a direct multi-sector disk_read()/disk_write(), FatFs decides whether the cached sector overlaps the direct-I/O range with: fp->sect - sect < cc (and the FF_FS_TINY variant fs->winsect - sect < cc) `sect`, `fp->sect` and `fs->winsect` are unsigned LBA_t. On 32-bit LBA_t builds, if the cached sector is below `sect`, the subtraction wraps to a huge value that can still compare `< cc`, so the code computes a bogus large offset: - in f_write() it mis-copies from the direct write buffer (data corruption); - in f_read() it is worse: memcpy(rbuff + (wrapped_offset * SS), ...) is an out-of-bounds WRITE into the caller-supplied read buffer. Add an explicit lower-bound check (fp->sect >= sect, resp. fs->winsect >= sect) before the range test on both the read and write paths and both the FF_FS_TINY and normal variants, so the condition is exactly "cached sector lies within [sect, sect + cc)". Record the CVE in the component SBOM. Reference: https://www.runzero.com/blog/fatfs-bugs/ --- components/fatfs/sbom.yml | 2 ++ components/fatfs/src/ff.c | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/components/fatfs/sbom.yml b/components/fatfs/sbom.yml index d69a672ce6c..6c994059fbb 100644 --- a/components/fatfs/sbom.yml +++ b/components/fatfs/sbom.yml @@ -8,3 +8,5 @@ cve-exclude-list: reason: Integer overflow in exFAT mount size validation. Patched by promoting the cluster-heap and bitmap-base multiplies to 64-bit in mount_volume(). - cve: CVE-2026-6683 reason: exFAT divide-by-zero when NumClusters == 0. The vulnerable exFAT PercInUse sync division was introduced in R0.16 and is not present in this R0.15 release; an empty cluster heap is additionally rejected at mount as defense-in-depth. + - cve: CVE-2026-6685 + reason: Unsigned-subtraction wrap in the dirty-cache refill check. Patched by requiring the cached sector to lie within the direct-I/O range in both f_write() and f_read(). diff --git a/components/fatfs/src/ff.c b/components/fatfs/src/ff.c index 3ae06c2b999..77a88e18fde 100644 --- a/components/fatfs/src/ff.c +++ b/components/fatfs/src/ff.c @@ -4004,11 +4004,11 @@ FRESULT f_read ( if (disk_read(fs->pdrv, rbuff, sect, cc) != RES_OK) ABORT(fs, FR_DISK_ERR); #if !FF_FS_READONLY && FF_FS_MINIMIZE <= 2 /* Replace one of the read sectors with cached data if it contains a dirty sector */ #if FF_FS_TINY - if (fs->wflag && fs->winsect - sect < cc) { + if (fs->wflag && fs->winsect >= sect && fs->winsect - sect < cc) { /* CVE-2026-6685: guard against unsigned wrap when winsect < sect (mis-offset would be an OOB write into rbuff) */ memcpy(rbuff + ((fs->winsect - sect) * SS(fs)), fs->win, SS(fs)); } #else - if ((fp->flag & FA_DIRTY) && fp->sect - sect < cc) { + if ((fp->flag & FA_DIRTY) && fp->sect >= sect && fp->sect - sect < cc) { /* CVE-2026-6685: guard against unsigned wrap when fp->sect < sect (mis-offset would be an OOB write into rbuff) */ memcpy(rbuff + ((fp->sect - sect) * SS(fs)), fp->buf, SS(fs)); } #endif @@ -4119,12 +4119,12 @@ FRESULT f_write ( if (disk_write(fs->pdrv, wbuff, sect, cc) != RES_OK) ABORT(fs, FR_DISK_ERR); #if FF_FS_MINIMIZE <= 2 #if FF_FS_TINY - if (fs->winsect - sect < cc) { /* Refill sector cache if it gets invalidated by the direct write */ + if (fs->winsect >= sect && fs->winsect - sect < cc) { /* Refill sector cache if it gets invalidated by the direct write (CVE-2026-6685: guard against unsigned wrap when winsect < sect) */ memcpy(fs->win, wbuff + ((fs->winsect - sect) * SS(fs)), SS(fs)); fs->wflag = 0; } #else - if (fp->sect - sect < cc) { /* Refill sector cache if it gets invalidated by the direct write */ + if (fp->sect >= sect && fp->sect - sect < cc) { /* Refill sector cache if it gets invalidated by the direct write (CVE-2026-6685: guard against unsigned wrap when fp->sect < sect) */ memcpy(fp->buf, wbuff + ((fp->sect - sect) * SS(fs)), SS(fs)); fp->flag &= (BYTE)~FA_DIRTY; } From 1cd2874952d8154477fc9e0cfc53d6aa6e43f5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Mon, 6 Jul 2026 13:38:59 +0200 Subject: [PATCH 4/6] fix(storage/fatfs): clamp exFAT volume-label length in f_getlabel() (CVE-2026-6687) f_getlabel() extracts the exFAT volume label with a loop bounded by the on-disk byte dj.dir[XDIR_NumLabel] (0-255): for (si = di = hs = 0; si < dj.dir[XDIR_NumLabel]; si++) wc = ld_16(dj.dir + XDIR_Label + si * 2); The exFAT label field holds at most 11 UTF-16 units (22 bytes). A crafted directory entry with a larger count both reads past the 22-byte label field and, through put_utf(... &label[di], 4), writes past the end of the caller-provided label buffer (the canonical API examples use small fixed stack buffers) -> stack buffer overflow. Clamp the character count to the exFAT maximum of 11 before the extraction loop. Record the CVE in the component SBOM. Note: f_getlabel() takes no destination-buffer size, so under UTF-8 output (FF_LFN_UNICODE == 2) 11 units can still expand to up to 34 bytes; the clamp downgrades this from attacker-unbounded to spec-bounded. ESP-IDF's VFS layer does not call f_getlabel(); direct callers on untrusted media should size their buffer accordingly. A complete fix requires an upstream size-aware API change. Reference: https://www.runzero.com/blog/fatfs-bugs/ --- components/fatfs/sbom.yml | 2 ++ components/fatfs/src/ff.c | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/fatfs/sbom.yml b/components/fatfs/sbom.yml index 6c994059fbb..6a11a9eddda 100644 --- a/components/fatfs/sbom.yml +++ b/components/fatfs/sbom.yml @@ -10,3 +10,5 @@ cve-exclude-list: reason: exFAT divide-by-zero when NumClusters == 0. The vulnerable exFAT PercInUse sync division was introduced in R0.16 and is not present in this R0.15 release; an empty cluster heap is additionally rejected at mount as defense-in-depth. - cve: CVE-2026-6685 reason: Unsigned-subtraction wrap in the dirty-cache refill check. Patched by requiring the cached sector to lie within the direct-I/O range in both f_write() and f_read(). + - cve: CVE-2026-6687 + reason: exFAT label-length overflow in f_getlabel(). Patched by clamping XDIR_NumLabel to the 11-unit exFAT label maximum. diff --git a/components/fatfs/src/ff.c b/components/fatfs/src/ff.c index 77a88e18fde..9d1a2ab5b2d 100644 --- a/components/fatfs/src/ff.c +++ b/components/fatfs/src/ff.c @@ -5411,9 +5411,11 @@ FRESULT f_getlabel ( #if FF_FS_EXFAT if (fs->fs_type == FS_EXFAT) { WCHAR hs; - UINT nw; + UINT nw, nchar; - for (si = di = hs = 0; si < dj.dir[XDIR_NumLabel]; si++) { /* Extract volume label from 83 entry */ + nchar = dj.dir[XDIR_NumLabel]; /* Number of UTF-16 characters in the label entry */ + if (nchar > 11) nchar = 11; /* CVE-2026-6687: clamp to the exFAT maximum (11) to prevent OOB read of the entry and overflow of the caller label buffer */ + for (si = di = hs = 0; si < nchar; si++) { /* Extract volume label from 83 entry */ wc = ld_word(dj.dir + XDIR_Label + si * 2); if (hs == 0 && IsSurrogate(wc)) { /* Is the code a surrogate? */ hs = wc; continue; From 655ff3269a60932ca20dafc8774db5ef1fbac315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Mon, 6 Jul 2026 13:39:24 +0200 Subject: [PATCH 5/6] fix(storage/fatfs): record non-applicable runZero 2026 CVEs in SBOM Document the three runZero "Seven FatFs bugs" CVEs that require no source change in this component, so vulnerability scanners have their disposition: - CVE-2026-6684: GPT partition-scan loop DoS. Already fixed upstream in R0.16, where test_gpt_header() caps the partition-entry count at 128. - CVE-2026-6686: read of uninitialized clusters after f_lseek() past EOF. Longstanding, behavioral; not a memory-safety defect and zero-filling every extended cluster is prohibitively costly on flash. - CVE-2026-6688: long-filename overflow in downstream callers. Not exposed in ESP-IDF; vfs_fat.c uses bounded copies and fname is bounded by FF_MAX_LFN. Reference: https://www.runzero.com/blog/fatfs-bugs/ --- components/fatfs/sbom.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/fatfs/sbom.yml b/components/fatfs/sbom.yml index 6a11a9eddda..e53b73b46ec 100644 --- a/components/fatfs/sbom.yml +++ b/components/fatfs/sbom.yml @@ -12,3 +12,9 @@ cve-exclude-list: reason: Unsigned-subtraction wrap in the dirty-cache refill check. Patched by requiring the cached sector to lie within the direct-I/O range in both f_write() and f_read(). - cve: CVE-2026-6687 reason: exFAT label-length overflow in f_getlabel(). Patched by clamping XDIR_NumLabel to the 11-unit exFAT label maximum. + - cve: CVE-2026-6684 + reason: GPT partition-scan loop DoS, classified by runZero as pre-R0.16 because the protective GPT validation was added upstream in R0.16. This release does not ship the R0.16 baseline; it ships FatFs R0.15 with Espressif patch2, which already backports that validation. test_gpt_header() checks the GPT header size, CRC and entry size and rejects any partition table declaring more than 128 entries before the scan loop runs, so the unbounded-scan condition is unreachable. An unpatched upstream R0.15 (without patch2) would remain vulnerable. + - cve: CVE-2026-6686 + reason: Read of uninitialized clusters after f_lseek() extends a file past EOF. Longstanding FatFs behavior; not a memory-safety defect and zero-filling every extended cluster is prohibitively costly on flash. Applications sharing media across trust boundaries must zero regions before exposing them. + - cve: CVE-2026-6688 + reason: Long-filename overflow in downstream callers copying FILINFO.fname. Not exposed in ESP-IDF; vfs_fat.c uses bounded snprintf/strlcpy and fname is bounded in-library by FF_MAX_LFN. From 9be9b3e60b536e179299cea6fe09eb06dcac47d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Rohl=C3=ADnek?= Date: Tue, 7 Jul 2026 10:34:31 +0200 Subject: [PATCH 6/6] fix(storage/fatfs): fix FAT32 mount integer overflow (CVE-2026-6682) The initial CVE-2026-6682 fix hardened the exFAT mount path, but the CVE as reported by runZero is a FAT32 defect in mount_volume() and is reachable in the default configuration (exFAT and 64-bit LBA disabled). This corrects the fix. Root cause: `fasize *= fs->n_fats` is a DWORD multiply with no overflow guard. A crafted BPB_FATSz32 such as 0x80000001 with NumFATs=2 wraps `fasize` to 0x00000002. The wrapped (too-small) FAT size places `fs->database` inside the FAT region, so a forged directory entry yields an attacker-controlled `finfo.fsize`; a caller using it as a read length overflows its buffer with attacker-controlled bytes (CVSS 7.6). Fix: reject per-FAT and reserved+FAT+root system-area sizes that overflow DWORD before they are used to derive the data-area base. The exFAT cluster-heap/bitmap 64-bit promotions are retained as defense-in-depth and relabeled (they are not CVE-2026-6682). SBOM reason updated. --- components/fatfs/sbom.yml | 2 +- components/fatfs/src/ff.c | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/components/fatfs/sbom.yml b/components/fatfs/sbom.yml index e53b73b46ec..d0ae5b6b4f2 100644 --- a/components/fatfs/sbom.yml +++ b/components/fatfs/sbom.yml @@ -5,7 +5,7 @@ originator: 'Person: ChaN' description: 'Generic FAT Filesystem Module for embedded systems.' cve-exclude-list: - cve: CVE-2026-6682 - reason: Integer overflow in exFAT mount size validation. Patched by promoting the cluster-heap and bitmap-base multiplies to 64-bit in mount_volume(). + reason: FAT32 integer overflow in mount_volume(). Patched by rejecting per-FAT and system-area sizes that overflow the 32-bit multiply before use; the exFAT cluster-heap/bitmap multiplies were additionally widened to 64-bit as defense-in-depth. - cve: CVE-2026-6683 reason: exFAT divide-by-zero when NumClusters == 0. The vulnerable exFAT PercInUse sync division was introduced in R0.16 and is not present in this R0.15 release; an empty cluster heap is additionally rejected at mount as defense-in-depth. - cve: CVE-2026-6685 diff --git a/components/fatfs/src/ff.c b/components/fatfs/src/ff.c index 9d1a2ab5b2d..87ecc07597d 100644 --- a/components/fatfs/src/ff.c +++ b/components/fatfs/src/ff.c @@ -3490,7 +3490,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->volbase = bsect; fs->database = bsect + ld_dword(fs->win + BPB_DataOfsEx); fs->fatbase = bsect + ld_dword(fs->win + BPB_FatOfsEx); - if (maxlba < (QWORD)fs->database + (QWORD)nclst * fs->csize) return FR_NO_FILESYSTEM; /* CVE-2026-6682: promote to 64-bit before multiply to avoid integer overflow that would accept an undersized volume */ + if (maxlba < (QWORD)fs->database + (QWORD)nclst * fs->csize) return FR_NO_FILESYSTEM; /* exFAT mount hardening (defense-in-depth): promote to 64-bit before multiply to avoid integer overflow that would accept an undersized volume */ fs->dirbase = ld_dword(fs->win + BPB_RootClusEx); /* Get bitmap location and check if it is contiguous (implementation assumption) */ @@ -3506,7 +3506,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ } bcl = ld_dword(fs->win + i + 20); /* Bitmap cluster */ if (bcl < 2 || bcl >= fs->n_fatent) return FR_NO_FILESYSTEM; /* (Wrong cluster#) */ - fs->bitbase = fs->database + (LBA_t)fs->csize * (bcl - 2); /* Bitmap sector (CVE-2026-6682: 64-bit multiply to avoid overflow) */ + fs->bitbase = fs->database + (LBA_t)fs->csize * (bcl - 2); /* Bitmap sector (exFAT mount hardening: 64-bit multiply to avoid overflow) */ for (;;) { /* Check if bitmap is contiguous */ if (move_window(fs, fs->fatbase + bcl / (SS(fs) / 4)) != FR_OK) return FR_DISK_ERR; cv = ld_dword(fs->win + bcl % (SS(fs) / 4) * 4); @@ -3529,6 +3529,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ fs->n_fats = fs->win[BPB_NumFATs]; /* Number of FATs */ if (fs->n_fats != 1 && fs->n_fats != 2) return FR_NO_FILESYSTEM; /* (Must be 1 or 2) */ + if (fs->n_fats == 2 && fasize > 0xFFFFFFFF / 2) return FR_NO_FILESYSTEM; /* CVE-2026-6682: reject a per-FAT size that overflows DWORD when multiplied by the FAT count; a wrapped (too-small) fasize would move the data area into the FAT region and let a crafted image forge a directory entry with an attacker-controlled file size */ fasize *= fs->n_fats; /* Number of sectors for FAT area */ fs->csize = fs->win[BPB_SecPerClus]; /* Cluster size */ @@ -3545,6 +3546,7 @@ static FRESULT mount_volume ( /* FR_OK(0): successful, !=0: an error occurred */ /* Determine the FAT sub type */ sysect = nrsv + fasize + fs->n_rootdir / (SS(fs) / SZDIRE); /* RSV + FAT + FF_DIR */ + if (sysect < fasize) return FR_NO_FILESYSTEM; /* CVE-2026-6682: reject reserved+FAT+root system-area size that overflows DWORD (same data-area displacement as the FAT-count overflow above) */ if (tsect < sysect) return FR_NO_FILESYSTEM; /* (Invalid volume size) */ nclst = (tsect - sysect) / fs->csize; /* Number of clusters */ if (nclst == 0) return FR_NO_FILESYSTEM; /* (Invalid volume size) */