fix(fleet): make framework shell assertions SIGPIPE-safe (#1100)
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful

This commit was merged in pull request #1100.
This commit is contained in:
2026-08-07 08:21:20 +00:00
parent 4fa2768962
commit df4c591ab4
3 changed files with 240 additions and 37 deletions
@@ -14,6 +14,82 @@ fail() {
exit 1
}
pane_command_clears_environment() {
local calls_file="$1"
local -a argv=()
local index
mapfile -d '' -t argv < "$calls_file"
for ((index = 0; index + 1 < ${#argv[@]}; index++)); do
if [ "${argv[$index]}" = /usr/bin/env ] && [ "${argv[$((index + 1))]}" = -i ]; then
return 0
fi
done
return 1
}
print_pane_argv() {
local calls_file="$1"
local -a argv=()
local bytes index
mapfile -d '' -t argv < "$calls_file"
bytes=$(wc -c < "$calls_file")
printf 'observed pane argv: records=%s bytes=%s\n' "${#argv[@]}" "$bytes" >&2
for ((index = 0; index < ${#argv[@]}; index++)); do
printf ' [%03d] %q\n' "$index" "${argv[$index]}" >&2
done
}
check_pane_environment_boundary() {
local calls_file="$1"
if pane_command_clears_environment "$calls_file"; then
return 0
fi
print_pane_argv "$calls_file"
return 1
}
contains_literal() {
grep -F -- "$2" <<< "$1" >/dev/null
}
contains_line() {
grep -xF -- "$2" <<< "$1" >/dev/null
}
# Portability regression: inspect the authoritative NUL-delimited argv instead
# of piping a newline reconstruction through `grep -q` under pipefail. The old
# pipeline could report failure after a successful match when an upstream
# producer received SIGPIPE. A large trailing argument keeps that failure class
# covered without making stream size part of the semantic contract.
PORTABILITY_CALLS="$ROOT/portability-calls"
printf -v PORTABILITY_PADDING '%*s' 32768 ''
PORTABILITY_PADDING=${PORTABILITY_PADDING// /x}
printf '%s\0' /usr/bin/env -i "$PORTABILITY_PADDING" > "$PORTABILITY_CALLS"
pane_command_clears_environment "$PORTABILITY_CALLS" || \
fail "valid large pane argv was rejected by the environment-boundary assertion"
assert_pane_boundary_rejected() {
local case_name="$1"
local expected_records="$2"
local diagnostic
if diagnostic=$(check_pane_environment_boundary "$PORTABILITY_CALLS" 2>&1); then
fail "pane boundary accepted invalid $case_name fixture"
fi
contains_literal "$diagnostic" "records=$expected_records bytes=" || \
fail "pane argv diagnostic omitted counts for $case_name fixture"
contains_literal "$diagnostic" '[000]' || \
fail "pane argv diagnostic omitted indexed arguments for $case_name fixture"
}
printf '%s\0' tmux -i > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected missing-env 2
printf '%s\0' /usr/bin/env HOME=/untrusted > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected missing-i 2
printf '%s\0' /usr/bin/env HOME=/untrusted -i > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected non-adjacent-i 3
printf '%s\0' -i /usr/bin/env > "$PORTABILITY_CALLS"
assert_pane_boundary_rejected reversed-boundary 2
cat > "$FAKE_BIN/tmux" <<'SHIM'
#!/usr/bin/env bash
set -euo pipefail
@@ -115,19 +191,19 @@ AGENT_VALID="coder0"
write_generated "$HOME_VALID" "$AGENT_VALID"
run_start "$HOME_VALID" "$AGENT_VALID"
valid_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$valid_args" | grep -qF new-session || fail "valid generated projection did not reach tmux"
echo "$valid_args" | grep -qF 'mosaic' || fail "fixed mosaic launcher command missing"
echo "$valid_args" | grep -qF 'yolo' || fail "fixed yolo launcher command missing"
echo "$valid_args" | grep -qF 'pi' || fail "roster runtime missing"
if echo "$valid_args" | grep -qF 'bash -c'; then
contains_literal "$valid_args" new-session || fail "valid generated projection did not reach tmux"
contains_literal "$valid_args" mosaic || fail "fixed mosaic launcher command missing"
contains_literal "$valid_args" yolo || fail "fixed yolo launcher command missing"
contains_literal "$valid_args" pi || fail "roster runtime missing"
if contains_literal "$valid_args" 'bash -c'; then
fail "launcher constructed a shell command payload"
fi
# The pane must start through an absolute clean-environment boundary. Its
# runtime command remains an argv vector, but no holder/session environment
# control variable can pass through the pane command.
echo "$valid_args" | grep -qxF '/usr/bin/env' || fail "pane does not use absolute env"
echo "$valid_args" | grep -qxF -- '-i' || fail "pane environment is not cleared"
check_pane_environment_boundary "$TMUX_CALLS" || \
fail "pane command did not use an adjacent /usr/bin/env -i boundary"
# Git identity is generated authority, not an optional or independently mutable
# local value. Each invalid form must fail before fake tmux receives a call.
@@ -156,7 +232,7 @@ assert_git_identity_rejected() {
fail "Git identity case $case_name was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before Git identity $case_name rejection"
echo "$output" | grep -qF "code=$expected_code" || \
contains_literal "$output" "code=$expected_code" || \
fail "Git identity $case_name diagnostic omitted code $expected_code"
}
@@ -176,7 +252,7 @@ if output=$(run_start "$HOME_UNSAFE_PARENT" coder-parent 2>&1); then
fail "generated file under a world-writable parent was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before unsafe parent rejection"
echo "$output" | grep -qF 'code=unsafe-permissions' || fail "unsafe parent diagnostic missing"
contains_literal "$output" 'code=unsafe-permissions' || fail "unsafe parent diagnostic missing"
: > "$TMUX_CALLS"
HOME_SYMLINK_PARENT="$ROOT/symlink-parent"
@@ -187,7 +263,7 @@ if output=$(run_start "$HOME_SYMLINK_PARENT" coder-symlink-parent 2>&1); then
fail "generated file under a symlinked parent was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before symlinked parent rejection"
echo "$output" | grep -qF 'code=unsafe-directory' || fail "symlinked parent diagnostic missing"
contains_literal "$output" 'code=unsafe-directory' || fail "symlinked parent diagnostic missing"
# Every managed ancestor is a boundary: MOSAIC_HOME, fleet, and agents. A
# symlink or group/world-writable ancestor must fail before environment parsing,
@@ -225,8 +301,8 @@ assert_managed_ancestor_rejected() {
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before $hazard $ancestor rejection"
[ ! -e "$home/work" ] || fail "workdir was created before $hazard $ancestor rejection"
echo "$output" | grep -qF "code=unsafe-" || fail "managed ancestor diagnostic missing"
if echo "$output" | grep -qF 'key=MOSAIC_AGENT_COMMAND'; then
contains_literal "$output" 'code=unsafe-' || fail "managed ancestor diagnostic missing"
if contains_literal "$output" 'key=MOSAIC_AGENT_COMMAND'; then
fail "environment parsing ran before $hazard $ancestor rejection"
fi
}
@@ -247,9 +323,9 @@ if output=$(run_start "$HOME_SHADOW" coder1 2>&1); then
fail "generated-key shadow was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before generated-key shadow rejection"
echo "$output" | grep -qF 'key=MOSAIC_AGENT_RUNTIME' || fail "shadow diagnostic omitted key"
echo "$output" | grep -qF 'sha256=' || fail "shadow diagnostic omitted hash"
if echo "$output" | grep -qF 'codex'; then
contains_literal "$output" 'key=MOSAIC_AGENT_RUNTIME' || fail "shadow diagnostic omitted key"
contains_literal "$output" 'sha256=' || fail "shadow diagnostic omitted hash"
if contains_literal "$output" codex; then
fail "shadow diagnostic leaked value"
fi
@@ -265,9 +341,9 @@ if output=$(run_start "$HOME_COMMAND" coder2 2>&1); then
fail "arbitrary command override was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before command rejection"
echo "$output" | grep -qF 'key=MOSAIC_AGENT_COMMAND' || fail "command diagnostic omitted key"
echo "$output" | grep -qF 'sha256=' || fail "command diagnostic omitted hash"
if echo "$output" | grep -qF "$COMMAND_VALUE"; then
contains_literal "$output" 'key=MOSAIC_AGENT_COMMAND' || fail "command diagnostic omitted key"
contains_literal "$output" 'sha256=' || fail "command diagnostic omitted hash"
if contains_literal "$output" "$COMMAND_VALUE"; then
fail "command diagnostic leaked command value"
fi
@@ -281,7 +357,7 @@ if output=$(run_start "$HOME_PERMS" coder3 2>&1); then
fail "world-readable local input was accepted"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before permissions rejection"
echo "$output" | grep -qF 'code=unsafe-permissions' || fail "permission diagnostic missing"
contains_literal "$output" 'code=unsafe-permissions' || fail "permission diagnostic missing"
# A unit/holder-like clean bootstrap must yield a pane with trusted HOME and
# computed PATH only. The pane command itself must not carry loader, shell
@@ -311,19 +387,17 @@ PATH="$PANE_STALE_PATH" \
MOSAIC_TEST_EXECUTE_PANE=1 \
"$START" coder-pane-boundary
pane_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$pane_args" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \
contains_line "$pane_args" "HOME=$PANE_TRUSTED_HOME" || \
fail "pane did not restore trusted HOME"
echo "$pane_args" | grep -qF "HOME=$PANE_STALE_HOME" && \
contains_literal "$pane_args" "HOME=$PANE_STALE_HOME" && \
fail "pane inherited stale HOME"
echo "$pane_args" | grep -qF "$PANE_STALE_PATH" && fail "pane inherited stale PATH"
contains_literal "$pane_args" "$PANE_STALE_PATH" && fail "pane inherited stale PATH"
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
echo "$pane_args" | grep -qF "$blocked" && fail "pane inherited $blocked"
contains_literal "$pane_args" "$blocked" && fail "pane inherited $blocked"
done
after_pane_env=$(printf '%s\n' "$pane_args" | grep -n -m1 -F '/usr/bin/env' | cut -d: -f1)
[ -n "$after_pane_env" ] || fail "pane command did not use absolute env"
printf '%s\n' "$pane_args" | tail -n +"$after_pane_env" | grep -qxF -- '-i' || \
fail "pane command did not clear its environment"
check_pane_environment_boundary "$TMUX_CALLS" || \
fail "pane command did not use an adjacent /usr/bin/env -i boundary"
pane_environment=$(tr '\0' '\n' < "$HOME_PANE_BOUNDARY/fleet/pane-environment")
# Exercise the repository launcher at $START, not the independently installed
# host copy. Set-compare every declared generated projection entry with the
@@ -337,11 +411,11 @@ if [ -n "$missing_or_changed_generated_environment" ]; then
missing_or_changed_keys=$(printf '%s\n' "$missing_or_changed_generated_environment" | cut -d= -f1 | paste -sd, -)
fail "runtime pane omitted or changed generated environment keys: $missing_or_changed_keys"
fi
echo "$pane_environment" | grep -qxF "HOME=$PANE_TRUSTED_HOME" || \
contains_line "$pane_environment" "HOME=$PANE_TRUSTED_HOME" || \
fail "runtime pane did not receive trusted HOME"
echo "$pane_environment" | grep -qF "$PANE_STALE_PATH" && fail "runtime pane received stale PATH"
contains_literal "$pane_environment" "$PANE_STALE_PATH" && fail "runtime pane received stale PATH"
for blocked in LD_PRELOAD= BASH_ENV= MOSAIC_UNTRUSTED_SENTINEL=; do
echo "$pane_environment" | grep -qF "$blocked" && fail "runtime pane received $blocked"
contains_literal "$pane_environment" "$blocked" && fail "runtime pane received $blocked"
done
write_interaction_generated() {
@@ -442,7 +516,7 @@ if output=$(run_interaction "$HOME_INTERACTION_MALFORMED" interaction-malformed
fail "interaction wrapper accepted malformed generated data"
fi
[ ! -s "$TMUX_CALLS" ] || fail "tmux ran before interaction strict-parser rejection"
echo "$output" | grep -qF 'code=unknown-key' || fail "interaction did not use shared strict parser first"
contains_literal "$output" 'code=unknown-key' || fail "interaction did not use shared strict parser first"
# A syntactically valid but policy-incompatible projection reaches the pinned
# interaction policy check only after strict parsing and never starts tmux.
@@ -455,9 +529,9 @@ if output=$(run_interaction "$HOME_INTERACTION_POLICY" interaction-policy 2>&1);
fail "interaction wrapper accepted a policy-incompatible projection"
fi
interaction_policy_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$interaction_policy_args" | grep -qF 'new-session' && \
contains_literal "$interaction_policy_args" new-session && \
fail "interaction pinned-policy rejection created a tmux session"
echo "$output" | grep -qF 'operator interaction service requires runtime pi' || \
contains_literal "$output" 'operator interaction service requires runtime pi' || \
fail "interaction pinned-policy check did not follow strict parsing"
# Exact stop derives the socket exclusively from the validated generated
@@ -470,10 +544,10 @@ HOME="$HOME_STOP" PATH="$FAKE_BIN:$PATH" MOSAIC_TEST_TMUX_CALLS="$TMUX_CALLS" \
MOSAIC_TEST_FLEET_OWNER=123e4567-e89b-12d3-a456-426614174000 \
MOSAIC_HOME="$HOME_STOP" MOSAIC_TMUX_SOCKET=ambient-socket "$START" --stop coder-stop
stop_args=$(tr '\0' '\n' < "$TMUX_CALLS")
echo "$stop_args" | grep -qxF 'mosaic-test' || fail "exact stop did not use the validated generated socket"
echo "$stop_args" | grep -qxF 'kill-session' || fail "exact stop did not request session termination"
echo "$stop_args" | grep -qxF '=coder-stop' || fail "exact stop did not exact-match the generated agent name"
if echo "$stop_args" | grep -qF 'ambient-socket'; then
contains_line "$stop_args" mosaic-test || fail "exact stop did not use the validated generated socket"
contains_line "$stop_args" kill-session || fail "exact stop did not request session termination"
contains_line "$stop_args" '=coder-stop' || fail "exact stop did not exact-match the generated agent name"
if contains_literal "$stop_args" ambient-socket; then
fail "exact stop trusted an ambient socket"
fi