Skip to content

chmod: Wrong handling of -H, -L, --no-dereference #8422

@julian-klode

Description

@julian-klode

Please find the attached test case comparing the output of chmod between GNU and uutils chmod (and chown, chgrp, but they are good).

test-case.sh.txt

Update 1:

test-case.sh.txt

-- more cases

It creates a scenario like this:

  • dir is a directory
  • dir/link-file is a symlink to target-file
  • dir/link-dir is a symlink to target-dir
  • target-file and target-dir/file are files

chmod can hence change the permissions of all files in the scenario by running it on dir. We also test passing dir/target-dir to see how it fares when passing a symlink to a dir.

The output shows that the -H, -L, and --no-dereference options are not correctly implemented (- is GNU, + is uutils):

============================= chmod -R go-rwx dir ============================
--- /dev/fd/63  2025-07-31 18:41:55.044252233 +0200
+++ /dev/fd/62  2025-07-31 18:41:55.045252250 +0200
@@ -5 +5 @@
-drwxrwxr-x 2 jak jak 18 Jul 31 18:41 ./target-dir
+drwx------ 2 jak jak 18 Jul 31 18:41 ./target-dir
@@ -7 +7 @@
--rw-rw-r-- 1 jak jak  0 Jul 31 18:41 ./target-file
+-rw------- 1 jak jak  0 Jul 31 18:41 ./target-file
============================= chmod -R go-rwx dir/link-dir ============================
============================= chmod -R go-rwx dir/link-file ============================
============================= chmod -H -R go-rwx dir ============================
--- /dev/fd/63  2025-07-31 18:41:55.134253752 +0200
+++ /dev/fd/62  2025-07-31 18:41:55.134253752 +0200
@@ -5 +5 @@
-drwxrwxr-x 2 jak jak 18 Jul 31 18:41 ./target-dir
+drwx------ 2 jak jak 18 Jul 31 18:41 ./target-dir
@@ -7 +7 @@
--rw-rw-r-- 1 jak jak  0 Jul 31 18:41 ./target-file
+-rw------- 1 jak jak  0 Jul 31 18:41 ./target-file
============================= chmod -H -R go-rwx dir/link-dir ============================
============================= chmod -H -R go-rwx dir/link-file ============================
============================= chmod -L -R go-rwx dir ============================
--- /dev/fd/63  2025-07-31 18:41:55.220255202 +0200
+++ /dev/fd/62  2025-07-31 18:41:55.220255202 +0200
@@ -6 +6 @@
--rw------- 1 jak jak  0 Jul 31 18:41 ./target-dir/file
+-rw-rw-r-- 1 jak jak  0 Jul 31 18:41 ./target-dir/file
============================= chmod -L -R go-rwx dir/link-dir ============================
============================= chmod -L -R go-rwx dir/link-file ============================
============================= chmod -P -R go-rwx dir ============================
============================= chmod -P -R go-rwx dir/link-dir ============================
============================= chmod -P -R go-rwx dir/link-file ============================
============================= chmod --dereference -R go-rwx dir ============================
============================= chmod --dereference -R go-rwx dir/link-dir ============================
============================= chmod --dereference -R go-rwx dir/link-file ============================
============================= chmod --no-dereference -R go-rwx dir ============================
============================= chmod --no-dereference -R go-rwx dir/link-dir ============================
--- /dev/fd/63  2025-07-31 18:41:55.507260044 +0200
+++ /dev/fd/62  2025-07-31 18:41:55.507260044 +0200
@@ -6 +6 @@
--rw------- 1 jak jak  0 Jul 31 18:41 ./target-dir/file
+-rw-rw-r-- 1 jak jak  0 Jul 31 18:41 ./target-dir/file
============================= chmod --no-dereference -R go-rwx dir/link-file ============================

In particular, this gets worse if you pass an absolute path: All symlinks are derferenced, as the check for TraverseSymlinks::First is considerably wrong:

    fn walk_dir(&self, file_path: &Path) -> UResult<()> {
        let mut r = self.chmod_file(file_path);
        // Determine whether to traverse symlinks based on `self.traverse_symlinks`
        let should_follow_symlink = match self.traverse_symlinks {
            TraverseSymlinks::All => true,
            TraverseSymlinks::First => {
                file_path == file_path.canonicalize().unwrap_or(file_path.to_path_buf())
            }
            TraverseSymlinks::None => false,
        };

It's not clear how this happened, first should deference symlinks that are specified directly as command-line arguments (see dir/link-dir) test cases above. So it stands to reason that selection needs to change to

    fn walk_dir(&self, file_path: &Path, bool is_argument) -> UResult<()> {
        let mut r = self.chmod_file(file_path);
        // Determine whether to traverse symlinks based on `self.traverse_symlinks`
        let should_follow_symlink = match self.traverse_symlinks {
            TraverseSymlinks::All => true,
            TraverseSymlinks::First => is_argument,
            TraverseSymlinks::None => false,
        };

or similar, but this doesn't resolve all issues above and causes failures in the test_chmod suite.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions