diff --git a/actions/setup/sh/commit_cache_memory_git.sh b/actions/setup/sh/commit_cache_memory_git.sh index 06ba245b014..2f4be1bdb0b 100644 --- a/actions/setup/sh/commit_cache_memory_git.sh +++ b/actions/setup/sh/commit_cache_memory_git.sh @@ -24,8 +24,54 @@ fi cd "$CACHE_DIR" +scrub_git_config_entries() { + local key_prefix="$1" + while IFS= read -r key_name; do + [ -n "$key_name" ] || continue + git config --unset-all "$key_name" >/dev/null 2>&1 || true + done < <( + git config --local --name-only --list 2>/dev/null \ + | grep -E -i "^${key_prefix}\\." \ + | sort -u + ) +} + +has_symlinked_git_metadata() { + [ -L .git ] || [ -n "$(find .git -type l -print -quit 2>/dev/null)" ] +} + +if has_symlinked_git_metadata; then + echo "Refusing to mutate symlinked cache-memory git metadata" >&2 + exit 1 +fi + +# Agent-written cache state may contain hooks or configuration that executes +# during staging or committing. Clear those command surfaces before either step. +if [ -d .git/hooks ]; then + find .git/hooks -mindepth 1 -maxdepth 1 \( -type f -o -type l \) ! -name '*.sample' -delete +fi +mkdir -p .git/info +rm -f .git/info/exclude .git/info/attributes .git/info/grafts .git/info/sparse-checkout +rm -f .git/config.worktree + +git config --unset-all extensions.worktreeConfig >/dev/null 2>&1 || true +git config --unset-all core.attributesFile >/dev/null 2>&1 || true +git config --unset-all core.fsmonitor >/dev/null 2>&1 || true +git config --unset-all core.sshCommand >/dev/null 2>&1 || true +git config --unset-all core.hooksPath >/dev/null 2>&1 || true +git config --unset-all core.worktree >/dev/null 2>&1 || true +git config --unset-all core.gitProxy >/dev/null 2>&1 || true +scrub_git_config_entries include +scrub_git_config_entries includeif +scrub_git_config_entries credential +scrub_git_config_entries alias +scrub_git_config_entries filter +scrub_git_config_entries merge + git config user.email "gh-aw@github.com" git config user.name "gh-aw" +git config core.hooksPath /dev/null +git config core.fsmonitor false # --- Log cache directory contents before commit --- echo "=== Cache directory: non-git files being committed ===" @@ -41,7 +87,7 @@ git add -A # Commit on the current integrity branch; allow empty commits in case # the agent made no changes (idempotent). -if git commit --allow-empty -m "run-${RUN_ID}" -q 2>/tmp/gh-aw-commit-err; then +if git -c commit.gpgSign=false commit --no-verify --allow-empty -m "run-${RUN_ID}" -q 2>/tmp/gh-aw-commit-err; then echo "Cache memory git commit complete (run: $RUN_ID)" else # Distinguish "nothing to commit" (benign) from real errors diff --git a/actions/setup/sh/commit_cache_memory_git_test.sh b/actions/setup/sh/commit_cache_memory_git_test.sh new file mode 100644 index 00000000000..49a57281c9a --- /dev/null +++ b/actions/setup/sh/commit_cache_memory_git_test.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="${SCRIPT_DIR}/commit_cache_memory_git.sh" + +TESTS_PASSED=0 +TESTS_FAILED=0 +WORKSPACE="$(mktemp -d)" + +cleanup() { + rm -rf "${WORKSPACE}" +} +trap cleanup EXIT + +assert() { + local name="$1" + shift + if "$@" 2>/dev/null; then + echo " ✓ ${name}" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + echo " ✗ ${name}" + TESTS_FAILED=$((TESTS_FAILED + 1)) + fi +} + +run_script() { + GH_AW_CACHE_DIR="$1" GITHUB_RUN_ID="test-run" bash "${SCRIPT}" 2>&1 +} + +echo "Testing commit_cache_memory_git.sh" +echo "" + +echo "Test 1: Script syntax is valid" +assert "script passes bash -n" bash -n "${SCRIPT}" +echo "" + +echo "Test 2: Agent-controlled hooks and filters cannot execute" +D="${WORKSPACE}/test2" +SENTINEL_HOOK="${WORKSPACE}/hook-executed" +SENTINEL_FILTER="${WORKSPACE}/filter-executed" +SENTINEL_SIGNING="${WORKSPACE}/signing-executed" +mkdir -p "${D}/evil-hooks" +git -C "${D}" init -q +git -C "${D}" config user.email "test@example.com" +git -C "${D}" config user.name "Test" +touch "${D}/initial" +git -C "${D}" add initial +git -C "${D}" commit -qm initial +git -C "${D}" config extensions.worktreeConfig true +git -C "${D}" config --worktree core.hooksPath "${D}/evil-hooks" +git -C "${D}" config --worktree filter.evil.clean "touch ${SENTINEL_FILTER}" +git -C "${D}" config core.worktree "${WORKSPACE}" +git -C "${D}" config core.gitProxy "touch ${WORKSPACE}/proxy-executed" +git -C "${D}" config commit.gpgSign true +git -C "${D}" config gpg.program "${D}/evil-gpg" +cat > "${D}/evil-hooks/post-commit" < "${D}/evil-gpg" < "${D}/agent-data" +printf 'agent-data filter=evil\n' > "${D}/.gitattributes" +run_script "${D}" >/dev/null +assert "post-commit hook was not executed" test ! -e "${SENTINEL_HOOK}" +assert "clean filter was not executed" test ! -e "${SENTINEL_FILTER}" +assert "signing program was not executed" test ! -e "${SENTINEL_SIGNING}" +assert "hooks path hardened" test "$(git -C "${D}" config --default '' core.hooksPath)" = "/dev/null" +assert "worktree override removed" test -z "$(git -C "${D}" config --local --get core.worktree || true)" +assert "git proxy removed" test -z "$(git -C "${D}" config --local --get core.gitProxy || true)" +assert "worktree config removed" test ! -e "${D}/.git/config.worktree" +assert "worktree config extension removed" test -z "$(git -C "${D}" config --local --get extensions.worktreeConfig || true)" +assert "agent changes committed" test "$(git -C "${D}" log -1 --format=%s)" = "run-test-run" +echo "" + +echo "Test 3: Symlinked git metadata is rejected without losing history" +D="${WORKSPACE}/test3" +REAL_GIT="${WORKSPACE}/test3-git" +mkdir -p "${D}" +git -C "${D}" init -q +git -C "${D}" config user.email "test@example.com" +git -C "${D}" config user.name "Test" +touch "${D}/initial" +git -C "${D}" add initial +git -C "${D}" commit -qm initial +INITIAL_COMMIT="$(git -C "${D}" rev-parse HEAD)" +mv "${D}/.git" "${REAL_GIT}" +ln -s "${REAL_GIT}" "${D}/.git" +if run_script "${D}" >/dev/null; then + SYMLINK_REJECTED=false +else + SYMLINK_REJECTED=true +fi +assert "symlinked metadata was rejected" test "${SYMLINK_REJECTED}" = true +assert "symlinked metadata was not replaced" test -L "${D}/.git" +assert "existing history was preserved" test "$(git -C "${D}" rev-parse HEAD)" = "${INITIAL_COMMIT}" +echo "" + +echo "Tests passed: ${TESTS_PASSED}" +echo "Tests failed: ${TESTS_FAILED}" + +if [ "${TESTS_FAILED}" -gt 0 ]; then + exit 1 +fi + +echo "✓ All tests passed!"