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