diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6dca8a8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,39 @@ +# skz-sloptrap + +This workspace is the source code of `sloptrap`, eg. your own runtime. +Your author, 'sk4nz', has ran the command `sloptrap path/to/skz-sloptrap` and you are living in your own reflection. + +# Recursive `slopsloptrap` + +sloptrap can recurse. + +If you need to prove it, spawn a child from this repo with sloptrap: + +`./sloptrap /workspace ...` + +This starts a child `slopsloptrap`. Use the child normally, as the end-user would. + +# Keep Memory + +Future sloptrap instances should add short, useful, durable notes they learn during their session to this file. + +Only add high-signal information that is likely to help later runs. +Keep additions brief. +Do not remove existing instructions unless they are outdated or wrong. + +--- +# Session Memory + +- When you make changes that affect how the runtime behaves and cannot test it with recursion, you can ask sk4nz to restart you. +- If recursion is blocked by the current session state and a restart is required, give sk4nz an exact prompt to paste into the next restarted session so it can continue from the current checkpoint. +- Useful checks in the skz-sloptrap repo: + `shellcheck sloptrap` + `bash tests/run_tests.sh` (you can also run them separately) +- Recursive nested-podman inside an already user-namespaced sloptrap needs subuid/subgid ranges derived from `/proc/self/uid_map` and `/proc/self/gid_map`; hardcoding `100000:65536` breaks `newuidmap`. +- When running tests from inside sloptrap, inherited `CODEX_HOME=/codex` plus `SLOPTRAP_PREFER_CODEX_HOME=1` can leak into host-style child launches; ignore that preference when `HOME` has been redirected elsewhere and the runtime hints still point into the inherited `/codex` tree. +- For real recursive `./sloptrap` launches, correct `/proc/self/uid_map` alone is not enough: if the current session still has stale read-only `/etc/subuid` and `/etc/subgid` mounts (for example `sk4nz:100000:65536`), the first nested podman launch still fails before child startup. +- Forcing `SLOPTRAP_CONTAINER_ENGINE=sloppodman` inside sloptrap also needs `TMPDIR` under `/workspace`; otherwise its build-context path guard rejects the staged Dockerfile under `/tmp` before you reach the real subuid/subgid problem. +- If a restarted session still inherits stale read-only `/etc/subuid` and `/etc/subgid` tmpfs mounts, an unprivileged agent cannot repair them in-place (`umount` says `must be superuser to unmount`); both podman and sloppodman stay blocked until the session starts without those mounts. +- Outer sloptrap launches no longer need `/etc/subuid` or `/etc/subgid` bind mounts: `nested-podman` now disables read-only rootfs and the container entrypoint synthesizes container-local subid files from `/proc/self/{uid,gid}_map` before dropping privileges. +- With stale subid mounts gone, recursive `./sloptrap /workspace` can still fail earlier during the inner image build: `crun` tries to open `/proc/sys/net/ipv4/ping_group_range` and gets `Read-only file system` while creating the build container. +- In a fragile nested-podman session, `podman system migrate` can make things worse: a state that still answered `podman info` fell back to repeated `newuidmap ... write to uid_map failed: Operation not permitted` failures afterward. diff --git a/README.md b/README.md index cb8afbf..fbe54c1 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Supported keys when the manifest is present: Values containing `$`, `` ` ``, or newlines are rejected to prevent command injection. Setting illegal keys or malformed values aborts the run before containers start. sloptrap always runs Codex with `--sandbox danger-full-access --ask-for-approval never`. `codex_args` is deprecated and rejected if present. -Capability trust is local state, not part of the repository. Builds for manifests that request capabilities require either an interactive trust confirmation or `--trust-capabilities`. Trusted capabilities can then be activated per run with `--enable-capability `. +Capability trust is local state, not part of the repository. Builds for manifests that request capabilities require either an interactive trust confirmation or `--trust-capabilities`. Once the current manifest is trusted, its requested capabilities are enabled automatically for that project configuration. ### `.sloptrapignore` @@ -80,7 +80,7 @@ Capability trust is local state, not part of the repository. Builds for manifest ## CLI Reference ``` -./sloptrap [--dry-run] [--print-config] [--trust-capabilities] [--enable-capability ...] [target ...] +./sloptrap [--dry-run] [--print-config] [--trust-capabilities] [target ...] ``` Options: @@ -88,7 +88,6 @@ Options: - `--dry-run` — print the container/engine commands that would run without executing them. - `--print-config` — output the resolved manifest values, defaults, and ignore list. - `--trust-capabilities` — trust the manifest's requested capabilities for the current build flow. -- `--enable-capability ` — enable a trusted runtime capability for this invocation. Repeat for multiple capabilities. - `-h, --help` — display usage. - `--` — stop option parsing; remaining arguments are treated as targets. @@ -128,11 +127,12 @@ The launcher executes targets sequentially, so `./sloptrap repo build run` perfo ### Capability Helpers -When a trusted capability is enabled for a run, the container includes helper commands: +When the current manifest's capabilities are trusted and enabled, the container includes helper commands: - `slop-apt install ` for session-scoped package installation. - `slopcap capture --interface [--filter ] [--output ] [--stdout]` for packet capture. - `sloppodman ...` for nested Podman workflows. `build` contexts and Dockerfiles must remain inside `/workspace`, and pushes are not supported. +- When `nested-podman` is enabled, sloptrap makes the container root filesystem writable long enough to synthesize container-local `/etc/subuid` and `/etc/subgid` files from the live namespace maps, so rootless nested Podman does not depend on host subid files. ## Execution Environment diff --git a/slopsloptrap/.sloptrap b/slopsloptrap/.sloptrap new file mode 100644 index 0000000..68276af --- /dev/null +++ b/slopsloptrap/.sloptrap @@ -0,0 +1,3 @@ +name=slopsloptrap +capabilities=nested-podman +allow_host_network=false diff --git a/sloptrap b/sloptrap index 0d90c05..a76e484 100755 --- a/sloptrap +++ b/sloptrap @@ -356,6 +356,120 @@ set -euo pipefail helper_pid="" +has_capability() { + local needle=$1 + local token + for token in ${SLOPTRAP_ACTIVE_CAPABILITIES:-}; do + if [[ $token == "$needle" ]]; then + return 0 + fi + done + return 1 +} + +detect_subid_range_from_map() { + local map_path=$1 + local account_id=$2 + awk -v account_id="$account_id" ' + NF < 3 { next } + { + ns_start = $1 + 0 + ns_count = $3 + 0 + if (ns_count <= 0) { + next + } + if (account_id >= ns_start && account_id < (ns_start + ns_count)) { + next + } + if (ns_count > best_count) { + best_start = ns_start + best_count = ns_count + } + } + END { + if (best_count > 0) { + printf "%s %s", best_start, best_count + exit 0 + } + exit 1 + } + ' "$map_path" +} + +ensure_subid_mapping_file() { + local destination=$1 + local account_name=$2 + local account_id=$3 + local range_start=$4 + local range_count=$5 + local tmp_file="${destination}.tmp" + + if [[ -r $destination ]] && awk -F: -v account_name="$account_name" -v account_id="$account_id" -v range_start="$range_start" -v range_count="$range_count" ' + ($1 == account_name || $1 == account_id) && $2 == range_start && $3 == range_count { found=1; exit } + END { exit(found ? 0 : 1) } + ' "$destination"; then + return 0 + fi + + [[ -e $destination && ! -L $destination ]] || : >"$destination" + awk -F: -v account_name="$account_name" -v account_id="$account_id" ' + $1 == account_name || $1 == account_id { next } + { print } + ' "$destination" >"$tmp_file" + if [[ -n $account_name && $account_name != "$account_id" ]]; then + printf '%s:%s:%s\n' "$account_name" "$range_start" "$range_count" >>"$tmp_file" + else + printf '%s:%s:%s\n' "$account_id" "$range_start" "$range_count" >>"$tmp_file" + fi + chmod 0644 "$tmp_file" 2>/dev/null || true + if ! mv "$tmp_file" "$destination" 2>/dev/null; then + cat "$tmp_file" >"$destination" + rm -f "$tmp_file" + fi +} + +lookup_account_name() { + local account_id=$1 + local passwd_entry="" + if passwd_entry=$(getent passwd "$account_id" 2>/dev/null); then + printf '%s\n' "${passwd_entry%%:*}" + return 0 + fi + return 1 +} + +ensure_subid_mappings() { + local account_id account_gid account_name="" + local range_start="" range_count="" gid_start="" gid_count="" + local detected_range="" + + account_id=${SLOPTRAP_HOST_UID:-$(id -u)} + account_gid=${SLOPTRAP_HOST_GID:-$(id -g)} + if ! account_name=$(lookup_account_name "$account_id"); then + account_name="" + fi + + if detected_range=$(detect_subid_range_from_map /proc/self/uid_map "$account_id" 2>/dev/null); then + read -r range_start range_count <<<"$detected_range" + elif [[ -n ${SLOPTRAP_PODMAN_SUBID_START:-} && -n ${SLOPTRAP_PODMAN_SUBID_COUNT:-} ]]; then + range_start=${SLOPTRAP_PODMAN_SUBID_START} + range_count=${SLOPTRAP_PODMAN_SUBID_COUNT} + fi + if detected_range=$(detect_subid_range_from_map /proc/self/gid_map "$account_gid" 2>/dev/null); then + read -r gid_start gid_count <<<"$detected_range" + elif [[ -n ${SLOPTRAP_PODMAN_SUBGID_START:-} && -n ${SLOPTRAP_PODMAN_SUBGID_COUNT:-} ]]; then + gid_start=${SLOPTRAP_PODMAN_SUBGID_START} + gid_count=${SLOPTRAP_PODMAN_SUBGID_COUNT} + fi + + if [[ -n $range_start && -n $range_count ]]; then + ensure_subid_mapping_file /etc/subuid "$account_name" "$account_id" "$range_start" "$range_count" + fi + if [[ -n $gid_start && -n $gid_count ]]; then + ensure_subid_mapping_file /etc/subgid "$account_name" "$account_id" "$gid_start" "$gid_count" + fi +} + cleanup() { if [[ -n $helper_pid ]]; then kill "$helper_pid" >/dev/null 2>&1 || true @@ -374,11 +488,11 @@ if [[ $(id -u) -eq 0 ]]; then queue_dir="$helper_dir/queue" mkdir -p "$queue_dir" chmod 711 "$helper_dir" - chmod 700 "$queue_dir" + chmod 1733 "$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" + if has_capability "nested-podman"; then + ensure_subid_mappings fi if [[ -n ${SLOPTRAP_ACTIVE_CAPABILITIES:-} ]]; then /usr/local/bin/sloptrap-helperd & @@ -412,7 +526,13 @@ cleanup_pidfile() { rm -f "$pidfile" } -trap cleanup_pidfile EXIT INT TERM HUP +shutdown_helper() { + cleanup_pidfile + exit 0 +} + +trap cleanup_pidfile EXIT +trap shutdown_helper INT TERM HUP printf '%s\n' "$$" >"$pidfile" chmod 644 "$pidfile" 2>/dev/null || true @@ -460,10 +580,35 @@ path_within_root() { claim_request_dir() { local request_dir=$1 + local owner_uid owner_gid [[ -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 + owner_uid=$(stat -c '%u' "$request_dir" 2>/dev/null || true) + owner_gid=$(stat -c '%g' "$request_dir" 2>/dev/null || true) + [[ $owner_uid =~ ^[0-9]+$ && $owner_gid =~ ^[0-9]+$ ]] || return 1 + REQUEST_OWNER_UID=$owner_uid + REQUEST_OWNER_GID=$owner_gid + if [[ $(id -u) -eq 0 ]]; then + chown root:root "$request_dir" 2>/dev/null || return 1 + chmod 700 "$request_dir" 2>/dev/null || return 1 + fi +} + +release_request_dir() { + local request_dir=$1 + local owner_uid=$2 + local owner_gid=$3 + local path + [[ $owner_uid =~ ^[0-9]+$ && $owner_gid =~ ^[0-9]+$ ]] || return 0 + for path in "$request_dir" "$request_dir/status" "$request_dir/stdout" "$request_dir/stderr"; do + [[ -e $path && ! -L $path ]] || continue + chown "$owner_uid:$owner_gid" "$path" 2>/dev/null || true + done chmod 700 "$request_dir" 2>/dev/null || true + for path in "$request_dir/status" "$request_dir/stdout" "$request_dir/stderr"; do + [[ -e $path && ! -L $path ]] || continue + chmod 600 "$path" 2>/dev/null || true + done } init_request_outputs() { @@ -636,6 +781,8 @@ while true; do [[ -d $request_dir ]] || continue [[ ! -f "$request_dir/status" ]] || continue pending_requests=1 + REQUEST_OWNER_UID="" + REQUEST_OWNER_GID="" if ! claim_request_dir "$request_dir"; then log_action "request" "unsafe=1 path=$request_dir" 2 continue @@ -663,6 +810,7 @@ while true; do log_action "$op" "unknown=1" 2 ;; esac + release_request_dir "$request_dir" "$REQUEST_OWNER_UID" "$REQUEST_OWNER_GID" done if [[ $pending_requests -eq 0 ]]; then sleep 1 @@ -716,8 +864,7 @@ ensure_helper_ready() { [[ -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" + chmod 1733 "$queue_dir" if [[ -r $pidfile ]]; then pid=$(<"$pidfile") if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then @@ -827,8 +974,7 @@ ensure_helper_ready() { [[ -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" + chmod 1733 "$queue_dir" if [[ -r $pidfile ]]; then pid=$(<"$pidfile") if [[ -n $pid ]] && kill -0 "$pid" 2>/dev/null; then @@ -961,6 +1107,10 @@ exit "$status" EOF ;; sloppodman) + # shellcheck disable=SC2034 + local workspace_root="" podman_root="" podman_runroot="" runtime_dir="" config_home="" + # shellcheck disable=SC2034 + local storage_driver="" storage_conf="" containers_conf="" subcommand="" cat <<'EOF' #!/usr/bin/env bash set -euo pipefail @@ -983,11 +1133,29 @@ if [[ $# -eq 0 ]]; then exit 2 fi +original_args=("$@") subcommand=$1 shift +subcommand_prefix=("$subcommand") case "$subcommand" in - pull|build|tag|run|ps|logs|stop|rm|inspect) + pull|build|tag|run|ps|logs|stop|rm|inspect|rmi) + ;; + image) + [[ $# -gt 0 ]] || { + printf 'sloppodman: image requires a subcommand\n' >&2 + exit 2 + } + case "$1" in + inspect|prune) + subcommand_prefix=(image "$1") + shift + ;; + *) + printf 'sloppodman: unsupported image subcommand %s\n' "$1" >&2 + exit 2 + ;; + esac ;; *) printf 'sloppodman: unsupported subcommand %s\n' "$subcommand" >&2 @@ -1004,7 +1172,161 @@ 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} runtime_dir=${XDG_RUNTIME_DIR:-/codex/capabilities/podman/runtime} -mkdir -p "$podman_root" "$podman_runroot" "$runtime_dir" +config_home=${SLOPTRAP_INNER_PODMAN_CONFIG_HOME:-$runtime_dir/config} +containers_conf_dir="$config_home/containers" +storage_driver=${SLOPTRAP_INNER_PODMAN_DRIVER:-vfs} +storage_conf="$containers_conf_dir/storage.conf" +containers_conf="$containers_conf_dir/containers.conf" +subuid_file=${SLOPTRAP_PODMAN_SUBUID_FILE:-/etc/subuid} +subgid_file=${SLOPTRAP_PODMAN_SUBGID_FILE:-/etc/subgid} +mkdir -p "$podman_root" "$podman_runroot" "$runtime_dir" "$containers_conf_dir" + +cat >"$storage_conf" <"$containers_conf" <<'CONTAINERS_CONF_EOF' +[engine] +cgroup_manager = "cgroupfs" +events_logger = "file" +CONTAINERS_CONF_EOF + +export CONTAINERS_STORAGE_CONF="$storage_conf" +export CONTAINERS_CONF="$containers_conf" +export BUILDAH_ISOLATION="${BUILDAH_ISOLATION:-chroot}" + +detect_subid_range_from_map() { + local map_path=$1 + local account_id=$2 + awk -v account_id="$account_id" ' + NF < 3 { next } + { + ns_start = $1 + 0 + ns_count = $3 + 0 + if (ns_count <= 0) { + next + } + if (account_id >= ns_start && account_id < (ns_start + ns_count)) { + next + } + if (ns_count > best_count) { + best_start = ns_start + best_count = ns_count + } + } + END { + if (best_count > 0) { + printf "%s %s", best_start, best_count + exit 0 + } + exit 1 + } + ' "$map_path" +} + +ensure_subid_mapping_file() { + local destination=$1 + local account_name=$2 + local account_id=$3 + local range_start=$4 + local range_count=$5 + local tmp_file="${destination}.tmp" + + if [[ -r $destination ]] && awk -F: -v account_name="$account_name" -v account_id="$account_id" -v range_start="$range_start" -v range_count="$range_count" ' + ($1 == account_name || $1 == account_id) && $2 == range_start && $3 == range_count { found=1; exit } + END { exit(found ? 0 : 1) } + ' "$destination"; then + return 0 + fi + + [[ -e $destination && ! -L $destination ]] || : >"$destination" + awk -F: -v account_name="$account_name" -v account_id="$account_id" ' + $1 == account_name || $1 == account_id { next } + { print } + ' "$destination" >"$tmp_file" + if [[ -n $account_name && $account_name != "$account_id" ]]; then + printf '%s:%s:%s\n' "$account_name" "$range_start" "$range_count" >>"$tmp_file" + else + printf '%s:%s:%s\n' "$account_id" "$range_start" "$range_count" >>"$tmp_file" + fi + chmod 0644 "$tmp_file" 2>/dev/null || true + if ! mv "$tmp_file" "$destination" 2>/dev/null; then + cat "$tmp_file" >"$destination" + rm -f "$tmp_file" + fi +} + +ensure_subid_mappings() { + local account_id account_name range_start range_count gid_start gid_count detected_range + account_id=${SLOPTRAP_PODMAN_CALLER_UID:-$(id -u)} + account_name=${SLOPTRAP_PODMAN_CALLER_USER:-} + if [[ -z $account_name ]]; then + account_name=$(id -un 2>/dev/null || true) + fi + range_start=${SLOPTRAP_PODMAN_SUBID_START:-100000} + range_count=${SLOPTRAP_PODMAN_SUBID_COUNT:-65536} + gid_start=${SLOPTRAP_PODMAN_SUBGID_START:-$range_start} + gid_count=${SLOPTRAP_PODMAN_SUBGID_COUNT:-$range_count} + if detected_range=$(detect_subid_range_from_map /proc/self/uid_map "$account_id" 2>/dev/null); then + read -r range_start range_count <<<"$detected_range" + fi + if detected_range=$(detect_subid_range_from_map /proc/self/gid_map "${SLOPTRAP_PODMAN_CALLER_GID:-$(id -g)}" 2>/dev/null); then + read -r gid_start gid_count <<<"$detected_range" + fi + [[ -e $subuid_file && ! -L $subuid_file ]] || : >"$subuid_file" + [[ -e $subgid_file && ! -L $subgid_file ]] || : >"$subgid_file" + ensure_subid_mapping_file "$subuid_file" "$account_name" "$account_id" "$range_start" "$range_count" + ensure_subid_mapping_file "$subgid_file" "$account_name" "$account_id" "$gid_start" "$gid_count" + if [[ $(id -u) -eq 0 ]]; then + ensure_subid_mapping_file "$subuid_file" root 0 "$range_start" "$range_count" + ensure_subid_mapping_file "$subgid_file" root 0 "$gid_start" "$gid_count" + fi +} + +exec_podman() { + local -a cmd=( + podman + --root "$podman_root" + --runroot "$podman_runroot" + --storage-driver "$storage_driver" + --cgroup-manager cgroupfs + --events-backend file + "${subcommand_prefix[@]}" "$@" + ) + + if [[ $(id -u) -eq 0 ]]; then + ensure_subid_mappings + exec "${cmd[@]}" + fi + if [[ ${SLOPTRAP_PODMAN_ESCALATED:-0} == 1 ]]; then + exec "${cmd[@]}" + fi + if ! command -v setpriv >/dev/null 2>&1; then + printf 'sloppodman: setpriv is required to enter the nested podman capability profile\n' >&2 + exit 1 + fi + chmod 0777 "$podman_root" "$podman_runroot" "$runtime_dir" 2>/dev/null || true + exec setpriv --reuid 0 --regid 0 --clear-groups -- env \ + CONTAINERS_STORAGE_CONF="$CONTAINERS_STORAGE_CONF" \ + CONTAINERS_CONF="$CONTAINERS_CONF" \ + BUILDAH_ISOLATION="$BUILDAH_ISOLATION" \ + XDG_RUNTIME_DIR="$runtime_dir" \ + SLOPTRAP_PODMAN_ESCALATED=1 \ + SLOPTRAP_PODMAN_CALLER_UID="${SLOPTRAP_PODMAN_CALLER_UID:-$(id -u)}" \ + SLOPTRAP_PODMAN_CALLER_GID="${SLOPTRAP_PODMAN_CALLER_GID:-$(id -g)}" \ + SLOPTRAP_PODMAN_CALLER_USER="${SLOPTRAP_PODMAN_CALLER_USER:-$(id -un 2>/dev/null || true)}" \ + "$0" "${original_args[@]}" +} resolve_inner_path() { local raw=$1 @@ -1180,7 +1502,7 @@ if [[ $subcommand == "run" ]]; then done fi -exec podman --root "$podman_root" --runroot "$podman_runroot" "$subcommand" "$@" +exec_podman "$@" EOF ;; *) @@ -1243,6 +1565,30 @@ prepare_build_context() { select_codex_home() { local preferred="$HOME/.codex" + if [[ -n ${CODEX_HOME:-} ]]; then + if [[ ${HOME:-} == "$CODEX_HOME" ]]; then + preferred="$CODEX_HOME" + elif [[ ${SLOPTRAP_PREFER_CODEX_HOME:-0} == "1" ]]; then + local inherited_runtime_home=false + local runtime_hint + for runtime_hint in \ + "${SLOPTRAP_CAPTURE_DIR:-}" \ + "${SLOPTRAP_AUDIT_LOG:-}" \ + "${SLOPTRAP_INNER_PODMAN_ROOT:-}" \ + "${SLOPTRAP_INNER_PODMAN_RUNROOT:-}" \ + "${XDG_CONFIG_HOME:-}" \ + "${XDG_CACHE_HOME:-}" \ + "${XDG_STATE_HOME:-}"; do + if [[ -n $runtime_hint && ( $runtime_hint == "$CODEX_HOME" || $runtime_hint == "$CODEX_HOME/"* ) ]]; then + inherited_runtime_home=true + break + fi + done + if ! $inherited_runtime_home; then + preferred="$CODEX_HOME" + fi + fi + fi if [[ -L $preferred ]]; then error "Codex home '$preferred' must not be a symlink" fi @@ -1269,6 +1615,59 @@ select_codex_home() { fi } +write_subid_mapping_file() { + local destination=$1 + local existing_path=$2 + local account_name=$3 + local account_id=$4 + local range_start=$5 + local range_count=$6 + + if [[ -r $existing_path ]]; then + awk -F: -v account_name="$account_name" -v account_id="$account_id" ' + $1 == account_name || $1 == account_id { next } + { print } + ' "$existing_path" >"$destination" + else + : >"$destination" + fi + if [[ -n $account_name && $account_name != "$account_id" ]]; then + printf '%s:%s:%s\n' "$account_name" "$range_start" "$range_count" >>"$destination" + else + printf '%s:%s:%s\n' "$account_id" "$range_start" "$range_count" >>"$destination" + fi + chmod 0644 "$destination" +} + +detect_subid_range_from_map() { + local map_path=$1 + local account_id=$2 + awk -v account_id="$account_id" ' + NF < 3 { next } + { + ns_start = $1 + 0 + ns_count = $3 + 0 + if (ns_count <= 0) { + next + } + if (account_id >= ns_start && account_id < (ns_start + ns_count)) { + next + } + if (ns_count > best_count) { + best_start = ns_start + best_count = ns_count + } + } + END { + if (best_count > 0) { + printf "%s %s", best_start, best_count + exit 0 + } + exit 1 + } + ' "$map_path" +} + compute_manifest_digest() { if [[ -f $MANIFEST_PATH ]]; then local digest @@ -2234,7 +2633,7 @@ prepare_container_runtime() { SLOPTRAP_CONTAINER_NAME=$(sanitize_engine_name "$SLOPTRAP_CONTAINER_NAME") local -a network_opts=(--network "$SLOPTRAP_NETWORK_NAME" --init) - local -a security_opts=(--cap-drop=ALL --security-opt no-new-privileges) + local -a security_opts=(--cap-drop=ALL) local -a capability_opts=() if [[ -n $SLOPTRAP_SECURITY_OPTS_EXTRA ]]; then local -a extra_opts=() @@ -2267,7 +2666,12 @@ prepare_container_runtime() { SLOPTRAP_RUN_AS_ROOT=true fi if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman"; then - capability_opts+=(--device /dev/fuse) + capability_opts+=(--device /dev/fuse --cap-add SYS_CHROOT --cap-add MKNOD) + security_opts+=(--security-opt seccomp=unconfined) + SLOPTRAP_ROOTFS_READONLY=0 + SLOPTRAP_RUN_AS_ROOT=true + else + security_opts+=(--security-opt no-new-privileges) fi if $SLOPTRAP_RUN_AS_ROOT; then capability_opts+=( @@ -2318,6 +2722,7 @@ prepare_container_runtime() { -e "SLOPTRAP_AUDIT_LOG=$SLOPTRAP_CODEX_HOME_CONT/state/capabilities.log" -e "SLOPTRAP_INNER_PODMAN_ROOT=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/storage" -e "SLOPTRAP_INNER_PODMAN_RUNROOT=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/run" + -e "SLOPTRAP_PREFER_CODEX_HOME=1" -e "XDG_RUNTIME_DIR=$SLOPTRAP_CODEX_HOME_CONT/capabilities/podman/runtime" ) if capability_list_contains "$ENABLED_CAPABILITIES" "nested-podman" && [[ $SLOPTRAP_NETWORK_NAME == "host" ]]; then @@ -2381,6 +2786,7 @@ build_image() { "$CONTAINER_ENGINE" build --quiet -t "$SLOPTRAP_IMAGE_NAME" -f "$SLOPTRAP_DOCKERFILE_PATH" + --network "$SLOPTRAP_NETWORK_NAME" --label "$SLOPTRAP_IMAGE_LABEL" --build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE" --build-arg "CAPABILITY_PACKAGES=$capability_packages_arg" @@ -2426,6 +2832,7 @@ rebuild_image() { "$CONTAINER_ENGINE" build --no-cache --quiet -t "$SLOPTRAP_IMAGE_NAME" -f "$SLOPTRAP_DOCKERFILE_PATH" + --network "$SLOPTRAP_NETWORK_NAME" --label "$SLOPTRAP_IMAGE_LABEL" --build-arg "BASE_PACKAGES=$SLOPTRAP_PACKAGES_BASE" --build-arg "CAPABILITY_PACKAGES=$capability_packages_arg" diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 242c245..d86cbb2 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -447,6 +447,99 @@ run_auth_file_mount() { teardown_stub_env } +run_codex_home_override() { + local scenario_dir codex_root + scenario_dir=$(cd "$TEST_ROOT/resume_target" && pwd -P) + printf '==> codex_home_override\n' + setup_stub_env + codex_root="$STUB_HOME/codex-root" + mkdir -p "$codex_root" + printf '{"access_token":"test"}\n' >"$codex_root/auth.json" + if ! PATH="$STUB_BIN:$PATH" HOME="$STUB_HOME" CODEX_HOME="$codex_root" SLOPTRAP_PREFER_CODEX_HOME=1 \ + FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ + "$SLOPTRAP_BIN" "$scenario_dir" /dev/null 2>&1; then + record_failure "codex_home_override: sloptrap exited non-zero" + teardown_stub_env + return + fi + if ! grep -q -- "-v ${codex_root}/auth.json:/codex/auth.json:Z" "$STUB_LOG"; then + record_failure "codex_home_override: missing CODEX_HOME auth file mount" + fi + if ! grep -q -- "-v ${codex_root}/sloptrap/state/" "$STUB_LOG"; then + record_failure "codex_home_override: missing CODEX_HOME project state bind mount" + fi + if grep -q -- "-v ${STUB_HOME}/.codex/auth.json:/codex/auth.json:Z" "$STUB_LOG"; then + record_failure "codex_home_override: should not fall back to HOME/.codex when CODEX_HOME is set" + fi + local first_run + first_run=$(grep "FAKE PODMAN: run " "$STUB_LOG" | head -n 1 || true) + if [[ -z $first_run || $first_run == *" login" ]]; then + record_failure "codex_home_override: existing CODEX_HOME auth should avoid login target" + fi + teardown_stub_env +} + +run_recursive_slopsloptrap() { + local scenario_dir temp_root codex_root + printf '==> recursive_slopsloptrap\n' + setup_stub_env + temp_root=$(mktemp -d) + scenario_dir="$temp_root/slopsloptrap" + codex_root="$temp_root/codex-root" + mkdir -p "$scenario_dir" "$codex_root" + cat >"$scenario_dir/.sloptrap" <<'EOF' +name=slopsloptrap +capabilities=nested-podman +allow_host_network=false +EOF + printf '{"access_token":"test"}\n' >"$codex_root/auth.json" + if ! PATH="$STUB_BIN:$PATH" HOME="$codex_root" CODEX_HOME="$codex_root" \ + FAKE_PODMAN_LOG="$STUB_LOG" FAKE_PODMAN_INSPECT_FAIL=1 \ + "$SLOPTRAP_BIN" --trust-capabilities "$scenario_dir" shell /dev/null 2>&1; then + record_failure "recursive_slopsloptrap: sloptrap exited non-zero" + teardown_stub_env + rm -rf "$temp_root" + return + fi + if ! grep -q -- "slopsloptrap-sloptrap-image" "$STUB_LOG"; then + record_failure "recursive_slopsloptrap: child image name missing" + fi + if ! grep -q -- "slopsloptrap-sloptrap-container" "$STUB_LOG"; then + record_failure "recursive_slopsloptrap: child container name missing" + fi + if ! grep -q -- "-v ${codex_root}/auth.json:/codex/auth.json:Z" "$STUB_LOG"; then + record_failure "recursive_slopsloptrap: missing recursive auth bind mount" + fi + if ! grep -q -- "-v ${codex_root}/sloptrap/state/" "$STUB_LOG"; then + record_failure "recursive_slopsloptrap: missing recursive state bind mount" + fi + if grep -q -- "-v ${codex_root}/.codex/auth.json:/codex/auth.json:Z" "$STUB_LOG"; then + record_failure "recursive_slopsloptrap: should not fall back to CODEX_HOME/.codex in recursive mode" + fi + if grep -q -- "/etc/subuid" "$STUB_LOG" || grep -q -- "/etc/subgid" "$STUB_LOG"; then + record_failure "recursive_slopsloptrap: launcher should not mount subid helper files" + fi + if grep -q -- "--read-only" "$STUB_LOG"; then + record_failure "recursive_slopsloptrap: nested podman should disable read-only rootfs" + fi + local first_run + first_run=$(grep "FAKE PODMAN: run " "$STUB_LOG" | head -n 1 || true) + if [[ -z $first_run || $first_run == *" login" ]]; then + record_failure "recursive_slopsloptrap: recursive auth should avoid login target" + fi + if [[ $first_run != *"/bin/bash"* ]]; then + record_failure "recursive_slopsloptrap: shell target did not reach child container run" + fi + if [[ $first_run == *"--user "* ]]; then + record_failure "recursive_slopsloptrap: nested podman shell should not force --user" + fi + if [[ $first_run != *"--cap-add SETUID"* || $first_run != *"--cap-add SETGID"* ]]; then + record_failure "recursive_slopsloptrap: nested podman shell missing privilege bootstrap capabilities" + fi + teardown_stub_env + rm -rf "$temp_root" +} + run_project_state_isolation() { local scenario_a scenario_b scenario_a=$(cd "$TEST_ROOT/resume_target" && pwd -P) @@ -735,6 +828,9 @@ run_capability_profiles() { 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 -- "FAKE PODMAN: build --quiet -t capability-repo-sloptrap-image -f .* --network host " "$STUB_LOG"; then + record_failure "capability_profiles: build should inherit host networking" + fi if ! grep -q -- "--cap-add NET_RAW" "$STUB_LOG"; then record_failure "capability_profiles: NET_RAW capability missing" fi @@ -744,6 +840,12 @@ 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 SYS_CHROOT" "$STUB_LOG"; then + record_failure "capability_profiles: SYS_CHROOT capability missing" + fi + if ! grep -q -- "--cap-add MKNOD" "$STUB_LOG"; then + record_failure "capability_profiles: MKNOD capability missing" + fi if ! grep -q -- "--cap-add SETUID" "$STUB_LOG"; then record_failure "capability_profiles: SETUID capability missing" fi @@ -759,6 +861,12 @@ run_capability_profiles() { if ! grep -q -- "--cap-add FOWNER" "$STUB_LOG"; then record_failure "capability_profiles: FOWNER capability missing" fi + if ! grep -q -- "--security-opt seccomp=unconfined" "$STUB_LOG"; then + record_failure "capability_profiles: nested podman seccomp override missing" + fi + if grep -q -- "--security-opt no-new-privileges" "$STUB_LOG"; then + record_failure "capability_profiles: nested podman should not force no-new-privileges" + fi if grep -q -- "--read-only" "$STUB_LOG"; then record_failure "capability_profiles: apt profile should disable read-only rootfs" fi @@ -780,6 +888,9 @@ run_capability_profiles() { if ! grep -q -- "SLOPTRAP_INNER_PODMAN_HOST_NETWORK=1" "$STUB_LOG"; then record_failure "capability_profiles: inner podman host-network mirror flag missing" fi + if grep -q -- "/etc/subuid" "$STUB_LOG" || grep -q -- "/etc/subgid" "$STUB_LOG"; then + record_failure "capability_profiles: launcher should not mount subid helper files" + 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) @@ -830,6 +941,8 @@ run_embedded_capability_helpers() { cat >"$helper_bin/podman" <<'EOF' #!/usr/bin/env bash set -euo pipefail +printf 'podman-env BUILDAH_ISOLATION=%s CONTAINERS_STORAGE_CONF=%s CONTAINERS_CONF=%s\n' \ + "${BUILDAH_ISOLATION:-}" "${CONTAINERS_STORAGE_CONF:-}" "${CONTAINERS_CONF:-}" >>"$TEST_TOOL_LOG" printf 'podman %s\n' "$*" >>"$TEST_TOOL_LOG" exit 0 EOF @@ -861,6 +974,7 @@ EOF cat >"$helper_bin/setpriv" <<'EOF' #!/usr/bin/env bash set -euo pipefail +printf 'setpriv %s\n' "$*" >>"$TEST_TOOL_LOG" while [[ $# -gt 0 ]]; do case "$1" in --reuid|--regid) @@ -883,7 +997,7 @@ 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 + || ! grep -q "chmod 1733 \"\\\$queue_dir\"" "$helper_bin/sloptrap-entrypoint"; then record_failure "embedded_capability_helpers: entrypoint did not expose helper queue to the dropped user" fi @@ -950,6 +1064,86 @@ EOF 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 + if ! grep -q -- 'setpriv --reuid 0 --regid 0 --clear-groups -- env ' "$tool_log"; then + record_failure "embedded_capability_helpers: sloppodman did not re-enter root before invoking podman" + 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" image inspect example/image >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: sloppodman rejected image inspect" + fi + if ! grep -q -- 'podman --root .* image inspect example/image' "$tool_log"; then + record_failure "embedded_capability_helpers: sloppodman did not forward image inspect" + fi + if ! grep -q -- '--storage-driver vfs' "$tool_log" \ + || ! grep -q -- '--cgroup-manager cgroupfs' "$tool_log" \ + || ! grep -q -- '--events-backend file' "$tool_log"; then + record_failure "embedded_capability_helpers: sloppodman did not apply the nested podman runtime defaults" + fi + if ! grep -q -- 'podman-env BUILDAH_ISOLATION=chroot ' "$tool_log"; then + record_failure "embedded_capability_helpers: sloppodman did not set BUILDAH_ISOLATION=chroot" + fi + if [[ ! -f $inner_runtime_dir/config/containers/storage.conf ]] \ + || [[ ! -f $inner_runtime_dir/config/containers/containers.conf ]]; then + record_failure "embedded_capability_helpers: sloppodman did not materialize its container config files" + elif ! grep -q -- 'ignore_chown_errors = "true"' "$inner_runtime_dir/config/containers/storage.conf"; then + record_failure "embedded_capability_helpers: storage.conf did not enable ignore_chown_errors" + fi + + local caller_uid caller_gid caller_user + caller_uid=$(id -u) + caller_gid=$(id -g) + caller_user=$(id -un) + cat >"$helper_bin/id" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +case "${1-}" in + -u) + printf '0\n' + ;; + -g) + printf '0\n' + ;; + -un) + printf 'root\n' + ;; + *) + printf 'uid=0(root) gid=0(root) groups=0(root)\n' + ;; +esac +EOF + chmod +x "$helper_bin/id" + local caller_subuid root_subuid caller_subgid root_subgid + local helper_subuid_file helper_subgid_file + helper_subuid_file="$temp_root/helper-subuid" + helper_subgid_file="$temp_root/helper-subgid" + 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" \ + SLOPTRAP_PODMAN_ESCALATED=1 \ + SLOPTRAP_PODMAN_CALLER_UID="$caller_uid" \ + SLOPTRAP_PODMAN_CALLER_GID="$caller_gid" \ + SLOPTRAP_PODMAN_CALLER_USER="$caller_user" \ + SLOPTRAP_PODMAN_SUBUID_FILE="$helper_subuid_file" \ + SLOPTRAP_PODMAN_SUBGID_FILE="$helper_subgid_file" \ + "$helper_bin/sloppodman" image inspect example/image >/dev/null 2>&1; then + record_failure "embedded_capability_helpers: sloppodman did not synthesize caller and root subid files" + fi + caller_subuid=$(awk -F: -v account="$caller_user" '$1 == account { print $2 ":" $3 }' "$helper_subuid_file" 2>/dev/null || true) + root_subuid=$(awk -F: '$1 == "root" { print $2 ":" $3 }' "$helper_subuid_file" 2>/dev/null || true) + caller_subgid=$(awk -F: -v account="$caller_user" '$1 == account { print $2 ":" $3 }' "$helper_subgid_file" 2>/dev/null || true) + root_subgid=$(awk -F: '$1 == "root" { print $2 ":" $3 }' "$helper_subgid_file" 2>/dev/null || true) + if [[ -z $caller_subuid || $caller_subuid != "$root_subuid" ]]; then + record_failure "embedded_capability_helpers: sloppodman did not mirror caller subuid data onto root" + fi + if [[ -z $caller_subgid || $caller_subgid != "$root_subgid" ]]; then + record_failure "embedded_capability_helpers: sloppodman did not mirror caller subgid data onto root" + fi TEST_TOOL_LOG="$tool_log" PATH="$helper_bin:$PATH" SLOPTRAP_HELPER_DIR="$helper_dir" \ SLOPTRAP_ACTIVE_CAPABILITIES="apt-install packet-capture" \ @@ -1074,6 +1268,8 @@ run_runtime_context_prompt run_sh_reexec run_resume_omits_runtime_context run_auth_file_mount +run_codex_home_override +run_recursive_slopsloptrap run_project_state_isolation run_auto_login_empty_auth run_codex_symlink_home