Skip to content

fs/fat: Preserve live chains and cursors across truncation. - #21462

Open
yushuailong wants to merge 2 commits into
apache:masterfrom
yushuailong:codex/fat-truncate-fix
Open

yushuailong wants to merge 2 commits into
apache:masterfrom
yushuailong:codex/fat-truncate-fix

Conversation

@yushuailong

Copy link
Copy Markdown
Contributor

Summary

Fix FAT file truncation leaving retained cluster chains, directory cache contents and traversal cursors inconsistent with the file's new length.

When shrinking a file, the retained tail cluster is currently marked free instead of being terminated with an end-of-chain marker. FAT traversal also reuses the directory cache, and a file that has grown without synchronization may still have an outdated start cluster in its directory entry.

When extending a file, the traversal cursor may not point to the actual EOF. This can affect retained data, zero-filling and subsequent writes, including recovery after an extension runs out of space.

This PR:

  • Uses FAT_EOF for the retained tail cluster in both linked and open-but-unlinked files.
  • Restores the directory sector after FAT traversal and uses the shared start cluster when the directory entry has not been synced.
  • Positions extension at the actual EOF while preserving the caller's file offset.
  • Resets shared traversal cursors after truncation or failed extension and marks changed metadata for synchronization.

The first commit contains the functional fix. The second commit only aligns three assignment lines introduced by the fix; it changes no non-whitespace text.

Impact

  • Applies to FAT12, FAT16 and FAT32 in the common FAT filesystem layer.
  • Fixes retained data, zero-extension, subsequent writes and metadata persistence after truncation.
  • Covers shared file handles and files unlinked while still open.
  • No API, Kconfig or on-disk format changes.
  • No dependency on board-specific changes or the local contiguous cluster I/O optimization.

Testing

Build host: macOS 26.6.2 ARM64, Apple Clang 21.0.0.
Target: sim:ostest.
Build: make completes successfully with LD: nuttx.
Static checks: nxstyle, checkpatch pass.
Runtime smoke test: ./nuttx starts and proceeds through enabled OSTest cases without an assertion in the observed run.

Terminate retained clusters with an EOC marker rather than marking
live file data free. Restore the directory sector after FAT traversal
and use the shared start cluster when the file has not been synced.

Position zero extension at actual EOF and reset shared traversal
cursors after success or failure. Mark changed metadata for sync so
subsequent writes and remounts retain the intended file contents.

Validation: RAM-disk simulator regressions covered FAT12/16/32,
linked and unlinked shrink/extend, caller offsets, shared handles,
remounts and writes after ENOSPC. Upstream failed 66 of 327 checks;
this fix passed all 327 in direct and forced-indirect modes.
The nucleo-h743zi:composite ARM build, image size and style checks
passed. No new hardware runtime testing was performed.

Assisted-by: Codex:GPT-6
Signed-off-by: yushuailong <yyyusl@qq.com>
Align adjacent assignments when positioning the EOF cursor and
preserving the directory sector, following the columnar layout
preferred by the NuttX coding style.

Only whitespace changes. Verified an empty whitespace-insensitive
diff, identical non-whitespace text and passing full-file checkpatch.

Assisted-by: Codex:GPT-6
Signed-off-by: yushuailong <yyyusl@qq.com>
@github-actions github-actions Bot added Area: File System File System issues Size: S The size of the change in this PR is small labels Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

esp32-devkitc

  • ROM: .flash.text -12 B (-0.0%, 129,080 B / 4,194,272 B, total: 3% used)
  • irom0_0_seg: .flash.text -12 B (-0.0%, 94,928 B / 3,342,304 B, total: 3% used)

hifive1-revb

  • flash: .text -4 B (-0.0%, 85,724 B / 4,194,304 B, total: 2% used)

mirtoo

  • kseg0_progmem: .text +44 B (+0.1%, 68,380 B / 131,072 B, total: 52% used)

qemu-armv8a

  • Code: .text.nsh_command -16 B (-0.0%, 360,176 B)

qemu-intel64

  • Code: .text -9 B (-0.0%, 8,693,106 B)

s698pm-dkit

  • Code: .text -32 B (-0.0%, 388,880 B)

stm32-nucleo-f103rb

  • flash: .text -8 B (-0.0%, 34,796 B / 131,072 B, total: 27% used)

No memory changes detected for:


Catch memory regressions in CI with MemBrowse. Free for open source.

@casaroli casaroli left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fat_open() with O_TRUNC (fs_fat32.c:558) still truncates the on-disk start cluster, the stale case this PR fixes in fat_truncate(). Create and write 3 clusters on fd1 with no sync, then open(O_WRONLY | O_TRUNC) on fd2: the free count drops by 3 and the raw FAT has 3 lost clusters.

Fix: when the file is already open, put s_startcluster into direntry before fat_dirtruncate(), as fat_truncate() does now.

The Testing section logs only sim:ostest, which does not mount FAT. The commit message cites 327 RAM-disk checks, but the test and its log are not in the PR.

Fix: add the test program or a link to it, and its log, to the PR body.

Comment thread fs/fat/fs_fat32.c

savepos = filep->f_pos;
filep->f_pos = oldsize;
ret = fat_get_sectors(filep, false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ftruncate() on a full volume returns EIO when the file is empty or ends at a cluster boundary, and ENOSPC when it ends inside a cluster: fat_get_sectors() turns a 0 from fat_extendchain() into -EIO. On a RAM disk with FAT12/16/32: errno 5, 5, 28.

Fix: return -ENOSPC from fat_get_sectors() when fat_extendchain() returns 0 (both calls).

Comment thread fs/fat/fs_fat32.c
}

ff->ff_bflags |= FFBUFF_MODIFIED;
ret = fat_dirextend(fs, ff, length);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A failed extension keeps the clusters it allocated. With 1 free cluster, ftruncate(fd, size + 10 clusters) returns ENOSPC, the free count stays 0, and after a remount the chain has 3 clusters for a size that needs 2. The PR body lists recovery after an extension runs out of space.

Fix: when fat_dirextend() fails, cut the chain back to oldsize.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: File System File System issues Size: S The size of the change in this PR is small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants