Files
Jitendra Bhati 22df19c09a chflags: use Capsicum capability mode
Run the file-hierarchy traversal inside Capsicum capability mode
using the fts_openat(3) API.  This confines chflags to the directory
hierarchies named on the command line, so a malicious or buggy tree
cannot redirect it at files elsewhere via a crafted symbolic link.

Because AT_FDCWD is rejected in capability mode, a directory descriptor
for the parent directory of every argument with an absolute path or a
path containing ".." is opened once before cap_enter().  Once every
descriptor is open, cap_enter() is called and the traversal acts through
fd-relative operations: chflagsat(fts_parent->fts_dirfd, fts_name).

With -L chflags follows symbolic links, which may point outside the
named hierarchy; chflag now rejects such accesses.  The new
--dereference-links-unsafely option disables the sandbox to restore the
historical behavior for the rare callers that rely on it.  But if the
symbolic was link was named directly on the command line, chflags will
still follow it (unless -h was given).

Add functional tests covering relative, absolute, "..", recursive
and mixed path arguments, and the symlink handling in both the
default and --dereference-links-unsafely modes; they skip on
filesystems that do not support the uchg flag.

Sponsored by:   Google LLC (GSoC 2026)
Reviewed by:	asomers
Relnotes:	yes (for the -L behavior change)
Pull Request:	https://github.com/freebsd/freebsd-src/pull/2375
2026-08-23 13:02:00 -06:00

239 lines
6.4 KiB
Bash

#
# Copyright 2017 Shivansh Rai
# Copyright (c) 2026 Jitendra Bhati
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
#
usage_output='usage: chflags'
# Skip the calling test if the work filesystem does not support
# setting the nodump file flag (e.g. some ZFS configurations).
require_chflags()
{
touch .chflags_probe
if ! chflags nodump .chflags_probe 2>/dev/null; then
rm -f .chflags_probe
atf_skip "filesystem does not support the nodump flag"
fi
rm -f .chflags_probe
}
atf_test_case invalid_usage
invalid_usage_head()
{
atf_set "descr" "Verify that an invalid usage with a supported option produces a valid error message"
}
invalid_usage_body()
{
atf_check -s not-exit:0 -e match:"$usage_output" chflags -f
atf_check -s not-exit:0 -e match:"$usage_output" chflags -H
atf_check -s not-exit:0 -e match:"$usage_output" chflags -h
atf_check -s not-exit:0 -e match:"$usage_output" chflags -L
atf_check -s not-exit:0 -e match:"$usage_output" chflags -P
atf_check -s not-exit:0 -e match:"$usage_output" chflags -R
atf_check -s not-exit:0 -e match:"$usage_output" chflags -v
}
atf_test_case no_arguments
no_arguments_head()
{
atf_set "descr" "Verify that chflags(1) fails and generates a valid usage message when no arguments are supplied"
}
no_arguments_body()
{
atf_check -s not-exit:0 -e match:"$usage_output" chflags
}
atf_test_case relative_path
relative_path_head()
{
atf_set "descr" "chflags sets flags on a relative path"
}
relative_path_body()
{
require_chflags
touch file
atf_check chflags nodump file
atf_check -o match:nodump stat -f "%Sf" file
}
atf_test_case absolute_path
absolute_path_head()
{
atf_set "descr" "chflags sets flags on an absolute path"
}
absolute_path_body()
{
require_chflags
touch file
atf_check chflags nodump "$(pwd)/file"
atf_check -o match:nodump stat -f "%Sf" file
}
atf_test_case dotdot_path
dotdot_path_head()
{
atf_set "descr" "chflags sets flags on a path containing .."
}
dotdot_path_body()
{
require_chflags
mkdir dir
touch file
cd dir
atf_check chflags nodump ../file
atf_check -o match:nodump stat -f "%Sf" ../file
}
atf_test_case recursive
recursive_head()
{
atf_set "descr" "chflags -R sets flags on a directory tree"
}
recursive_body()
{
require_chflags
mkdir -p dir/sub
touch dir/file dir/sub/file
atf_check chflags -R nodump dir
atf_check -o match:nodump stat -f "%Sf" dir/file
atf_check -o match:nodump stat -f "%Sf" dir/sub/file
}
atf_test_case mixed_paths
mixed_paths_head()
{
atf_set "descr" "chflags handles relative, absolute and .. paths together"
}
mixed_paths_body()
{
require_chflags
mkdir dir
touch a dir/b c
cd dir
atf_check chflags nodump b "$(pwd)/../a" ../c
atf_check -o match:nodump stat -f "%Sf" b
atf_check -o match:nodump stat -f "%Sf" ../a
atf_check -o match:nodump stat -f "%Sf" ../c
}
atf_test_case outside_symlink_rejected
outside_symlink_rejected_head()
{
atf_set "descr" "chflags -RL does not follow a symlink pointing " \
"outside the traversal by default"
}
outside_symlink_rejected_body()
{
require_chflags
mkdir -p foo/bar/baz
touch target
ln -s ../../../target foo/bar/baz/link
atf_check -s not-exit:0 -e ignore chflags -RL nodump foo/bar
atf_check -o match:"target[[:space:]]*-" stat -f "%N %Sf" target
}
atf_test_case outside_symlink_unsafe
outside_symlink_unsafe_head()
{
atf_set "descr" "chflags -RL --dereference-links-unsafely follows " \
"a symlink pointing outside the traversal"
}
outside_symlink_unsafe_body()
{
require_chflags
mkdir -p foo/bar/baz
touch target
ln -s ../../../target foo/bar/baz/link
atf_check chflags -RL --dereference-links-unsafely nodump foo/bar
atf_check -o match:nodump stat -f "%Sf" target
}
atf_test_case symlink_no_h
symlink_no_h_head()
{
atf_set "descr" "chflags on a symlink without -h changes the target"
}
symlink_no_h_body()
{
require_chflags
mkdir dir
touch target
ln -s ../target dir/link
atf_check chflags nodump dir/link
atf_check -o match:nodump stat -f "%Sf" target
atf_check -o not-match:nodump stat -f "%Sf" dir/link
}
atf_test_case symlink_rootlevel_H
symlink_rootlevel_H_head()
{
atf_set "descr" "chflags -RH follows a symbolic link to a" \
" directory named on the command line"
}
symlink_rootlevel_H_body()
{
require_chflags
mkdir dir realdir
touch realdir/file
ln -s ../realdir dir/dlink
atf_check chflags -RH nodump dir/dlink
atf_check -o match:nodump stat -f "%Sf" realdir/file
atf_check -o not-match:nodump stat -f "%Sf" dir/dlink
}
atf_test_case symlink_rootlevel_H_dead
symlink_rootlevel_H_dead_head()
{
atf_set "descr" "chflags -RH on a broken symbolic link fails" \
" gracefully"
}
symlink_rootlevel_H_dead_body()
{
require_chflags
mkdir dir
ln -s ../nonexistent dir/dlink
atf_check -s not-exit:0 -e match:"No such file or directory" \
chflags -RH nodump dir/dlink
}
atf_init_test_cases()
{
atf_add_test_case invalid_usage
atf_add_test_case no_arguments
atf_add_test_case relative_path
atf_add_test_case absolute_path
atf_add_test_case dotdot_path
atf_add_test_case recursive
atf_add_test_case mixed_paths
atf_add_test_case symlink_no_h
atf_add_test_case symlink_rootlevel_H
atf_add_test_case symlink_rootlevel_H_dead
atf_add_test_case outside_symlink_rejected
atf_add_test_case outside_symlink_unsafe
}