* 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


Reply via email to