xref: /freebsd/libexec/rc/tests/svcj_test.sh (revision b5dcd8f9d6ae702adbc1627839b5235a28839370)
1#-
2# SPDX-License-Identifier: BSD-2-Clause
3#
4# Copyright (c) 2026 Alexander Leidinger <netchild@FreeBSD.org>
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10#    notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright
12#    notice, this list of conditions and the following disclaimer in the
13#    documentation and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25# SUCH DAMAGE.
26#
27# Service jail behaviour of run_rc_command().  Each case generates an rc.d
28# script, drives it the way an operator would, and inspects the jail and the
29# process that came out.
30#
31# Every case does that inside a chroot of its own, built in its ATF work
32# directory, because the paths a service jail uses are absolute and would
33# otherwise be the running system's: service(8) searches /etc/rc.d and
34# ${local_startup} only, and a service jail re-enters its own script through
35# it, so a fixture below the work directory is unreachable from inside the
36# jail.  The chroot is read-only nullfs mounts of the system directories plus
37# a copied /etc, and rc.subr's "jail -c path=/" then roots the service jail
38# at it.  Nothing outside the work directory is written at any point.
39
40# svcj_svcname
41#	The service name for the current case.  It becomes a jail name and a
42#	shell variable prefix, so it is reduced to alphanumerics.
43svcj_svcname()
44{
45	echo "svcjt$(atf_get ident | tr -cd '[:alnum:]')"
46}
47
48# svcj_chroot
49#	The root of this case's chroot, on the host.
50svcj_chroot()
51{
52	echo "$(pwd)/svcjtestchroot"
53}
54
55# svcj_rcpath
56#	This case's rc.d script, as seen from inside the chroot.  The chroot's
57#	/etc is a copy, so using /etc/rc.d here cannot disturb the running
58#	system's, and an rcorder(8) run over /etc/rc.d/* -- which another test
59#	in this directory does -- never sees the fixture.
60svcj_rcpath()
61{
62	echo "/etc/rc.d/$(svcj_svcname)"
63}
64
65# svcj_workdir
66#	Where a case keeps its pid file and its markers, as seen from inside
67#	the chroot.
68svcj_workdir()
69{
70	echo "/var/run/svcjt.$(svcj_svcname)"
71}
72
73# svcj_hostrc / svcj_hostdir
74#	The same two paths as seen from outside, for the assertions.
75svcj_hostrc()
76{
77	echo "$(svcj_chroot)$(svcj_rcpath)"
78}
79
80svcj_hostdir()
81{
82	echo "$(svcj_chroot)$(svcj_workdir)"
83}
84
85# svcj_require
86#	Requirements shared by every case: root, and room for one more jail.
87svcj_require()
88{
89	local max cur
90
91	if [ "$(id -u)" -ne 0 ]; then
92		atf_skip "creating a service jail requires root"
93	fi
94	# rc.subr bounds itself against children.max only when it is already
95	# jailed, so the check is made under the same condition.
96	if [ "$(sysctl -n security.jail.jailed)" -ne 0 ]; then
97		max=$(sysctl -n security.jail.children.max)
98		cur=$(sysctl -n security.jail.children.cur)
99		if [ "$max" -eq 0 ] || [ $((max - cur)) -eq 0 ]; then
100			atf_skip "no child jail available inside this jail"
101		fi
102	fi
103}
104
105# svcj_mkchroot
106#	Build this case's chroot.  Whether it can be built is decided by
107#	trying, not by asking whether this is a jail: a jail with allow.mount
108#	and the nullfs and devfs sub-options set can run these cases, and a
109#	machine whose kernel has no nullfs cannot, jailed or not.
110svcj_mkchroot()
111{
112	local c d
113
114	c=$(svcj_chroot)
115	mkdir -p "$c" || atf_fail "cannot create $c"
116	# This becomes a root directory, and the kernel checks search
117	# permission on it for every lookup an unprivileged process inside
118	# makes.  At 0700 the ${name}_user cases fail in su(1).
119	chmod 0755 "$c"
120	: > "$(pwd)/svcj.mounts"
121
122	for d in bin sbin lib libexec usr; do
123		mkdir -p "$c/$d" || atf_fail "cannot create $c/$d"
124		if ! mount -t nullfs -o ro "/$d" "$c/$d"; then
125			svcj_umount
126			atf_skip "cannot nullfs-mount /$d here"
127		fi
128		# Read-only is a safety property.  A recursive delete of this
129		# tree with a mount still under it would run through the
130		# mount.
131		echo "$c/$d" >> "$(pwd)/svcj.mounts"
132	done
133
134	mkdir -p "$c/dev" || atf_fail "cannot create $c/dev"
135	if ! mount -t devfs devfs "$c/dev"; then
136		svcj_umount
137		atf_skip "cannot mount devfs here"
138	fi
139	echo "$c/dev" >> "$(pwd)/svcj.mounts"
140
141	mkdir -p "$c/etc" "$c/var/run" "$c/var/log" "$c/var/tmp" \
142	    "$c/var/empty" "$c/tmp" "$c/root" || atf_fail "cannot populate $c"
143	chmod 01777 "$c/tmp" "$c/var/tmp"
144
145	# /etc is copied rather than mounted because the fixture, the pid
146	# files and rc.conf all live in it.
147	cp -a /etc/. "$c/etc/" || atf_fail "cannot copy /etc into $c"
148	rm -rf "$c/etc/rc.conf.d"
149	mkdir -p "$c/etc/rc.conf.d" || atf_fail "cannot create rc.conf.d"
150	echo 'hostname="svcjtest"' > "$c/etc/rc.conf"
151}
152
153# svcj_umount
154#	Unmount what svcj_mkchroot mounted, in reverse.  The verdict is what
155#	is still mounted afterwards, not what umount(8) returned.
156svcj_umount()
157{
158	local f d
159
160	f="$(pwd)/svcj.mounts"
161	[ -f "$f" ] || return 0
162	for d in $(tail -r "$f"); do
163		umount "$d" 2>/dev/null || umount -f "$d" 2>/dev/null
164	done
165	if mount | grep -q -F " on $(svcj_chroot)"; then
166		return 1
167	fi
168	: > "$f"
169	return 0
170}
171
172# svcj_fixture
173#	Build the chroot and install this case's rc.d script into it, reading
174#	the case-specific lines from standard input.  Those lines go after
175#	load_rc_config, so that they win over rc.conf, and before
176#	run_rc_command.
177svcj_fixture()
178{
179	local svc c wd
180
181	svc=$(svcj_svcname)
182
183	# Recorded for the cleanup routine.
184	echo "$svc" > "$(pwd)/svcj.name"
185
186	svcj_mkchroot
187	c=$(svcj_chroot)
188	wd=$(svcj_workdir)
189	mkdir -p "$c$wd" || atf_fail "cannot create $c$wd"
190	chmod 0777 "$c$wd"
191
192	{
193		echo "#!/bin/sh"
194		echo "# PROVIDE: $svc"
195		echo ". /etc/rc.subr"
196		echo "name=\"$svc\""
197		echo "rcvar=\"${svc}_enable\""
198		echo "load_rc_config \$name"
199		echo "pidfile=\"$wd/$svc.pid\""
200		cat
201		echo "run_rc_command \"\$1\""
202	} > "$(svcj_hostrc)"
203	chmod 0755 "$(svcj_hostrc)"
204}
205
206# svcj_daemon_lines
207#	The stock daemon used by most cases: one sleep(1), pid in ${pidfile}.
208svcj_daemon_lines()
209{
210	echo 'command="/usr/sbin/daemon"'
211	echo 'command_args="-p $pidfile -- /bin/sleep 300"'
212	echo 'procname="/bin/sleep"'
213}
214
215# svcj_jid
216#	The jid of this case's service jail, empty when there is none.
217svcj_jid()
218{
219	jls -j "svcj-$(svcj_svcname)" jid 2>/dev/null
220}
221
222# svcj_pid
223#	The pid the daemon recorded, empty when there is none yet.  daemon(8)
224#	writes the file from a child, so a read right after the rc.d script
225#	returns can lose the race; wait for it.
226svcj_pid()
227{
228	local f pid junk
229
230	f="$(svcj_hostdir)/$(svcj_svcname).pid"
231	svcj_wait_file "$f" || return 1
232	read pid junk < "$f"
233	echo "$pid"
234}
235
236# svcj_wait_file <path>
237#	True once the file exists and has content.  The bound is a
238#	deadline rather than a delay: it costs nothing on a machine that
239#	is quick, and 30s is far enough above what a loaded one needs
240#	that expiry means the thing waited for is not coming.
241svcj_wait_file()
242{
243	local i
244
245	i=0
246	while [ $i -lt 300 ]; do
247		[ -s "$1" ] && return 0
248		sleep 0.1
249		i=$((i + 1))
250	done
251	return 1
252}
253
254# svcj_gone <pid>
255#	True once the pid is no longer alive.
256svcj_gone()
257{
258	local i
259
260	i=0
261	while [ $i -lt 300 ]; do
262		kill -0 "$1" 2>/dev/null || return 0
263		sleep 0.1
264		i=$((i + 1))
265	done
266	return 1
267}
268
269svcj_cleanup()
270{
271	local svc c pid junk
272
273	[ -f "$(pwd)/svcj.name" ] || return 0
274	read svc junk < "$(pwd)/svcj.name"
275	c=$(svcj_chroot)
276
277	jail -R "svcj-$svc" 2>/dev/null
278	if [ -f "$c/var/run/svcjt.$svc/$svc.pid" ]; then
279		read pid junk < "$c/var/run/svcjt.$svc/$svc.pid"
280		if [ -n "$pid" ]; then
281			kill -9 "$pid" 2>/dev/null
282			svcj_gone "$pid"
283		fi
284	fi
285
286	if svcj_umount; then
287		rm -rf "$c"
288	else
289		# Leave the tree alone while anything is still mounted under
290		# it.  Kyua's own work directory cleanup does not unmount
291		# either, so a recursive delete here would run through the
292		# mount.
293		echo "svcj_test: still mounted under $c, not removing it" >&2
294		mount | grep -F " on $c" >&2
295		return 1
296	fi
297}
298
299atf_test_case start_stop cleanup
300start_stop_head()
301{
302	atf_set "descr" "A service with \${name}_svcj=YES starts inside" \
303	    "svcj-\${name}, and stop removes both the process and the jail"
304	atf_set "require.user" "root"
305}
306start_stop_body()
307{
308	local svc c rcs jid pid
309
310	svcj_require
311	svc=$(svcj_svcname)
312
313	svcj_fixture <<-EOF
314	$(svcj_daemon_lines)
315	${svc}_svcj="YES"
316	EOF
317	c=$(svcj_chroot)
318	rcs=$(svcj_rcpath)
319
320	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
321	    onestart
322
323	jid=$(svcj_jid)
324	[ -n "$jid" ] || atf_fail "no service jail svcj-$svc was created"
325	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
326
327	# The daemon really is in that jail, and not merely on the host.
328	atf_check -o inline:"$jid\n" \
329	    /bin/sh -c "ps -o jid= -p $pid | tr -d ' '"
330
331	# And that jail is rooted at this case's chroot.
332	atf_check -o inline:"$c\n" jls -j "svcj-$svc" path
333
334	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
335	    onestop
336	svcj_gone "$pid" || atf_fail "the daemon survived stop"
337	[ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop"
338}
339start_stop_cleanup()
340{
341	svcj_cleanup
342}
343
344atf_test_case all_enable cleanup
345all_enable_head()
346{
347	atf_set "descr" "svcj_all_enable=YES puts a service that has no" \
348	    "\${name}_svcj of its own into a service jail"
349	atf_set "require.user" "root"
350}
351all_enable_body()
352{
353	local svc c rcs
354
355	svcj_require
356	svc=$(svcj_svcname)
357
358	svcj_fixture <<-EOF
359	$(svcj_daemon_lines)
360	svcj_all_enable="YES"
361	EOF
362	c=$(svcj_chroot)
363	rcs=$(svcj_rcpath)
364
365	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
366	    onestart
367
368	[ -n "$(svcj_jid)" ] || atf_fail "no service jail svcj-$svc"
369}
370all_enable_cleanup()
371{
372	svcj_cleanup
373}
374
375atf_test_case all_enable_quiet cleanup
376all_enable_quiet_head()
377{
378	atf_set "descr" "svcj_all_enable=YES does not make rc.subr complain" \
379	    "about an unset \${name}_svcj"
380	atf_set "require.user" "root"
381}
382all_enable_quiet_body()
383{
384	local svc c rcs
385
386	svcj_require
387	svc=$(svcj_svcname)
388
389	svcj_fixture <<-EOF
390	$(svcj_daemon_lines)
391	svcj_all_enable="YES"
392	EOF
393	c=$(svcj_chroot)
394	rcs=$(svcj_rcpath)
395
396	# The warning is the visible half of the defect above, and the half
397	# an administrator sees once per service on every boot.
398	atf_check -s exit:0 -o ignore -e not-match:'is not set properly' \
399	    /usr/sbin/chroot "$c" "$rcs" onestart
400}
401all_enable_quiet_cleanup()
402{
403	svcj_cleanup
404}
405
406atf_test_case all_enable_rcconfd cleanup
407all_enable_rcconfd_head()
408{
409	atf_set "descr" "svcj_all_enable=YES set in a configuration file" \
410	    "enables a service jail"
411	atf_set "require.user" "root"
412}
413all_enable_rcconfd_body()
414{
415	local svc c rcs
416
417	svcj_require
418	svc=$(svcj_svcname)
419
420	svcj_fixture <<-EOF
421	$(svcj_daemon_lines)
422	EOF
423	c=$(svcj_chroot)
424	rcs=$(svcj_rcpath)
425
426	# The other svcj_all_enable cases set the variable inside the rc.d
427	# script.  This one goes through load_rc_config and rc.conf.d.
428	echo 'svcj_all_enable="YES"' > "$c/etc/rc.conf.d/$svc"
429
430	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
431	    onestart
432
433	[ -n "$(svcj_jid)" ] || atf_fail "no service jail svcj-$svc"
434}
435all_enable_rcconfd_cleanup()
436{
437	svcj_cleanup
438}
439
440atf_test_case stop_as_user cleanup
441stop_as_user_head()
442{
443	atf_set "descr" "stop terminates the process of a service jail whose" \
444	    "\${name}_user is set"
445	atf_set "require.user" "root"
446}
447stop_as_user_body()
448{
449	local svc c rcs pid
450
451	svcj_require
452	svc=$(svcj_svcname)
453
454	svcj_fixture <<-EOF
455	$(svcj_daemon_lines)
456	${svc}_svcj="YES"
457	${svc}_user="nobody"
458	EOF
459	c=$(svcj_chroot)
460	rcs=$(svcj_rcpath)
461
462	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
463	    onestart
464	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
465	atf_check -o inline:"nobody\n" \
466	    /bin/sh -c "ps -o user= -p $pid | tr -d ' '"
467
468	# The signal is sent from the host, as ${name}_user, against a process
469	# in a subordinate jail.  Since 8a5ceebece03 that needs either
470	# PRIV_SIGNAL_DIFFJAIL or allow.unprivileged_parent_tampering on the
471	# target jail, and a service jail is created with neither.
472	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
473	    onestop
474	svcj_gone "$pid" || atf_fail "the daemon survived stop"
475	[ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop"
476}
477stop_as_user_cleanup()
478{
479	svcj_cleanup
480}
481
482atf_test_case svcj_toggle_after_stop cleanup
483svcj_toggle_after_stop_head()
484{
485	atf_set "descr" "\${name}_svcj may be changed between a stop and the" \
486	    "next start, and the service ends up jailed"
487	atf_set "require.user" "root"
488}
489svcj_toggle_after_stop_body()
490{
491	local svc c rcs pid
492
493	svcj_require
494	svc=$(svcj_svcname)
495
496	# ${name}_svcj is only meaningful for a service that is not running:
497	# it decides where the next start puts the process, and stop reads it
498	# again to decide where to send the signal.  Changing it underneath a
499	# running service therefore has no supported answer, and this case
500	# pins the sequence that does -- stop, change, start.
501	svcj_fixture <<-EOF
502	$(svcj_daemon_lines)
503	${svc}_svcj="NO"
504	EOF
505	c=$(svcj_chroot)
506	rcs=$(svcj_rcpath)
507
508	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
509	    onestart
510	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
511	[ -z "$(svcj_jid)" ] ||
512	    atf_fail "a jail was created although ${svc}_svcj was NO"
513
514	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
515	    onestop
516	svcj_gone "$pid" || atf_fail "the unjailed daemon survived stop"
517
518	atf_check -s exit:0 sed -i "" "s/${svc}_svcj=\"NO\"/${svc}_svcj=\"YES\"/" \
519	    "$(svcj_hostrc)"
520	rm -f "$(svcj_hostdir)/$svc.pid"
521
522	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
523	    onestart
524	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
525	[ -n "$(svcj_jid)" ] ||
526	    atf_fail "no jail although ${svc}_svcj is now YES"
527
528	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
529	    onestop
530	svcj_gone "$pid" || atf_fail "the jailed daemon survived stop"
531	[ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop"
532}
533svcj_toggle_after_stop_cleanup()
534{
535	svcj_cleanup
536}
537
538atf_test_case reload_as_user cleanup
539reload_as_user_head()
540{
541	atf_set "descr" "reload signals the process of a service jail whose" \
542	    "\${name}_user is set"
543	atf_set "require.user" "root"
544}
545reload_as_user_body()
546{
547	local svc c rcs pid
548
549	svcj_require
550	svc=$(svcj_svcname)
551
552	svcj_fixture <<-EOF
553	$(svcj_daemon_lines)
554	extra_commands="reload"
555	${svc}_svcj="YES"
556	${svc}_user="nobody"
557	EOF
558	c=$(svcj_chroot)
559	rcs=$(svcj_rcpath)
560
561	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
562	    onestart
563	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
564
565	# reload builds its signal with the same helper as stop and fails for
566	# the same reason.  sleep(1) has no handler for SIGHUP, so the process
567	# going away is the evidence that the signal was delivered.
568	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
569	    onereload
570	svcj_gone "$pid" || atf_fail "the daemon did not receive the signal"
571}
572reload_as_user_cleanup()
573{
574	svcj_cleanup
575}
576
577atf_test_case restart_cmd cleanup
578restart_cmd_head()
579{
580	atf_set "descr" "A service jail's own restart_cmd is executed"
581	atf_set "require.user" "root"
582}
583restart_cmd_body()
584{
585	local svc c rcs wd
586
587	svcj_require
588	svc=$(svcj_svcname)
589	wd=$(svcj_workdir)
590
591	svcj_fixture <<-EOF
592	$(svcj_daemon_lines)
593	${svc}_svcj="YES"
594	restart_cmd="${svc}_restart"
595	${svc}_restart()
596	{
597		: > "$wd/restart.ran"
598	}
599	EOF
600	c=$(svcj_chroot)
601	rcs=$(svcj_rcpath)
602
603	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
604	    onestart
605
606	# A service jail's custom methods are dispatched through a case
607	# statement whose restart branch is empty, so the method is skipped
608	# and the exit status is still zero.
609	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
610	    onerestart
611	[ -f "$(svcj_hostdir)/restart.ran" ] ||
612	    atf_fail "restart_cmd was not run"
613}
614restart_cmd_cleanup()
615{
616	svcj_cleanup
617}
618
619atf_test_case status_cmd cleanup
620status_cmd_head()
621{
622	atf_set "descr" "A service jail's own status_cmd is executed"
623	atf_set "require.user" "root"
624}
625status_cmd_body()
626{
627	local svc c rcs wd
628
629	svcj_require
630	svc=$(svcj_svcname)
631	wd=$(svcj_workdir)
632
633	svcj_fixture <<-EOF
634	$(svcj_daemon_lines)
635	${svc}_svcj="YES"
636	status_cmd="${svc}_status"
637	${svc}_status()
638	{
639		: > "$wd/status.ran"
640	}
641	EOF
642	c=$(svcj_chroot)
643	rcs=$(svcj_rcpath)
644
645	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
646	    onestart
647
648	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
649	    onestatus
650	[ -f "$(svcj_hostdir)/status.ran" ] ||
651	    atf_fail "status_cmd was not run"
652}
653status_cmd_cleanup()
654{
655	svcj_cleanup
656}
657
658atf_test_case restart_cmd_when_stopped cleanup
659restart_cmd_when_stopped_head()
660{
661	atf_set "descr" "restart on a stopped service that has its own" \
662	    "restart_cmd starts it, rather than failing in the absent jail"
663	atf_set "require.user" "root"
664}
665restart_cmd_when_stopped_body()
666{
667	local svc c rcs wd jid pid
668
669	svcj_require
670	svc=$(svcj_svcname)
671	wd=$(svcj_workdir)
672
673	svcj_fixture <<-EOF
674	$(svcj_daemon_lines)
675	${svc}_svcj="YES"
676	restart_cmd="${svc}_restart"
677	${svc}_restart()
678	{
679		: > "$wd/restart.ran"
680	}
681	EOF
682	c=$(svcj_chroot)
683	rcs=$(svcj_rcpath)
684
685	# Nothing is running, so there is no service jail to enter.  A
686	# restart is a stop and a start, and only the start half has
687	# anything to do.
688	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
689	    onerestart
690
691	jid=$(svcj_jid)
692	[ -n "$jid" ] || atf_fail "restart did not create svcj-$svc"
693	pid=$(svcj_pid) || atf_fail "restart did not start the daemon"
694	atf_check -o inline:"$jid\n" \
695	    /bin/sh -c "ps -o jid= -p $pid | tr -d ' '"
696
697	# The script's own method is for restarting something that runs.
698	if [ -f "$(svcj_hostdir)/restart.ran" ]; then
699		atf_fail "restart_cmd ran although nothing was running"
700	fi
701}
702restart_cmd_when_stopped_cleanup()
703{
704	svcj_cleanup
705}
706
707atf_test_case status_cmd_when_stopped cleanup
708status_cmd_when_stopped_head()
709{
710	atf_set "descr" "status on a stopped service that has its own" \
711	    "status_cmd fails and creates no service jail"
712	atf_set "require.user" "root"
713}
714status_cmd_when_stopped_body()
715{
716	local svc c rcs wd
717
718	svcj_require
719	svc=$(svcj_svcname)
720	wd=$(svcj_workdir)
721
722	svcj_fixture <<-EOF
723	$(svcj_daemon_lines)
724	${svc}_svcj="YES"
725	status_cmd="${svc}_status"
726	${svc}_status()
727	{
728		: > "$wd/status.ran"
729	}
730	EOF
731	c=$(svcj_chroot)
732	rcs=$(svcj_rcpath)
733
734	# Unlike restart there is nothing else to do here: a service that is
735	# not running has no status to report from inside a jail that does
736	# not exist.
737	atf_check -s not-exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" \
738	    "$rcs" onestatus
739	[ -z "$(svcj_jid)" ] || atf_fail "status created svcj-$svc"
740	if [ -f "$(svcj_hostdir)/status.ran" ]; then
741		atf_fail "status_cmd ran outside the service jail"
742	fi
743}
744status_cmd_when_stopped_cleanup()
745{
746	svcj_cleanup
747}
748
749atf_test_case stop_removes_dead_jail cleanup
750stop_removes_dead_jail_head()
751{
752	atf_set "descr" "A service jail is gone once its only process is," \
753	    "and stop then behaves like an unjailed service"
754	atf_set "require.user" "root"
755}
756stop_removes_dead_jail_body()
757{
758	local svc c rcs pid
759
760	svcj_require
761	svc=$(svcj_svcname)
762
763	svcj_fixture <<-EOF
764	$(svcj_daemon_lines)
765	${svc}_svcj="YES"
766	EOF
767	c=$(svcj_chroot)
768	rcs=$(svcj_rcpath)
769
770	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
771	    onestart
772	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
773
774	# A daemon that died on its own.
775	atf_check -s exit:0 kill -9 "$pid"
776	svcj_gone "$pid" || atf_fail "the daemon could not be killed"
777
778	# The jail is already gone at this point, and not because stop did
779	# anything: jail(8) clears the temporary "persist" parameter once
780	# exec.start has run, so a service jail is reaped together with its
781	# last process.  stop then takes its "not running?" branch and exits
782	# 1, which is the same answer it gives for an unjailed service.
783	[ -z "$(svcj_jid)" ] ||
784	    atf_fail "svcj-$svc outlived its only process"
785
786	atf_check -s exit:1 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
787	    onestop
788	[ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop"
789}
790stop_removes_dead_jail_cleanup()
791{
792	svcj_cleanup
793}
794
795atf_test_case stop_orphan_jail_with_child cleanup
796stop_orphan_jail_with_child_head()
797{
798	atf_set "descr" "stop removes the service jail when the process it" \
799	    "tracks is gone but another process is still in the jail"
800	atf_set "require.user" "root"
801}
802stop_orphan_jail_with_child_body()
803{
804	local svc c rcs wd pid
805
806	svcj_require
807	svc=$(svcj_svcname)
808	wd=$(svcj_workdir)
809
810	# The service leaves a second process behind in the jail, which is
811	# what an ordinary daemon with a worker or a helper does.  Only the
812	# first one is in ${pidfile}.
813	svcj_fixture <<-EOF
814	command="/usr/sbin/daemon"
815	command_args="-p \$pidfile -- /bin/sh $wd/child.sh"
816	procname="/bin/sleep"
817	${svc}_svcj="YES"
818	EOF
819	c=$(svcj_chroot)
820	rcs=$(svcj_rcpath)
821	cat > "$(svcj_hostdir)/child.sh" <<-EOF
822	sleep 600 >/dev/null 2>&1 &
823	echo started > $wd/child.started
824	exec sleep 300
825	EOF
826
827	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
828	    onestart
829	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
830
831	# ${pidfile} is written before the service has run, so the helper
832	# is not there yet.  Killing the tracked process first would take
833	# the jail with it and measure the single-process case instead.
834	svcj_wait_file "$(svcj_hostdir)/child.started" ||
835	    atf_fail "the helper never started"
836
837	atf_check -s exit:0 kill -9 "$pid"
838	svcj_gone "$pid" || atf_fail "the tracked process could not be killed"
839
840	# Unlike the single-process case, the jail is still alive here: the
841	# helper holds it open.  So this is the shape in which an orphaned
842	# service jail is actually reachable.
843	[ -n "$(svcj_jid)" ] ||
844	    atf_fail "the helper did not keep svcj-$svc alive; " \
845	        "this case is no longer testing what it says"
846
847	/usr/sbin/chroot "$c" "$rcs" onestop >/dev/null 2>&1
848
849	# stop returns from its "not running?" branch before reaching the
850	# jail removal, so the jail and its helper survive -- and the next
851	# start fails because svcj-${name} already exists.
852	[ -z "$(svcj_jid)" ] || atf_fail "svcj-$svc survived stop"
853}
854stop_orphan_jail_with_child_cleanup()
855{
856	svcj_cleanup
857}
858
859atf_test_case audit_user cleanup
860audit_user_head()
861{
862	atf_set "descr" "\${name}_audit_user works for a service in a" \
863	    "service jail"
864	atf_set "require.user" "root"
865}
866audit_user_body()
867{
868	local svc c rcs
869
870	svcj_require
871	svc=$(svcj_svcname)
872
873	# Control: the same setaudit(8) invocation has to work outside, or
874	# what the service jail does with it says nothing.  Where it does not
875	# -- no audit support, or an enclosing jail without allow.setaudit --
876	# it skips.
877	setaudit -U -a root /usr/bin/true 2>/dev/null ||
878	    atf_skip "setaudit(8) does not work here, so the service jail" \
879	        "cannot be asked about it"
880
881	# setaudit(8) is prefixed to the command inside the jail, where
882	# PRIV_AUDIT_GETAUDIT and PRIV_AUDIT_SETAUDIT need allow.setaudit.
883	# That is not in a jail's default allow set, so the service can only
884	# work if the option below asks for it.
885	svcj_fixture <<-EOF
886	$(svcj_daemon_lines)
887	${svc}_svcj="YES"
888	${svc}_svcj_options="setaudit"
889	${svc}_audit_user="root"
890	EOF
891	c=$(svcj_chroot)
892	rcs=$(svcj_rcpath)
893
894	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
895	    onestart
896	svcj_pid > /dev/null || atf_fail "the daemon did not start"
897}
898audit_user_cleanup()
899{
900	svcj_cleanup
901}
902
903atf_test_case nice_negative cleanup
904nice_negative_head()
905{
906	atf_set "descr" "A negative \${name}_nice is dropped for a service in" \
907	    "a service jail"
908	atf_set "require.user" "root"
909}
910nice_negative_body()
911{
912	local svc c rcs pid
913
914	svcj_require
915	svc=$(svcj_svcname)
916
917	svcj_fixture <<-EOF
918	$(svcj_daemon_lines)
919	${svc}_svcj="YES"
920	${svc}_nice="-5"
921	EOF
922	c=$(svcj_chroot)
923	rcs=$(svcj_rcpath)
924
925	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
926	    onestart
927	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
928
929	# Raising a process' priority needs PRIV_SCHED_SETPRIORITY, which a
930	# jail is never granted.  nice(1) only warns when setpriority(2)
931	# fails and execs the command regardless, so the request is dropped
932	# and the service comes up at whatever its login class gives it --
933	# the behaviour rc.conf(5) documents.  Asserted as "not the value
934	# asked for" rather than as a literal, because the login class
935	# supplies the priority actually in use.
936	atf_check -o match:'^-?[0-9]+$' -o not-inline:"-5\n" \
937	    /bin/sh -c "ps -o nice= -p $pid | tr -d ' '"
938}
939nice_negative_cleanup()
940{
941	svcj_cleanup
942}
943
944atf_test_case oomprotect cleanup
945oomprotect_head()
946{
947	atf_set "descr" "\${name}_oomprotect protects a service that runs in" \
948	    "a service jail"
949	atf_set "require.user" "root"
950}
951oomprotect_body()
952{
953	local svc c rcs pid
954
955	svcj_require
956	# Decided by trying it, not by asking whether this is a jail:
957	# protect(1) needs PRIV_VM_MADV_PROTECT, and what matters is whether
958	# this process has it.
959	protect /usr/bin/true 2>/dev/null ||
960	    atf_skip "protect(1) is not permitted here"
961	svc=$(svcj_svcname)
962
963	svcj_fixture <<-EOF
964	$(svcj_daemon_lines)
965	${svc}_svcj="YES"
966	${svc}_oomprotect="yes"
967	EOF
968	c=$(svcj_chroot)
969	rcs=$(svcj_rcpath)
970
971	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
972	    onestart
973	pid=$(svcj_pid) || atf_fail "the daemon wrote no pid file"
974
975	# protect(1) is skipped inside the jail and applied afterwards from
976	# outside, against the pid rc.subr found in the jail.
977	atf_check -o match:'^..1..... .......0$' -e empty \
978	    ps -p "$pid" -ax -o flags,flags2
979}
980oomprotect_cleanup()
981{
982	svcj_cleanup
983}
984
985atf_test_case options_sysvipc_conflict cleanup
986options_sysvipc_conflict_head()
987{
988	atf_set "descr" "Asking for both sysvipc and sysvipcnew fails without" \
989	    "creating a jail"
990	atf_set "require.user" "root"
991}
992options_sysvipc_conflict_body()
993{
994	local svc c rcs
995
996	svcj_require
997	svc=$(svcj_svcname)
998
999	svcj_fixture <<-EOF
1000	$(svcj_daemon_lines)
1001	${svc}_svcj="YES"
1002	${svc}_svcj_options="sysvipc sysvipcnew"
1003	EOF
1004	c=$(svcj_chroot)
1005	rcs=$(svcj_rcpath)
1006
1007	atf_check -s exit:1 -o match:'more than one sysvipc option' -e ignore \
1008	    /usr/sbin/chroot "$c" "$rcs" onestart
1009	[ -z "$(svcj_jid)" ] || atf_fail "a jail was created anyway"
1010}
1011options_sysvipc_conflict_cleanup()
1012{
1013	svcj_cleanup
1014}
1015
1016atf_test_case options_unknown cleanup
1017options_unknown_head()
1018{
1019	atf_set "descr" "An unrecognised \${name}_svcj_options keyword is" \
1020	    "reported"
1021	atf_set "require.user" "root"
1022}
1023options_unknown_body()
1024{
1025	local svc c rcs
1026
1027	svcj_require
1028	svc=$(svcj_svcname)
1029
1030	svcj_fixture <<-EOF
1031	$(svcj_daemon_lines)
1032	${svc}_svcj="YES"
1033	${svc}_svcj_options="net_basic nosuchoption"
1034	EOF
1035	c=$(svcj_chroot)
1036	rcs=$(svcj_rcpath)
1037
1038	# The keyword is reported and the service starts with the options
1039	# that were understood.  Whether a typo should instead be fatal is a
1040	# policy question; this pins today's answer so that changing it is
1041	# deliberate.
1042	atf_check -s exit:0 -e ignore \
1043	    -o match:'unknown service jail option: nosuchoption' \
1044	    /usr/sbin/chroot "$c" "$rcs" onestart
1045	[ -n "$(svcj_jid)" ] || atf_fail "no service jail svcj-$svc"
1046}
1047options_unknown_cleanup()
1048{
1049	svcj_cleanup
1050}
1051
1052atf_test_case options_ipaddrs cleanup
1053options_ipaddrs_head()
1054{
1055	atf_set "descr" "\${name}_svcj_ipaddrs restricts the service jail to" \
1056	    "the listed addresses"
1057	atf_set "require.user" "root"
1058}
1059options_ipaddrs_body()
1060{
1061	local svc c rcs
1062
1063	svcj_require
1064	# A jail can only hand an address to a child jail if it holds it
1065	# itself.
1066	ifconfig lo0 inet 2>/dev/null | grep -q 'inet 127\.0\.0\.1 ' ||
1067	    atf_skip "127.0.0.1 is not available here"
1068	svc=$(svcj_svcname)
1069
1070	svcj_fixture <<-EOF
1071	$(svcj_daemon_lines)
1072	${svc}_svcj="YES"
1073	${svc}_svcj_options="netv4"
1074	${svc}_svcj_ipaddrs="127.0.0.1"
1075	EOF
1076	c=$(svcj_chroot)
1077	rcs=$(svcj_rcpath)
1078
1079	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
1080	    onestart
1081	atf_check -o inline:"127.0.0.1\n" jls -j "svcj-$svc" ip4.addr
1082}
1083options_ipaddrs_cleanup()
1084{
1085	svcj_cleanup
1086}
1087
1088atf_test_case extra_command_placement cleanup
1089extra_command_placement_head()
1090{
1091	atf_set "descr" "A script's own extra command runs on the host unless" \
1092	    "the script declares \${name}_\${cmd}_svcj_enable"
1093	atf_set "require.user" "root"
1094}
1095extra_command_placement_body()
1096{
1097	local svc c rcs wd jid
1098
1099	svcj_require
1100	svc=$(svcj_svcname)
1101	wd=$(svcj_workdir)
1102
1103	# The probe reports the jail id of the SERVICE's own process, as seen
1104	# from wherever the probe itself is running.  The kernel answers that
1105	# question relative to the asking process: fill_kinfo_proc() reports a
1106	# jail id of 0 for a process in the asker's own prison, and the real
1107	# prison id otherwise.  So the daemon reads as the jail's own jid from
1108	# outside and as 0 from inside -- which asserts that the method ran in
1109	# *the service's* jail, not merely somewhere jailed.
1110	# ${name}_${cmd}_svcj_enable is a declaration by the author of the rc
1111	# script, not a knob for the administrator, so both halves below set
1112	# it in the script itself.  It is read only on the branch that a
1113	# service with ${name}_svcj=YES reaches, which is why a script may
1114	# set it unconditionally.
1115	svcj_fixture <<-EOF
1116	$(svcj_daemon_lines)
1117	${svc}_svcj="YES"
1118	extra_commands="probe"
1119	probe_cmd="${svc}_probe"
1120	${svc}_probe_svcj_enable="NO"
1121	${svc}_probe()
1122	{
1123		ps -o jid= -p \$(cat "\$pidfile") | tr -d ' ' > "$wd/probe.out"
1124	}
1125	EOF
1126	c=$(svcj_chroot)
1127	rcs=$(svcj_rcpath)
1128
1129	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
1130	    onestart
1131	jid=$(svcj_jid)
1132	[ -n "$jid" ] || atf_fail "no service jail svcj-$svc was created"
1133	[ "$jid" -ne 0 ] || atf_fail "svcj-$svc has jid 0, so this case " \
1134	    "cannot tell inside from outside"
1135
1136	# Not declared: the method runs outside the service jail, and sees
1137	# the daemon carrying the jail's id.
1138	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
1139	    oneprobe
1140	atf_check -o inline:"$jid\n" cat "$(svcj_hostdir)/probe.out"
1141
1142	# Declared: it runs inside, in the same prison as the daemon.
1143	rm -f "$(svcj_hostdir)/probe.out"
1144	atf_check -s exit:0 sed -i "" \
1145	    "s/${svc}_probe_svcj_enable=\"NO\"/${svc}_probe_svcj_enable=\"YES\"/" \
1146	    "$(svcj_hostrc)"
1147	atf_check -s exit:0 -o ignore -e ignore /usr/sbin/chroot "$c" "$rcs" \
1148	    oneprobe
1149	atf_check -o inline:"0\n" cat "$(svcj_hostdir)/probe.out"
1150}
1151extra_command_placement_cleanup()
1152{
1153	svcj_cleanup
1154}
1155
1156atf_init_test_cases()
1157{
1158	atf_add_test_case start_stop
1159	atf_add_test_case all_enable
1160	atf_add_test_case all_enable_quiet
1161	atf_add_test_case all_enable_rcconfd
1162	atf_add_test_case stop_as_user
1163	atf_add_test_case svcj_toggle_after_stop
1164	atf_add_test_case reload_as_user
1165	atf_add_test_case restart_cmd
1166	atf_add_test_case status_cmd
1167	atf_add_test_case restart_cmd_when_stopped
1168	atf_add_test_case status_cmd_when_stopped
1169	atf_add_test_case stop_removes_dead_jail
1170	atf_add_test_case stop_orphan_jail_with_child
1171	atf_add_test_case audit_user
1172	atf_add_test_case nice_negative
1173	atf_add_test_case oomprotect
1174	atf_add_test_case options_sysvipc_conflict
1175	atf_add_test_case options_unknown
1176	atf_add_test_case options_ipaddrs
1177	atf_add_test_case extra_command_placement
1178}
1179