On 06/10/2026 01:19, Aaron Rainbolt wrote:
I occasionally run into a situation where I need a root-owned
script to change the ownership or permissions of a file or directory
located in a directory that is accessible by an unprivileged user. This
requires some tricky work to do safely, since the user could create a
symlink where I expect there to be a directory, pointing at some
sensitive file or directory. This can be worked around in some
situations by checking for symlinks first if the script runs during
early boot where TOCTOU isn't an issue, or passing --no-dereference,
but that doesn't help if I have to dig into a directory that itself may
be a symlink. For instance, if I want to do:
chown user:user -- /home/user/path/to/dir
There is no way to do this safely in-place. I can't use setpriv to drop
privileges since chown requires root. I can't check if /home/user/path,
/home/user/path/to, and /home/user/path/to/dir are symlinks first
because that leaves a TOCTOU vulnerability. --no-dereference doesn't
work because chown will dig through the 'path' and 'to' dirs which may
be symlinks to something important. There are ugly ways around this
like copying 'dir' somewhere else, fixing ownership, then deleting the
original and moving the fixed version back, but that's obviously
non-ideal.
Would it be possible to add a `--no-canonicalize` feature to `chown`
and `chmod` (and possibly other utilities where it might make sense)
that throws an error if any portion of the path being acted on does not
exist or goes through a symlink?
Right. Currently -h and -P relate to the basename or traversed components.I.e.
-h will induce AT_SYMLINK_NOFOLLOW on the final fchownat2(),
but intermediate symlinks in a specified path are still followed by the kernel.
Note this is an unusual requirement I think
because the final specified component(s) would have
to match the existing names in the redirected location,
so it doesn't seem like a general security issue.
If you did want to implement this more restrictive handling,
then it would need a new option and that might be implemented
by using openat2(..., RESOLVE_NO_SYMLINKS), and doing the fchownat()
relative to that dir fd.
However it might also be implemented externally
in a race free manner by opening the directory
and verifying its physical path? Something like:
chown_no_symlinks() {(
mode=$1; shift
p=$(realpath -sm -- "$1")
d=$(dirname -- "$p") f=$(basename -- "$p")
cd -P -- "$d" &&
test "$(pwd -P)" = "$d" || { echo 'symlink detected'; return 1; } &&
chown -h -- $mode "./$f"
)}
cheers,
Padraig