chgrp now accepts the --from=OWNER:GROUP option to restrict changes to files
with matching current OWNER and/or GROUP, as already supported by chown(1).
+ chmod adds support for -h, -H,-L,-P, and --dereference options, providing
+ more control over symlink handling. This supports more secure handling of
+ CLI arguments, and is more consistent with chown, and chmod on other systems.
+
cp now accepts the --keep-directory-symlink option (like tar), to preserve
and follow existing symlinks to directories in the destination.
@cindex symbolic link to directory, controlling traversal of
-The following options modify how @command{chown} and @command{chgrp}
@c FIXME: note that 'du' has these options, too, but they have slightly
@c different meaning.
-traverse a hierarchy when the @option{--recursive} (@option{-R})
-option is also specified.
+The following options modify how @command {chmod}, @command{chown},
+and @command{chgrp} traverse a hierarchy when
+the @option{--recursive} (@option{-R}) option is also specified.
If more than one of the following options is specified, only the final
one takes effect.
These options specify whether processing a symbolic link to a directory
@opindex -P
@cindex symbolic link to directory, never traverse
Do not traverse any symbolic links.
+@end macro
+@choptP
+
+@macro choptDefault
This is the default if none of @option{-H}, @option{-L},
or @option{-P} is specified.
@end macro
-@choptP
+@choptDefault
@end table
@xref{Traversing symlinks}.
@choptP
+@choptDefault
@xref{Traversing symlinks}.
@end table
@xref{Traversing symlinks}.
@choptP
+@choptDefault
@xref{Traversing symlinks}.
@end table
@end example
@cindex symbolic links, permissions of
-@command{chmod} never changes the permissions of symbolic links, since
-the @command{chmod} system call cannot change their permissions.
-This is not a problem since the permissions of symbolic links are
-never used. However, for each symbolic link listed on the command
+@command{chmod} doesn't change the permissions of symbolic links, since
+the @command{chmod} system call cannot change their permissions on most systems,
+and most systems ignore permissions of symbolic links.
+However, for each symbolic link listed on the command
line, @command{chmod} changes the permissions of the pointed-to file.
In contrast, @command{chmod} ignores symbolic links encountered during
-recursive directory traversals.
+recursive directory traversals. Options that modify this behavior
+are described below.
Only a process whose effective user ID matches the user ID of the file,
or a process with appropriate privileges, is permitted to change the
Verbosely describe the action for each @var{file} whose permissions
actually change.
+@item --dereference
+@opindex --dereference
+@cindex symbolic links, changing mode
+Do not act on symbolic links themselves but rather on what they point to.
+This is the default for command line arguments, but not for
+symbolic links encountered when recursing.
+@warnOptDerefWithRec
+
+@item -h
+@itemx --no-dereference
+@opindex -h
+@opindex --no-dereference
+@cindex symbolic links, changing mode
+Act on symbolic links themselves instead of what they point to.
+On systems that do not support this, no diagnostic is issued,
+but see @option{--verbose}.
+
@item -f
@itemx --silent
@itemx --quiet
@cindex recursively changing access permissions
Recursively change permissions of directories and their contents.
+@choptH
+@choptDefault
+@xref{Traversing symlinks}.
+
+@choptL
+@warnOptDerefWithRec
+@xref{Traversing symlinks}.
+
+@choptP
+@xref{Traversing symlinks}.
+
@end table
@exitstatus
in the file's group, with the same values.
.PP
.B chmod
-never changes the permissions of symbolic links; the
+doesn't change the permissions of symbolic links; the
.B chmod
-system call cannot change their permissions. This is not a problem
-since the permissions of symbolic links are never used.
+system call cannot change their permissions on most systems,
+and most systems ignore permissions of symbolic links.
However, for each symbolic link listed on the command line,
.B chmod
changes the permissions of the pointed-to file.
In contrast,
.B chmod
ignores symbolic links encountered during recursive directory
-traversals.
+traversals. Options that modify this behavior are described in the
+.B OPTIONS
+section.
.SH "SETUID AND SETGID BITS"
.B chmod
clears the set-group-ID bit of a
/* If true, change the modes of directories recursively. */
static bool recurse;
+/* 1 if --dereference, 0 if --no-dereference, -1 if neither has been
+ specified. */
+static int dereference = -1;
+
/* If true, force silence (suppress most of error messages). */
static bool force_silent;
non-character as a pseudo short option, starting with CHAR_MAX + 1. */
enum
{
- NO_PRESERVE_ROOT = CHAR_MAX + 1,
+ DEREFERENCE_OPTION = CHAR_MAX + 1,
+ NO_PRESERVE_ROOT,
PRESERVE_ROOT,
REFERENCE_FILE_OPTION
};
static struct option const long_options[] =
{
{"changes", no_argument, nullptr, 'c'},
+ {"dereference", no_argument, nullptr, DEREFERENCE_OPTION},
{"recursive", no_argument, nullptr, 'R'},
+ {"no-dereference", no_argument, nullptr, 'h'},
{"no-preserve-root", no_argument, nullptr, NO_PRESERVE_ROOT},
{"preserve-root", no_argument, nullptr, PRESERVE_ROOT},
{"quiet", no_argument, nullptr, 'f'},
const struct stat *file_stats = ent->fts_statp;
struct change_status ch = {0};
ch.status = CH_NO_STAT;
+ struct stat stat_buf;
switch (ent->fts_info)
{
break;
case FTS_SLNONE:
- if (! force_silent)
- error (0, 0, _("cannot operate on dangling symlink %s"),
- quoteaf (file_full_name));
+ if (dereference)
+ {
+ if (! force_silent)
+ error (0, 0, _("cannot operate on dangling symlink %s"),
+ quoteaf (file_full_name));
+ break;
+ }
+ ch.status = CH_NOT_APPLIED;
+ break;
+
+ case FTS_SL:
+ if (dereference == 1)
+ {
+ if (fstatat (fts->fts_cwd_fd, file, &stat_buf, 0) != 0)
+ {
+ if (! force_silent)
+ error (0, errno, _("cannot dereference %s"),
+ quoteaf (file_full_name));
+ break;
+ }
+
+ file_stats = &stat_buf;
+ }
+ ch.status = CH_NOT_APPLIED;
break;
case FTS_DC: /* directory that causes cycles */
return false;
}
- if (ch.status == CH_NOT_APPLIED && ! S_ISLNK (file_stats->st_mode))
+ /* With -H (default) or -P, (without -h), avoid operating on symlinks.
+ With -L, S_ISLNK should be false, and with -RP, dereference is 0. */
+ if (ch.status == CH_NOT_APPLIED
+ && ! (S_ISLNK (file_stats->st_mode) && dereference == -1))
{
ch.old_mode = file_stats->st_mode;
ch.new_mode = mode_adjust (ch.old_mode, S_ISDIR (ch.old_mode) != 0,
umask_value, change, nullptr);
- if (chmodat (fts->fts_cwd_fd, file, ch.new_mode) == 0)
+ /* XXX: Racy if FILE is now replaced with a symlink, which is
+ a potential security issue with -[H]R. */
+ if (fchmodat (fts->fts_cwd_fd, file, ch.new_mode,
+ dereference ? 0 : AT_SYMLINK_NOFOLLOW) == 0)
ch.status = CH_SUCCEEDED;
else
{
- if (! force_silent)
- error (0, errno, _("changing permissions of %s"),
- quoteaf (file_full_name));
- ch.status = CH_FAILED;
+ if (! is_ENOTSUP (errno))
+ {
+ if (! force_silent)
+ error (0, errno, _("changing permissions of %s"),
+ quoteaf (file_full_name));
+
+ ch.status = CH_FAILED;
+ }
+ /* else treat not supported as not applied. */
}
}
-c, --changes like verbose but report only when a change is made\n\
-f, --silent, --quiet suppress most error messages\n\
-v, --verbose output a diagnostic for every file processed\n\
+"), stdout);
+ fputs (_("\
+ --dereference affect the referent of each symbolic link,\n\
+ rather than the symbolic link itself\n\
+ -h, --no-dereference affect each symbolic link, rather than the referent\n\
"), stdout);
fputs (_("\
--no-preserve-root do not treat '/' specially (the default)\n\
fputs (_("\
-R, --recursive change files and directories recursively\n\
"), stdout);
+ emit_symlink_recurse_options ("-H");
fputs (HELP_OPTION_DESCRIPTION, stdout);
fputs (VERSION_OPTION_DESCRIPTION, stdout);
fputs (_("\
bool preserve_root = false;
char const *reference_file = nullptr;
int c;
+ int bit_flags = FTS_COMFOLLOW | FTS_PHYSICAL;
initialize_main (&argc, &argv);
set_program_name (argv[0]);
recurse = force_silent = diagnose_surprises = false;
while ((c = getopt_long (argc, argv,
- ("Rcfvr::w::x::X::s::t::u::g::o::a::,::+::=::"
+ ("HLPRcfhvr::w::x::X::s::t::u::g::o::a::,::+::=::"
"0::1::2::3::4::5::6::7::"),
long_options, nullptr))
!= -1)
{
switch (c)
{
+
+ case 'H': /* Traverse command-line symlinks-to-directories. */
+ bit_flags = FTS_COMFOLLOW | FTS_PHYSICAL;
+ break;
+
+ case 'L': /* Traverse all symlinks-to-directories. */
+ bit_flags = FTS_LOGICAL;
+ break;
+
+ case 'P': /* Traverse no symlinks-to-directories. */
+ bit_flags = FTS_PHYSICAL;
+ break;
+
+ case 'h': /* --no-dereference: affect symlinks */
+ dereference = 0;
+ break;
+
+ case DEREFERENCE_OPTION: /* --dereference: affect the referent
+ of each symlink */
+ dereference = 1;
+ break;
+
case 'r':
case 'w':
case 'x':
}
}
+ if (recurse)
+ {
+ if (bit_flags == FTS_PHYSICAL)
+ {
+ if (dereference == 1)
+ error (EXIT_FAILURE, 0,
+ _("-R --dereference requires either -H or -L"));
+ dereference = 0;
+ }
+ }
+
if (reference_file)
{
if (mode)
root_dev_ino = nullptr;
}
- ok = process_files (argv + optind,
- FTS_COMFOLLOW | FTS_PHYSICAL | FTS_DEFER_STAT);
+ bit_flags |= FTS_DEFER_STAT;
+ ok = process_files (argv + optind, bit_flags);
main_exit (ok ? EXIT_SUCCESS : EXIT_FAILURE);
}
fputs (_("\
-R, --recursive operate on files and directories recursively\n\
"), stdout);
- fputs (_("\
-\n\
-The following options modify how a hierarchy is traversed when the -R\n\
-option is also specified. If more than one is specified, only the final\n\
-one takes effect.\n\
-\n\
- -H if a command line argument is a symbolic link\n\
- to a directory, traverse it\n\
- -L traverse every symbolic link to a directory\n\
- encountered\n\
- -P do not traverse any symbolic links (default)\n\
-\n\
-"), stdout);
+ emit_symlink_recurse_options ("-P");
fputs (HELP_OPTION_DESCRIPTION, stdout);
fputs (VERSION_OPTION_DESCRIPTION, stdout);
if (chown_mode == CHOWN_CHOWN)
"), stdout);
}
+static inline void
+emit_symlink_recurse_options (char const *default_opt)
+{
+ printf (_("\
+\n\
+The following options modify how a hierarchy is traversed when the -R\n\
+option is also specified. If more than one is specified, only the final\n\
+one takes effect. '%s' is the default.\n\
+\n\
+ -H if a command line argument is a symbolic link\n\
+ to a directory, traverse it\n\
+ -L traverse every symbolic link to a directory\n\
+ encountered\n\
+ -P do not traverse any symbolic links\n\
+\n\
+"), default_opt);
+}
+
static inline void
emit_exec_status (char const *program)
{
--- /dev/null
+#!/bin/sh
+# Verify chmod symlink handling options
+
+# Copyright (C) 2024 Free Software Foundation, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+. "${srcdir=.}/tests/init.sh"; path_prepend_ ./src
+print_ver_ chmod
+
+#dirs
+mkdir -p a/b a/c || framework_failure_
+#files
+touch a/b/file a/c/file || framework_failure_
+#dangling link
+ln -s foo a/dangle || framework_failure_
+#link to file
+ln -s ../b/file a/c/link || framework_failure_
+#link to dir
+ln -s b a/dirlink || framework_failure_
+
+# tree -F a
+# a/
+# |-- b/
+# | '-- file
+# |-- c/
+# | |-- file
+# | '-- link -> ../b/file
+# |-- dangle -> foo
+# '-- dirlink -> b/
+
+reset_modes() { chmod 777 a/b a/c a/b/file a/c/file || fail=1; }
+count_755() { test "$(grep 'rwxr-xr-x' 'out' | wc -l)" = "$1"; }
+
+reset_modes
+# -R (with default -H) does not deref traversed symlinks (only cli args)
+chmod 755 -R a/c || fail=1
+ls -l a/b > out || framework_failure_
+count_755 0 || fail=1
+ls -lR a/c > out || framework_failure_
+count_755 1 || fail=1
+
+reset_modes
+# set a/c a/c/file and a/b/file (through symlink) to 755
+chmod 755 -LR a/c || fail=1
+ls -ld a/c a/c/file a/b/file > out || framework_failure_
+count_755 3 || { cat out; fail=1; }
+
+reset_modes
+# do not set /a/b/file through symlink (should try to chmod the link itself)
+chmod 755 -RP a/c/ || fail=1
+ls -l a/b > out || framework_failure_
+count_755 0 || fail=1
+
+reset_modes
+# set /a/b/file through symlink
+chmod 755 --dereference a/c/link || fail=1
+ls -l a/b > out || framework_failure_
+count_755 1 || fail=1
+
+reset_modes
+# do not set /a/b/file through symlink (should try to chmod the link itself)
+chmod 755 --no-dereference a/c/link 2>err || fail=1
+ls -l a/b > out || framework_failure_
+count_755 0 || fail=1
+
+# Dangling links should not induce an error if not dereferencing
+for noderef in '-h' '-RP' '-P'; do
+ chmod 755 --no-dereference a/dangle 2>err || fail=1
+done
+# Dangling links should induce an error if dereferencing
+for deref in '' '--deref' '-R'; do
+ returns_ 1 chmod 755 $deref a/dangle 2>err || fail=1
+done
+
+Exit $fail
tests/chmod/thru-dangling.sh \
tests/chmod/umask-x.sh \
tests/chmod/usage.sh \
+ tests/chmod/symlinks.sh \
tests/chown/deref.sh \
tests/chown/preserve-root.sh \
tests/chown/separator.sh \