From nobody Sun Aug 30 17:43:55 2026 X-Original-To: dev-commits-src-all@mlmmj.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mlmmj.nyi.freebsd.org (Postfix) with ESMTP id 4hXzy33fVyz6rBpN for ; Sun, 30 Aug 2026 17:43:55 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (4096 bits) server-digest SHA256 client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "YR2" (not verified)) by mx1.freebsd.org (Postfix) with ESMTPS id 4hXzy31jXKz3g5m for ; Sun, 30 Aug 2026 17:43:55 +0000 (UTC) (envelope-from git@FreeBSD.org) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1788111835; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=n/+JS8e8mGH5hTJYgDfTSiQ0/R/xJ0zTDsEzM3A9oyw=; b=FmWbfJm2fSFdVwsvH8HS3bQRFliW6eWDK/AvblwYCtMJTeA+A+4DtJ3yxfsrlZzMzyuaLm UXs0jgUN9i7SZLkP8FkZs3es/q3ZdOrpE0+ATOe+W5/C5sqBG54HWM9z5DH4IMpV+R9byZ 6idZ9UdKceriBnM0qxkO8vpWQx8gQTNcvi1UZLRfdmZgDFrbLReqBgyhy4dxIfkI+9LH/p IrIy0LGPUwIzjReUDStIo04iaUVNtHq4XdUwHxSl8GCBiJnqpWIKXS20FgpwYRyqho4p7k Sq9jQSzaixKtRZ4Tb4SnuCTm4nmRJoROLgwkzLqhGP4aFVRBsyKtdX8DHH0IEw== ARC-Seal: i=1; s=dkim; d=freebsd.org; t=1788111835; a=rsa-sha256; cv=none; b=RBt8yuCq6BzlTijEKSbbnOteGhJiLaYbMLzid/31t2Ht9bANUzhc3pdEZ5YnsbdQSo3cRX q47NY/xB7/6Lgcu8PgnQ2PR0DKXE6C47nz2CR03nazFxspH0Pt4jMUFKjlLs3N3qwvLBeF rWfjOE/wE7lR5hhSvZ4sVTJe/2RMzSIvmYEvYSLNhAy9Bs33NUkRuO07k6dbj/hmMUqoYZ 8Cxt+zX+/eO5biZUVFZuEg0Dp4XU2Vrq6Y5Gt/Szs73p/QJWXT/E74URbFuQFbOHW68Jxg +7GTUKrvj8zQCKRqkTGojpkja2CZLFveyMwWp6XFNU9uPS5SmDqa6Tslqnfpbw== ARC-Authentication-Results: i=1; mx1.freebsd.org; none ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=freebsd.org; s=dkim; t=1788111835; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=n/+JS8e8mGH5hTJYgDfTSiQ0/R/xJ0zTDsEzM3A9oyw=; b=qG0ac7R8ZrhPfpeKfBhlDK6xkzbobuidiunUqc+VAEEF5UE7dhb53Gw+xMc1t3sbMwC2dF /akBSzy2pHC2qEHDOXZxuX85AC4wekXjfDPXcn72yPIJA+XTbhNJQsNPj7tKn1gTaxwQbk fhvjdfRfuTjMYK9oc+JjP5/JOXCdQWnr50/aCqYXM6M+YozbuwuoLbvI11DQ5qM93b+bZO HskufuiJ/GwkR1IaNzctcBe1jpj603akke9IqbraAQXoNkUqdxAJzkJzVOR/VJQM5Xq7vN 7+0en1/OMHGI2vjPbGSuOnb3fhKdGnuVaCAf1o/oY3q60ius/B063il2CHs9vg== Received: from gitrepo.freebsd.org (gitrepo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:5]) by mxrelay.nyi.freebsd.org (Postfix) with ESMTP id 4hXzy30Y7tzcB6 for ; Sun, 30 Aug 2026 17:43:55 +0000 (UTC) (envelope-from git@FreeBSD.org) Received: from git (uid 1279) (envelope-from git@FreeBSD.org) id 26807 by gitrepo.freebsd.org (DragonFly Mail Agent v0.13+ on gitrepo.freebsd.org); Sun, 30 Aug 2026 17:43:55 +0000 To: src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-main@FreeBSD.org From: Alexander Leidinger Subject: git: b5dcd8f9d6ae - main - rc.subr tests: service jail behaviour of run_rc_command List-Id: Commit messages for all branches of the src repository List-Archive: https://lists.freebsd.org/archives/dev-commits-src-all List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-BeenThere: dev-commits-src-all@freebsd.org Sender: owner-dev-commits-src-all@FreeBSD.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit X-Git-Committer: netchild X-Git-Repository: src X-Git-Refname: refs/heads/main X-Git-Reftype: branch X-Git-Commit: b5dcd8f9d6ae702adbc1627839b5235a28839370 Auto-Submitted: auto-generated Date: Sun, 30 Aug 2026 17:43:55 +0000 Message-Id: <6a946bdb.26807.5f83c2be@gitrepo.freebsd.org> The branch main has been updated by netchild: URL: https://cgit.FreeBSD.org/src/commit/?id=b5dcd8f9d6ae702adbc1627839b5235a28839370 commit b5dcd8f9d6ae702adbc1627839b5235a28839370 Author: Alexander Leidinger AuthorDate: 2026-08-30 09:45:02 +0000 Commit: Alexander Leidinger CommitDate: 2026-08-30 17:43:28 +0000 rc.subr tests: service jail behaviour of run_rc_command Twenty cases over where each rc option and each method executes for a jailed service, the jail's lifetime, and the svcj option handling. Each case drives the service inside a chroot built in its ATF work directory. MFC after: 1 week MFC to: stable/15 Assisted-by: Claude Code (Opus 5) --- libexec/rc/tests/Makefile | 2 +- libexec/rc/tests/svcj_test.sh | 1178 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1179 insertions(+), 1 deletion(-) diff --git a/libexec/rc/tests/Makefile b/libexec/rc/tests/Makefile index 3a6eafea292d..e58f2331f53b 100644 --- a/libexec/rc/tests/Makefile +++ b/libexec/rc/tests/Makefile @@ -1,4 +1,4 @@ -ATF_TESTS_SH+= rc_subr_test safe_eval_test +ATF_TESTS_SH+= rc_subr_test safe_eval_test svcj_test # allow running this as part of the build - in DIRDEPS_BUILD at least .if ${.MAKE.LEVEL} > 0 && ${MACHINE:Nhost*} == "" diff --git a/libexec/rc/tests/svcj_test.sh b/libexec/rc/tests/svcj_test.sh new file mode 100644 index 000000000000..c0ac64364f5b --- /dev/null +++ b/libexec/rc/tests/svcj_test.sh @@ -0,0 +1,1178 @@ +#- +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2026 Alexander Leidinger +# +# 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. +# +# Service jail behaviour of run_rc_command(). Each case generates an rc.d +# script, drives it the way an operator would, and inspects the jail and the +# process that came out. +# +# Every case does that inside a chroot of its own, built in its ATF work +# directory, because the paths a service jail uses are absolute and would +# otherwise be the running system's: service(8) searches /etc/rc.d and +# ${local_startup} only, and a service jail re-enters its own script through +# it, so a fixture below the work directory is unreachable from inside the +# jail. The chroot is read-only nullfs mounts of the system directories plus +# a copied /etc, and rc.subr's "jail -c path=/" then roots the service jail +# at it. Nothing outside the work directory is written at any point. + +# svcj_svcname +# The service name for the current case. It becomes a jail name and a +# shell variable prefix, so it is reduced to alphanumerics. +svcj_svcname() +{ + echo "svcjt$(atf_get ident | tr -cd '[:alnum:]')" +} + +# svcj_chroot +# The root of this case's chroot, on the host. +svcj_chroot() +{ + echo "$(pwd)/svcjtestchroot" +} + +# svcj_rcpath +# This case's rc.d script, as seen from inside the chroot. The chroot's +# /etc is a copy, so using /etc/rc.d here cannot disturb the running +# system's, and an rcorder(8) run over /etc/rc.d/* -- which another test +# in this directory does -- never sees the fixture. +svcj_rcpath() +{ + echo "/etc/rc.d/$(svcj_svcname)" +} + +# svcj_workdir +# Where a case keeps its pid file and its markers, as seen from inside +# the chroot. +svcj_workdir() +{ + echo "/var/run/svcjt.$(svcj_svcname)" +} + +# svcj_hostrc / svcj_hostdir +# The same two paths as seen from outside, for the assertions. +svcj_hostrc() +{ + echo "$(svcj_chroot)$(svcj_rcpath)" +} + +svcj_hostdir() +{ + echo "$(svcj_chroot)$(svcj_workdir)" +} + +# svcj_require +# Requirements shared by every case: root, and room for one more jail. +svcj_require() +{ + local max cur + + if [ "$(id -u)" -ne 0 ]; then + atf_skip "creating a service jail requires root" + fi + # rc.subr bounds itself against children.max only when it is already + # jailed, so the check is made under the same condition. + if [ "$(sysctl -n security.jail.jailed)" -ne 0 ]; then + max=$(sysctl -n security.jail.children.max) + cur=$(sysctl -n security.jail.children.cur) + if [ "$max" -eq 0 ] || [ $((max - cur)) -eq 0 ]; then + atf_skip "no child jail available inside this jail" + fi + fi +} + +# svcj_mkchroot +# Build this case's chroot. Whether it can be built is decided by +# trying, not by asking whether this is a jail: a jail with allow.mount +# and the nullfs and devfs sub-options set can run these cases, and a +# machine whose kernel has no nullfs cannot, jailed or not. +svcj_mkchroot() +{ + local c d + + c=$(svcj_chroot) + mkdir -p "$c" || atf_fail "cannot create $c" + # This becomes a root directory, and the kernel checks search + # permission on it for every lookup an unprivileged process inside + # makes. At 0700 the ${name}_user cases fail in su(1). + chmod 0755 "$c" + : > "$(pwd)/svcj.mounts" + + for d in bin sbin lib libexec usr; do + mkdir -p "$c/$d" || atf_fail "cannot create $c/$d" + if ! mount -t nullfs -o ro "/$d" "$c/$d"; then + svcj_umount + atf_skip "cannot nullfs-mount /$d here" + fi + # Read-only is a safety property. A recursive delete of this + # tree with a mount still under it would run through the + # mount. + echo "$c/$d" >> "$(pwd)/svcj.mounts" + done + + mkdir -p "$c/dev" || atf_fail "cannot create $c/dev" + if ! mount -t devfs devfs "$c/dev"; then + svcj_umount + atf_skip "cannot mount devfs here" + fi + echo "$c/dev" >> "$(pwd)/svcj.mounts" + + mkdir -p "$c/etc" "$c/var/run" "$c/var/log" "$c/var/tmp" \ + "$c/var/empty" "$c/tmp" "$c/root" || atf_fail "cannot populate $c" + chmod 01777 "$c/tmp" "$c/var/tmp" + + # /etc is copied rather than mounted because the fixture, the pid + # files and rc.conf all live in it. + cp -a /etc/. "$c/etc/" || atf_fail "cannot copy /etc into $c" + rm -rf "$c/etc/rc.conf.d" + mkdir -p "$c/etc/rc.conf.d" || atf_fail "cannot create rc.conf.d" + echo 'hostname="svcjtest"' > "$c/etc/rc.conf" +} + +# svcj_umount +# Unmount what svcj_mkchroot mounted, in reverse. The verdict is what +# is still mounted afterwards, not what umount(8) returned. +svcj_umount() +{ + local f d + + f="$(pwd)/svcj.mounts" + [ -f "$f" ] || return 0 + for d in $(tail -r "$f"); do + umount "$d" 2>/dev/null || umount -f "$d" 2>/dev/null + done + if mount | grep -q -F " on $(svcj_chroot)"; then + return 1 + fi + : > "$f" + return 0 +} + +# svcj_fixture +# Build the chroot and install this case's rc.d script into it, reading +# the case-specific lines from standard input. Those lines go after +# load_rc_config, so that they win over rc.conf, and before +# run_rc_command. +svcj_fixture() +{ + local svc c wd + + svc=$(svcj_svcname) + + # Recorded for the cleanup routine. + echo "$svc" > "$(pwd)/svcj.name" + + svcj_mkchroot + c=$(svcj_chroot) + wd=$(svcj_workdir) + mkdir -p "$c$wd" || atf_fail "cannot create $c$wd" + chmod 0777 "$c$wd" + + { + echo "#!/bin/sh" + echo "# PROVIDE: $svc" + echo ". /etc/rc.subr" + echo "name=\"$svc\"" + echo "rcvar=\"${svc}_enable\"" + echo "load_rc_config \$name" + echo "pidfile=\"$wd/$svc.pid\"" + cat + echo "run_rc_command \"\$1\"" + } > "$(svcj_hostrc)" + chmod 0755 "$(svcj_hostrc)" +} + +# svcj_daemon_lines +# The stock daemon used by most cases: one sleep(1), pid in ${pidfile}. +svcj_daemon_lines() +{ + echo 'command="/usr/sbin/daemon"' + echo 'command_args="-p $pidfile -- /bin/sleep 300"' + echo 'procname="/bin/sleep"' +} + +# svcj_jid +# The jid of this case's service jail, empty when there is none. +svcj_jid() +{ + jls -j "svcj-$(svcj_svcname)" jid 2>/dev/null +} + +# svcj_pid +# The pid the daemon recorded, empty when there is none yet. daemon(8) +# writes the file from a child, so a read right after the rc.d script +# returns can lose the race; wait for it. +svcj_pid() +{ + local f pid junk + + f="$(svcj_hostdir)/$(svcj_svcname).pid" + svcj_wait_file "$f" || return 1 + read pid junk < "$f" + echo "$pid" +} + +# svcj_wait_file +# True once the file exists and has content. The bound is a +# deadline rather than a delay: it costs nothing on a machine that +# is quick, and 30s is far enough above what a loaded one needs +# that expiry means the thing waited for is not coming. +svcj_wait_file() +{ + local i + + i=0 + while [ $i -lt 300 ]; do + [ -s "$1" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + return 1 +} + +# svcj_gone +# True once the pid is no longer alive. +svcj_gone() +{ + local i + + i=0 + while [ $i -lt 300 ]; do + kill -0 "$1" 2>/dev/null || return 0 + sleep 0.1 + i=$((i + 1)) + done + return 1 +} + +svcj_cleanup() +{ + local svc c pid junk + + [ -f "$(pwd)/svcj.name" ] || return 0 + read svc junk < "$(pwd)/svcj.name" + c=$(svcj_chroot) + + jail -R "svcj-$svc" 2>/dev/null + if [ -f "$c/var/run/svcjt.$svc/$svc.pid" ]; then + read pid junk < "$c/var/run/svcjt.$svc/$svc.pid" + if [ -n "$pid" ]; then + kill -9 "$pid" 2>/dev/null + svcj_gone "$pid" + fi + fi + + if svcj_umount; then + rm -rf "$c" + else + # Leave the tree alone while anything is still mounted under + # it. Kyua's own work directory cleanup does not unmount + # either, so a recursive delete here would run through the + # mount. + echo "svcj_test: still mounted under $c, not removing it" >&2 + mount | grep -F " on $c" >&2 + return 1 + fi +} + +atf_test_case start_stop cleanup +start_stop_head() +{ + atf_set "descr" "A service with \${name}_svcj=YES starts inside" \ + "svcj-\${name}, and stop removes both the process and the jail" + atf_set "require.user" "root" +} +start_stop_body() +{ + local svc c rcs jid pid + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + + jid=$(svcj_jid) + [ -n "$jid" ] || atf_fail "no service jail svcj-$svc was created" + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + + # The daemon really is in that jail, and not merely on the host. + atf_check -o inline:"$jid\n" \ + /bin/sh -c "ps -o jid= -p $pid | tr -d ' '" + + # And that jail is rooted at this case's chroot. + atf_check -o inline:"$c\n" jls -j "svcj-$svc" path + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestop + svcj_gone "$pid" || atf_fail "the daemon survived stop" + [ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop" +} +start_stop_cleanup() +{ + svcj_cleanup +} + +atf_test_case all_enable cleanup +all_enable_head() +{ + atf_set "descr" "svcj_all_enable=YES puts a service that has no" \ + "\${name}_svcj of its own into a service jail" + atf_set "require.user" "root" +} +all_enable_body() +{ + local svc c rcs + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + svcj_all_enable="YES" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + + [ -n "$(svcj_jid)" ] || atf_fail "no service jail svcj-$svc" +} +all_enable_cleanup() +{ + svcj_cleanup +} + +atf_test_case all_enable_quiet cleanup +all_enable_quiet_head() +{ + atf_set "descr" "svcj_all_enable=YES does not make rc.subr complain" \ + "about an unset \${name}_svcj" + atf_set "require.user" "root" +} +all_enable_quiet_body() +{ + local svc c rcs + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + svcj_all_enable="YES" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + # The warning is the visible half of the defect above, and the half + # an administrator sees once per service on every boot. + atf_check -s exit:0 -o ignore -e not-match:'is not set properly' \ + /usr/sbin/chroot "$c" "$rcs" onestart +} +all_enable_quiet_cleanup() +{ + svcj_cleanup +} + +atf_test_case all_enable_rcconfd cleanup +all_enable_rcconfd_head() +{ + atf_set "descr" "svcj_all_enable=YES set in a configuration file" \ + "enables a service jail" + atf_set "require.user" "root" +} +all_enable_rcconfd_body() +{ + local svc c rcs + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + # The other svcj_all_enable cases set the variable inside the rc.d + # script. This one goes through load_rc_config and rc.conf.d. + echo 'svcj_all_enable="YES"' > "$c/etc/rc.conf.d/$svc" + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + + [ -n "$(svcj_jid)" ] || atf_fail "no service jail svcj-$svc" +} +all_enable_rcconfd_cleanup() +{ + svcj_cleanup +} + +atf_test_case stop_as_user cleanup +stop_as_user_head() +{ + atf_set "descr" "stop terminates the process of a service jail whose" \ + "\${name}_user is set" + atf_set "require.user" "root" +} +stop_as_user_body() +{ + local svc c rcs pid + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + ${svc}_user="nobody" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + atf_check -o inline:"nobody\n" \ + /bin/sh -c "ps -o user= -p $pid | tr -d ' '" + + # The signal is sent from the host, as ${name}_user, against a process + # in a subordinate jail. Since 8a5ceebece03 that needs either + # PRIV_SIGNAL_DIFFJAIL or allow.unprivileged_parent_tampering on the + # target jail, and a service jail is created with neither. + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestop + svcj_gone "$pid" || atf_fail "the daemon survived stop" + [ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop" +} +stop_as_user_cleanup() +{ + svcj_cleanup +} + +atf_test_case svcj_toggle_after_stop cleanup +svcj_toggle_after_stop_head() +{ + atf_set "descr" "\${name}_svcj may be changed between a stop and the" \ + "next start, and the service ends up jailed" + atf_set "require.user" "root" +} +svcj_toggle_after_stop_body() +{ + local svc c rcs pid + + svcj_require + svc=$(svcj_svcname) + + # ${name}_svcj is only meaningful for a service that is not running: + # it decides where the next start puts the process, and stop reads it + # again to decide where to send the signal. Changing it underneath a + # running service therefore has no supported answer, and this case + # pins the sequence that does -- stop, change, start. + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="NO" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + [ -z "$(svcj_jid)" ] || + atf_fail "a jail was created although ${svc}_svcj was NO" + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestop + svcj_gone "$pid" || atf_fail "the unjailed daemon survived stop" + + atf_check -s exit:0 sed -i "" "s/${svc}_svcj=\"NO\"/${svc}_svcj=\"YES\"/" \ + "$(svcj_hostrc)" + rm -f "$(svcj_hostdir)/$svc.pid" + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + [ -n "$(svcj_jid)" ] || + atf_fail "no jail although ${svc}_svcj is now YES" + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestop + svcj_gone "$pid" || atf_fail "the jailed daemon survived stop" + [ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop" +} +svcj_toggle_after_stop_cleanup() +{ + svcj_cleanup +} + +atf_test_case reload_as_user cleanup +reload_as_user_head() +{ + atf_set "descr" "reload signals the process of a service jail whose" \ + "\${name}_user is set" + atf_set "require.user" "root" +} +reload_as_user_body() +{ + local svc c rcs pid + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + extra_commands="reload" + ${svc}_svcj="YES" + ${svc}_user="nobody" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + + # reload builds its signal with the same helper as stop and fails for + # the same reason. sleep(1) has no handler for SIGHUP, so the process + # going away is the evidence that the signal was delivered. + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onereload + svcj_gone "$pid" || atf_fail "the daemon did not receive the signal" +} +reload_as_user_cleanup() +{ + svcj_cleanup +} + +atf_test_case restart_cmd cleanup +restart_cmd_head() +{ + atf_set "descr" "A service jail's own restart_cmd is executed" + atf_set "require.user" "root" +} +restart_cmd_body() +{ + local svc c rcs wd + + svcj_require + svc=$(svcj_svcname) + wd=$(svcj_workdir) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + restart_cmd="${svc}_restart" + ${svc}_restart() + { + : > "$wd/restart.ran" + } + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + + # A service jail's custom methods are dispatched through a case + # statement whose restart branch is empty, so the method is skipped + # and the exit status is still zero. + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onerestart + [ -f "$(svcj_hostdir)/restart.ran" ] || + atf_fail "restart_cmd was not run" +} +restart_cmd_cleanup() +{ + svcj_cleanup +} + +atf_test_case status_cmd cleanup +status_cmd_head() +{ + atf_set "descr" "A service jail's own status_cmd is executed" + atf_set "require.user" "root" +} +status_cmd_body() +{ + local svc c rcs wd + + svcj_require + svc=$(svcj_svcname) + wd=$(svcj_workdir) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + status_cmd="${svc}_status" + ${svc}_status() + { + : > "$wd/status.ran" + } + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestatus + [ -f "$(svcj_hostdir)/status.ran" ] || + atf_fail "status_cmd was not run" +} +status_cmd_cleanup() +{ + svcj_cleanup +} + +atf_test_case restart_cmd_when_stopped cleanup +restart_cmd_when_stopped_head() +{ + atf_set "descr" "restart on a stopped service that has its own" \ + "restart_cmd starts it, rather than failing in the absent jail" + atf_set "require.user" "root" +} +restart_cmd_when_stopped_body() +{ + local svc c rcs wd jid pid + + svcj_require + svc=$(svcj_svcname) + wd=$(svcj_workdir) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + restart_cmd="${svc}_restart" + ${svc}_restart() + { + : > "$wd/restart.ran" + } + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + # Nothing is running, so there is no service jail to enter. A + # restart is a stop and a start, and only the start half has + # anything to do. + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onerestart + + jid=$(svcj_jid) + [ -n "$jid" ] || atf_fail "restart did not create svcj-$svc" + pid=$(svcj_pid) || atf_fail "restart did not start the daemon" + atf_check -o inline:"$jid\n" \ + /bin/sh -c "ps -o jid= -p $pid | tr -d ' '" + + # The script's own method is for restarting something that runs. + if [ -f "$(svcj_hostdir)/restart.ran" ]; then + atf_fail "restart_cmd ran although nothing was running" + fi +} +restart_cmd_when_stopped_cleanup() +{ + svcj_cleanup +} + +atf_test_case status_cmd_when_stopped cleanup +status_cmd_when_stopped_head() +{ + atf_set "descr" "status on a stopped service that has its own" \ + "status_cmd fails and creates no service jail" + atf_set "require.user" "root" +} +status_cmd_when_stopped_body() +{ + local svc c rcs wd + + svcj_require + svc=$(svcj_svcname) + wd=$(svcj_workdir) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + status_cmd="${svc}_status" + ${svc}_status() + { + : > "$wd/status.ran" + } + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + # Unlike restart there is nothing else to do here: a service that is + # not running has no status to report from inside a jail that does + # not exist. + atf_check -s not-exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" \ + "$rcs" onestatus + [ -z "$(svcj_jid)" ] || atf_fail "status created svcj-$svc" + if [ -f "$(svcj_hostdir)/status.ran" ]; then + atf_fail "status_cmd ran outside the service jail" + fi +} +status_cmd_when_stopped_cleanup() +{ + svcj_cleanup +} + +atf_test_case stop_removes_dead_jail cleanup +stop_removes_dead_jail_head() +{ + atf_set "descr" "A service jail is gone once its only process is," \ + "and stop then behaves like an unjailed service" + atf_set "require.user" "root" +} +stop_removes_dead_jail_body() +{ + local svc c rcs pid + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + + # A daemon that died on its own. + atf_check -s exit:0 kill -9 "$pid" + svcj_gone "$pid" || atf_fail "the daemon could not be killed" + + # The jail is already gone at this point, and not because stop did + # anything: jail(8) clears the temporary "persist" parameter once + # exec.start has run, so a service jail is reaped together with its + # last process. stop then takes its "not running?" branch and exits + # 1, which is the same answer it gives for an unjailed service. + [ -z "$(svcj_jid)" ] || + atf_fail "svcj-$svc outlived its only process" + + atf_check -s exit:1 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestop + [ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop" +} +stop_removes_dead_jail_cleanup() +{ + svcj_cleanup +} + +atf_test_case stop_orphan_jail_with_child cleanup +stop_orphan_jail_with_child_head() +{ + atf_set "descr" "stop removes the service jail when the process it" \ + "tracks is gone but another process is still in the jail" + atf_set "require.user" "root" +} +stop_orphan_jail_with_child_body() +{ + local svc c rcs wd pid + + svcj_require + svc=$(svcj_svcname) + wd=$(svcj_workdir) + + # The service leaves a second process behind in the jail, which is + # what an ordinary daemon with a worker or a helper does. Only the + # first one is in ${pidfile}. + svcj_fixture <<-EOF + command="/usr/sbin/daemon" + command_args="-p \$pidfile -- /bin/sh $wd/child.sh" + procname="/bin/sleep" + ${svc}_svcj="YES" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + cat > "$(svcj_hostdir)/child.sh" <<-EOF + sleep 600 >/dev/null 2>&1 & + echo started > $wd/child.started + exec sleep 300 + EOF + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + + # ${pidfile} is written before the service has run, so the helper + # is not there yet. Killing the tracked process first would take + # the jail with it and measure the single-process case instead. + svcj_wait_file "$(svcj_hostdir)/child.started" || + atf_fail "the helper never started" + + atf_check -s exit:0 kill -9 "$pid" + svcj_gone "$pid" || atf_fail "the tracked process could not be killed" + + # Unlike the single-process case, the jail is still alive here: the + # helper holds it open. So this is the shape in which an orphaned + # service jail is actually reachable. + [ -n "$(svcj_jid)" ] || + atf_fail "the helper did not keep svcj-$svc alive; " \ + "this case is no longer testing what it says" + + /usr/sbin/chroot "$c" "$rcs" onestop >/dev/null 2>&1 + + # stop returns from its "not running?" branch before reaching the + # jail removal, so the jail and its helper survive -- and the next + # start fails because svcj-${name} already exists. + [ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop" +} +stop_orphan_jail_with_child_cleanup() +{ + svcj_cleanup +} + +atf_test_case audit_user cleanup +audit_user_head() +{ + atf_set "descr" "\${name}_audit_user works for a service in a" \ + "service jail" + atf_set "require.user" "root" +} +audit_user_body() +{ + local svc c rcs + + svcj_require + svc=$(svcj_svcname) + + # Control: the same setaudit(8) invocation has to work outside, or + # what the service jail does with it says nothing. Where it does not + # -- no audit support, or an enclosing jail without allow.setaudit -- + # it skips. + setaudit -U -a root /usr/bin/true 2>/dev/null || + atf_skip "setaudit(8) does not work here, so the service jail" \ + "cannot be asked about it" + + # setaudit(8) is prefixed to the command inside the jail, where + # PRIV_AUDIT_GETAUDIT and PRIV_AUDIT_SETAUDIT need allow.setaudit. + # That is not in a jail's default allow set, so the service can only + # work if the option below asks for it. + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + ${svc}_svcj_options="setaudit" + ${svc}_audit_user="root" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + svcj_pid > /dev/null || atf_fail "the daemon did not start" +} +audit_user_cleanup() +{ + svcj_cleanup +} + +atf_test_case nice_negative cleanup +nice_negative_head() +{ + atf_set "descr" "A negative \${name}_nice is dropped for a service in" \ + "a service jail" + atf_set "require.user" "root" +} +nice_negative_body() +{ + local svc c rcs pid + + svcj_require + svc=$(svcj_svcname) + + svcj_fixture <<-EOF + $(svcj_daemon_lines) + ${svc}_svcj="YES" + ${svc}_nice="-5" + EOF + c=$(svcj_chroot) + rcs=$(svcj_rcpath) + + atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \ + onestart + pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file" + + # Raising a process' priority needs PRIV_SCHED_SETPRIORITY, which a + # jail is never granted. nice(1) only warns when setpriority(2) + # fails and execs the command regardless, so the request is dropped + # and the service comes up at whatever its login class gives it -- + # the behaviour rc.conf(5) documents. Asserted as "not the value + # asked for" rather than as a literal, because the login class + # supplies the priority actually in use. + atf_check -o match:'^-?[0-9]+$' -o not-inline:"-5\n" \ + /bin/sh -c "ps -o nice= -p $pid | tr -d ' '" +} +nice_negative_cleanup() +{ + svcj_cleanup +} + +atf_test_case oomprotect cleanup +oomprotect_head() +{ + atf_set "descr" "\${name}_oomprotect protects a service that runs in" \ *** 231 LINES SKIPPED ***