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/
This commit is contained in:
Tomáš Rohlínek
2026-07-06 13:38:29 +02:00
parent 895de2abee
commit e6e7c9f4bc
2 changed files with 6 additions and 4 deletions

View File

@@ -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. Patched by rejecting an empty cluster heap at mount and guarding the divisor (n_fatent > 2) in sync_fs().
- 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().

View File

@@ -4084,11 +4084,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
@@ -4199,12 +4199,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;
}