diff --git a/.changeset/patch-reclaim-sandbox-rootless-eacces.md b/.changeset/patch-reclaim-sandbox-rootless-eacces.md new file mode 100644 index 00000000000..960913d147a --- /dev/null +++ b/.changeset/patch-reclaim-sandbox-rootless-eacces.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Reclaim root-owned `/tmp/gh-aw/sandbox` before AWF `writeConfigs()` to prevent `EACCES: mkdir /tmp/gh-aw/sandbox/firewall/logs` on runners with rootless-container residue. diff --git a/actions/setup/sh/create_gh_aw_tmp_dir.sh b/actions/setup/sh/create_gh_aw_tmp_dir.sh index bd7c0a92aeb..1b96db8fdfc 100755 --- a/actions/setup/sh/create_gh_aw_tmp_dir.sh +++ b/actions/setup/sh/create_gh_aw_tmp_dir.sh @@ -1,5 +1,24 @@ #!/usr/bin/env bash set +o histexpand + +# Reclaim /tmp/gh-aw/sandbox if it is not writable by the current user (e.g. root-owned, left by +# a prior rootless container run on the same runner). A root-owned sandbox causes AWF's +# writeConfigs() to fail with EACCES when it tries to mkdir /tmp/gh-aw/sandbox/firewall/logs — +# killing the run before the agent is ever invoked. The chmod-based fallback in AWF also fails +# Permission denied for the same reason, so the only reliable fix is to remove and recreate the +# tree here, before AWF starts. +sandbox_dir="/tmp/gh-aw/sandbox" +if [ -d "${sandbox_dir}" ] && ! [ -w "${sandbox_dir}" ]; then + echo "[WARN] ${sandbox_dir} is not writable by the current user (uid $(id -u)); reclaiming before AWF starts..." + if sudo rm -rf "${sandbox_dir}" 2>/dev/null; then + echo "Removed stale non-writable ${sandbox_dir} via sudo" + elif rm -rf "${sandbox_dir}" 2>/dev/null; then + echo "Removed stale ${sandbox_dir}" + else + echo "[WARN] Failed to remove ${sandbox_dir}; AWF writeConfigs() may fail with EACCES" >&2 + fi +fi + mkdir -p /tmp/gh-aw/agent mkdir -p /tmp/gh-aw/sandbox/agent/logs echo "Created /tmp/gh-aw/agent directory for agentic workflow temporary files" diff --git a/actions/setup/sh/create_gh_aw_tmp_dir_test.sh b/actions/setup/sh/create_gh_aw_tmp_dir_test.sh new file mode 100644 index 00000000000..b4d2fb3fd0d --- /dev/null +++ b/actions/setup/sh/create_gh_aw_tmp_dir_test.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set +o histexpand + +# Test script for create_gh_aw_tmp_dir.sh +# Run: bash create_gh_aw_tmp_dir_test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="${SCRIPT_DIR}/create_gh_aw_tmp_dir.sh" + +TESTS_PASSED=0 +TESTS_FAILED=0 + +assert() { + local name="$1" + local condition="$2" + if eval "${condition}" 2>/dev/null; then + echo " ✓ ${name}" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo " ✗ ${name}" + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi +} + +echo "Testing create_gh_aw_tmp_dir.sh" +echo "" + +# ── Test 1: Script syntax is valid ────────────────────────────────────────── +echo "Test 1: Script syntax is valid" +assert "script passes bash -n" "bash -n '${SCRIPT}'" +echo "" + +# ── Test 2: Creates expected directories when they don't exist ─────────────── +echo "Test 2: Creates expected directories from scratch" +set +e +OUTPUT="$(bash "${SCRIPT}" 2>&1)" +EXIT_CODE=$? +set -e +assert "script exits 0" "[ '${EXIT_CODE}' = '0' ]" +assert "/tmp/gh-aw/agent directory created" "[ -d /tmp/gh-aw/agent ]" +assert "/tmp/gh-aw/sandbox/agent/logs directory created" "[ -d /tmp/gh-aw/sandbox/agent/logs ]" +assert "output mentions created directory" "echo '${OUTPUT}' | grep -q 'Created /tmp/gh-aw/agent directory'" +echo "" + +# ── Test 3: Preserves sandbox owned by current user ────────────────────────── +echo "Test 3: Preserves sandbox directory when owned by current user" +mkdir -p /tmp/gh-aw/sandbox +MARKER_FILE="/tmp/gh-aw/sandbox/.owner-check-marker" +touch "${MARKER_FILE}" +set +e +OUTPUT="$(bash "${SCRIPT}" 2>&1)" +EXIT_CODE=$? +set -e +assert "script exits 0 with existing user-owned sandbox" "[ '${EXIT_CODE}' = '0' ]" +assert "/tmp/gh-aw/sandbox/agent/logs created" "[ -d /tmp/gh-aw/sandbox/agent/logs ]" +# Marker should still be present — sandbox was NOT removed since we own it and it is writable +assert "user-owned sandbox is preserved (marker still present)" "[ -f '${MARKER_FILE}' ]" +assert "no WARN about reclaiming in output" "! echo '${OUTPUT}' | grep -q 'reclaiming'" +rm -f "${MARKER_FILE}" +echo "" + +# ── Test 4: Reclaims sandbox that is not writable by the current user ───────── +# We simulate a non-writable sandbox by creating a read-only directory. A fake sudo records +# its arguments and performs the actual removal (rm -rf works here because the *parent* +# /tmp/gh-aw is writable by the current user even when the child has mode 555). +echo "Test 4: Reclaims sandbox that is not writable (simulated)" +mkdir -p /tmp/gh-aw/sandbox +MARKER_FILE="/tmp/gh-aw/sandbox/.non-writable-marker" +touch "${MARKER_FILE}" +# Make the sandbox not writable by the current user +chmod 555 /tmp/gh-aw/sandbox + +FAKE_BIN="$(mktemp -d)" +SUDO_ARGS_FILE="${FAKE_BIN}/sudo_args" + +# Fake sudo: record the full argument list, then execute the real command. +# Before running rm -rf, fix permissions on mode-555 directories so the +# removal succeeds (mimicking what root privilege provides in production). +cat > "${FAKE_BIN}/sudo" <> "${SUDO_ARGS_FILE}" +# Fix permissions on any non-writable subdirectories before removal +# (root bypasses DAC checks; simulate that by chmod-ing first) +for arg in "\$@"; do + if [ -d "\${arg}" ]; then + find "\${arg}" -type d -not -perm -u+w -exec chmod u+w {} + 2>/dev/null || true + fi +done +exec "\$@" +EOF +chmod +x "${FAKE_BIN}/sudo" + +set +e +OUTPUT="$(PATH="${FAKE_BIN}:${PATH}" bash "${SCRIPT}" 2>&1)" +EXIT_CODE=$? +set -e + +assert "script exits 0 with non-writable sandbox" "[ '${EXIT_CODE}' = '0' ]" +assert "sudo was invoked" "[ -f '${SUDO_ARGS_FILE}' ]" +assert "sudo was called with rm -rf and the sandbox path" "grep -q 'rm -rf.*sandbox' '${SUDO_ARGS_FILE}'" +assert "WARN about reclaiming appears in output" "echo '${OUTPUT}' | grep -q 'reclaiming'" +assert "/tmp/gh-aw/sandbox/agent/logs recreated after removal" "[ -d /tmp/gh-aw/sandbox/agent/logs ]" +assert "non-writable sandbox was removed (marker gone)" "[ ! -f '${MARKER_FILE}' ]" + +rm -rf "${FAKE_BIN}" +echo "" + +# ── Summary ────────────────────────────────────────────────────────────────── +echo "Results: ${TESTS_PASSED} passed, ${TESTS_FAILED} failed" +if [ "${TESTS_FAILED}" -gt 0 ]; then + exit 1 +fi +