#!/bin/sh # Copyright (C) 2026 Doan Luu Duc Tai # # 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 . 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