diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist --- a/etc/mtree/BSD.tests.dist +++ b/etc/mtree/BSD.tests.dist @@ -1259,6 +1259,8 @@ .. chown .. + chroot + .. ctladm .. daemon diff --git a/usr.sbin/chroot/Makefile b/usr.sbin/chroot/Makefile --- a/usr.sbin/chroot/Makefile +++ b/usr.sbin/chroot/Makefile @@ -1,4 +1,9 @@ +.include + PROG= chroot MAN= chroot.8 +HAS_TESTS= +SUBDIR.${MK_TESTS}+= tests + .include diff --git a/usr.sbin/chroot/tests/Makefile b/usr.sbin/chroot/tests/Makefile new file mode 100644 --- /dev/null +++ b/usr.sbin/chroot/tests/Makefile @@ -0,0 +1,9 @@ +PACKAGE= tests + +ATF_TESTS_SH= chroot_test +# Just in case: we may manipulate security.bsd.unprivileged_chroot here if the +# current value doesn't suit our needs, so we probably shouldn't run it in +# parallel with anything else to be safe. +TEST_METADATA.chroot_test= is_exclusive="true" + +.include diff --git a/usr.sbin/chroot/tests/chroot_test.sh b/usr.sbin/chroot/tests/chroot_test.sh new file mode 100755 --- /dev/null +++ b/usr.sbin/chroot/tests/chroot_test.sh @@ -0,0 +1,253 @@ +# +# Copyright (c) 2025 Kyle Evans +# +# SPDX-License-Identifier: BSD-2-Clause +# + +allow_unpriv_chroot() +{ + unpriv_chroot=$(sysctl -n security.bsd.unprivileged_chroot) + if [ "$unpriv_chroot" -eq 1 ]; then + # Nothing to do here. + return + fi + + if sysctl security.bsd.unprivileged_chroot=1; then + echo "security.bsd.unprivileged_chroot=\"$unpriv_chroot\"" > sysctl.restore + else + atf_skip "Could not set security.bsd.unprivileged_chroot=1" + fi +} + +restore_unpriv_chroot() +{ + if [ -s sysctl.restore ]; then + sysctl -f sysctl.restore + fi +} + +# makeroot root [user...] +makeroot() +{ + local rootdir="$1" + shift + + if [ -z "$rootdir" -o "$rootdir" = "/" ]; then + atf_fail "bad makeroot usage, rootdir empty or /" + fi + + # Construct a minimal chroot to investigate, copy in some /rescue bits + # to investigate it with. + mkdir -p "$rootdir"/etc + + if [ "$#" -gt 0 ]; then + for user in "$@"; do + id -P "$user" >> "$rootdir"/etc/master.passwd + grep "$user" /etc/group >> "$rootdir"/etc/group.raw + done + + sed -i '' -Ee 's/^([^:]+):([^:])+:/\1:*:/' \ + "$rootdir"/etc/master.passwd + sort -u "$rootdir"/etc/group.raw > "$rootdir"/etc/group + rm "$rootdir"/etc/group.raw + pwd_mkdb -p -d "$rootdir"/etc "$rootdir"/etc/master.passwd + fi + + cp /rescue/rescue "$rootdir" + + for prog in id ls; do + ln "$rootdir"/rescue "$rootdir"/"$prog" + done +} + +atf_test_case chroot_basic +chroot_basic_head() +{ + atf_set "descr" "Test that chroot works for root" + atf_set "require.user" "root" +} +chroot_basic_body() +{ + makeroot rootdir root + + atf_check -o save:chroot.user chroot rootdir /id -un + atf_check -o save:chroot.contents chroot rootdir /ls / + + # chroot doesn't change privileges without requesting it. + read chrootuser < chroot.user + atf_check_equal "root" "$chrootuser" + + # Confirm again that we actually did chroot + atf_check -s not-exit:0 grep -q 'COPYRIGHT' chroot.contents +} + +# do_chroot rootdir [flags] -- helper to collect various facts about the +# environment we want to chroot into, the caller will verify those. +# +# Generates: +# - chroot.user +# - chroot.group +# - chroot.agroups +# - chroot.contents +# +# chroot.contents will be somewhat sanity checked here to not be /. +do_chroot() +{ + local rootdir=$1 + shift + + atf_check -o save:chroot.user \ + chroot "$@" $rootdir /id -un + atf_check -o save:chroot.group \ + chroot "$@" $rootdir /id -gn + atf_check -o save:chroot.agroups -x \ + "chroot $@ $rootdir /id -G | tr ' ' '\n'" + atf_check -o save:chroot.contents \ + chroot "$@" $rootdir /ls / + + # Basic sanity check that we actually chrooted. + atf_check -s not-exit:0 grep -q 'COPYRIGHT' chroot.contents +} + +atf_test_case chroot_user +chroot_user_head() +{ + atf_set "descr" "Test that chroot -u works" + atf_set "require.user" "root" + atf_set "require.config" "unprivileged-user" +} +chroot_user_body() +{ + unpriv_user=$(atf_config_get "unprivileged-user") + priv_group=$(id -gn) + + makeroot rootdir root "$unpriv_user" + + do_chroot rootdir -u "$unpriv_user" + + # We requested only a new -u this time. + read chrootuser < chroot.user + atf_check_equal "$unpriv_user" "$chrootuser" + + read chrootgroup < chroot.group + atf_check_equal "$priv_group" "$chrootgroup" +} + +atf_test_case chroot_group +chroot_group_head() +{ + atf_set "descr" "Test that chroot -g works" + atf_set "require.user" "root" + atf_set "require.config" "unprivileged-user" +} +chroot_group_body() +{ + unpriv_user=$(atf_config_get "unprivileged-user") + unpriv_group=$(id -gn "$unpriv_user") + + makeroot rootdir root "$unpriv_user" + + do_chroot rootdir -g "$unpriv_group" + + # We requested only a new -g this time. + read chrootuser < chroot.user + atf_check_equal "root" "$chrootuser" + + read chrootgroup < chroot.group + atf_check_equal "$unpriv_group" "$chrootgroup" +} + +atf_test_case chroot_grouplist +chroot_grouplist_head() +{ + atf_set "descr" "Test that chroot -G works" + atf_set "require.user" "root" + atf_set "require.config" "unprivileged-user" +} +chroot_grouplist_body() +{ + unpriv_user=$(atf_config_get "unprivileged-user") + unpriv_group=$(id -gn "$unpriv_user") + unpriv_groupid=$(id -g "$unpriv_user") + priv_group=$(id -gn) + priv_groupid=$(id -g) + + makeroot rootdir root "$unpriv_user" + + # Testing both that a list works, and that group name resolution is + # fine. + do_chroot rootdir -G "$unpriv_group,$priv_group" + + # We requested only a grouplist this time. + read chrootuser < chroot.user + atf_check_equal "root" "$chrootuser" + + read chrootgroup < chroot.group + atf_check_equal "$priv_group" "$chrootgroup" + + priv_observed=0 + unpriv_observed=0 + while read group; do + if [ "$group" -eq "$priv_groupid" ]; then + priv_observed=1 + continue + elif [ "$group" -eq "$unpriv_groupid" ]; then + unpriv_observed=1 + continue + fi + + atf_fail "Unexpected group $group in supplementary list" + done < chroot.agroups + + if [ "$priv_observed" -eq 0 ]; then + atf_fail "Privileged group not observed" + fi + if [ "$unpriv_observed" -eq 0 ]; then + atf_fail "Unprivileged group not observed" + fi +} + +atf_test_case unpriv_chroot "cleanup" +unpriv_chroot_head() +{ + atf_set "descr" "Test that chroot -n works for unprivileged users" + + # We're going to drop privileges after the test begins, we just don't + # want to assume the default for security.bsd.unprivileged_chroot. The + # test will twiddle it as necessary first. + atf_set "require.user" "root" + atf_set "require.config" "unprivileged-user" +} +unpriv_chroot_body() +{ + unpriv_user=$(atf_config_get "unprivileged-user") + + allow_unpriv_chroot + + # This one is effectively the chroot_user test, but using -n instead. + makeroot rootdir root "$unpriv_user" + + atf_check -o save:chroot.user \ + su -m "$unpriv_user" -c 'chroot -n rootdir /id -un' + atf_check -o save:chroot.contents \ + su -m "$unpriv_user" -c 'chroot -n rootdir /ls -l' + + read chrootuser < chroot.user + atf_check_equal "$unpriv_user" "$chrootuser" + + # Confirm again that we actually did chroot + atf_check -s not-exit:0 grep -q 'COPYRIGHT' chroot.contents +} +unpriv_chroot_cleanup() +{ + restore_unpriv_chroot +} + +atf_init_test_cases() +{ + atf_add_test_case chroot_basic + atf_add_test_case chroot_user + atf_add_test_case chroot_group + atf_add_test_case chroot_grouplist + atf_add_test_case unpriv_chroot +}