Files
archy/scripts/test-c01-normal-systemd-isolated.sh
T

178 lines
10 KiB
Bash

#!/usr/bin/env bash
# Normal selectors only; exit 77 means prerequisite/native composition BLOCKED.
set -euo pipefail
main() {
taskset -pc 0,1 $$
ROOT=$(git rev-parse --show-toplevel)
ATTEMPT=${1:?unique attempt required}
REPORT=${2:?new absolute receipt directory required}
TARGET=${3:?absolute private or serialized Cargo target cache required}
PREVIOUS=${4:-}
[[ "$ATTEMPT" =~ ^[a-z0-9-]+$ && "$REPORT" = /* && ! -e "$REPORT" && "$TARGET" = /* ]]
[[ $(id -u) != 0 && $(podman info --format '{{.Host.Security.Rootless}}') == true ]]
umask 077
exec 9>/tmp/opencode/release-heavy.lock
flock 9
NAME="qualification-c01-normal-$ATTEMPT"
engine=(podman)
container_extra=()
pasta_pid=
pasta_start=
if [[ -n "$PREVIOUS" ]]; then
python3 -B "$ROOT/scripts/tests/package_c01_normal_fixture.py" replay "$PREVIOUS" "$REPORT"
else
python3 -B "$ROOT/scripts/tests/package_c01_normal_fixture.py" freeze "$ROOT" "$REPORT"
fi
FROZEN="$REPORT/source"
cleanup() {
local status=$?
trap - EXIT
if [[ -n "$pasta_pid" && -n "$pasta_start" ]]; then
sudo -n python3 -c 'import os,signal,sys
try:
fd=os.pidfd_open(int(sys.argv[1])); raw=open("/proc/"+sys.argv[1]+"/stat").read(); fields=raw[raw.rindex(")")+2:].split()
if fields[19]==sys.argv[2]: signal.pidfd_send_signal(fd,signal.SIGTERM)
os.close(fd)
except ProcessLookupError: pass
except FileNotFoundError: pass' "$pasta_pid" "$pasta_start" || true
wait "$pasta_pid" 2>/dev/null || true
fi
if "${engine[@]}" container exists "$NAME"; then
"${engine[@]}" exec "$NAME" journalctl --no-pager > "$REPORT/journal.log" 2>&1 || true
"${engine[@]}" logs "$NAME" > "$REPORT/container.log" 2>&1 || true
"${engine[@]}" cp "$NAME:/var/lib/c01-normal" "$REPORT/native-evidence" 2>/dev/null || true
"${engine[@]}" cp "$NAME:/var/lib/c01-fixture/receipts" "$REPORT/prerequisite-evidence" 2>/dev/null || true
"${engine[@]}" inspect "$NAME" > "$REPORT/container-inspect.json" || true
"${engine[@]}" stop --time 10 "$NAME" > "$REPORT/stop.log" 2>&1 || true
fi
if [[ ${C01_FIXTURE_ROOTFUL:-0} == 1 ]]; then
for directory in "$REPORT/native-evidence" "$REPORT/prerequisite-evidence"; do
[[ ! -d "$directory" ]] || sudo -n chown -R "$(id -u):$(id -g)" "$directory"
done
fi
python3 -B "$FROZEN/scripts/tests/package_c01_normal_fixture.py" verify "$ROOT" "$REPORT" || status=1
printf '%s\n' "$status" > "$REPORT/exit-code.txt"
exit "$status"
}
trap cleanup EXIT
if [[ -z "$PREVIOUS" ]]; then
if [[ -n ${C01_FIXTURE_NATIVE_FROM:-} ]]; then
python3 -B "$FROZEN/scripts/tests/package_c01_normal_fixture.py" reuse-native "$C01_FIXTURE_NATIVE_FROM" "$REPORT"
else
# Keep the compiler in the enclosing durable service's control group. Moving
# it to a sibling scope could outlive this owner and release its flock early.
taskset -c 0,1 \
env CARGO_TARGET_DIR="$TARGET" CARGO_BUILD_JOBS=1 CARGO_INCREMENTAL=0 \
cargo build --locked --offline --manifest-path "$FROZEN/core/Cargo.toml" --bin archipelago --config 'profile.dev.package.archipelago.debug=0' \
> "$REPORT/build.log" 2>&1
python3 -B "$FROZEN/scripts/tests/package_c01_normal_fixture.py" artifact "$TARGET/debug/archipelago" "$REPORT"
python3 -B "$FROZEN/scripts/tests/package_c01_tmux_fixture.py" "$REPORT"
fi
fi
IMAGE=$(podman image inspect localhost/archipelago-rootfs:latest --format '{{.Id}}')
printf '%s\n' "$IMAGE" > "$REPORT/image-id.txt"
if [[ ${C01_FIXTURE_ROOTFUL:-0} == 1 ]]; then
# Never select installed rootful container storage or host networking. The
# entire fixture subtree belongs to this durable job's delegated cgroup.
store="$(dirname "$REPORT")/composed-rootful-store"
ls -d "$(dirname "$REPORT")" >/dev/null
sudo -n install -d -m 0700 "$store"
parent=$(python3 -c 'from pathlib import Path; p=Path(Path("/proc/self/cgroup").read_text().strip().split(":",2)[2]); assert p.name=="supervisor"; print(p.parent)')
engine=(sudo -n env "CONTAINERS_CONF_OVERRIDE=$REPORT/containers.conf" podman \
"--root=$store/root" "--runroot=$store/run" \
--storage-driver=overlay --cgroup-manager=cgroupfs --events-backend=file)
# The outer generic containers-default MAC profile prevents PID1's generator
# sandbox from mounting its own namespace. Product C01 confinement/account/IPC
# checks remain unchanged; this outer fixture is not AppArmor acceptance.
container_extra=(--cgroup-parent="$parent" --security-opt=apparmor=unconfined)
printf '%s\n' '{"realm":"rootful-private-store-network-none","outer_apparmor":"unconfined for nested PID1 sandbox; not product confinement acceptance","cgroup_parent":"'"$parent"'"}' > "$REPORT/realm.json"
if ! "${engine[@]}" image exists "$IMAGE"; then
archive=${C01_FIXTURE_IMAGE_ARCHIVE:-$REPORT/base-image.tar}
if [[ -z ${C01_FIXTURE_IMAGE_ARCHIVE:-} ]]; then
podman save --format oci-archive --output "$archive" "$IMAGE"
fi
"${engine[@]}" load --input "$archive" > "$REPORT/image-load.log" 2>&1
"${engine[@]}" image exists "$IMAGE"
fi
fi
! "${engine[@]}" container exists "$NAME"
CONTAINERS_CONF_OVERRIDE="$REPORT/containers.conf" "${engine[@]}" run --detach --tty --pull=never --name "$NAME" --hostname c01-prerequisite-qualification \
"${container_extra[@]}" \
--network=none --cgroupns=private --systemd=always --security-opt=unmask=ALL \
--cap-add=SYS_ADMIN --cap-add=SYS_PTRACE --cap-add=NET_ADMIN \
--memory=8g --memory-swap=8g --cpus=2 --pids-limit=512 --tmpfs /run --tmpfs /tmp \
--entrypoint /sbin/init "$IMAGE" --unit=basic.target --log-target=console > "$REPORT/container-id.txt"
"${engine[@]}" exec "$NAME" install -d /opt/c01-fixture
"${engine[@]}" cp "$REPORT/source.tar" "$NAME:/opt/c01-fixture/source.tar"
"${engine[@]}" cp "$REPORT/normal-native.tar" "$NAME:/opt/c01-fixture/normal-native.tar"
"${engine[@]}" cp "$REPORT/source-sha256.json" "$NAME:/opt/c01-fixture/source-sha256.json"
"${engine[@]}" cp "$REPORT/native-artifact-sha256.json" "$NAME:/opt/c01-fixture/native-artifact-sha256.json"
"${engine[@]}" cp "$REPORT/tmux-runtime.tar" "$NAME:/opt/c01-fixture/tmux-runtime.tar"
"${engine[@]}" exec "$NAME" tar -xf /opt/c01-fixture/source.tar -C /opt/c01-fixture
"${engine[@]}" exec "$NAME" chown -R root:root /opt/c01-fixture
if [[ ${C01_FIXTURE_NTS:-0} == 1 ]]; then
[[ ${C01_FIXTURE_ROOTFUL:-0} == 1 ]]
python3 -B "$FROZEN/scripts/tests/prepare_c01_nts_dependencies.py" "$REPORT"
for file in chrony-runtime.tar chrony-artifact.json nts-egress.nft nts-addresses.json; do
"${engine[@]}" cp "$REPORT/$file" "$NAME:/opt/c01-fixture/$file"
done
"${engine[@]}" exec "$NAME" chown -R root:root /opt/c01-fixture
"${engine[@]}" exec "$NAME" tar -xf /opt/c01-fixture/chrony-runtime.tar -C /
"${engine[@]}" exec "$NAME" /usr/sbin/nft -f /opt/c01-fixture/nts-egress.nft
# No host bridge/firewall changes. pasta creates its TAP only in this exact
# owned fixture namespace and has no inbound port forwards. Initial-userns
# root is required to join this rootful netns; SYS_TIME is explicitly removed.
pid=$("${engine[@]}" inspect "$NAME" --format '{{.State.Pid}}')
sudo -n python3 -c 'import ctypes,fcntl,os,select,signal,subprocess,sys
pid=int(sys.argv[1]); pin=os.pidfd_open(pid); ns=os.open(f"/proc/{pid}/ns/net",os.O_RDONLY|os.O_CLOEXEC)
group=open(f"/proc/{pid}/cgroup").read().strip().split(":",2)[2]
assert group.startswith(sys.argv[2]+"/") and not select.select([pin],[],[],0)[0]
assert fcntl.ioctl(ns,0xb703)==0x40000000 and os.fstat(ns).st_ino!=os.stat("/proc/1/ns/net").st_ino
libc=ctypes.CDLL(None,use_errno=True)
assert libc.unshare(0x20000)==0 and libc.mount(None,b"/",None,16384|(1<<18),None)==0
assert libc.mount(b"tmpfs",b"/tmp",b"tmpfs",2|4|8,b"mode=0755,size=1m")==0
target="/tmp/c01-nts-net"
fd=os.open(target,os.O_CREAT|os.O_EXCL|os.O_RDONLY,0o444); os.close(fd)
assert libc.mount(f"/proc/self/fd/{ns}".encode(),target.encode(),None,4096,None)==0
assert (os.stat(target).st_dev,os.stat(target).st_ino)==(os.fstat(ns).st_dev,os.fstat(ns).st_ino)
class Header(ctypes.Structure): _fields_=[("version",ctypes.c_uint), ("pid",ctypes.c_int)]
class Caps(ctypes.Structure): _fields_=[("effective",ctypes.c_uint), ("permitted",ctypes.c_uint), ("inheritable",ctypes.c_uint)]
header=Header(0x20080522,0); caps=(Caps*2)()
assert libc.capget(ctypes.byref(header),caps)==0
for field in ("effective","permitted","inheritable"): setattr(caps[0],field,getattr(caps[0],field)&~(1<<25))
assert libc.capset(ctypes.byref(header),caps)==0 and libc.prctl(24,25,0,0,0)==0 and libc.prctl(38,1,0,0,0)==0
child=None; stopping=False
def stop(*_):
global stopping
stopping=True
if child is not None: child.terminate()
signal.signal(signal.SIGTERM,stop)
try:
# This parent watches the original container pidfd. Do not ask pasta to watch
# a namespace pathname after its own filesystem sandbox has hidden that path.
child=subprocess.Popen(["/usr/bin/pasta","--foreground","--runas","0:0","--no-netns-quit","--config-net","--no-map-gw","--no-dhcp","--no-dhcpv6","--no-ndp","--no-ra","--no-icmp","-4","-t","none","-u","none","-T","none","-U","none","--ns-ifname","c01-nts-egress","--netns",target])
if stopping: child.terminate()
while child.poll() is None:
if select.select([pin],[],[],.1)[0]: stop()
raise SystemExit(child.wait())
finally:
if child is not None and child.poll() is None: child.terminate(); child.wait()
os.close(ns); os.close(pin)' \
"$pid" "$parent" > "$REPORT/pasta.log" 2>&1 &
pasta_pid=$!
pasta_start=$(sudo -n python3 -c 'import sys; raw=open("/proc/"+sys.argv[1]+"/stat").read(); fields=raw[raw.rindex(")")+2:].split(); assert fields[1]==sys.argv[2]; print(fields[19])' "$pasta_pid" "$$")
for attempt in {1..40}; do
if "${engine[@]}" exec "$NAME" python3 -c 'import json,subprocess; v=json.loads(subprocess.check_output(["ip","-j","-4","address","show","dev","c01-nts-egress"])); assert any(x.get("addr_info") for x in v)' >/dev/null 2>&1; then break; fi
sleep .1
done
"${engine[@]}" exec "$NAME" python3 -c 'import json,subprocess; v=json.loads(subprocess.check_output(["ip","-j","-4","address","show","dev","c01-nts-egress"])); assert any(x.get("addr_info") for x in v), "fixture NTS interface unavailable"'
printf '%s\n' 'Only loopback, established replies, and pinned public NTS TCP4460/NTP UDP123; no DNS/host gateway/inbound forwarding' > "$REPORT/nts-egress-scope.txt"
fi
"${engine[@]}" exec --env C01_NORMAL_PID1=1 --env "C01_POWER_COMPONENT=${C01_POWER_COMPONENT:-0}" "$NAME" python3 -B \
/opt/c01-fixture/tests/regression/c01-normal-systemd.py 2>&1 | tee "$REPORT/test.log"
}
# Parse the complete driver before long builds; editing the worktree must not
# move Bash's file cursor into a different command after a compiler returns.
main "$@"; exit "$?"