diff options
| author | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-11-01 07:13:45 -0600 |
|---|---|---|
| committer | Doan Luu Duc Tai <doan@luuduct.ai> | 2026-11-01 07:13:45 -0600 |
| commit | 9349a2de648e241da8e00ba7ae25cb2a877ff3e9 (patch) | |
| tree | 2fea913ee0aaff4370877f4a91c911db2fe37592 /infra/guix | |
| parent | 5b89c370d5974f057306a52089f14c337565fd1d (diff) | |
feat(guix): carry prebuilt runtime and recover preserved state
Diffstat (limited to 'infra/guix')
| -rw-r--r-- | infra/guix/bootstrap.sh | 86 | ||||
| -rw-r--r-- | infra/guix/build.sh | 37 | ||||
| -rw-r--r-- | infra/guix/bundle.sh | 56 | ||||
| -rw-r--r-- | infra/guix/containers.conf | 24 | ||||
| -rw-r--r-- | infra/guix/manifest.scm | 19 | ||||
| -rw-r--r-- | infra/guix/restore.sh | 90 | ||||
| -rw-r--r-- | infra/guix/run.sh | 45 | ||||
| -rw-r--r-- | infra/guix/test-bootstrap.sh | 118 | ||||
| -rw-r--r-- | infra/guix/test-recovery.sh | 133 | ||||
| -rw-r--r-- | infra/guix/test.sh | 166 |
10 files changed, 774 insertions, 0 deletions
diff --git a/infra/guix/bootstrap.sh b/infra/guix/bootstrap.sh new file mode 100644 index 0000000..6f53dbc --- /dev/null +++ b/infra/guix/bootstrap.sh @@ -0,0 +1,86 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +umask 077 +fail() { printf '%s\n' "$1" >&2; exit 1; } +safe_directory() { + [ -d "$1" ] && [ ! -L "$1" ] && [ "$(stat -c %u "$1")" = 0 ] && + [ -z "$(find "$1" -maxdepth 0 -perm /022 -print)" ] +} +[ "$#" = 0 ] || fail 'Usage: sudo ./stack-recovery/bootstrap' +[ "$(id -u)" = 0 ] || fail 'Run bootstrap explicitly as root; restore runs as the service user.' +[ "$(uname -sm)" = 'Linux x86_64' ] || fail 'Requires x86_64 Linux.' +command -v flock >/dev/null || fail 'Host util-linux flock is required.' +command -v diff >/dev/null || fail 'Host GNU diffutils is required.' +BUNDLE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +(cd "$BUNDLE" && sha256sum --check --strict SHA256SUMS) +[ ! -e /var/guix ] && [ ! -L /var/guix ] || fail 'Managed Guix installation found; refusing bootstrap.' +[ ! -L /gnu ] && [ ! -L /gnu/store ] || fail 'Symlink /gnu or store; refusing bootstrap.' +STAGE= +INITIAL= +trap '[ -z "$STAGE" ] || rm -rf -- "$STAGE"; [ -z "$INITIAL" ] || rm -rf -- "$INITIAL"' EXIT HUP INT TERM +if [ ! -e /gnu ]; then + INITIAL=$(mktemp -d /.stack-guix.XXXXXX) + chmod 755 "$INITIAL" + mkdir -m 755 "$INITIAL/store" "$INITIAL/.stack-runtimes" + mv -T --no-clobber -- "$INITIAL" /gnu + rm -rf -- "$INITIAL" + INITIAL= +fi +safe_directory /gnu && safe_directory /gnu/.stack-runtimes || fail 'Foreign or writable /gnu ownership metadata; refusing bootstrap.' +exec 9>/gnu/.stack-runtimes/install.lock +flock 9 +if [ -e /gnu/store ]; then + safe_directory /gnu/store || fail 'Foreign or writable Guix store; refusing bootstrap.' +else + [ ! -L /gnu/store ] || fail 'Symlink store; refusing bootstrap.' + mkdir -m 755 /gnu/store +fi +DIGEST=$(sha256sum "$BUNDLE/runtime.tar.gz") +DIGEST=${DIGEST%% *} +MARKER=/gnu/.stack-runtimes/$DIGEST +if [ -e "$MARKER" ] || [ -L "$MARKER" ]; then + [ -f "$MARKER" ] && [ ! -L "$MARKER" ] && [ "$(stat -c %u "$MARKER")" = 0 ] && + [ -z "$(find "$MARKER" -perm /222 -print)" ] && [ "$(cat "$MARKER")" = "$DIGEST" ] || + fail 'Invalid completed-runtime marker; refusing bootstrap.' + printf '%s\n' 'Guix runtime already installed.' + exit 0 +fi +STAGE=$(mktemp -d /gnu/.stack-stage.XXXXXX) +tar -xzf "$BUNDLE/runtime.tar.gz" --no-same-owner -C "$STAGE" +[ -d "$STAGE/gnu" ] && [ ! -L "$STAGE/gnu" ] && + [ -d "$STAGE/gnu/store" ] && [ ! -L "$STAGE/gnu/store" ] || fail 'Runtime has no canonical Guix store.' +set -- "$STAGE/gnu/store/"* +[ -e "$1" ] || [ -L "$1" ] || fail 'Runtime Guix store is empty.' +chown -hR 0:0 "$STAGE/gnu/store" +chmod -R a-w "$STAGE/gnu/store" +for ENTRY do + TARGET=/gnu/store/${ENTRY##*/} + if [ -e "$TARGET" ] || [ -L "$TARGET" ]; then + diff -qr --no-dereference -- "$ENTRY" "$TARGET" || fail 'Conflicting Guix store entry; existing data retained.' + [ -z "$(find "$TARGET" \( ! -user 0 -o \( ! -type l -perm /222 \) \) -print -quit)" ] || + fail 'Existing Guix store entry has unsafe ownership or write permissions.' + else + mv -T --no-clobber -- "$ENTRY" "$TARGET" + [ ! -e "$ENTRY" ] && [ ! -L "$ENTRY" ] || fail 'Guix store entry appeared during installation; retry bootstrap.' + fi +done +printf '%s\n' "$DIGEST" > "$STAGE/complete" +chmod 444 "$STAGE/complete" +mv -T --no-clobber -- "$STAGE/complete" "$MARKER" +[ ! -e "$STAGE/complete" ] || fail 'Runtime marker appeared during installation; retry bootstrap.' +printf '%s\n' 'Guix runtime installed. Run restore as the service user.' diff --git a/infra/guix/build.sh b/infra/guix/build.sh new file mode 100644 index 0000000..e5a0da8 --- /dev/null +++ b/infra/guix/build.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +umask 077 +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +[ "$(uname -s):$(uname -m)" = Linux:x86_64 ] || { echo 'Requires x86_64 Linux.' >&2; exit 1; } +command -v guix >/dev/null || { echo "Guix is required on the build machine." >&2; exit 1; } +mkdir -p "$ROOT/bin" +STAGE=$(mktemp -d "$ROOT/bin/.guix-build.XXXXXX") +trap 'chmod -R u+w -- "$STAGE"; rm -rf -- "$STAGE"' EXIT HUP INT TERM +guix describe --format=channels > "$STAGE/channels.scm" +PACK=$(guix time-machine -C "$STAGE/channels.scm" -- pack \ + --system=x86_64-linux -S /bin=bin -S /etc=etc \ + -m "$ROOT/infra/guix/manifest.scm") +cp -- "$PACK" "$STAGE/runtime.tar.gz" +mkdir "$STAGE/runtime" +tar -xzf "$STAGE/runtime.tar.gz" --no-same-owner -C "$STAGE/runtime" +PATH="$STAGE/runtime/bin:$PATH" CONTAINERS_CONF="$ROOT/infra/guix/containers.conf" \ + podman --root "$STAGE/storage" \ + --runroot "$STAGE/run" info >/dev/null +mv -- "$STAGE/runtime.tar.gz" "$ROOT/bin/guix-runtime.tar.gz" +mv -- "$STAGE/channels.scm" "$ROOT/bin/guix-channels.scm" +printf '%s\n' 'Guix runtime built. Full clean-host recovery test still required.' diff --git a/infra/guix/bundle.sh b/infra/guix/bundle.sh new file mode 100644 index 0000000..c6d5bb0 --- /dev/null +++ b/infra/guix/bundle.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +umask 077 +[ "$#" = 3 ] || { echo 'Usage: sh infra/guix/bundle.sh BACKUP.tar.gz.gpg RELEASE.tar.gz OUTPUT.tar.gz' >&2; exit 1; } +[ -f "$1" ] || { echo 'Encrypted backup missing.' >&2; exit 1; } +case "$1" in *.tar.gz.gpg) ;; *) echo 'Requires an encrypted stack backup.' >&2; exit 1 ;; esac +[ -f "$2" ] || { echo 'Trusted release missing.' >&2; exit 1; } +case "$2" in *.tar.gz) ;; *) echo 'Requires a trusted public release archive.' >&2; exit 1 ;; esac +case "$3" in *.tar.gz) ;; *) echo 'Output must end in .tar.gz.' >&2; exit 1 ;; esac +[ ! -e "$3" ] || { echo 'Output already exists.' >&2; exit 1; } +STAGE=$(mktemp -d "$(dirname -- "$3")/.recovery.XXXXXX") +trap 'rm -rf -- "$STAGE"' EXIT HUP INT TERM +tar -xOf "$2" stack/bin/stack > "$STAGE/verify" +chmod 700 "$STAGE/verify" +"$STAGE/verify" restore --release "$2" "$STAGE/release" +RELEASE="$STAGE/release" +[ ! -e "$RELEASE/.env" ] && [ ! -e "$RELEASE/var/data" ] || { + echo 'Release contains private application state; refusing plaintext publication.' >&2; exit 1; +} +for FILE in bin/stack bin/guix-runtime.tar.gz bin/guix-channels.scm \ + bin/chat/server bin/chat/crypto.wasm bin/chat/SOURCE.tar.gz infra/services.toml \ + infra/guix/bootstrap.sh infra/guix/restore.sh infra/guix/run.sh \ + infra/guix/containers.conf var/image-lock.json var/images.tar LICENSE; do + [ -f "$RELEASE/$FILE" ] || { echo "Release missing $FILE; build a complete release first." >&2; exit 1; } +done +mkdir "$STAGE/stack-recovery" +cp -- "$1" "$STAGE/stack-recovery/backup.tar.gz.gpg" +cp -- "$2" "$STAGE/stack-recovery/release.tar.gz" +cp -- "$RELEASE/bin/stack" "$STAGE/stack-recovery/stack" +cp -- "$RELEASE/bin/guix-runtime.tar.gz" "$STAGE/stack-recovery/runtime.tar.gz" +cp -- "$RELEASE/bin/guix-channels.scm" "$STAGE/stack-recovery/channels.scm" +cp -- "$RELEASE/infra/guix/restore.sh" "$STAGE/stack-recovery/restore" +cp -- "$RELEASE/infra/guix/bootstrap.sh" "$STAGE/stack-recovery/bootstrap" +cp -- "$RELEASE/infra/guix/run.sh" "$STAGE/stack-recovery/run" +cp -- "$RELEASE/infra/guix/containers.conf" "$STAGE/stack-recovery/containers.conf" +cp -- "$RELEASE/LICENSE" "$STAGE/stack-recovery/LICENSE" +chmod 700 "$STAGE/stack-recovery/restore" "$STAGE/stack-recovery/bootstrap" "$STAGE/stack-recovery/run" "$STAGE/stack-recovery/stack" +(cd "$STAGE/stack-recovery" && sha256sum backup.tar.gz.gpg release.tar.gz stack runtime.tar.gz channels.scm restore bootstrap run containers.conf LICENSE > SHA256SUMS) +tar -czf "$STAGE/bundle.tar.gz" -C "$STAGE" stack-recovery +ln -- "$STAGE/bundle.tar.gz" "$3" +printf 'Recovery bundle: %s\n' "$3" diff --git a/infra/guix/containers.conf b/infra/guix/containers.conf new file mode 100644 index 0000000..ab74f9e --- /dev/null +++ b/infra/guix/containers.conf @@ -0,0 +1,24 @@ +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +[engine] +conmon_path = [] +runtime = "crun" + +[engine.runtimes] +crun = [] + +[network] +default_rootless_network_cmd = "slirp4netns" diff --git a/infra/guix/manifest.scm b/infra/guix/manifest.scm new file mode 100644 index 0000000..7e9cf19 --- /dev/null +++ b/infra/guix/manifest.scm @@ -0,0 +1,19 @@ +; Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +; +; This program is free software: you can redistribute it and/or modify +; it under the terms of the GNU Affero General Public License as +; published by the Free Software Foundation, either version 3 of the +; License, or (at your option) any later version. +; +; This program is distributed in the hope that it will be useful, +; but WITHOUT ANY WARRANTY; without even the implied warranty of +; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +; GNU Affero General Public License for more details. +; +; You should have received a copy of the GNU Affero General Public License +; along with this program. If not, see <https://www.gnu.org/licenses/>. + +(specifications->manifest + '("bash" "coreutils" "tar" "gzip" "git" "gnupg" "nss-certs" + "podman" "crun" "conmon" "slirp4netns" "fuse-overlayfs" + "netavark" "aardvark-dns")) diff --git a/infra/guix/restore.sh b/infra/guix/restore.sh new file mode 100644 index 0000000..22e7e10 --- /dev/null +++ b/infra/guix/restore.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +umask 077 +[ "$#" = 1 ] || { echo 'Usage: ./restore /absolute/path/to/new-stack' >&2; exit 1; } +case "$1" in /*) ;; *) echo 'Destination must be absolute.' >&2; exit 1 ;; esac +[ "$(uname -s):$(uname -m)" = Linux:x86_64 ] || { echo 'Requires x86_64 Linux.' >&2; exit 1; } +[ "$(id -u)" != 0 ] || { echo 'Run as the service user, not root.' >&2; exit 1; } +BUNDLE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +(cd "$BUNDLE" && sha256sum --check --strict SHA256SUMS) +RELEASE_DIGEST=$(sha256sum "$BUNDLE/release.tar.gz") +RELEASE_DIGEST=${RELEASE_DIGEST%% *} +BACKUP_DIGEST=$(sha256sum "$BUNDLE/backup.tar.gz.gpg") +BACKUP_DIGEST=${BACKUP_DIGEST%% *} +INTENT="$RELEASE_DIGEST:$BACKUP_DIGEST" +MARKER="$1/var/guix-recovery" +pending() { + [ -f "$MARKER" ] && [ ! -L "$MARKER" ] && [ -O "$MARKER" ] && + [ "$(cat "$MARKER")" = "$INTENT" ] +} +RESUME=false +if [ -e "$1" ] || [ -L "$1" ]; then + pending || { echo 'Destination exists without a matching pending recovery; refusing to overwrite.' >&2; exit 1; } + RESUME=true +fi +DIGEST=$(sha256sum "$BUNDLE/runtime.tar.gz") +DIGEST=${DIGEST%% *} +[ -f "/gnu/.stack-runtimes/$DIGEST" ] && [ "$(stat -c %u "/gnu/.stack-runtimes/$DIGEST")" = 0 ] || { + echo 'Install the trusted closure first: sudo ./stack-recovery/bootstrap' >&2; exit 1; +} +for HELPER in newuidmap newgidmap; do + command -v "$HELPER" >/dev/null || { echo "Install host $HELPER before restoring." >&2; exit 1; } +done +for MAPPING in /etc/subuid /etc/subgid; do + awk -F: -v name="$(id -un)" -v uid="$(id -u)" \ + '($1 == name || $1 == uid) && $3 >= 65536 { found=1 } END { exit !found }' "$MAPPING" || { + echo "Allocate at least 65536 subordinate IDs in $MAPPING for $(id -un)." >&2; exit 1; + } +done +[ -n "${XDG_RUNTIME_DIR:-}" ] && [ -d "$XDG_RUNTIME_DIR" ] && [ -w "$XDG_RUNTIME_DIR" ] || { + echo 'A writable XDG_RUNTIME_DIR is required; use a normal login session.' >&2; exit 1; +} +[ "$(cat /proc/sys/net/ipv4/ip_unprivileged_port_start)" -le 80 ] || { + echo 'Host administrator must set net.ipv4.ip_unprivileged_port_start=80.' >&2; exit 1; +} +STAGE=$(mktemp -d) +trap 'chmod -R u+w -- "$STAGE"; rm -rf -- "$STAGE"' EXIT HUP INT TERM +mkdir "$STAGE/runtime" +tar -xzf "$BUNDLE/runtime.tar.gz" --no-same-owner -C "$STAGE/runtime" +[ -x "$STAGE/runtime/bin/podman" ] && [ -x "$STAGE/runtime/bin/gpg" ] || { + echo 'Guix closure incomplete; rerun the trusted capsule bootstrap.' >&2; exit 1; +} +PATH="$STAGE/runtime/bin:$PATH" +CONTAINERS_CONF="$BUNDLE/containers.conf" +export PATH CONTAINERS_CONF +podman --root "$STAGE/storage" --runroot "$STAGE/run" info >/dev/null +if [ "$RESUME" = false ]; then + "$BUNDLE/stack" restore "$BUNDLE/backup.tar.gz.gpg" "$1" +fi +LOCK="$1/var/guix-recovery.lock" +[ ! -L "$LOCK" ] && { [ ! -e "$LOCK" ] || [ -f "$LOCK" ]; } || { + echo 'Recovery lock is not a regular file.' >&2; exit 1; +} +exec 9>>"$LOCK" +flock -n 9 || { echo 'Another recovery is running for this destination.' >&2; exit 1; } +if [ "$RESUME" = true ]; then + pending || { echo 'Pending recovery changed; refusing activation.' >&2; exit 1; } +else + printf '%s\n' "$INTENT" > "$MARKER" +fi +[ -f "$1/var/image-lock.json" ] && [ -f "$1/var/images.tar" ] || { + echo 'Backup has no complete image export; recovered data retained. Use backup --include-images.' >&2; exit 1; +} +printf '%s\n' 'Data restored. Activating paired release; rerun this restore command after any failure.' +"$BUNDLE/stack" --root "$1" activate "$BUNDLE/release.tar.gz" 9>&- +rm -- "$MARKER" diff --git a/infra/guix/run.sh b/infra/guix/run.sh new file mode 100644 index 0000000..e1b9b99 --- /dev/null +++ b/infra/guix/run.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +if [ -f "$ROOT/bin/guix-runtime.tar.gz" ]; then + DIGEST=$(sha256sum "$ROOT/bin/guix-runtime.tar.gz") + DIGEST=${DIGEST%% *} + RUNTIME="$ROOT/var/guix/$DIGEST" + if [ ! -d "$RUNTIME" ]; then + umask 077 + mkdir -p "$ROOT/var/guix" + STAGE=$(mktemp -d "$ROOT/var/guix/.unpack.XXXXXX") + trap 'chmod -R u+w -- "$STAGE"; rm -rf -- "$STAGE"' EXIT HUP INT TERM + tar -xzf "$ROOT/bin/guix-runtime.tar.gz" --no-same-owner -C "$STAGE" + [ -x "$STAGE/bin/podman" ] && [ -x "$STAGE/bin/gpg" ] || { + echo 'Guix closure unavailable; run the trusted recovery capsule bootstrap as root first.' >&2; exit 1; + } + mv -T -- "$STAGE" "$RUNTIME" + trap - EXIT HUP INT TERM + fi + [ -x "$RUNTIME/bin/podman" ] && [ -x "$RUNTIME/bin/gpg" ] || { + echo 'Guix closure unavailable; run the trusted recovery capsule bootstrap as root first.' >&2; exit 1; + } + PATH="$RUNTIME/bin:$PATH" + CONTAINERS_CONF="$ROOT/infra/guix/containers.conf" + export PATH CONTAINERS_CONF + SSL_CERT_DIR="$RUNTIME/etc/ssl/certs" + SSL_CERT_FILE="$SSL_CERT_DIR/ca-certificates.crt" + export SSL_CERT_DIR SSL_CERT_FILE +fi +exec "$ROOT/bin/stack" --root "$ROOT" "$@" diff --git a/infra/guix/test-bootstrap.sh b/infra/guix/test-bootstrap.sh new file mode 100644 index 0000000..470ebeb --- /dev/null +++ b/infra/guix/test-bootstrap.sh @@ -0,0 +1,118 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +umask 077 +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +command -v bwrap >/dev/null || { echo 'Bootstrap tests require bubblewrap.' >&2; exit 1; } +STAGE=$(mktemp -d) +trap 'chmod -R u+w "$STAGE"; rm -rf -- "$STAGE"' EXIT HUP INT TERM +mkdir -p "$STAGE/capsule" "$STAGE/pack/gnu/store/00000000000000000000000000000000-tool/bin" \ + "$STAGE/pack/gnu/store/11111111111111111111111111111111-profile/bin" "$STAGE/pack/etc" +cp "$ROOT/infra/guix/bootstrap.sh" "$STAGE/capsule/bootstrap" +printf '%s\n' '#!/bin/sh' 'printf "%s\n" canonical-tool' > "$STAGE/pack/gnu/store/00000000000000000000000000000000-tool/bin/tool" +chmod 755 "$STAGE/pack/gnu/store/00000000000000000000000000000000-tool/bin/tool" +ln -s /gnu/store/00000000000000000000000000000000-tool/bin/tool "$STAGE/pack/gnu/store/11111111111111111111111111111111-profile/bin/tool" +ln -s /gnu/store/11111111111111111111111111111111-profile/bin "$STAGE/pack/bin" +printf '%s\n' 'Must stay inside pack.' > "$STAGE/pack/etc/pack-sentinel" +tar -czf "$STAGE/capsule/runtime.tar.gz" -C "$STAGE/pack" . +(cd "$STAGE/capsule" && sha256sum bootstrap runtime.tar.gz > SHA256SUMS) +cp -a "$STAGE/capsule" "$STAGE/tampered" +printf '%s\n' 'tamper' >> "$STAGE/tampered/runtime.tar.gz" +cp -a "$STAGE/capsule" "$STAGE/next" +mkdir "$STAGE/pack/gnu/store/22222222222222222222222222222222-next" +printf '%s\n' 'Second closure.' > "$STAGE/pack/gnu/store/22222222222222222222222222222222-next/value" +tar -czf "$STAGE/next/runtime.tar.gz" -C "$STAGE/pack" . +(cd "$STAGE/next" && sha256sum bootstrap runtime.tar.gz > SHA256SUMS) +bwrap --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_DAC_OVERRIDE \ + --tmpfs / --ro-bind /usr /usr \ + --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib /lib64 \ + --ro-bind "$STAGE/capsule" /capsule --ro-bind "$STAGE/next" /next \ + --ro-bind "$STAGE/tampered" /tampered --proc /proc --dev /dev --tmpfs /tmp \ + --chdir / /bin/sh -eu <<'TEST' +fail() { printf '%s\n' "$1" >&2; exit 1; } +reject() { if sh "$1/bootstrap"; then fail "$2"; fi; } +TOOL=/gnu/store/00000000000000000000000000000000-tool/bin/tool +PROFILE=/gnu/store/11111111111111111111111111111111-profile +mkdir /etc +printf '%s\n' 'Host configuration.' > /etc/host-sentinel +[ ! -e /gnu ] && [ ! -e /var/guix ] +if sh /capsule/bootstrap extra; then fail 'Accepted bootstrap arguments.'; fi +reject /tampered 'Accepted altered runtime.' +[ ! -e /gnu ] +sh /capsule/bootstrap > /tmp/bootstrap-first.log & +FIRST=$! +sh /capsule/bootstrap +wait "$FIRST" +DIGEST=$(sha256sum /capsule/runtime.tar.gz) +DIGEST=${DIGEST%% *} +[ "$(cat "/gnu/.stack-runtimes/$DIGEST")" = "$DIGEST" ] +[ -z "$(find /gnu/store -mindepth 1 ! -user 0 -print -quit)" ] +[ -z "$(find /gnu/store -mindepth 1 ! -type l -perm /222 -print -quit)" ] +[ "$("$PROFILE/bin/tool")" = canonical-tool ] +[ "$(readlink "$PROFILE/bin/tool")" = "$TOOL" ] +[ "$(readlink /bin)" = usr/bin ] +[ "$(cat /etc/host-sentinel)" = 'Host configuration.' ] +[ ! -e /etc/pack-sentinel ] +INODE=$(stat -c %i "/gnu/.stack-runtimes/$DIGEST") +sh /capsule/bootstrap +[ "$(stat -c %i "/gnu/.stack-runtimes/$DIGEST")" = "$INODE" ] +rm "/gnu/.stack-runtimes/$DIGEST" +chmod -R u+w "$PROFILE" +rm -rf "$PROFILE" +sh /capsule/bootstrap +[ "$("$PROFILE/bin/tool")" = canonical-tool ] +[ -f "/gnu/.stack-runtimes/$DIGEST" ] +[ -z "$(find /gnu -maxdepth 1 -name '.stack-stage.*' -print -quit)" ] +sh /next/bootstrap +[ "$("$PROFILE/bin/tool")" = canonical-tool ] +[ "$(cat /gnu/store/22222222222222222222222222222222-next/value)" = 'Second closure.' ] +[ -f "/gnu/.stack-runtimes/$DIGEST" ] +rm "/gnu/.stack-runtimes/$DIGEST" +chmod u+w "$TOOL" +printf '%s\n' 'conflicting content' >> "$TOOL" +chmod a-w "$TOOL" +reject /capsule 'Overwrote conflicting store entry.' +[ ! -e "/gnu/.stack-runtimes/$DIGEST" ] +[ -z "$(find /gnu -maxdepth 1 -name '.stack-stage.*' -print -quit)" ] +tail -n 1 "$TOOL" | grep -Fx 'conflicting content' +chmod -R u+w /gnu +rm -rf /gnu +mkdir -p /var/guix +reject /capsule 'Accepted managed Guix installation.' +[ ! -e /gnu ] +rm -rf /var/guix +mkdir -p /gnu/store +printf '%s\n' 'Foreign store.' > /gnu/store/sentinel +reject /capsule 'Accepted foreign store.' +[ "$(cat /gnu/store/sentinel)" = 'Foreign store.' ] +rm -rf /gnu +mkdir /outside +ln -s /outside /gnu +reject /capsule 'Accepted symlink /gnu.' +[ -z "$(find /outside -mindepth 1 -print -quit)" ] +rm /gnu +mkdir -p /gnu/.stack-runtimes +chmod 755 /gnu /gnu/.stack-runtimes +ln -s /outside /gnu/store +reject /capsule 'Accepted symlink store.' +[ -z "$(find /outside -mindepth 1 -print -quit)" ] +rm /gnu/store +chmod 777 /gnu/.stack-runtimes +reject /capsule 'Accepted writable ownership metadata.' +[ ! -e /gnu/store ] +printf '%s\n' 'Canonical bootstrap, retry, idempotency, preservation and refusal checks passed.' +TEST diff --git a/infra/guix/test-recovery.sh b/infra/guix/test-recovery.sh new file mode 100644 index 0000000..c03ed2f --- /dev/null +++ b/infra/guix/test-recovery.sh @@ -0,0 +1,133 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +umask 077 +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +command -v bwrap >/dev/null || { echo 'Recovery tests require bubblewrap.' >&2; exit 1; } +STAGE=$(mktemp -d) +trap 'chmod -R u+w "$STAGE"; rm -rf -- "$STAGE"' EXIT HUP INT TERM +mkdir -p "$STAGE/capsule" "$STAGE/runtime/bin" "$STAGE/test-bin" +cp "$ROOT/infra/guix/restore.sh" "$STAGE/capsule/restore" +cp "$ROOT/infra/guix/run.sh" "$STAGE/capsule/run" +cp "$ROOT/infra/guix/containers.conf" "$STAGE/capsule/containers.conf" +printf '%s\n' 'opaque encrypted backup fixture' > "$STAGE/capsule/backup.tar.gz.gpg" +printf '%s\n' 'opaque release fixture' > "$STAGE/capsule/release.tar.gz" +printf '%s\n' 80 > "$STAGE/lowport" +printf '%s\n' '#!/bin/sh' 'exit 0' > "$STAGE/runtime/bin/gpg" +cat > "$STAGE/runtime/bin/podman" <<'PODMAN' +#!/bin/sh +set -eu +[ "$CONTAINERS_CONF" = /capsule/containers.conf ] +printf '%s\n' "$*" >> /tmp/podman.calls +PODMAN +cat > "$STAGE/test-bin/id" <<'ID' +#!/bin/sh +case "$1" in -u) echo 1000 ;; -un) echo service ;; *) exit 1 ;; esac +ID +for HELPER in newuidmap newgidmap; do + printf '%s\n' '#!/bin/sh' 'exit 0' > "$STAGE/test-bin/$HELPER" +done +cat > "$STAGE/capsule/stack" <<'STACK' +#!/bin/sh +set -eu +[ "$CONTAINERS_CONF" = /capsule/containers.conf ] +case "$(command -v podman)" in /tmp/*/runtime/bin/podman) ;; *) exit 97 ;; esac +case "$1" in + restore) + [ "$#" = 3 ] && [ "$2" = /capsule/backup.tar.gz.gpg ] + mkdir -p "$3/var/data" "$3/bin" + printf '%s\n' private-environment > "$3/.env" + printf '%s\n' encrypted-history > "$3/var/data/history" + printf '%s\n' images > "$3/var/images.tar" + printf '%s\n' image-lock > "$3/var/image-lock.json" + printf '%s\n' '#!/bin/sh' 'exit 99' > "$3/bin/stack" + chmod 700 "$3/bin/stack" + printf '%s\n' old-source > "$3/source" + echo restored >> /tmp/restorations + ;; + --root) + [ "$#" = 4 ] && [ "$3" = activate ] && [ "$4" = /capsule/release.tar.gz ] + [ ! -e /proc/self/fd/9 ] + [ "$(cat "$4")" = 'opaque release fixture' ] + echo activated >> /tmp/activations + [ ! -f /tmp/fail-activation ] || exit 42 + mkdir -p "$2/infra/guix" + printf '%s\n' '#!/bin/sh' 'echo new-management' > "$2/bin/stack" + chmod 700 "$2/bin/stack" + printf '%s\n' new-source > "$2/source" + cp /capsule/run "$2/infra/guix/run.sh" + cp /capsule/run "$2/bin/run" + cp /capsule/containers.conf "$2/infra/guix/containers.conf" + ;; + *) exit 98 ;; +esac +STACK +chmod 700 "$STAGE/capsule/stack" "$STAGE/runtime/bin/"* "$STAGE/test-bin/"* +tar -czf "$STAGE/capsule/runtime.tar.gz" -C "$STAGE/runtime" . +(cd "$STAGE/capsule" && sha256sum backup.tar.gz.gpg release.tar.gz runtime.tar.gz restore run stack containers.conf > SHA256SUMS) +bwrap --unshare-user --uid 0 --gid 0 --unshare-pid --cap-add CAP_DAC_OVERRIDE \ + --tmpfs / --ro-bind /usr /usr \ + --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib /lib64 \ + --ro-bind "$STAGE/capsule" /fixture --ro-bind "$STAGE/test-bin" /test-bin \ + --proc /proc --dev /dev --tmpfs /tmp \ + --ro-bind "$STAGE/lowport" /proc/sys/net/ipv4/ip_unprivileged_port_start \ + --setenv PATH /test-bin:/usr/bin --setenv XDG_RUNTIME_DIR /tmp/xdg \ + --chdir / /bin/sh -eu <<'TEST' +fail() { printf '%s\n' "$1" >&2; exit 1; } +reject() { if sh /capsule/restore "$1"; then fail "$2"; fi; } +cp -a /fixture /capsule +mkdir -p /etc /gnu/.stack-runtimes /tmp/xdg +printf '%s\n' service:100000:65536 > /etc/subuid +cp /etc/subuid /etc/subgid +DIGEST=$(sha256sum /capsule/runtime.tar.gz) +DIGEST=${DIGEST%% *} +printf '%s\n' "$DIGEST" > "/gnu/.stack-runtimes/$DIGEST" +touch /tmp/fail-activation +STATUS=0 +sh /capsule/restore /recovered || STATUS=$? +[ "$STATUS" = 42 ] || fail 'Did not reach the paired release activation.' +[ -f /recovered/var/guix-recovery ] +[ "$(cat /recovered/.env)" = private-environment ] +[ "$(cat /recovered/var/data/history)" = encrypted-history ] +[ "$(cat /recovered/source)" = old-source ] +cp /capsule/backup.tar.gz.gpg /tmp/original-backup +echo changed >> /capsule/backup.tar.gz.gpg +(cd /capsule && sha256sum backup.tar.gz.gpg release.tar.gz runtime.tar.gz restore run stack containers.conf > SHA256SUMS) +reject /recovered 'Accepted a different backup for pending recovery.' +[ "$(wc -l < /tmp/restorations)" = 1 ] && [ "$(wc -l < /tmp/activations)" = 1 ] +cp /tmp/original-backup /capsule/backup.tar.gz.gpg +(cd /capsule && sha256sum backup.tar.gz.gpg release.tar.gz runtime.tar.gz restore run stack containers.conf > SHA256SUMS) +exec 9>/recovered/var/guix-recovery.lock +flock 9 +reject /recovered 'Accepted concurrent recovery.' 9>&- +exec 9>&- +[ "$(wc -l < /tmp/activations)" = 1 ] +rm /tmp/fail-activation +sh /capsule/restore /recovered +[ "$(wc -l < /tmp/restorations)" = 1 ] && [ "$(wc -l < /tmp/activations)" = 2 ] +[ ! -e /recovered/var/guix-recovery ] +[ "$(/recovered/bin/stack)" = new-management ] && [ "$(cat /recovered/source)" = new-source ] +cmp /capsule/run /recovered/infra/guix/run.sh +cmp /capsule/containers.conf /recovered/infra/guix/containers.conf +[ "$(cat /recovered/.env)" = private-environment ] +[ "$(cat /recovered/var/data/history)" = encrypted-history ] +mkdir /unrelated +reject /unrelated 'Accepted unrelated existing destination.' +reject /recovered 'Repeated a completed recovery.' +[ "$(wc -l < /tmp/restorations)" = 1 ] && [ "$(wc -l < /tmp/activations)" = 2 ] +printf '%s\n' 'Synthetic recovery orchestration, resume, intent, locks and preservation passed; no real encryption or container lifecycle proved.' +TEST diff --git a/infra/guix/test.sh b/infra/guix/test.sh new file mode 100644 index 0000000..4425303 --- /dev/null +++ b/infra/guix/test.sh @@ -0,0 +1,166 @@ +#!/bin/sh +# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. + +set -eu +umask 077 +ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd) +for SCRIPT in "$ROOT/infra/guix/"*.sh; do + sh -n "$SCRIPT" +done +sh "$ROOT/infra/guix/test-bootstrap.sh" +sh "$ROOT/infra/guix/test-recovery.sh" +STAGE=$(mktemp -d) +trap 'find "$STAGE" -type d -exec chmod u+w {} +; rm -rf -- "$STAGE"' EXIT HUP INT TERM +mkdir -p "$STAGE/source/infra/guix" "$STAGE/source/bin" "$STAGE/fake/bin" "$STAGE/toolbin" \ + "$STAGE/fake/gnu/store/readonly" "$STAGE/sentinel" +cp "$ROOT/infra/guix/"*.sh "$STAGE/source/infra/guix/" +cp "$ROOT/infra/podman.conf" "$STAGE/source/infra/podman.conf" +cp "$ROOT/infra/guix/containers.conf" "$STAGE/source/infra/guix/containers.conf" +printf '%s\n' 'ciphertext fixture' > "$STAGE/backup.tar.gz.gpg" +printf '%s\n' 'channels fixture' > "$STAGE/source/bin/guix-channels.scm" +printf '%s\n' '#!/bin/sh' 'printf "%s\n" "${CONTAINERS_CONF:-}" > "$0.environment"' 'printf "%s\n" "$@"' > "$STAGE/source/bin/stack" +chmod 700 "$STAGE/source/bin/stack" +for TOOL in podman gpg; do + printf '%s\n' '#!/bin/sh' 'exit 0' > "$STAGE/fake/bin/$TOOL" + chmod 700 "$STAGE/fake/bin/$TOOL" +done +printf '%s\n' 'Immutable runtime content.' > "$STAGE/fake/gnu/store/readonly/value" +printf '%s\n' 'Independent sentinel.' > "$STAGE/sentinel/value" +ln -s "$STAGE/sentinel" "$STAGE/fake/gnu/store/readonly/outside" +chmod 555 "$STAGE/sentinel" +chmod 444 "$STAGE/sentinel/value" +chmod 555 "$STAGE/fake/gnu/store" "$STAGE/fake/gnu/store/readonly" +chmod 444 "$STAGE/fake/gnu/store/readonly/value" +tar -czf "$STAGE/runtime.tar.gz" -C "$STAGE/fake" . +chmod -R u+w "$STAGE/fake/gnu/store" +cat > "$STAGE/toolbin/guix" <<'GUIX' +#!/bin/sh +set -eu +case "$1" in + describe) printf '%s\n' 'channels fixture' ;; + time-machine) printf '%s\n' "$FAKE_PACK" ;; + *) exit 1 ;; +esac +GUIX +chmod 700 "$STAGE/toolbin/guix" +FAKE_PACK="$STAGE/runtime.tar.gz" PATH="$STAGE/toolbin:$PATH" sh "$STAGE/source/infra/guix/build.sh" +[ -z "$(find "$STAGE/source/bin" -maxdepth 1 -name '.guix-build.*' -print -quit)" ] || { + echo 'Build left immutable staging directories.' >&2; exit 1; +} +[ "$(stat -c %a "$STAGE/sentinel")" = 555 ] +[ "$(stat -c %a "$STAGE/sentinel/value")" = 444 ] +cmp "$STAGE/runtime.tar.gz" "$STAGE/source/bin/guix-runtime.tar.gz" +mkdir -p "$STAGE/release/stack/bin/chat" "$STAGE/release/stack/infra/guix" "$STAGE/release/stack/var" +cp "$STAGE/source/bin/guix-runtime.tar.gz" "$STAGE/source/bin/guix-channels.scm" "$STAGE/release/stack/bin/" +cp "$STAGE/source/infra/guix/"*.sh "$STAGE/release/stack/infra/guix/" +cp "$ROOT/infra/guix/containers.conf" "$STAGE/release/stack/infra/guix/containers.conf" +cp "$ROOT/LICENSE" "$STAGE/release/stack/LICENSE" +printf '%s\n' 'services fixture' > "$STAGE/release/stack/infra/services.toml" +printf '%s\n' 'image lock fixture' > "$STAGE/release/stack/var/image-lock.json" +printf '%s\n' 'image export fixture' > "$STAGE/release/stack/var/images.tar" +for ARTIFACT in server crypto.wasm SOURCE.tar.gz; do + printf '%s\n' 'chat artifact fixture' > "$STAGE/release/stack/bin/chat/$ARTIFACT" +done +cat > "$STAGE/release/stack/bin/stack" <<'STACK' +#!/bin/sh +set -eu +[ "$#" = 4 ] && [ "$1" = restore ] && [ "$2" = --release ] || exit 1 +mkdir "$4" +tar -xzf "$3" --strip-components=1 -C "$4" +STACK +chmod 700 "$STAGE/release/stack/bin/stack" +tar -czf "$STAGE/release.tar.gz" -C "$STAGE/release" stack +sh "$STAGE/source/infra/guix/bundle.sh" "$STAGE/backup.tar.gz.gpg" "$STAGE/release.tar.gz" "$STAGE/recovery.tar.gz" +if sh "$STAGE/source/infra/guix/bundle.sh" "$STAGE/backup.tar.gz.gpg" "$STAGE/release.tar.gz" "$STAGE/recovery.tar.gz"; then + echo 'Overwrote existing archive.' >&2; exit 1 +fi +tar -xzf "$STAGE/recovery.tar.gz" -C "$STAGE" +[ -x "$STAGE/stack-recovery/bootstrap" ] || { echo 'Store bootstrap missing from capsule.' >&2; exit 1; } +cmp "$ROOT/infra/guix/containers.conf" "$STAGE/stack-recovery/containers.conf" +(cd "$STAGE/stack-recovery" && sha256sum --check --strict SHA256SUMS) +cmp "$STAGE/backup.tar.gz.gpg" "$STAGE/stack-recovery/backup.tar.gz.gpg" +cmp "$STAGE/release.tar.gz" "$STAGE/stack-recovery/release.tar.gz" +cmp "$STAGE/release/stack/bin/stack" "$STAGE/stack-recovery/stack" +cmp "$STAGE/release/stack/LICENSE" "$STAGE/stack-recovery/LICENSE" +[ "$(stat -c %a "$STAGE/recovery.tar.gz")" = 600 ] +if cmp -s "$STAGE/source/bin/stack" "$STAGE/stack-recovery/stack"; then + echo 'Bundled source-tree executable instead of release executable.' >&2; exit 1 +fi +if "$STAGE/stack-recovery/restore" "$STAGE/source"; then + echo 'Accepted existing restore destination.' >&2; exit 1 +fi +cp "$STAGE/source/infra/guix/run.sh" "$STAGE/source/bin/run" +OUTPUT=$(sh "$STAGE/source/bin/run" status) +EXPECTED=$(printf '%s\n' --root "$STAGE/source" status) +[ "$OUTPUT" = "$EXPECTED" ] +[ "$(cat "$STAGE/source/bin/stack.environment")" = "$STAGE/source/infra/guix/containers.conf" ] +mv "$STAGE/source/bin/guix-runtime.tar.gz" "$STAGE/saved-runtime.tar.gz" +OUTPUT=$(env -u CONTAINERS_CONF sh "$STAGE/source/bin/run" status) +[ "$OUTPUT" = "$EXPECTED" ] +[ "$(cat "$STAGE/source/bin/stack.environment")" = '' ] +mv "$STAGE/saved-runtime.tar.gz" "$STAGE/source/bin/guix-runtime.tar.gz" +find "$STAGE/source/var/guix" -name podman | read -r MATCH +PUBLISHED=$(sha256sum "$STAGE/source/bin/guix-runtime.tar.gz") +PUBLISHED=${PUBLISHED%% *} +DIRECTORY_MODE=$(stat -c %a "$STAGE/source/var/guix/$PUBLISHED/gnu/store/readonly") +FILE_MODE=$(stat -c %a "$STAGE/source/var/guix/$PUBLISHED/gnu/store/readonly/value") +chmod -R a-w "$STAGE/fake/gnu/store" +tar -czf "$STAGE/failed-runtime.tar.gz" -C "$STAGE/fake" bin/podman gnu +chmod -R u+w "$STAGE/fake/gnu/store" +cp "$STAGE/failed-runtime.tar.gz" "$STAGE/source/bin/guix-runtime.tar.gz" +if sh "$STAGE/source/bin/run" status; then + echo 'Accepted runtime missing GnuPG.' >&2; exit 1; +fi +[ -z "$(find "$STAGE/source/var/guix" -maxdepth 1 -name '.unpack.*' -print -quit)" ] || { + echo 'Failed launcher left immutable staging directories.' >&2; exit 1; +} +[ -x "$STAGE/source/var/guix/$PUBLISHED/bin/gpg" ] +[ "$(stat -c %a "$STAGE/source/var/guix/$PUBLISHED/gnu/store/readonly")" = "$DIRECTORY_MODE" ] +[ "$(stat -c %a "$STAGE/source/var/guix/$PUBLISHED/gnu/store/readonly/value")" = "$FILE_MODE" ] +[ "$(stat -c %a "$STAGE/sentinel")" = 555 ] +[ "$(stat -c %a "$STAGE/sentinel/value")" = 444 ] +cmp "$STAGE/runtime.tar.gz" "$STAGE/stack-recovery/runtime.tar.gz" +cp "$STAGE/runtime.tar.gz" "$STAGE/source/bin/guix-runtime.tar.gz" +printf 'tamper' >> "$STAGE/stack-recovery/backup.tar.gz.gpg" +if (cd "$STAGE/stack-recovery" && sha256sum --check --strict SHA256SUMS); then + echo 'Accepted changed ciphertext.' >&2; exit 1 +fi +rm "$STAGE/release/stack/bin/guix-channels.scm" +tar -czf "$STAGE/missing.tar.gz" -C "$STAGE/release" stack +if sh "$STAGE/source/infra/guix/bundle.sh" "$STAGE/backup.tar.gz.gpg" "$STAGE/missing.tar.gz" "$STAGE/other.tar.gz"; then + echo 'Accepted missing provenance.' >&2; exit 1 +fi +cp "$STAGE/source/bin/guix-channels.scm" "$STAGE/release/stack/bin/guix-channels.scm" +for ARTIFACT in image-lock.json images.tar; do + mv "$STAGE/release/stack/var/$ARTIFACT" "$STAGE/$ARTIFACT" + tar -czf "$STAGE/missing-image.tar.gz" -C "$STAGE/release" stack + if sh "$STAGE/source/infra/guix/bundle.sh" "$STAGE/backup.tar.gz.gpg" "$STAGE/missing-image.tar.gz" "$STAGE/missing-image-output.tar.gz"; then + echo 'Accepted release without complete saved images.' >&2; exit 1 + fi + [ ! -e "$STAGE/missing-image-output.tar.gz" ] + mv "$STAGE/$ARTIFACT" "$STAGE/release/stack/var/$ARTIFACT" +done +for PRIVATE in .env var/data; do + mkdir -p "$(dirname "$STAGE/release/stack/$PRIVATE")" + printf '%s\n' 'private fixture' > "$STAGE/release/stack/$PRIVATE" + tar -czf "$STAGE/private.tar.gz" -C "$STAGE/release" stack + if sh "$STAGE/source/infra/guix/bundle.sh" "$STAGE/backup.tar.gz.gpg" "$STAGE/private.tar.gz" "$STAGE/private-output.tar.gz"; then + echo 'Accepted private release contents.' >&2; exit 1 + fi + [ ! -e "$STAGE/private-output.tar.gz" ] + rm "$STAGE/release/stack/$PRIVATE" +done +printf '%s\n' 'Bundle, immutable cleanup, integrity, overwrite, launcher, and missing-artifact checks passed.' |
