The check that rejects moving a directory into its own subtree was tied to
CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION, so a build that only wanted
POSIX replacement semantics got the guard as a side effect, and the default
build kept the exposure the guard exists for: f_rename() does not detect the
case and links the directory into its own tree, after which the directory is
reachable only from inside itself and the volume is corrupt. The two
behaviours are unrelated, so give the guard its own option,
CONFIG_FATFS_VFS_RENAME_REJECTS_SELF_NESTING (default n), and let either be
enabled without the other.
Resolve the destination by start cluster rather than comparing path bytes.
FatFs matches names through directory entries, so a destination spelled as an
8.3 alias, or differing only in the case of a non-ASCII character, denotes the
same directory as the source and slipped past the previous string prefix
check, which folded ASCII case only. Long file names are enabled by default,
so the aliases exist in the default configuration. The walk runs only when the
source is a directory, leaving a file rename one directory open that fails
immediately.
Fold the two conditional variants of vfs_fat_rename() into a single
implementation. The variant guarded by the replace option had lost the stat
cache invalidation the unguarded one performs, so a readdir-cached entry could
answer a later stat() for a path that had just been renamed; the invalidation
now applies to every configuration.
Co-authored-by: Cursor <cursoragent@cursor.com>
POSIX rename() silently replaces the destination when it already exists,
while FatFs' f_rename() refuses with FR_EXIST. rename() on a FAT mount
therefore fails with EEXIST where the same call succeeds on other file
systems, and every caller that wants portable behaviour has to remove the
destination itself.
Add CONFIG_FATFS_VFS_RENAME_REPLACES_DESTINATION (default n, so existing
behaviour is unchanged) to emulate the POSIX semantics: when f_rename()
reports FR_EXIST, remove the destination and retry, all under the lock
already held for the rename so no other VFS caller observes the gap.
The POSIX rules on what may replace what are applied before anything is
removed, because f_unlink() deletes empty directories as readily as
files and would otherwise discard a directory to make way for a file:
renaming a file onto a directory fails with EISDIR, a directory onto a
file with ENOTDIR, and a directory onto a non-empty directory with
ENOTEMPTY. Without the option all of these keep failing with EEXIST.
Moving a directory into its own subtree is rejected with EINVAL as well.
f_rename() does not check for this and links the directory into its own
tree, losing its contents, so this is a correctness fix rather than a
matter of which error is reported.
Renaming an entry to itself needs no special handling: f_rename() reports
FR_EXIST only when the destination resolves to a different directory
entry, so a self-rename, including one written with a different spelling
of the same name, already succeeds.
The emulation cannot be atomic, nor can it honour the POSIX guarantee
that a failed rename leaves an instance of the destination in place: FAT
cannot replace a directory entry in one step, so an interruption between
removing the destination and completing the rename can leave neither
name. This is documented in the option's help text.
F_SETFL was replacing the whole flags word, so fcntl(fd, F_SETFL, O_APPEND)
made F_GETFL report O_RDONLY|O_APPEND. Keep O_ACCMODE and apply only POSIX
status flags.
Co-authored-by: Cursor <cursoragent@cursor.com>