Skip to content

fs: POSIX filesystem bug fixes (rename/canonicalize/LINK_MAX) - #19991

Merged
xiaoxiang781216 merged 9 commits into
apache:masterfrom
Kaben123:pse52-04-fixes
Aug 28, 2026
Merged

fs: POSIX filesystem bug fixes (rename/canonicalize/LINK_MAX)#19991
xiaoxiang781216 merged 9 commits into
apache:masterfrom
Kaben123:pse52-04-fixes

Conversation

@Kaben123

Copy link
Copy Markdown

Summary

This PR contains 8 bug-fix commits for the NuttX POSIX filesystem layer:

  1. fs/smartfs: change fs_heap to lib_tempbuffer (prerequisite for tcp_conn: Check if the remote address is unspecified #5)
  2. fs/inode: fix off-by-one in _inode_checkpath NAME_MAX check
  3. fs/vfs/rename: fix rename to same file and rename to subdirectory
  4. fs/inode: canonicalize path before inode search to fix .. resolution
  5. fs: remove redundant .. handling in FS layers after VFS canonicalization
  6. fs/inode: fix relative-path truncation causing wrong EISDIR
  7. fs/vfs: fix link() returning EXDEV instead of ENAMETOOLONG
  8. libc/limits: increase LINK_MAX to 128 and fix pathconf

Dependencies

This PR depends on:

Depends-On: #19990

Testing

  • Build verified: sim:nsh with FS_HOSTFS + FS_LINKS + FS_SMARTFS, zero errors/warnings
  • Integration tested on top of PR#1 + PR#2 + PR#3 combined baseline

Impact

  • Fixes POSIX rename semantics (same-file via hardlink, subdirectory detection)
  • Fixes path canonicalization for .. resolution across mount boundaries
  • Fixes link() error code confusion (EXDEV vs ENAMETOOLONG)
  • Removes ~360 lines of redundant .. handling now handled by VFS layer
  • Raises LINK_MAX from 8 to 128 to match modern POSIX expectations

@github-actions github-actions Bot added Area: File System File System issues Size: L The size of the change in this PR is large labels Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

MemBrowse Memory Report

arduino-mega2560

  • flash: .text +226 B (+0.3%, 65,086 B / 262,144 B, total: 25% used)

esp32-devkitc

  • ROM: .flash.text +200 B (+0.2%, 124,868 B / 4,194,272 B, total: 3% used)
  • irom0_0_seg: .flash.text +200 B (+0.2%, 89,072 B / 3,342,304 B, total: 3% used)

hifive1-revb

  • flash: .text +132 B (+0.2%, 83,716 B / 4,194,304 B, total: 2% used)

mirtoo

  • kseg0_progmem: .text +212 B (+0.3%, 67,744 B / 131,072 B, total: 52% used)

qemu-armv8a

  • Code: .text.hostfs_mkpath.constprop.0 -152 B, .text.inode_search +256 B, .text.rename +84 B (+0.0%, 319,504 B)

qemu-intel64

  • Code: .text +220 B (+0.0%, 8,659,714 B)

rx65n-rsk2mb

  • ROM: .text +192 B (+0.2%, 86,880 B / 2,097,152 B, total: 4% used)

s698pm-dkit

  • Code: .text +672 B (+0.2%, 365,168 B)

stm32-nucleo-f103rb

  • flash: .text +124 B (+0.4%, 34,400 B / 131,072 B, total: 26% used)

@Kaben123
Kaben123 force-pushed the pse52-04-fixes branch 2 times, most recently from 488daee to f73a936 Compare August 28, 2026 07:47
zhaoxingyu12 and others added 9 commits August 28, 2026 15:53
Replace fs_heap_malloc/free with lib_get_tempbuffer/lib_put_tempbuffer
in smartfs_finddirentry(). This aligns smartfs with the common VFS
tempbuffer allocation pattern and is a prerequisite for the
canonicalization cleanup that removes redundant ".." handling from
individual filesystem layers.

Signed-off-by: zhaoxingyu1 <zhaoxingyu1@xiaomi.com>
The loop condition 'namelen <= NAME_MAX' allowed filenames of
NAME_MAX+1 characters to pass validation. When the filename segment
reached exactly NAME_MAX+1 chars and was at the end of the path
string, the loop exited due to *path == '\0' and returned OK instead
of -ENAMETOOLONG.

Fix by moving the NAME_MAX check inside the loop body with an
immediate return on violation. Also fix the post-loop return to
explicitly check pathlen >= PATH_MAX instead of relying on *path
which conflated the two exit conditions.

Before: creat() with 97-char filename (NAME_MAX=96) succeeded
After:  creat() with 97-char filename correctly returns ENAMETOOLONG

Signed-off-by: yukangzhi <yukangzhi@xiaomi.com>
Fix two POSIX compliance issues in mountptrename():

1. When old and new are hard links to the same file (same st_dev and
   st_ino), POSIX requires rename() to succeed without removing either
   link. Previously, NuttX would unlink(new) then rename(old, new),
   effectively losing one link. Fix by comparing inode identity before
   any destructive operation.

2. When new is a subdirectory of old (e.g., rename('a', 'a/b')), POSIX
   requires EINVAL. Previously, NuttX would rmdir(new) first, then the
   filesystem's rename() would fail -- but new was already deleted,
   causing data loss. Fix by detecting the subdirectory relationship
   (newrelpath starts with oldrelpath + '/') before any rmdir/unlink.

Signed-off-by: yukangzhi <yukangzhi@xiaomi.com>
Add _inode_canonicalize() to remove '.' and '..' segments from the
absolute path before the inode tree traversal begins. This fixes the
case where paths containing '..' that resolve back to a mountpoint
root (e.g., /tmp/subdir/..) were not being handed to the filesystem.

Previously, _compute_path_depth() returned 0 for such paths, causing
the VFS to skip the mountpoint and attempt to find 'subdir' in the
pseudo filesystem -- which fails with ENOTDIR.

With canonicalization, /tmp/subdir/.. becomes /tmp before the search,
so the mountpoint is correctly matched. This fixes chdir('..'),
stat('../..'), opendir('../..'), and similar operations from within
mountpoint subdirectories.

The implementation uses an in-place two-pointer algorithm with no
additional stack allocation, safe for NuttX's small kernel stacks.

Signed-off-by: yukangzhi <yukangzhi@xiaomi.com>
Since _inode_canonicalize() now resolves all "." and ".." segments
in the common VFS layer before inode search, the relpath passed to
each filesystem will never contain ".." segments.  Remove the
now-dead ".." handling code from individual filesystem layers and
the inode search internals.

Files modified (redundant ".." path resolution removed):
- fs/hostfs/hostfs.c: remove depth-tracking escape check in
  hostfs_mkpath(), simplify to direct path concatenation.
- fs/rpmsgfs/rpmsgfs.c: same as hostfs, remove depth-tracking in
  rpmsgfs_mkpath().
- fs/smartfs/smartfs_utils.c: remove "." and ".." segment checks
  in smartfs_finddirentry(), de-indent the remaining search logic.
- fs/inode/fs_inodesearch.c: remove _inode_isdotdot() function,
  simplify _compute_path_depth() to only count forward segments,
  remove dead else-if branch in _inode_search().

Files NOT modified (and why):
- fs/littlefs/littlefs/lfs.c: third-party upstream library (git
  submodule), must not be modified locally.
- fs/fatfs/fatfs/source/ff.c: third-party upstream library.
- fs/lwext4/lwext4/src/ext4*.c: third-party upstream library.
- fs/cromfs/fs_cromfs.c: handles "." and ".." as directory entries
  (structural, not path resolution), so its code stays.
- fs/vfs/fs_symlink.c: constructs relative paths containing ".."
  (writes, not parses relpath).

Signed-off-by: yukangzhi <yukangzhi@xiaomi.com>
Root cause: _inode_search() built the absolute form of a relative
path with snprintf(buf, PATH_MAX, "%s/%s", cwd, path), silently
truncating it when cwd + "/" + path exceeded PATH_MAX. The truncated
buffer was then handed to _inode_canonicalize(), which collapsed
".." segments against the wrong cut-off suffix. A valid relative
path of PATH_MAX-1 bytes (legal per pathconf(_PC_PATH_MAX)) could
thus collapse onto a directory and open() returned EISDIR instead
of resolving the file.

Fix: size the temp buffer to hold the full uncanonicalized
"<cwd>/<path>" form so canonicalization sees the complete path.
lib_get_tempbuffer falls back to a malloc'd buffer when the size
exceeds PATH_MAX (CONFIG_LIBC_TEMPBUFFER_MALLOC). The existing
PATH_MAX check in _inode_canonicalize() still rejects any
canonicalized result that is too long, so ENAMETOOLONG semantics
are preserved.

Signed-off-by: dengwenqi <dengwenqi@xiaomi.com>
When inode_find() for path2 fails due to ENAMETOOLONG (or ELOOP),
the else branch incorrectly falls through to the EXDEV check based
on whether target is a mountpoint.  This causes link() to report
EXDEV for overly long path2, violating POSIX which requires
ENAMETOOLONG in this case.

Fix by propagating the original inode_find() error code when it is
not ENOENT or ENOTDIR (i.e., not a simple "path does not exist"
condition).

Signed-off-by: yukangzhi <yukangzhi@xiaomi.com>
Increase LINK_MAX from _POSIX_LINK_MAX (8) to 128 to allow
directories to have a reasonable number of subdirectories while
still enforcing a hard link limit.

Also fix pathconf(_PC_LINK_MAX) to return the actual LINK_MAX
value instead of the minimum _POSIX_LINK_MAX.

Signed-off-by: yukangzhi <yukangzhi@xiaomi.com>
Fix coding style violations detected by CI whole-file nxstyle scan:
- fs/smartfs/smartfs_utils.c: add missing braces after if (L334),
  fix bad alignment (L414), fix switch brace alignment (L1531)
- fs/hostfs/hostfs.c: add blank line after declaration (L576)

These are pre-existing style issues in master, not introduced by
this PR, but reported because CI checks the entire touched file.

Signed-off-by: yukangzhi <yukangzhi@xiaomi.com>
@github-actions

Copy link
Copy Markdown

🔗 Cross-repo PR dependencies

The read-only Build run reported the following dependent PR(s) and fetched head SHA(s):

CI run: https://github.com/apache/nuttx/actions/runs/33153204810

@acassis

acassis commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@Kaben123 please verify why this PR is increasing from +124 to +672 in some boards

@Kaben123

Kaben123 commented Aug 28, 2026

Copy link
Copy Markdown
Author

@Kaben123 please verify why this PR is increasing from +124 to +672 in some boards

Thanks @acassis. The variance comes from per-board Kconfig, not from bloat in the patch:

  • s698pm-dkit/nsh enables CONFIG_DEBUG_ASSERTIONS=y + CONFIG_DEBUG_FEATURES=y; stm32-nucleo-f103rb/nsh enables neither. The touched functions (_inode_search, _inode_checkpath, _inode_canonicalize, rename, link) contain several DEBUGASSERT()s — each expands to a call + panic string on s698pm and to nothing on f103rb.
  • s698pm also lacks CONFIG_DEBUG_FULLOPT=y (→ -O0) while f103rb has it (→ -Os), and SPARC is fixed 4-byte insns vs Thumb-2's ~2-byte average. Compounded: 1.2 × 2.5 × 1.7 ≈ 5×, matching 672 / 124 ≈ 5.4×.

MemBrowse per-symbol data confirms the growth is confined to the functions this PR touches (e.g. qemu-armv8a: .text.inode_search +256, .text.rename +84) — no unexpected symbols, no duplicated inlining.

@acassis

acassis commented Aug 28, 2026

Copy link
Copy Markdown
Contributor
  • 124

Thank you, the real increase is +124, right?

@xiaoxiang781216
xiaoxiang781216 merged commit 72e0b29 into apache:master Aug 28, 2026
53 checks passed
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: L The size of the change in this PR is large

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants