Files
stack/scripts/case2/sandbox-check.sh
jason.woltje abc66a5988 row 227 R3 (#1728): guard the test-case2.sh sandbox, sweep cwd links literally, refuse a long TMPDIR
P1: a failed mktemp, mkdir or output redirect now stops the script with
exit 2 before it derives a path, sets a trap or starts anything. The
sandbox root is checked to be absolute, a new empty directory this user
owns and its own physical path, and its device:inode is recorded.
sweep_sandbox and remove_sandbox in scripts/case2/sandbox.sh each refuse
unless the root still matches, so an early, late or empty call gets a
refusal instead of a sweep of the host.

P2: the sweep reads every /proc/<pid>/cwd target with one find and
compares it as a string, root or root plus "/", so *, ? and [ in the
path are literal. A cwd in a deleted directory under the root matches.

P3: a TMPDIR whose tools socket path would pass the 107-byte limit in
packages/seat/src/session.mjs is refused at entry with one line naming
the limit and both lengths. The length comes from the socket path built
on the real sandbox root.

scripts/case2/sandbox-check.sh checks the guard and the match against
dummy symlinks; nothing in it sends a signal.
2026-11-02 16:27:56 -06:00

123 lines
7.1 KiB
Bash

#!/usr/bin/env bash
# C2-14 (#1728, row 227 R3): checks for scripts/case2/sandbox.sh, the sandbox guard and the literal
# cwd match behind the kill sweep in scripts/test-case2.sh. The match runs against dummy symlinks in
# a scratch directory. Nothing here sends a signal: kill is a stub that records its arguments, so
# even a broken guard could not reach a process. One sweep call and the timing line read every
# /proc/<pid>/cwd target, as the suite's sweep does, with a root that holds no process.
# Run: bash scripts/case2/sandbox-check.sh (exit 0 when every check holds)
set -uo pipefail
cd "$(dirname "$0")/../.." || exit 2
. scripts/case2/sandbox.sh || exit 2
case "${TMPDIR:-/tmp}" in /*) ;; *) echo 'sandbox-check: TMPDIR must be absolute' >&2; exit 2 ;; esac
made="$(mktemp -d)" || exit 2
case "$made" in /?*) ;; *) exit 2 ;; esac
W="$(cd -P -- "$made" && pwd -P)" && WID="$(sandbox_id "$W")" && sandbox_ok "$W" "$WID" || exit 2
trap 'remove_sandbox "$W" "$WID"' EXIT
kill() { printf '%s\n' "$*" >>"$W/kill-calls"; return 0; }
PASS=0 FAIL=0
expect() { # expect NAME WANT(0|1) COMMAND...: COMMAND exits 0 when WANT is 0, non-zero when 1
local name=$1 want=$2 got
shift 2
"$@" 2>/dev/null; got=$?
if { [ "$want" = 0 ] && [ "$got" = 0 ]; } || { [ "$want" = 1 ] && [ "$got" != 0 ]; }; then
PASS=$((PASS+1)); echo "ok $name"
else
FAIL=$((FAIL+1)); echo "FAIL $name (exit $got)"
fi
}
same() { # same NAME WANT GOT
if [ "$2" = "$3" ]; then PASS=$((PASS+1)); echo "ok $1"
else FAIL=$((FAIL+1)); printf 'FAIL %s\n want %q\n got %q\n' "$1" "$2" "$3"; fi
}
# The guard. Roots with *, ? and [ are real directories here; the guard takes them as paths.
for name in 'a[bc]' 's*t' 'q?x' 'ab' 'sXt' 'qZx' 'keep'; do mkdir -p -- "$W/$name/tmp.example/root"; done
R="$W/a[bc]/tmp.example"
RID=$(sandbox_id "$R")
mkdir -- "$W/real" && ln -s -- "$W/real" "$W/alias"
expect 'guard: the created root with its id' 0 sandbox_ok "$R" "$RID"
expect 'guard: empty root' 1 sandbox_ok '' "$RID"
expect 'guard: empty id' 1 sandbox_ok "$R" ''
expect 'guard: relative root' 1 sandbox_ok 'scripts/case2' "$(sandbox_id scripts/case2)"
expect 'guard: missing root' 1 sandbox_ok "$W/missing" "$RID"
expect 'guard: / as root' 1 sandbox_ok / "$(sandbox_id /)"
expect 'guard: trailing slash' 1 sandbox_ok "$R/" "$RID"
expect 'guard: a . part' 1 sandbox_ok "$W/a[bc]/./tmp.example" "$RID"
expect 'guard: a symlink as root' 1 sandbox_ok "$W/alias" "$(sandbox_id "$W/real")"
expect 'guard: a path through a symlink' 1 sandbox_ok "$W/alias/../a[bc]/tmp.example" "$RID"
expect 'guard: the right path, another id' 1 sandbox_ok "$R" "$(sandbox_id "$W/ab/tmp.example")"
expect 'guard: a sibling with the root id' 1 sandbox_ok "$W/ab/tmp.example" "$RID"
expect 'guard: root with *' 0 sandbox_ok "$W/s*t/tmp.example" "$(sandbox_id "$W/s*t/tmp.example")"
expect 'guard: root with ?' 0 sandbox_ok "$W/q?x/tmp.example" "$(sandbox_id "$W/q?x/tmp.example")"
expect 'guard: empty directory' 0 dir_empty "$W/real"
touch -- "$W/real/.dot"
expect 'guard: a dot entry is not empty' 1 dir_empty "$W/real"
# The sweep and the removal refuse on their own, before they read a link or remove anything.
expect 'sweep: refuses an empty root' 1 sweep_sandbox '' ''
expect 'sweep: refuses / as root' 1 sweep_sandbox / "$(sandbox_id /)"
expect 'sweep: refuses a missing root' 1 sweep_sandbox "$W/missing" "$RID"
expect 'remove: refuses an empty root' 1 remove_sandbox '' ''
expect 'remove: refuses a root with the wrong id' 1 remove_sandbox "$W/keep" "$RID"
expect 'remove: the refused root is still there' 0 test -d "$W/keep/tmp.example/root"
expect 'sweep: a valid root with no process in it' 0 sweep_sandbox "$W/keep" "$(sandbox_id "$W/keep")"
same 'sweep: no signal was asked for' '' "$(cat -- "$W/kill-calls" 2>/dev/null)"
# The match. Dummy links stand in for /proc/<pid>/cwd; a regular file stands in for a link this
# user may not read (find prints an empty target for both).
n=0
link() { n=$((n+1)); mkdir -p -- "$W/links/$n"; ln -s -- "$1" "$W/links/$n/cwd"; LINKS+=("$W/links/$n/cwd"); }
match() { # match ROOT: the indices of the dummy links cwd_links_in selects, space-separated
local l out=''
while IFS= read -r -d '' l; do l=${l#"$W/links/"}; out="$out ${l%/cwd}"; done \
< <(cwd_links_in "$1" "${LINKS[@]}")
printf '%s' "${out# }"
}
LINKS=()
link "$R" # 1 the root itself
link "$R/root" # 2 under the root
link "$R/root/x (deleted)" # 3 a deleted directory under the root
link "$R (deleted)" # 4 another directory: the root exists
link "$W/ab/tmp.example" # 5 what [bc] would match as a pattern
link "$W/ab/tmp.example/root" # 6 the same, deeper
link "$W/a[bc]/tmp.example2" # 7 a sibling sharing a prefix
link "$W/a[bc]/tmp.exampl" # 8 a shorter sibling
link / # 9
link /opt/unrelated/work # 10
link "$R/line"$'\n'"break" # 11 a newline, under the root
link "$R/a"$'\t'"b" # 12 a tab, under the root
link "$R"$'\n' # 13 the root plus a newline: another directory
n=$((n+1)); mkdir -p -- "$W/links/$n"; : >"$W/links/$n/cwd"; LINKS+=("$W/links/$n/cwd") # 14 no target
link "$W/s*t/tmp.example/root" # 15
link "$W/sXt/tmp.example/root" # 16 what * would match as a pattern
link "$W/q?x/tmp.example/root" # 17
link "$W/qZx/tmp.example/root" # 18 what ? would match as a pattern
same 'match: root with [bc] selects only its own tree, deleted dirs inside included' '1 2 3 11 12' "$(match "$R")"
same 'match: root with *' '15' "$(match "$W/s*t/tmp.example")"
same 'match: root with ?' '17' "$(match "$W/q?x/tmp.example")"
same 'match: the ab sibling selects only its own tree' '5 6' "$(match "$W/ab/tmp.example")"
same 'match: an empty root selects nothing' '' "$(match '')"
same 'match: / as root selects nothing' '' "$(match /)"
same 'match: a relative root selects nothing' '' "$(match 'tmp.example')"
same 'match: no links reads nothing' '' "$(cwd_links_in "$R" | tr '\0' ' ')"
# The pid of a link: only /proc/<digits>/cwd.
same 'pid: /proc/123/cwd' '123' "$(link_pid /proc/123/cwd)"
expect 'pid: /proc/self/cwd' 1 link_pid /proc/self/cwd
expect 'pid: /proc//cwd' 1 link_pid /proc//cwd
expect 'pid: /proc/12/task/13/cwd' 1 link_pid /proc/12/task/13/cwd
expect 'pid: a dummy link' 1 link_pid "$W/links/1/cwd"
# The socket shape and byte length.
same 'socket: shape' "$R/root/data/launches/acme/r000000000000/tools.sock" "$(socket_shape "$R")"
same 'socket: bytes, not characters' 5 "$(LC_ALL=C.UTF-8 byte_length $'/a/\xc3\xa9')"
links=(/proc/[0-9]*/cwd)
start=${EPOCHREALTIME/[.,]/}
hits=$(cwd_links_in "$W/keep" "${links[@]}" | tr -cd '\0' | wc -c)
end=${EPOCHREALTIME/[.,]/}
same "speed: no process under an empty root among ${#links[@]} cwd links" 0 "$hits"
echo "time: one read of ${#links[@]} cwd links took $(( (end - start) / 1000 )) ms"
printf '\nsandbox-check: %s passed, %s failed\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]