From 183cd727be5968fec3ce43176ebb1fa255f8976b Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Sun, 13 Sep 2026 13:53:45 +0300 Subject: [PATCH 1/2] fix(test): isolate smoke harness daemon runtime scripts/smoke-test.sh retires the account daemon from seven call sites, but its wrappers sandbox only HOME/TMPDIR/CBM_CACHE_DIR and only CBM_RUNTIME_DIR moves the daemon rendezvous, so every retirement reached the operator's live daemon. Source scripts/test-runtime.sh so every product process runs under a harness-owned runtime and cache, and clean that root up from the EXIT trap. Add tests/test_smoke_runtime_isolation_contract.sh, which fails before this change, and wire it into scripts/test.sh. Part of #1696. Signed-off-by: Anton Standrik --- scripts/smoke-test.sh | 17 +++- scripts/test.sh | 3 + .../test_smoke_runtime_isolation_contract.sh | 82 +++++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100755 tests/test_smoke_runtime_isolation_contract.sh diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 3371fc424..e996da847 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -22,6 +22,8 @@ they stage the release fixture, start the fixture server, and sandbox HOME/TEMP/agent-config destinations. Called bare, the download/checksum/ install-script phases (12-13) SKIP for lack of a fixture server, and the run mutates the REAL profile — the venue-parity contract forbids that in any venue. +The daemon runtime and cache are private to the run either way: every product +process is started under a CBM_RUNTIME_DIR/CBM_CACHE_DIR this harness owns. Arguments: product binary to smoke @@ -41,6 +43,15 @@ if [ -n "$SMOKE_MODE" ] && [ "$SMOKE_MODE" != "--agent-config-only" ]; then fi REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" +# Every product process below — the phases, the install/update E2E and the +# daemon retirements — must reach a daemon rendezvous this run owns. Only +# CBM_RUNTIME_DIR moves that rendezvous; the wrappers' HOME/TMPDIR/CBM_CACHE_DIR +# sandbox does not, so without this the retirements land on the operator's live +# account daemon (#1691, #1696). +# shellcheck source=test-runtime.sh +source "$REPO_ROOT/scripts/test-runtime.sh" +cbm_test_runtime_init + smoke_mktemp_file() { if [ -n "${SMOKE_TEMP_ROOT:-}" ]; then mktemp "$SMOKE_TEMP_ROOT/cbm-smoke.XXXXXX" @@ -100,8 +111,8 @@ copy_smoke_binary() { cp "$BINARY" "$destination" } -# Retire the shared account daemon (if one is running) and wait until it -# reports not-running. Install/uninstall flows leave an ephemeral daemon +# Retire this run's private account daemon (if one is running) and wait until +# it reports not-running. Install/uninstall flows leave an ephemeral daemon # draining asynchronously whose mapped generation backing and open logs # block rm on Windows (POSIX rm doesn't care) — so every cleanup of a # fixture HOME that received an install, and the final cache removal, must @@ -149,7 +160,7 @@ CODEX_LIFECYCLE_HOME="" if command -v cygpath &>/dev/null; then TMPDIR=$(cygpath -m "$TMPDIR") fi -trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"' EXIT +trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"; cbm_test_runtime_cleanup "$BINARY"' EXIT CLI_STDERR=$(smoke_mktemp_file) # 10 of the cli call sites assign directly (VAR=$(cli ...)). Under diff --git a/scripts/test.sh b/scripts/test.sh index 50d65643f..0845a2e77 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -272,6 +272,9 @@ bash "$ROOT/tests/test_release_gate_chain_contract.sh" echo "=== Step 0t: test runtime isolation contract (#1691) ===" bash "$ROOT/tests/test_runtime_isolation_contract.sh" +echo "=== Step 0t2: smoke harness runtime isolation contract (#1696) ===" +bash "$ROOT/tests/test_smoke_runtime_isolation_contract.sh" + echo "=== Step 0u: shell line-ending contract ===" bash "$ROOT/tests/test_shell_line_endings.sh" diff --git a/tests/test_smoke_runtime_isolation_contract.sh b/tests/test_smoke_runtime_isolation_contract.sh new file mode 100755 index 000000000..e91e1d38d --- /dev/null +++ b/tests/test_smoke_runtime_isolation_contract.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runtime-isolation contract for the smoke harness (#1696, follow-up to #1691). +# +# scripts/smoke-test.sh is the process that actually starts the product during +# a smoke run, and it retires "the account daemon" seven times through +# `daemon stop`. Its wrappers sandbox HOME/XDG/TMPDIR and CBM_CACHE_DIR, but +# only CBM_RUNTIME_DIR moves the daemon rendezvous (docs/CONFIGURATION.md), so +# without a private runtime every one of those stops lands on the operator's +# live daemon. Drive the harness with an environment-probe fixture and require +# that no product process ever receives the caller's runtime or cache. + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +normalize_path() { + local path=${1%$'\r'} + if command -v cygpath >/dev/null 2>&1; then + cygpath -u "$path" 2>/dev/null && return 0 + fi + printf '%s\n' "${path//\\//}" +} + +ENV_PROBE="$WORKDIR/environment-probe" +cat > "$ENV_PROBE" <<'EOF' +#!/usr/bin/env bash +printf '%s\t%s\n' "${CBM_CACHE_DIR-}" "${CBM_RUNTIME_DIR-}" >> "$CBM_SMOKE_ENV_PROBE" +case "${1-}" in + --version) echo "v0.0.0-probe"; exit 0 ;; + daemon) [[ "${2-}" == status ]] && exit 1; exit 0 ;; +esac +echo '{}' +exit 0 +EOF +chmod +x "$ENV_PROBE" + +CALLER_CACHE="$WORKDIR/caller-cache" +CALLER_RUNTIME="$WORKDIR/caller-runtime" +ENV_LOG="$WORKDIR/environment.log" +mkdir -p "$CALLER_CACHE" "$CALLER_RUNTIME" + +# The fixture answers nothing beyond --version, so the smoke fails early; only +# the environment it handed to the product is under test here. +CBM_CACHE_DIR="$CALLER_CACHE" \ +CBM_RUNTIME_DIR="$CALLER_RUNTIME" \ +CBM_SMOKE_ENV_PROBE="$ENV_LOG" \ + "$ROOT/scripts/smoke-test.sh" "$ENV_PROBE" > "$WORKDIR/smoke.out" 2>&1 || true + +[[ -s "$ENV_LOG" ]] || fail "smoke-test did not execute the environment-probe fixture" + +CALLER_CACHE_NORMALIZED=$(normalize_path "$CALLER_CACHE") +CALLER_RUNTIME_NORMALIZED=$(normalize_path "$CALLER_RUNTIME") +private_root="" +while IFS=$'\t' read -r child_cache_raw child_runtime_raw; do + child_cache=$(normalize_path "$child_cache_raw") + child_runtime=$(normalize_path "$child_runtime_raw") + if [[ -z "$child_runtime" || "$child_runtime" == "$CALLER_RUNTIME_NORMALIZED" ]]; then + fail "smoke-test exposed the caller CBM_RUNTIME_DIR to a product process" + fi + if [[ -z "$child_cache" || "$child_cache" == "$CALLER_CACHE_NORMALIZED" ]]; then + fail "smoke-test exposed the caller CBM_CACHE_DIR to a product process" + fi + if [[ "${child_runtime%/*}" != "${child_cache%/*}" || + "${child_runtime##*/}" != "runtime" || "${child_cache##*/}" != "cache" ]]; then + fail "smoke runtime/cache were not isolated beneath one private root" + fi + if [[ -n "$private_root" && "$private_root" != "${child_runtime%/*}" ]]; then + fail "smoke-test switched private roots mid-run" + fi + private_root="${child_runtime%/*}" +done < "$ENV_LOG" + +[[ ! -e "$private_root" ]] || fail "smoke-test left its private root behind: $private_root" + +echo "PASS: smoke harness isolates its daemon runtime and cache from the caller" From fa138acf44525ef94cfd6e16772d601da5623aef Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 21 Sep 2026 11:29:04 +0300 Subject: [PATCH 2/2] fix(test): arm smoke runtime cleanup at init, retire the daemon first Review on #2199: cbm_test_runtime_init ran more than a hundred lines before the only EXIT trap, so under `set -e` a failure in the fixture mktemp or its cygpath conversion left the private root behind, and the fixture trap ran smoke_rmtree ahead of the runtime cleanup. Arm `trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT` immediately after a successful init, as soak-test.sh and memlab.sh do. The fixture trap that replaces it now retires the private daemon first and removes the fixtures second, so no earlier cleanup step stands between the exit and the runtime cleanup; on Windows the retirement is also what unblocks the fixture rm. tests/test_smoke_runtime_isolation_contract.sh pins the early trap to the window between the init call and the first fixture; the pin fails against the previous head. Part of #1696. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Anton Standrik --- scripts/smoke-test.sh | 11 +++++++++- .../test_smoke_runtime_isolation_contract.sh | 21 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index e996da847..d1c64c6b7 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -51,6 +51,11 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)" # shellcheck source=test-runtime.sh source "$REPO_ROOT/scripts/test-runtime.sh" cbm_test_runtime_init +# Armed here rather than only with the fixture trap below: the fixture mktemp +# and its cygpath conversion sit between the two, and under `set -e` a failure +# there would otherwise leave the private root behind. The fixture trap +# replaces this one and keeps the same cleanup as its first step. +trap 'cbm_test_runtime_cleanup "$BINARY"' EXIT smoke_mktemp_file() { if [ -n "${SMOKE_TEMP_ROOT:-}" ]; then @@ -160,7 +165,11 @@ CODEX_LIFECYCLE_HOME="" if command -v cygpath &>/dev/null; then TMPDIR=$(cygpath -m "$TMPDIR") fi -trap 'smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"; cbm_test_runtime_cleanup "$BINARY"' EXIT +# Runtime cleanup first, so no earlier cleanup step stands between the exit +# and the private daemon's retirement; on Windows that retirement is also what +# unblocks the fixture rm (mapped binary, open logs). smoke_rmtree never fails, +# so the fixture removal still runs after it. +trap 'cbm_test_runtime_cleanup "$BINARY"; smoke_rmtree "$TMPDIR" "${DRYRUN_HOME:-}" "${CODEX_LIFECYCLE_HOME:-}"' EXIT CLI_STDERR=$(smoke_mktemp_file) # 10 of the cli call sites assign directly (VAR=$(cli ...)). Under diff --git a/tests/test_smoke_runtime_isolation_contract.sh b/tests/test_smoke_runtime_isolation_contract.sh index e91e1d38d..31ba7a7b6 100755 --- a/tests/test_smoke_runtime_isolation_contract.sh +++ b/tests/test_smoke_runtime_isolation_contract.sh @@ -79,4 +79,25 @@ done < "$ENV_LOG" [[ ! -e "$private_root" ]] || fail "smoke-test left its private root behind: $private_root" +# The run above exits through the fixture trap, so it cannot show what happens +# to the private root when the harness dies before that trap exists — the +# fixture mktemp and its cygpath conversion are in that window, and under +# `set -e` either can end the run. Reproducing that failure would mean scanning +# the shared /tmp parent for orphaned roots, which races every other harness +# test in the same suite, so pin the ordering instead: the cleanup trap is +# armed between the init call and the first fixture work. +smoke="$ROOT/scripts/smoke-test.sh" +smoke_line_of() { + # A missing pattern is the failure this check reports, not a reason to end + # the test silently under `set -e`. + grep -n "$1" "$smoke" | head -1 | cut -d: -f1 || true +} +init_line=$(smoke_line_of '^cbm_test_runtime_init$') +early_trap_line=$(smoke_line_of "^trap 'cbm_test_runtime_cleanup \"\$BINARY\"' EXIT\$") +fixture_line=$(smoke_line_of '^TMPDIR=\$(smoke_mktemp_dir)$') +if [[ -z "$init_line" || -z "$early_trap_line" || -z "$fixture_line" ]] || + ((early_trap_line < init_line || early_trap_line > fixture_line)); then + fail "smoke-test must arm the runtime cleanup trap between cbm_test_runtime_init and its first fixture" +fi + echo "PASS: smoke harness isolates its daemon runtime and cache from the caller"