#!/usr/bin/env bash # Run the test suite where it cannot reach the real home directory, and fail # if anything tried to. # # Two different problems, both handled here because neither is enough alone: # # containment a sandbox stops a test writing to ~/.config/atgc # detection an assertion says that a test tried # # A sandbox on its own is silent. The escape still happens, into a home that # is thrown away, and nobody learns anything -- which is how four unit tests # came to be depositing a `did:plc:alice` directory in a real configuration # directory while the suite stayed green. So each binary gets a fresh home # that is inspected afterwards, and the run is refused if one is not empty. # # The binaries are built outside the sandbox and only *run* inside it, so # nothing here needs a toolchain, a network, or an image. set -uo pipefail cd "$(dirname "$0")/.." repo=$PWD homes=$repo/target/test-homes rm -rf "$homes"; mkdir -p "$homes" echo "building test binaries..." # Only artifacts cargo marks as tests. The same stream carries the `atgc` # binary itself, and running the CLI with a test runner's flags is not a test # failing -- it is a command refusing an argument it has never heard of. mapfile -t bins < <(cargo test --no-run --message-format=json 2>/dev/null | python3 -c ' import json, sys for line in sys.stdin: try: m = json.loads(line) except ValueError: continue if m.get("reason") == "compiler-artifact" and m.get("executable") \ and m.get("profile", {}).get("test"): print(m["executable"]) ' | sort -u) if [ ${#bins[@]} -eq 0 ]; then echo "no test binaries were built" >&2 exit 1 fi # `--dev-bind / /` then a tmpfs over the home: everything the toolchain needs # stays visible, and the one directory the suite must not touch is replaced by # an empty one. The cargo and rustup trees are bound back read-only because a # test binary may still resolve a linker or a runtime out of them. # Bubblewrap is the containment half and it is optional: without it the run # still gets a fresh home per binary and still refuses an escape, which is the # half that finds bugs. A CI image that lacks it -- or forbids user namespaces # -- gets the detection anyway rather than a skipped check. # ATGC_NO_BWRAP=1 takes the fallback on a machine that has bubblewrap, which # is the only way to exercise that path before CI does. have_bwrap=0 if [ -z "${ATGC_NO_BWRAP:-}" ] && bwrap --dev-bind / / true >/dev/null 2>&1; then have_bwrap=1 fi [ $have_bwrap -eq 1 ] || echo "note: bubblewrap unavailable; isolating by HOME alone" sandbox() { local home=$1; shift if [ $have_bwrap -eq 0 ]; then env HOME="$home" XDG_CONFIG_HOME="$home/.config" "$@" return fi bwrap --dev-bind / / \ --tmpfs "$HOME" \ --tmpfs /tmp \ --bind "$repo" "$repo" \ --bind "$home" "$home" \ ${CARGO_HOME:+--ro-bind "$CARGO_HOME" "$CARGO_HOME"} \ --ro-bind-try "$HOME/.cargo" "$HOME/.cargo" \ --ro-bind-try "$HOME/.rustup" "$HOME/.rustup" \ --setenv HOME "$home" \ --setenv XDG_CONFIG_HOME "$home/.config" \ --die-with-parent \ "$@" } failed=0 for bin in "${bins[@]}"; do name=$(basename "$bin") home=$homes/$name mkdir -p "$home" if ! sandbox "$home" "$bin" --quiet; then echo "FAIL $name" failed=1 fi # The home is inspected from outside, after the sandbox is gone. Anything # here is a test that reached for the real configuration directory and # would have found it on a developer's machine. if [ -n "$(ls -A "$home" 2>/dev/null)" ]; then echo "ESCAPED $name wrote outside its temporary directories:" find "$home" | sed 's|^| |' failed=1 fi done if [ $failed -eq 0 ]; then echo "all ${#bins[@]} test binaries passed, and none touched the home directory" fi exit $failed