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.
114 lines
5.0 KiB
Bash
114 lines
5.0 KiB
Bash
# shellcheck shell=bash
|
|
# C2-14 (#1728, row 227 R3): the sandbox root of scripts/test-case2.sh and the cleanup that hangs
|
|
# off it. test-case2.sh sources this file before it allocates anything. The kill sweep and the
|
|
# removal each check the root themselves, so a caller that runs them early, late or with an empty
|
|
# value gets a refusal, never a sweep of the host. scripts/case2/sandbox-check.sh exercises the
|
|
# guard and the match against dummy symlinks.
|
|
|
|
# The tools socket is the longest Unix socket path a run builds. packages/seat/src/session.mjs
|
|
# newRun puts it at <data root>/launches/<business>/r<12 hex digits>/tools.sock and refuses a path
|
|
# over SOCKET_LIMIT bytes. The driver's data root is <sandbox>/root/data (fixture() in
|
|
# packages/cli/tests/helpers.mjs, given <sandbox>/root by test-case2.sh) and its business is acme
|
|
# (scripts/case2/e2e.mjs). socket_shape builds that path on a real sandbox root with a run id of the
|
|
# same length, so its byte length is the length the run will have.
|
|
SOCKET_LIMIT=107
|
|
socket_shape() { printf '%s/root/data/launches/acme/r%s/tools.sock' "$1" 000000000000; }
|
|
|
|
# The length of $1 in bytes, not characters, whatever the caller's locale.
|
|
byte_length() {
|
|
local LC_ALL=C
|
|
printf '%s' "${#1}"
|
|
}
|
|
|
|
# The device:inode of $1, the identity recorded when mktemp creates the sandbox.
|
|
sandbox_id() { stat -c '%d:%i' -- "$1" 2>/dev/null; }
|
|
|
|
# sandbox_ok ROOT ID: ROOT is the sandbox this run created. It is non-empty, absolute and not /;
|
|
# it is its own physical path (no symlink in it, no trailing slash, no . or .. part), which is the
|
|
# form a /proc/<pid>/cwd link reads; it is a directory this user owns; and its device:inode is ID.
|
|
sandbox_ok() {
|
|
local root=${1-} id=${2-} physical
|
|
[ -n "$root" ] && [ -n "$id" ] || return 1
|
|
case "$root" in /?*) ;; *) return 1 ;; esac
|
|
[ -d "$root" ] && [ ! -L "$root" ] && [ -O "$root" ] || return 1
|
|
physical=$(cd -P -- "$root" 2>/dev/null && pwd -P) || return 1
|
|
[ "$physical" = "$root" ] || return 1
|
|
[ "$(sandbox_id "$root")" = "$id" ]
|
|
}
|
|
|
|
# dir_empty DIR: DIR holds no entry, dot entries included.
|
|
dir_empty() (
|
|
shopt -s nullglob dotglob
|
|
set -- "$1"/*
|
|
[ "$#" -eq 0 ]
|
|
)
|
|
|
|
# cwd_in ROOT TARGET: TARGET, a cwd link target, is ROOT or lies under ROOT/. The two are compared
|
|
# as strings. Quoted text in a case pattern matches only itself, so *, ? and [ in ROOT are literal;
|
|
# the only pattern character is the /* written here. A cwd in a deleted directory reads
|
|
# "<path> (deleted)". One under ROOT/ still matches. "ROOT (deleted)" does not: the sweep runs only
|
|
# after sandbox_ok has shown ROOT exists, so that target is another directory, one whose name ends
|
|
# in " (deleted)".
|
|
cwd_in() {
|
|
case "${1-}" in /?*) ;; *) return 1 ;; esac
|
|
case "${2-}" in "$1" | "$1"/*) return 0 ;; esac
|
|
return 1
|
|
}
|
|
|
|
# cwd_links_in ROOT LINK...: print, each followed by NUL, every LINK whose target cwd_in puts in
|
|
# ROOT. One find reads every target (%l), so the cost is one process however many the host runs. A
|
|
# link this user may not read, another user's process, gives an empty target, which never matches.
|
|
# Each record is LINK, a tab, then TARGET; LINK has no tab and TARGET may hold anything but NUL.
|
|
cwd_links_in() {
|
|
local root=$1 record
|
|
shift
|
|
[ "$#" -gt 0 ] || return 0
|
|
while IFS= read -r -d '' record; do
|
|
if cwd_in "$root" "${record#*$'\t'}"; then printf '%s\0' "${record%%$'\t'*}"; fi
|
|
done < <(command find "$@" -maxdepth 0 -printf '%p\t%l\0' 2>/dev/null)
|
|
}
|
|
|
|
# link_pid LINK: the pid in a link of the form /proc/<digits>/cwd, or failure for anything else.
|
|
link_pid() {
|
|
local pid=${1#/proc/}
|
|
[ "$pid" != "$1" ] || return 1
|
|
pid=${pid%/cwd}
|
|
case "$pid" in '' | *[!0-9]*) return 1 ;; esac
|
|
printf '%s' "$pid"
|
|
}
|
|
|
|
# sweep_sandbox ROOT ID: SIGKILL every process whose cwd is in ROOT. It refuses, before it reads any
|
|
# link, unless sandbox_ok ROOT ID. Up to three passes, so a child forked during one pass is caught
|
|
# by the next. Prints a note when it killed something; returns 1 when it refused.
|
|
sweep_sandbox() {
|
|
local root=${1-} id=${2-} link pid pass hit
|
|
local -A killed=()
|
|
if ! sandbox_ok "$root" "$id"; then
|
|
printf 'test-case2: kill sweep refused: %q is not the sandbox this run created\n' "$root" >&2
|
|
return 1
|
|
fi
|
|
for pass in 1 2 3; do
|
|
hit=0
|
|
while IFS= read -r -d '' link; do
|
|
pid=$(link_pid "$link") || continue
|
|
if kill -KILL "$pid" 2>/dev/null; then hit=1; killed[$pid]=1; fi
|
|
done < <(cwd_links_in "$root" /proc/[0-9]*/cwd)
|
|
[ "$hit" = 1 ] || break
|
|
done
|
|
if [ "${#killed[@]}" -gt 0 ]; then
|
|
echo "note: ${#killed[@]} process(es) under the sandbox outlived the driver; sent SIGKILL"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# remove_sandbox ROOT ID: remove ROOT, refusing unless sandbox_ok ROOT ID. --one-file-system keeps
|
|
# rm out of any mount left under it.
|
|
remove_sandbox() {
|
|
local root=${1-} id=${2-}
|
|
if ! sandbox_ok "$root" "$id"; then
|
|
printf 'test-case2: removal refused: %q is not the sandbox this run created\n' "$root" >&2
|
|
return 1
|
|
fi
|
|
rm -rf --one-file-system -- "$root"
|
|
}
|