Fix skipped processing of items after a symlink to '/'.

* src/chmod.c (process_file): Only skip with directory
visited in pre-order.
* tests/chmod/symlinks.sh: Add a test case (skipped if root).
* NEWS: Mention the bug fix.

Discovered by Anthropic's OSS Scanner, as bug ANT-2026-HWDZFHEJ.
---
 NEWS                    |  4 ++++
 src/chmod.c             | 11 +++++++----
 tests/chmod/symlinks.sh | 21 +++++++++++++++++++++
 3 files changed, 32 insertions(+), 4 deletions(-)

diff --git a/NEWS b/NEWS
index 3361336ef..b920865ad 100644
--- a/NEWS
+++ b/NEWS
@@ -15,6 +15,10 @@ GNU coreutils NEWS                                    -*- 
outline -*-
   when only a partial context is specified.
   [bug introduced with the chcon program in coreutils-6.9.90]
 
+  'chmod -R --dereference --preserve-root' no longer silently skips the
+  entry following a symbolic link to '/' encountered during traversal.
+  [bug introduced in coreutils-9.5]
+
   'chroot --userspec=USER' now uses the primary group of USER from inside the
   chroot.  Also this no longer sets supplemental groups of an unrelated user,
   or reads freed memory, when the USER ID is only known outside the chroot.
diff --git a/src/chmod.c b/src/chmod.c
index 68f37e181..58310157f 100644
--- a/src/chmod.c
+++ b/src/chmod.c
@@ -306,10 +306,13 @@ process_file (FTS *fts, FTSENT *ent)
       && ROOT_DEV_INO_CHECK (root_dev_ino, file_stats))
     {
       ROOT_DEV_INO_WARN (file_full_name);
-      /* Tell fts not to traverse into this hierarchy.  */
-      fts_set (fts, ent, FTS_SKIP);
-      /* Ensure that we do not process "/" on the second visit.  */
-      ignore_value (fts_read (fts));
+      if (ent->fts_info == FTS_D)  /* preorder directory.  */
+        {
+          /* Tell fts not to traverse into this hierarchy.  */
+          fts_set (fts, ent, FTS_SKIP);
+          /* Ensure that we do not process "/" on the second visit.  */
+          ignore_value (fts_read (fts));
+        }
       return false;
     }
 
diff --git a/tests/chmod/symlinks.sh b/tests/chmod/symlinks.sh
index e1b01259a..2578a08bb 100755
--- a/tests/chmod/symlinks.sh
+++ b/tests/chmod/symlinks.sh
@@ -84,6 +84,27 @@ for deref in '' '--deref' '-R'; do
   returns_ 1 chmod 755 $deref a/dangle 2>err || fail=1
 done
 
+# From 9.5 to 9.12 when -R --preserve-root --dereference
+# encountered a symlink to /, the following entry was skipped.
+if ! uid_is_privileged_; then  # Just in case
+  root_mode=$(stat -c %a /) || framework_failure_  # Just in case
+  case $root_mode in  # Just in case
+    555|755)
+      mkdir r || framework_failure_
+      for i in 1 2 3 4; do
+        touch r/f$i && ln -s / r/l$i || framework_failure_
+      done
+      chmod 600 r/f? || framework_failure_
+      returns_ 1 chmod -R --dereference --preserve-root "$root_mode" r \
+        2>err || fail=1
+      for i in 1 2 3 4; do
+        test "$(stat -c %a r/f$i)" = "$root_mode" || { ls -l r; fail=1; }
+      done
+      test "$(grep -c 'dangerous to operate' err)" = 4 || { cat err; fail=1; }
+      ;;
+  esac
+fi
+
 # A symlink pointing back to an ancestor forms a cycle.  With -L, chmod
 # follows symlinks while recursing, so it must detect the cycle and stop
 # rather than descend into it forever.
-- 
2.56.0


Reply via email to