diff --git a/.sloptrap b/.sloptrap index d09d324..e29f161 100644 --- a/.sloptrap +++ b/.sloptrap @@ -1,4 +1,4 @@ name=skz-sloptrap -packages_extra=bash make shellcheck jq podman +packages_extra=bash make shellcheck jq podman iproute2 strace capabilities=apt-install nested-podman packet-capture allow_host_network=false diff --git a/sloptrap b/sloptrap index 9e2dfa6..0d90c05 100755 --- a/sloptrap +++ b/sloptrap @@ -370,13 +370,23 @@ if [[ $# -eq 0 ]]; then fi if [[ $(id -u) -eq 0 ]]; then - helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper} - mkdir -p "$helper_dir/queue" - chmod 700 "$helper_dir" + helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} + queue_dir="$helper_dir/queue" + mkdir -p "$queue_dir" + chmod 711 "$helper_dir" + chmod 700 "$queue_dir" + target_uid=${SLOPTRAP_HOST_UID:-} + target_gid=${SLOPTRAP_HOST_GID:-} + if [[ -n $target_uid && -n $target_gid ]]; then + chown "$target_uid:$target_gid" "$queue_dir" + fi if [[ -n ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then /usr/local/bin/sloptrap-helperd & helper_pid=$! fi + if [[ -n $target_uid && -n $target_gid ]]; then + exec setpriv --reuid "$target_uid" --regid "$target_gid" --clear-groups -- "$@" + fi exec runuser -u sloptrap --preserve-environment -- "$@" fi @@ -387,13 +397,25 @@ EOF cat <<'EOF' #!/usr/bin/env bash set -euo pipefail +umask 077 -helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper} +helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" caps=${SLOPTRAP_ACTIVE_CAPABILITIES:-} audit_log=${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log} +capture_root=${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures} +workspace_root=${SLOPTRAP_WORKDIR:-/workspace} +pidfile="$helper_dir/helperd.pid" mkdir -p "$queue_dir" "$(dirname "$audit_log")" +cleanup_pidfile() { + rm -f "$pidfile" +} + +trap cleanup_pidfile EXIT INT TERM HUP +printf '%s\n' "$$" >"$pidfile" +chmod 644 "$pidfile" 2>/dev/null || true + has_capability() { local needle=$1 local token @@ -412,6 +434,74 @@ log_action() { printf '%s op=%s status=%s %s\n' "$(date -u +%FT%TZ)" "$op" "$status" "$details" >>"$audit_log" } +resolve_path() { + local raw=$1 + if command -v realpath >/dev/null 2>&1; then + realpath -m "$raw" + return + fi + case "$raw" in + /*) printf '%s\n' "$raw" ;; + *) printf '%s/%s\n' "$(pwd -P)" "$raw" ;; + esac +} + +path_within_root() { + local root path + root=$(resolve_path "$1") + path=$(resolve_path "$2") + case "$path" in + "$root"|"${root}/"*) + return 0 + ;; + esac + return 1 +} + +claim_request_dir() { + local request_dir=$1 + [[ -d $request_dir && ! -L $request_dir ]] || return 1 + path_within_root "$queue_dir" "$request_dir" || return 1 + chown root:root "$request_dir" 2>/dev/null || true + chmod 700 "$request_dir" 2>/dev/null || true +} + +init_request_outputs() { + local request_dir=$1 + local path + [[ ! -e "$request_dir/status" && ! -L "$request_dir/status" ]] || return 1 + for path in "$request_dir/stdout" "$request_dir/stderr"; do + [[ ! -L $path ]] || return 1 + rm -f "$path" + : >"$path" + chmod 600 "$path" 2>/dev/null || true + done +} + +read_request_value() { + local path=$1 + [[ -f $path && ! -L $path ]] || return 1 + cat "$path" +} + +validate_package_name() { + local package=$1 + [[ $package =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*$ ]] +} + +validate_packet_interface() { + local iface=$1 + [[ $iface =~ ^[A-Za-z0-9][A-Za-z0-9_.:@+-]*$ ]] +} + +validate_capture_path() { + local path=$1 + local resolved + resolved=$(resolve_path "$path") + path_within_root "$capture_root" "$resolved" && return 0 + path_within_root "$workspace_root" "$resolved" +} + write_status() { local request_dir=$1 local status=$2 @@ -427,7 +517,7 @@ run_apt_install() { return } local packages_file="$request_dir/packages" - if [[ ! -f $packages_file ]]; then + if [[ ! -f $packages_file || -L $packages_file ]]; then printf 'missing package list\n' >"$request_dir/stderr" write_status "$request_dir" 2 log_action "apt-install" "packages=missing" 2 @@ -440,6 +530,15 @@ run_apt_install() { log_action "apt-install" "packages=empty" 2 return fi + local package + for package in "${packages[@]}"; do + if ! validate_package_name "$package"; then + printf 'invalid package name %s\n' "$package" >"$request_dir/stderr" + write_status "$request_dir" 2 + log_action "apt-install" "packages=invalid" 2 + return + fi + done if apt-get update >"$request_dir/stdout" 2>"$request_dir/stderr" \ && apt-get install -y --no-install-recommends "${packages[@]}" >>"$request_dir/stdout" 2>>"$request_dir/stderr"; then write_status "$request_dir" 0 @@ -466,25 +565,47 @@ run_packet_capture() { return } local iface filter_file output_file stdout_mode - iface=$(<"$iface_file") + iface=$(read_request_value "$iface_file" || true) filter_file="$request_dir/filter" output_file="$request_dir/output" stdout_mode=0 - [[ -f "$request_dir/stdout_mode" ]] && stdout_mode=$(<"$request_dir/stdout_mode") - local -a cmd=(tcpdump -i "$iface") - if [[ -s $filter_file ]]; then - local filter - filter=$(<"$filter_file") - local -a filter_tokens=() - read -r -a filter_tokens <<< "$filter" - cmd+=("${filter_tokens[@]}") + [[ -f "$request_dir/stdout_mode" ]] && stdout_mode=$(read_request_value "$request_dir/stdout_mode" || true) + if ! validate_packet_interface "$iface"; then + printf 'invalid interface %s\n' "$iface" >"$request_dir/stderr" + write_status "$request_dir" 2 + log_action "packet-capture" "interface=invalid" 2 + return fi + if [[ $stdout_mode != "0" && $stdout_mode != "1" ]]; then + printf 'invalid stdout mode %s\n' "$stdout_mode" >"$request_dir/stderr" + write_status "$request_dir" 2 + log_action "packet-capture" "interface=$iface stdout=invalid" 2 + return + fi + local -a cmd=(tcpdump -i "$iface") if [[ -s $output_file ]]; then local capture_path - capture_path=$(<"$output_file") + capture_path=$(read_request_value "$output_file" || true) + if ! validate_capture_path "$capture_path"; then + printf 'output path must stay within %s or %s\n' "$capture_root" "$workspace_root" >"$request_dir/stderr" + write_status "$request_dir" 2 + log_action "packet-capture" "interface=$iface output=invalid" 2 + return + fi mkdir -p "$(dirname "$capture_path")" cmd+=(-w "$capture_path") fi + if [[ -s $filter_file ]]; then + local filter + filter=$(read_request_value "$filter_file" || true) + local -a filter_tokens=() + if [[ -n $filter ]]; then + read -r -a filter_tokens <<< "$filter" + fi + if [[ ${#filter_tokens[@]} -gt 0 ]]; then + cmd+=(-- "${filter_tokens[@]}") + fi + fi if [[ $stdout_mode == "1" ]]; then "${cmd[@]}" >"$request_dir/stdout" 2>"$request_dir/stderr" || { write_status "$request_dir" 1 @@ -506,6 +627,7 @@ while true; do shopt -s nullglob request_dirs=("$queue_dir"/*.req) shopt -u nullglob + pending_requests=0 if [[ ${#request_dirs[@]} -eq 0 ]]; then sleep 1 continue @@ -513,9 +635,21 @@ while true; do for request_dir in "${request_dirs[@]}"; do [[ -d $request_dir ]] || continue [[ ! -f "$request_dir/status" ]] || continue - op=$(<"$request_dir/op") - : >"$request_dir/stdout" - : >"$request_dir/stderr" + pending_requests=1 + if ! claim_request_dir "$request_dir"; then + log_action "request" "unsafe=1 path=$request_dir" 2 + continue + fi + if ! init_request_outputs "$request_dir"; then + log_action "request" "unsafe-output=1 path=$request_dir" 2 + continue + fi + if ! op=$(read_request_value "$request_dir/op"); then + printf 'missing operation\n' >"$request_dir/stderr" + write_status "$request_dir" 2 + log_action "request" "op=missing" 2 + continue + fi case "$op" in apt-install) run_apt_install "$request_dir" @@ -530,6 +664,9 @@ while true; do ;; esac done + if [[ $pending_requests -eq 0 ]]; then + sleep 1 + fi done EOF ;; @@ -538,9 +675,68 @@ EOF #!/usr/bin/env bash set -euo pipefail -helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper} +helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" -mkdir -p "$queue_dir" +pidfile="$helper_dir/helperd.pid" + +helper_running() { + local pid + [[ -r $pidfile ]] || return 1 + pid=$(<"$pidfile") + [[ -n $pid ]] || return 1 + kill -0 "$pid" 2>/dev/null +} + +ensure_helper_ready() { + if [[ -w $queue_dir ]] && helper_running; then + return 0 + fi + if [[ -z ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then + printf 'slop-apt: capability helper is not available in this session\n' >&2 + exit 1 + fi + if ! command -v setpriv >/dev/null 2>&1; then + printf 'slop-apt: setpriv is required to bootstrap the capability helper\n' >&2 + exit 1 + fi + setpriv --reuid 0 --regid 0 --clear-groups -- env \ + SLOPTRAP_HELPER_DIR="$helper_dir" \ + SLOPTRAP_ACTIVE_CAPABILITIES="${SLOPTRAP_ACTIVE_CAPABILITIES:-}" \ + SLOPTRAP_AUDIT_LOG="${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log}" \ + SLOPTRAP_CAPTURE_DIR="${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures}" \ + SLOPTRAP_WORKDIR="${SLOPTRAP_WORKDIR:-/workspace}" \ + SLOPTRAP_HOST_UID="${SLOPTRAP_HOST_UID:-$(id -u)}" \ + SLOPTRAP_HOST_GID="${SLOPTRAP_HOST_GID:-$(id -g)}" \ + bash -c ' + set -euo pipefail + helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} + queue_dir="$helper_dir/queue" + pidfile="$helper_dir/helperd.pid" + helper_bin=$(command -v sloptrap-helperd) + [[ -n $helper_bin ]] || exit 1 + mkdir -p "$queue_dir" + chmod 711 "$helper_dir" + chmod 700 "$queue_dir" + chown "$SLOPTRAP_HOST_UID:$SLOPTRAP_HOST_GID" "$queue_dir" + if [[ -r $pidfile ]]; then + pid=$(<"$pidfile") + if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then + exit 0 + fi + fi + "$helper_bin" >/dev/null 2>&1 & + for ((i=0; i<30; i+=1)); do + if [[ -r $pidfile ]]; then + pid=$(<"$pidfile") + if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then + exit 0 + fi + fi + sleep 0.1 + done + exit 1 + ' +} if [[ ${1-} != "install" ]]; then printf 'usage: slop-apt install \n' >&2 @@ -554,12 +750,14 @@ if [[ $# -eq 0 ]]; then fi for package in "$@"; do - if [[ ! $package =~ ^[A-Za-z0-9+.-]+$ ]]; then + if [[ ! $package =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*$ ]]; then printf 'slop-apt: invalid package name %s\n' "$package" >&2 exit 2 fi done +ensure_helper_ready + request_dir=$(mktemp -d "$queue_dir/request.XXXXXX.req") trap 'rm -rf "$request_dir"' EXIT INT TERM HUP printf 'apt-install\n' >"$request_dir/op" @@ -585,10 +783,97 @@ EOF #!/usr/bin/env bash set -euo pipefail -helper_dir=${SLOPTRAP_HELPER_DIR:-/run/sloptrap-helper} +helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} queue_dir="$helper_dir/queue" default_output=${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures} -mkdir -p "$queue_dir" "$default_output" +workspace_root=${SLOPTRAP_WORKDIR:-/workspace} +pidfile="$helper_dir/helperd.pid" +mkdir -p "$default_output" + +helper_running() { + local pid + [[ -r $pidfile ]] || return 1 + pid=$(<"$pidfile") + [[ -n $pid ]] || return 1 + kill -0 "$pid" 2>/dev/null +} + +ensure_helper_ready() { + if [[ -w $queue_dir ]] && helper_running; then + return 0 + fi + if [[ -z ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then + printf 'slopcap: capability helper is not available in this session\n' >&2 + exit 1 + fi + if ! command -v setpriv >/dev/null 2>&1; then + printf 'slopcap: setpriv is required to bootstrap the capability helper\n' >&2 + exit 1 + fi + setpriv --reuid 0 --regid 0 --clear-groups -- env \ + SLOPTRAP_HELPER_DIR="$helper_dir" \ + SLOPTRAP_ACTIVE_CAPABILITIES="${SLOPTRAP_ACTIVE_CAPABILITIES:-}" \ + SLOPTRAP_AUDIT_LOG="${SLOPTRAP_AUDIT_LOG:-/codex/state/capabilities.log}" \ + SLOPTRAP_CAPTURE_DIR="${SLOPTRAP_CAPTURE_DIR:-/codex/state/captures}" \ + SLOPTRAP_WORKDIR="${SLOPTRAP_WORKDIR:-/workspace}" \ + SLOPTRAP_HOST_UID="${SLOPTRAP_HOST_UID:-$(id -u)}" \ + SLOPTRAP_HOST_GID="${SLOPTRAP_HOST_GID:-$(id -g)}" \ + bash -c ' + set -euo pipefail + helper_dir=${SLOPTRAP_HELPER_DIR:-/tmp/sloptrap-helper} + queue_dir="$helper_dir/queue" + pidfile="$helper_dir/helperd.pid" + helper_bin=$(command -v sloptrap-helperd) + [[ -n $helper_bin ]] || exit 1 + mkdir -p "$queue_dir" + chmod 711 "$helper_dir" + chmod 700 "$queue_dir" + chown "$SLOPTRAP_HOST_UID:$SLOPTRAP_HOST_GID" "$queue_dir" + if [[ -r $pidfile ]]; then + pid=$(<"$pidfile") + if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then + exit 0 + fi + fi + "$helper_bin" >/dev/null 2>&1 & + for ((i=0; i<30; i+=1)); do + if [[ -r $pidfile ]]; then + pid=$(<"$pidfile") + if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then + exit 0 + fi + fi + sleep 0.1 + done + exit 1 + ' +} + +resolve_requested_path() { + local raw=$1 + if command -v realpath >/dev/null 2>&1; then + realpath -m "$raw" + return + fi + case "$raw" in + /*) printf '%s\n' "$raw" ;; + *) printf '%s/%s\n' "$(pwd -P)" "$raw" ;; + esac +} + +validate_output_path() { + local path=$1 + local resolved capture_root workspace_path + resolved=$(resolve_requested_path "$path") + capture_root=$(resolve_requested_path "$default_output") + workspace_path=$(resolve_requested_path "$workspace_root") + case "$resolved" in + "$capture_root"|"${capture_root}/"*|"$workspace_path"|"${workspace_path}/"*) + return 0 + ;; + esac + return 1 +} if [[ ${1-} != "capture" ]]; then printf 'usage: slopcap capture --interface [--filter ] [--output ] [--stdout]\n' >&2 @@ -633,6 +918,12 @@ done if [[ -z $output && $stdout_mode -eq 0 ]]; then output="$default_output/capture-$(date +%s).pcap" fi +if [[ -n $output ]] && ! validate_output_path "$output"; then + printf 'slopcap: output path must stay within %s or %s\n' "$default_output" "$workspace_root" >&2 + exit 2 +fi + +ensure_helper_ready request_dir=$(mktemp -d "$queue_dir/request.XXXXXX.req") trap 'rm -rf "$request_dir"' EXIT INT TERM HUP @@ -674,6 +965,19 @@ EOF #!/usr/bin/env bash set -euo pipefail +caps=${SLOPTRAP_ACTIVE_CAPABILITIES:-} + +has_capability() { + local needle=$1 + local token + for token in $caps; do + if [[ $token == "$needle" ]]; then + return 0 + fi + done + return 1 +} + if [[ $# -eq 0 ]]; then printf 'usage: sloppodman ...\n' >&2 exit 2 @@ -691,6 +995,11 @@ case "$subcommand" in ;; esac +has_capability "nested-podman" || { + printf 'sloppodman: capability nested-podman is not active\n' >&2 + exit 126 +} + workspace_root=${SLOPTRAP_WORKDIR:-/workspace} podman_root=${SLOPTRAP_INNER_PODMAN_ROOT:-/codex/capabilities/podman/storage} podman_runroot=${SLOPTRAP_INNER_PODMAN_RUNROOT:-/codex/capabilities/podman/run} @@ -721,6 +1030,63 @@ validate_workspace_path() { esac } +reject_flag() { + local flag=$1 + printf 'sloppodman: %s is not permitted\n' "$flag" >&2 + exit 2 +} + +validate_volume_spec() { + local spec=$1 + local source + if [[ $spec != *:* ]]; then + return 0 + fi + source=${spec%%:*} + case "$source" in + "" ) + return 0 + ;; + .|..|/*|./*|../*|*/*) + validate_workspace_path "$source" + ;; + * ) + return 0 + ;; + esac +} + +validate_mount_spec() { + local spec=$1 + local type="" + local source="" + local part + IFS=',' read -r -a mount_parts <<< "$spec" + for part in "${mount_parts[@]}"; do + case "$part" in + type=*) + type=${part#type=} + ;; + source=*|src=*) + source=${part#*=} + ;; + esac + done + case "$type" in + volume|tmpfs) + return 0 + ;; + bind) + [[ -n $source ]] || reject_flag "--mount" + validate_workspace_path "$source" + return 0 + ;; + *) + reject_flag "--mount" + ;; + esac +} + if [[ $subcommand == "build" ]]; then args=("$@") context="" @@ -733,6 +1099,9 @@ if [[ $subcommand == "build" ]]; then (( idx < ${#args[@]} )) || { printf 'sloppodman: %s requires a path\n' "$arg" >&2; exit 2; } validate_workspace_path "${args[$idx]}" ;; + --file=*) + validate_workspace_path "${arg#*=}" + ;; --network) ((idx+=1)) (( idx < ${#args[@]} )) || { printf 'sloppodman: --network requires a value\n' >&2; exit 2; } @@ -741,6 +1110,15 @@ if [[ $subcommand == "build" ]]; then exit 2 fi ;; + --network=*) + if [[ ${arg#*=} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then + printf 'sloppodman: host networking is not available in this session\n' >&2 + exit 2 + fi + ;; + -v|--volume|--volume=*|--mount|--mount=*|--security-opt|--security-opt=*|--cap-add|--cap-add=*|--cap-drop|--cap-drop=*|--device|--device=*|--privileged|--privileged=*|--rootfs|--rootfs=*) + reject_flag "${arg%%=*}" + ;; esac ((idx+=1)) done @@ -755,14 +1133,49 @@ if [[ $subcommand == "run" ]]; then idx=0 while (( idx < ${#args[@]} )); do arg=${args[$idx]} - if [[ $arg == "--network" ]]; then - ((idx+=1)) - (( idx < ${#args[@]} )) || { printf 'sloppodman: --network requires a value\n' >&2; exit 2; } - if [[ ${args[$idx]} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then - printf 'sloppodman: host networking is not available in this session\n' >&2 - exit 2 - fi - fi + case "$arg" in + --) + break + ;; + --network) + ((idx+=1)) + (( idx < ${#args[@]} )) || { printf 'sloppodman: --network requires a value\n' >&2; exit 2; } + if [[ ${args[$idx]} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then + printf 'sloppodman: host networking is not available in this session\n' >&2 + exit 2 + fi + ;; + --network=*) + if [[ ${arg#*=} == "host" && ${SLOPTRAP_INNER_PODMAN_HOST_NETWORK:-0} != 1 ]]; then + printf 'sloppodman: host networking is not available in this session\n' >&2 + exit 2 + fi + ;; + -v|--volume) + ((idx+=1)) + (( idx < ${#args[@]} )) || { printf 'sloppodman: %s requires a value\n' "$arg" >&2; exit 2; } + validate_volume_spec "${args[$idx]}" + ;; + --volume=*) + validate_volume_spec "${arg#*=}" + ;; + --mount) + ((idx+=1)) + (( idx < ${#args[@]} )) || { printf 'sloppodman: --mount requires a value\n' >&2; exit 2; } + validate_mount_spec "${args[$idx]}" + ;; + --mount=*) + validate_mount_spec "${arg#*=}" + ;; + --privileged|--privileged=*|--cap-add|--cap-add=*|--cap-drop|--cap-drop=*|--device|--device=*|--security-opt|--security-opt=*|--rootfs|--rootfs=*|--pid|--pid=*|--ipc|--ipc=*|--uts|--uts=*|--userns|--userns=*|--cgroupns|--cgroupns=*) + reject_flag "${arg%%=*}" + ;; + -*) + ;; + *) + break + ;; + esac ((idx+=1)) done fi @@ -880,6 +1293,11 @@ ensure_capability_state_paths() { ensure_codex_directory "$CAPABILITY_TRUST_ROOT_HOST" "sloptrap capability trust root" ensure_codex_directory "$(dirname "$CAPABILITY_BUILD_STAMP_HOST")" "sloptrap capability build stamp root" ensure_codex_directory "$CAPABILITY_STATE_HOST" "project capability state" + if capability_list_contains "$REQUESTED_CAPABILITIES" "nested-podman"; then + ensure_codex_directory "$CAPABILITY_STATE_HOST/podman-storage" "nested podman storage state" + ensure_codex_directory "$CAPABILITY_STATE_HOST/podman-run" "nested podman runroot state" + ensure_codex_directory "$CAPABILITY_STATE_HOST/podman-runtime" "nested podman runtime state" + fi } capability_trust_matches_current() { @@ -1189,7 +1607,7 @@ validate_package_list() { [[ -z $raw ]] && return 0 local token for token in $raw; do - if [[ ! $token =~ ^[A-Za-z0-9+.-]+$ ]]; then + if [[ ! $token =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*$ ]]; then error "$source: invalid package name '$token' in '$key'" fi done @@ -1465,16 +1883,7 @@ print_manifest_summary() { } build_runtime_context_prompt() { - local manifest_present prompt manifest_capabilities trusted enabled network_mode - manifest_present="false" - if [[ -f $MANIFEST_PATH ]]; then - manifest_present="true" - fi - manifest_capabilities=${REQUESTED_CAPABILITIES:-none} - trusted="none" - if [[ -n $REQUESTED_CAPABILITIES ]] && capability_trust_matches_current; then - trusted=$REQUESTED_CAPABILITIES - fi + local prompt enabled network_mode enabled=${ENABLED_CAPABILITIES:-none} network_mode="isolated" if [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then @@ -1484,28 +1893,11 @@ build_runtime_context_prompt() { You are running inside sloptrap, which confines Codex inside a container. This startup note describes the sloptrap runtime only; it does not replace higher-priority instructions from AGENTS.md or the system. -Container layout: -- /workspace is the project mount. -- /codex is persistent Codex state for this project. -- The project manifest path is /workspace/.sloptrap and it may be absent. - -Manifest key meanings: -- name: labels the sloptrap project/image/container names. -- packages_extra: Debian packages added when the image was built. -- capabilities: privileged features requested by the manifest; only capabilities enabled for this run are currently usable. -- allow_host_network: enables host networking when true; otherwise networking is isolated. - Current resolved sloptrap state: -- manifest_present=$manifest_present -- project_name=$PROJECT_NAME -- packages_extra=${PACKAGES_EXTRA:-none} -- manifest_capabilities=$manifest_capabilities -- trusted_capabilities=$trusted -- enabled_capabilities=$enabled -- network_mode=$network_mode -- runtime_flags=$CODEX_ARGS_DISPLAY - -If you need exact project configuration, inspect /workspace/.sloptrap directly. +- name=$PROJECT_NAME (project/image/container label) +- packages_extra=${PACKAGES_EXTRA:-none} (Debian packages added at build time) +- enabled_capabilities=$enabled (privileged features usable in this run) +- network_mode=$network_mode (host when host networking is enabled; otherwise isolated) EOF ) printf '%s' "$prompt" @@ -1785,7 +2177,7 @@ prepare_container_runtime() { SLOPTRAP_PACKAGES_CAPABILITY+=" tcpdump" fi if capability_list_contains "$REQUESTED_CAPABILITIES" "nested-podman"; then - SLOPTRAP_PACKAGES_CAPABILITY+=" podman fuse-overlayfs slirp4netns" + SLOPTRAP_PACKAGES_CAPABILITY+=" podman uidmap fuse-overlayfs slirp4netns" fi SLOPTRAP_PACKAGES_CAPABILITY=$(normalize_package_list "$SLOPTRAP_PACKAGES_CAPABILITY") local default_codex_archive @@ -1877,6 +2269,15 @@ prepare_container_runtime() { if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman"; then capability_opts+=(--device /dev/fuse) fi + if $SLOPTRAP_RUN_AS_ROOT; then + capability_opts+=( + --cap-add SETUID + --cap-add SETGID + --cap-add CHOWN + --cap-add DAC_OVERRIDE + --cap-add FOWNER + ) + fi local rootfs_flag=() case "${SLOPTRAP_ROOTFS_READONLY,,}" in @@ -1911,7 +2312,7 @@ prepare_container_runtime() { -e "XDG_STATE_HOME=$SLOPTRAP_CODEX_HOME_CONT/state" -e "CODEX_HOME=$SLOPTRAP_CODEX_HOME_CONT" -e "SLOPTRAP_WORKDIR=$SLOPTRAP_WORKDIR" - -e "SLOPTRAP_HELPER_DIR=/run/sloptrap-helper" + -e "SLOPTRAP_HELPER_DIR=/tmp/sloptrap-helper" -e "SLOPTRAP_ACTIVE_CAPABILITIES=$ENABLED_CAPABILITIES" -e "SLOPTRAP_CAPTURE_DIR=$SLOPTRAP_CODEX_HOME_CONT/state/captures" -e "SLOPTRAP_AUDIT_LOG=$SLOPTRAP_CODEX_HOME_CONT/state/capabilities.log" @@ -1926,12 +2327,19 @@ prepare_container_runtime() { local uid gid uid=$(id -u) gid=$(id -g) + env_args+=( + -e "SLOPTRAP_HOST_UID=$uid" + -e "SLOPTRAP_HOST_GID=$gid" + ) local -a user_opts=("--user" "$uid:$gid") if [[ $CONTAINER_ENGINE == "podman" ]]; then user_opts=(--userns="keep-id:uid=$uid,gid=$gid" "${user_opts[@]}") fi if $SLOPTRAP_RUN_AS_ROOT; then user_opts=() + if [[ $CONTAINER_ENGINE == "podman" ]]; then + user_opts=(--userns="keep-id:uid=$uid,gid=$gid") + fi fi CONTAINER_SHARED_OPTS=( diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 1b090f0..242c245 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -211,6 +211,33 @@ teardown_stub_env() { rm -f "${STUB_LOG:-}" } +extract_embedded_helper() { + local helper=$1 + local output=$2 + if ! awk -v helper="$helper" ' + $0 ~ "^[[:space:]]*" helper "\\)$" { state=1; next } + state == 1 && /cat <<'\''EOF'\''$/ { state=2; next } + state == 2 && /^EOF$/ { exit } + state == 2 { print } + ' "$SLOPTRAP_BIN" >"$output"; then + return 1 + fi + chmod +x "$output" +} + +wait_for_path() { + local path=$1 + local attempts=${2:-100} + while (( attempts > 0 )); do + if [[ -e $path ]]; then + return 0 + fi + sleep 0.1 + ((attempts-=1)) + done + return 1 +} + record_failure() { failures+=("$1") } @@ -302,6 +329,22 @@ run_secret_mask() { teardown_stub_env } +run_git_ignore_mask() { + local scenario_dir="$ROOT_DIR" + printf '==> git_ignore_mask\n' + setup_stub_env + if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ + "$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" /dev/null 2>&1; then + record_failure "git_ignore_mask: sloptrap exited non-zero" + teardown_stub_env + return + fi + if ! grep -q -- "--mount type=tmpfs,target=/workspace/.git" "$STUB_LOG"; then + record_failure "git_ignore_mask: .git was not masked with tmpfs" + fi + teardown_stub_env +} + run_resume_target() { local scenario_dir="$TEST_ROOT/resume_target" printf '==> resume_target\n' @@ -335,10 +378,9 @@ run_runtime_context_prompt() { if [[ -z $run_line || $run_line != *"You are running inside sloptrap"* ]]; then record_failure "runtime_context_prompt: startup prompt missing from fresh run" fi - if ! grep -q -- "manifest_present=true" "$STUB_LOG" \ - || ! grep -q -- "manifest_capabilities=apt-install nested-podman packet-capture" "$STUB_LOG" \ - || ! grep -q -- "trusted_capabilities=apt-install nested-podman packet-capture" "$STUB_LOG" \ - || ! grep -q -- "enabled_capabilities=apt-install nested-podman packet-capture" "$STUB_LOG"; then + if ! grep -q -- "name=capability-repo" "$STUB_LOG" \ + || ! grep -q -- "enabled_capabilities=apt-install nested-podman packet-capture" "$STUB_LOG" \ + || ! grep -q -- "network_mode=host" "$STUB_LOG"; then record_failure "runtime_context_prompt: runtime summary missing manifest or capability state" fi if [[ -n $login_line && $login_line == *"You are running inside sloptrap"* ]]; then @@ -690,7 +732,7 @@ run_capability_profiles() { teardown_stub_env return fi - if ! grep -q -- "CAPABILITY_PACKAGES=tcpdump podman fuse-overlayfs slirp4netns" "$STUB_LOG"; then + if ! grep -q -- "CAPABILITY_PACKAGES=tcpdump podman uidmap fuse-overlayfs slirp4netns" "$STUB_LOG"; then record_failure "capability_profiles: build arg for capability packages missing" fi if ! grep -q -- "--cap-add NET_RAW" "$STUB_LOG"; then @@ -702,21 +744,279 @@ run_capability_profiles() { if ! grep -q -- "--device /dev/fuse" "$STUB_LOG"; then record_failure "capability_profiles: /dev/fuse device missing" fi + if ! grep -q -- "--cap-add SETUID" "$STUB_LOG"; then + record_failure "capability_profiles: SETUID capability missing" + fi + if ! grep -q -- "--cap-add SETGID" "$STUB_LOG"; then + record_failure "capability_profiles: SETGID capability missing" + fi + if ! grep -q -- "--cap-add CHOWN" "$STUB_LOG"; then + record_failure "capability_profiles: CHOWN capability missing" + fi + if ! grep -q -- "--cap-add DAC_OVERRIDE" "$STUB_LOG"; then + record_failure "capability_profiles: DAC_OVERRIDE capability missing" + fi + if ! grep -q -- "--cap-add FOWNER" "$STUB_LOG"; then + record_failure "capability_profiles: FOWNER capability missing" + fi if grep -q -- "--read-only" "$STUB_LOG"; then record_failure "capability_profiles: apt profile should disable read-only rootfs" fi if grep -q -- "--user " "$STUB_LOG"; then record_failure "capability_profiles: capability-enabled run should not force --user" fi + if ! grep -q -- "--userns=keep-id:uid=$(id -u),gid=$(id -g)" "$STUB_LOG"; then + record_failure "capability_profiles: podman keep-id user namespace missing" + fi if ! grep -q -- "SLOPTRAP_ACTIVE_CAPABILITIES=apt-install nested-podman packet-capture" "$STUB_LOG"; then record_failure "capability_profiles: active capability environment missing" fi + if ! grep -q -- "SLOPTRAP_HOST_UID=$(id -u)" "$STUB_LOG"; then + record_failure "capability_profiles: host uid environment missing" + fi + if ! grep -q -- "SLOPTRAP_HOST_GID=$(id -g)" "$STUB_LOG"; then + record_failure "capability_profiles: host gid environment missing" + fi if ! grep -q -- "SLOPTRAP_INNER_PODMAN_HOST_NETWORK=1" "$STUB_LOG"; then record_failure "capability_profiles: inner podman host-network mirror flag missing" fi + local state_root capability_dir + state_root="$STUB_HOME/.codex/sloptrap/state" + capability_dir=$(find "$state_root" -mindepth 2 -maxdepth 2 -type d -name capabilities | head -n 1 || true) + if [[ -z $capability_dir ]]; then + record_failure "capability_profiles: project capability state directory missing" + else + if [[ ! -d $capability_dir/podman-storage ]]; then + record_failure "capability_profiles: nested podman storage state missing" + fi + if [[ ! -d $capability_dir/podman-run ]]; then + record_failure "capability_profiles: nested podman runroot state missing" + fi + if [[ ! -d $capability_dir/podman-runtime ]]; then + record_failure "capability_profiles: nested podman runtime state missing" + fi + fi teardown_stub_env } +run_embedded_capability_helpers() { + printf '==> embedded_capability_helpers\n' + local temp_root helper_bin helper_dir workspace_dir capture_dir tool_log helper_pid + local inner_podman_root inner_podman_runroot inner_runtime_dir + temp_root=$(mktemp -d) + helper_bin="$temp_root/bin" + helper_dir="$temp_root/helper" + workspace_dir="$temp_root/workspace" + capture_dir="$temp_root/captures" + tool_log="$temp_root/tool.log" + inner_podman_root="$temp_root/podman-storage" + inner_podman_runroot="$temp_root/podman-run" + inner_runtime_dir="$temp_root/podman-runtime" + helper_pid="" + mkdir -p "$helper_bin" "$helper_dir/queue" "$workspace_dir/data" "$capture_dir" \ + "$inner_podman_root" "$inner_podman_runroot" "$inner_runtime_dir" + : >"$tool_log" + + if ! extract_embedded_helper "sloptrap-entrypoint" "$helper_bin/sloptrap-entrypoint" \ + || ! extract_embedded_helper "sloptrap-helperd" "$helper_bin/sloptrap-helperd" \ + || ! extract_embedded_helper "slop-apt" "$helper_bin/slop-apt" \ + || ! extract_embedded_helper "slopcap" "$helper_bin/slopcap" \ + || ! extract_embedded_helper "sloppodman" "$helper_bin/sloppodman"; then + record_failure "embedded_capability_helpers: failed to extract embedded helper scripts" + rm -rf "$temp_root" + return + fi + + cat >"$helper_bin/podman" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf 'podman %s\n' "$*" >>"$TEST_TOOL_LOG" +exit 0 +EOF + cat >"$helper_bin/apt-get" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf 'apt-get %s\n' "$*" >>"$TEST_TOOL_LOG" +exit 0 +EOF + cat >"$helper_bin/tcpdump" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +printf 'tcpdump %s\n' "$*" >>"$TEST_TOOL_LOG" +output="" +prev="" +for arg in "$@"; do + if [[ $prev == "-w" ]]; then + output=$arg + break + fi + prev=$arg +done +if [[ -n $output ]]; then + mkdir -p "$(dirname "$output")" + : >"$output" +fi +exit 0 +EOF + cat >"$helper_bin/setpriv" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +while [[ $# -gt 0 ]]; do + case "$1" in + --reuid|--regid) + shift 2 + ;; + --clear-groups) + shift + ;; + --) + shift + break + ;; + *) + break + ;; + esac +done +exec "$@" +EOF + chmod +x "$helper_bin/podman" "$helper_bin/apt-get" "$helper_bin/tcpdump" "$helper_bin/setpriv" + + if ! grep -q "chmod 711 \"\\\$helper_dir\"" "$helper_bin/sloptrap-entrypoint" \ + || ! grep -q "chown \"\\\$target_uid:\\\$target_gid\" \"\\\$queue_dir\"" "$helper_bin/sloptrap-entrypoint"; then + record_failure "embedded_capability_helpers: entrypoint did not expose helper queue to the dropped user" + fi + + local autostart_helper_dir + autostart_helper_dir="$temp_root/helper-autostart" + if ! TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$autostart_helper_dir" \ + SLOPTRAP_ACTIVE_CAPABILITIES="apt-install packet-capture" \ + SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \ + SLOPTRAP_AUDIT_LOG="$temp_root/autostart-audit.log" \ + SLOPTRAP_HOST_UID="$(id -u)" SLOPTRAP_HOST_GID="$(id -g)" \ + "$helper_bin/slop-apt" install jq >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: slop-apt did not self-bootstrap the helper daemon" + fi + if [[ ! -r $autostart_helper_dir/helperd.pid ]]; then + record_failure "embedded_capability_helpers: helper self-bootstrap did not create a pid file" + else + kill "$(cat "$autostart_helper_dir/helperd.pid")" >/dev/null 2>&1 || true + wait "$(cat "$autostart_helper_dir/helperd.pid")" >/dev/null 2>&1 || true + fi + + local missing_cap_status=0 + if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_ACTIVE_CAPABILITIES="" \ + SLOPTRAP_WORKDIR="$workspace_dir" \ + SLOPTRAP_INNER_PODMAN_ROOT="$inner_podman_root" \ + SLOPTRAP_INNER_PODMAN_RUNROOT="$inner_podman_runroot" \ + XDG_RUNTIME_DIR="$inner_runtime_dir" \ + "$helper_bin/sloppodman" ps >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: sloppodman should reject runs without nested-podman" + else + missing_cap_status=$? + fi + if [[ $missing_cap_status -ne 126 ]]; then + record_failure "embedded_capability_helpers: sloppodman returned the wrong status when capability was absent" + fi + + if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_ACTIVE_CAPABILITIES="nested-podman" \ + SLOPTRAP_WORKDIR="$workspace_dir" \ + SLOPTRAP_INNER_PODMAN_ROOT="$inner_podman_root" \ + SLOPTRAP_INNER_PODMAN_RUNROOT="$inner_podman_runroot" \ + XDG_RUNTIME_DIR="$inner_runtime_dir" \ + "$helper_bin/sloppodman" run --privileged example/image >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: sloppodman allowed --privileged" + fi + + if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_ACTIVE_CAPABILITIES="nested-podman" \ + SLOPTRAP_WORKDIR="$workspace_dir" \ + SLOPTRAP_INNER_PODMAN_ROOT="$inner_podman_root" \ + SLOPTRAP_INNER_PODMAN_RUNROOT="$inner_podman_runroot" \ + XDG_RUNTIME_DIR="$inner_runtime_dir" \ + "$helper_bin/sloppodman" run -v /tmp:/host example/image >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: sloppodman allowed an out-of-workspace bind mount" + fi + + if ! ( + cd "$workspace_dir" && TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" \ + SLOPTRAP_ACTIVE_CAPABILITIES="nested-podman" SLOPTRAP_WORKDIR="$workspace_dir" \ + SLOPTRAP_INNER_PODMAN_ROOT="$inner_podman_root" \ + SLOPTRAP_INNER_PODMAN_RUNROOT="$inner_podman_runroot" \ + XDG_RUNTIME_DIR="$inner_runtime_dir" \ + "$helper_bin/sloppodman" run -v ./data:/data example/image true >/dev/null 2>&1 + ); then + record_failure "embedded_capability_helpers: sloppodman rejected a workspace-local bind mount" + fi + if ! grep -q -- 'podman --root ' "$tool_log" || ! grep -q -- '-v ./data:/data' "$tool_log"; then + record_failure "embedded_capability_helpers: sloppodman did not invoke podman with the validated run arguments" + fi + + TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \ + SLOPTRAP_ACTIVE_CAPABILITIES="apt-install packet-capture" \ + SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \ + SLOPTRAP_AUDIT_LOG="$temp_root/audit.log" "$helper_bin/sloptrap-helperd" >/dev/null 2>&1 & + helper_pid=$! + + if ! TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \ + "$helper_bin/slop-apt" install jq >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: slop-apt failed against the embedded helper daemon" + fi + if ! grep -q -- 'apt-get install -y --no-install-recommends jq' "$tool_log"; then + record_failure "embedded_capability_helpers: slop-apt did not reach apt-get install" + fi + + local bad_apt_request + bad_apt_request=$(mktemp -d "$helper_dir/queue/request.XXXXXX.req") + printf 'apt-install\n' >"$bad_apt_request/op" + printf '%s\n' '--allow-unauthenticated' >"$bad_apt_request/packages" + if ! wait_for_path "$bad_apt_request/status"; then + record_failure "embedded_capability_helpers: helper daemon did not answer the invalid apt request" + elif [[ $(<"$bad_apt_request/status") != "2" ]]; then + record_failure "embedded_capability_helpers: invalid apt request returned the wrong status" + fi + if [[ -s "$bad_apt_request/stderr" ]] && ! grep -q -- 'invalid package name' "$bad_apt_request/stderr"; then + record_failure "embedded_capability_helpers: invalid apt request did not explain the rejection" + fi + + if TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \ + SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \ + "$helper_bin/slopcap" capture --interface eth0 --output /tmp/escape.pcap >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: slopcap accepted an out-of-bounds output path" + fi + + if ! TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \ + SLOPTRAP_CAPTURE_DIR="$capture_dir" SLOPTRAP_WORKDIR="$workspace_dir" \ + "$helper_bin/slopcap" capture --interface eth0 --filter 'tcp port 80' \ + --output "$workspace_dir/capture.pcap" >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: slopcap failed for a workspace-local capture file" + fi + if ! grep -q -- "tcpdump -i eth0 -w $workspace_dir/capture.pcap -- tcp port 80" "$tool_log"; then + record_failure "embedded_capability_helpers: slopcap did not invoke tcpdump with the expected guarded arguments" + fi + + local bad_capture_request + bad_capture_request=$(mktemp -d "$helper_dir/queue/request.XXXXXX.req") + printf 'packet-capture\n' >"$bad_capture_request/op" + printf 'eth0\n' >"$bad_capture_request/interface" + printf '\n' >"$bad_capture_request/filter" + printf '/tmp/escape.pcap\n' >"$bad_capture_request/output" + printf '0\n' >"$bad_capture_request/stdout_mode" + if ! wait_for_path "$bad_capture_request/status"; then + record_failure "embedded_capability_helpers: helper daemon did not answer the invalid capture request" + elif [[ $(<"$bad_capture_request/status") != "2" ]]; then + record_failure "embedded_capability_helpers: invalid capture request returned the wrong status" + fi + if [[ -s "$bad_capture_request/stderr" ]] && ! grep -q -- 'output path must stay within' "$bad_capture_request/stderr"; then + record_failure "embedded_capability_helpers: invalid capture request did not explain the rejection" + fi + + if [[ -n $helper_pid ]]; then + kill "$helper_pid" >/dev/null 2>&1 || true + wait "$helper_pid" >/dev/null 2>&1 || true + fi + rm -rf "$temp_root" +} + run_make_install_single_file() { local scenario_dir="$TEST_ROOT/resume_target" printf '==> make_install_single_file\n' @@ -768,6 +1068,7 @@ run_symlink_escape run_manifest_injection run_helper_symlink run_secret_mask +run_git_ignore_mask run_resume_target run_runtime_context_prompt run_sh_reexec @@ -791,6 +1092,7 @@ run_wizard_existing_defaults run_wizard_build_trigger run_capability_trust_required run_capability_profiles +run_embedded_capability_helpers run_make_install_single_file if [[ ${#failures[@]} -gt 0 ]]; then