* src/chmod.c (process_file): Fall back to the racy behavior
on older systems without /proc.
* tests/chmod/symlink-enotsup.sh: Add a test case where
only the first chmod fails.
* NEWS: Adjust the NEWS accordingly.
---
NEWS | 3 ++-
src/chmod.c | 6 +++++-
tests/chmod/symlink-enotsup.sh | 13 +++++++++++++
3 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/NEWS b/NEWS
index 3f4d4184d..ce8076938 100644
--- a/NEWS
+++ b/NEWS
@@ -17,7 +17,8 @@ GNU coreutils NEWS -*-
outline -*-
'chmod -R' and 'chmod -h' no longer silently skip permission changes on
files and directories on Linux kernels before 6.6 when /proc is unavailable.
- These operations now diagnose the unsupported changes and exit with failure.
+ These operations now retry after checking that the file is not a symlink.
+ This fallback can follow a symlink substituted after that check.
[bug introduced in coreutils-9.5]
'chmod -R --dereference --preserve-root' no longer silently skips the
diff --git a/src/chmod.c b/src/chmod.c
index 5a7b41c2b..ffeeb976a 100644
--- a/src/chmod.c
+++ b/src/chmod.c
@@ -345,8 +345,12 @@ process_file (FTS *fts, FTSENT *ent)
&& ignorable_traversal_errno (errno))
return true;
unsupported_symlink = is_symlink == 1;
+ /* Fall back despite the risk of symlink replacement. */
+ if (is_symlink == 0
+ && fchmodat (fts->fts_cwd_fd, file, ch.new_mode, 0) == 0)
+ ch.status = CH_SUCCEEDED;
}
- if (! unsupported_symlink)
+ if (! unsupported_symlink && ch.status != CH_SUCCEEDED)
{
if (! force_silent)
error (0, chmod_errno, _("changing permissions of %s"),
diff --git a/tests/chmod/symlink-enotsup.sh b/tests/chmod/symlink-enotsup.sh
index b3b93e770..b8d382b45 100755
--- a/tests/chmod/symlink-enotsup.sh
+++ b/tests/chmod/symlink-enotsup.sh
@@ -51,4 +51,17 @@ for deref in --dereference --no-dereference; do
done
done
+# Retry without AT_SYMLINK_NOFOLLOW when only the first call fails.
+inject='fchmodat2:error=EOPNOTSUPP:when=1'
+if strace --quiet=all -o /dev/null -e inject="$inject" true 2>/dev/null; then
+ chmod 600 file || framework_failure_
+ strace --quiet=all -o trace -e inject="$inject" \
+ chmod --no-dereference 644 file >out 2>err || fail=1
+ grep ' (INJECTED)' trace > /dev/null \
+ || skip_ 'strace did not inject the requested failure'
+ compare /dev/null out || fail=1
+ compare /dev/null err || fail=1
+ test "$(stat -c %a file)" = 644 || fail=1
+fi
+
Exit $fail
--
2.56.0