Files
2026-09-15 15:24:03 -04:00

114 lines
5.0 KiB
Bash
Executable File

#!/bin/bash
# =====================================================================
# The only thing vmd is allowed to run as root.
#
# vmd runs as owuivmd and needs four privileged things: start and stop
# the VM unit, create a project dataset with a quota, and start and stop
# virtiofsd. Granting it `systemctl` or `zfs` outright would be granting
# it every unit and every dataset on this host, so it gets this instead:
# a fixed verb list, one optional argument, and a pattern that argument
# must match before it is used for anything.
#
# The sudoers entry names this path and nothing else. Everything here is
# an array-form command - there is no `eval`, no unquoted expansion and
# no shell metacharacter anywhere a project name can reach.
#
# THE PATTERN IS DUPLICATED ON PURPOSE
#
# It also lives in vm/vmd/project.py, and tests/test_vm_tasks.py asserts
# the two are identical. Importing it would mean root code reading a
# module out of a tree that vmd's own user can write to, which is exactly
# backwards. Two copies and a test that they agree is the cheaper risk.
# =====================================================================
set -euo pipefail
PROJECT_RE='^[a-z0-9][a-z0-9-]{0,29}[a-z0-9]$'
PROJECTS_DATASET="Pool/vmprojects"
# The pool root is filled in by install.sh. This runs under sudo, where
# the environment is not preserved, so it cannot read a setting the way
# vmd does - the value has to be baked in when it is installed.
PROJECTS_ROOT="@POOL_ROOT@/vmprojects"
QUOTA="10G"
RESERVED=" base iso state run git gitea owuivm "
die() { printf 'owuivm-ctl: %s\n' "$*" >&2; exit 64; }
check_name() {
local n="${1:-}"
[[ "$n" =~ $PROJECT_RE ]] || die "'$n' is not a valid project name"
[[ "$RESERVED" == *" $n "* ]] && die "'$n' is reserved"
return 0
}
verb="${1:-}"; shift || true
case "$verb" in
vm-start) exec /usr/bin/systemctl start owuivm.service ;;
vm-stop) exec /usr/bin/systemctl stop owuivm.service ;;
vm-status) exec /usr/bin/systemctl is-active owuivm.service ;;
# `reset-failed` on its own, so a unit that died once cannot wedge
# every later session behind "start request repeated too quickly".
vm-reset) exec /usr/bin/systemctl reset-failed owuivm.service ;;
project-ensure)
name="${1:-}"; check_name "$name"
if ! /usr/sbin/zfs list -H -o name -- "$PROJECTS_DATASET/$name" >/dev/null 2>&1; then
# Via systemd-run, not directly. `zfs create` MOUNTS the new
# dataset, and mount() is not in @system-service - which is the
# syscall filter vmd runs under and which every child of vmd
# inherits, sudo included. Run directly it dies with SIGSYS, and
# "Bad system call" from zfs is a genuinely confusing way to learn
# that. systemd-run spawns from PID 1 instead, so this lands in its
# own unit outside vmd's cgroup, and vmd's own filter stays tight
# rather than being widened to @mount for one call.
/usr/bin/systemd-run --quiet --wait --collect \
--unit="owuivm-zfs-create" --property=Type=oneshot \
-- /usr/sbin/zfs create -o "quota=$QUOTA" -- "$PROJECTS_DATASET/$name"
fi
# setgid so everything the guest writes stays in a group vmd and
# virtiofsd can both still read afterwards.
chown owuivmd:owuivm -- "$PROJECTS_ROOT/$name"
chmod 2770 -- "$PROJECTS_ROOT/$name"
printf '%s\n' "$PROJECTS_ROOT/$name"
;;
project-usage)
name="${1:-}"; check_name "$name"
exec /usr/sbin/zfs get -Hp -o value used,quota -- "$PROJECTS_DATASET/$name"
;;
vfs-start)
name="${1:-}"; check_name "$name"
session="${2:-}"
[[ "$session" =~ ^[0-9a-f]{32}$ ]] || die "session id is not 32 hex characters"
# Type=exec, NOT notify: virtiofsd never sends sd_notify(READY=1), so
# systemd waits the full start timeout and then kills a daemon that
# was working perfectly - the log says "Waiting for vhost-user socket
# connection..." right up until it is terminated. Readiness here is
# the socket file appearing, which vmd waits for itself.
exec /usr/bin/systemd-run \
--unit="owuivm-virtiofsd" --collect --property=Type=exec \
--property=Restart=no --property=NoNewPrivileges=yes \
--property=ProtectSystem=strict \
--property="ReadWritePaths=/run/owuivm @POOL_ROOT@/vmprojects/$name" \
--property=PrivateNetwork=yes \
-- /usr/libexec/virtiofsd \
--socket-path="/run/owuivm/$session/vfs.sock" \
--socket-group=owuivm \
--shared-dir="$PROJECTS_ROOT/$name" \
--sandbox=namespace \
--modcaps=-mknod:-setfcap
# No --xattr: it is a bare flag in virtiofsd 1.10 (there is no
# --xattr=false), and extended attributes are off unless it is given.
# Leaving them off is what is wanted - an xattr is another thing the
# guest can set on a file that then lives on the host's array.
;;
vfs-stop)
/usr/bin/systemctl stop owuivm-virtiofsd.service >/dev/null 2>&1 || true
/usr/bin/systemctl reset-failed owuivm-virtiofsd.service >/dev/null 2>&1 || true
;;
*) die "unknown verb '${verb}'" ;;
esac