178 lines
10 KiB
Bash
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 "$?"
|