From 21727d9f17dc0ac25207942ff70263689e4bf418 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 00:59:33 +0000 Subject: [PATCH 1/4] Initial plan From d1d394d813750c7232cde42451529c3ee55a85fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:07:19 +0000 Subject: [PATCH 2/4] Harden post-agent cache git commit Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/sh/commit_cache_memory_git.sh | 51 ++++++++++++- .../setup/sh/commit_cache_memory_git_test.sh | 74 +++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 actions/setup/sh/commit_cache_memory_git_test.sh diff --git a/actions/setup/sh/commit_cache_memory_git.sh b/actions/setup/sh/commit_cache_memory_git.sh index 06ba245b014..ba2830f65e5 100644 --- a/actions/setup/sh/commit_cache_memory_git.sh +++ b/actions/setup/sh/commit_cache_memory_git.sh @@ -24,8 +24,57 @@ 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() { + local path + for path in .git .git/config .git/info .git/hooks; do + if [ -L "$path" ]; then + return 0 + fi + done + return 1 +} + +if has_symlinked_git_metadata; then + echo "WARNING: Detected symlinked cache-memory git metadata; reinitializing git metadata" + rm -rf .git + git init -q +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 + +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 +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 +90,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 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..81e0a505c48 --- /dev/null +++ b/actions/setup/sh/commit_cache_memory_git_test.sh @@ -0,0 +1,74 @@ +#!/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" + 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 +} + +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" +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 core.hooksPath "${D}/evil-hooks" +git -C "${D}" config filter.evil.clean "touch ${SENTINEL_FILTER}" +cat > "${D}/evil-hooks/pre-commit" < "${D}/agent-data" +printf 'agent-data filter=evil\n' > "${D}/.gitattributes" +run_script "${D}" >/dev/null +assert "pre-commit hook was not executed" "[ ! -e '${SENTINEL_HOOK}' ]" +assert "clean filter was not executed" "[ ! -e '${SENTINEL_FILTER}' ]" +assert "hooks path hardened" "[ \"\$(git -C '${D}' config --default '' core.hooksPath)\" = '/dev/null' ]" +assert "filter configuration removed" "! git -C '${D}' config --local --name-only --get-regexp '^filter\\.' >/dev/null 2>&1" +assert "agent changes committed" "git -C '${D}' log -1 --format=%s | grep -qx 'run-test-run'" +echo "" + +echo "Tests passed: ${TESTS_PASSED}" +echo "Tests failed: ${TESTS_FAILED}" + +if [ "${TESTS_FAILED}" -gt 0 ]; then + exit 1 +fi + +echo "✓ All tests passed!" From 74861fa899a7679319a913ad5d73b51af00a5a57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 01:37:07 +0000 Subject: [PATCH 3/4] Preserve history for symlinked git metadata Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/sh/commit_cache_memory_git.sh | 15 ++++------- .../setup/sh/commit_cache_memory_git_test.sh | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/actions/setup/sh/commit_cache_memory_git.sh b/actions/setup/sh/commit_cache_memory_git.sh index ba2830f65e5..790ed2aafb9 100644 --- a/actions/setup/sh/commit_cache_memory_git.sh +++ b/actions/setup/sh/commit_cache_memory_git.sh @@ -37,19 +37,12 @@ scrub_git_config_entries() { } has_symlinked_git_metadata() { - local path - for path in .git .git/config .git/info .git/hooks; do - if [ -L "$path" ]; then - return 0 - fi - done - return 1 + [ -L .git ] || [ -n "$(find .git -type l -print -quit 2>/dev/null)" ] } if has_symlinked_git_metadata; then - echo "WARNING: Detected symlinked cache-memory git metadata; reinitializing git metadata" - rm -rf .git - git init -q + echo "Refusing to mutate symlinked cache-memory git metadata" >&2 + exit 1 fi # Agent-written cache state may contain hooks or configuration that executes @@ -64,6 +57,8 @@ 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 diff --git a/actions/setup/sh/commit_cache_memory_git_test.sh b/actions/setup/sh/commit_cache_memory_git_test.sh index 81e0a505c48..65b517173f2 100644 --- a/actions/setup/sh/commit_cache_memory_git_test.sh +++ b/actions/setup/sh/commit_cache_memory_git_test.sh @@ -48,6 +48,8 @@ touch "${D}/initial" git -C "${D}" add initial git -C "${D}" commit -qm initial git -C "${D}" config core.hooksPath "${D}/evil-hooks" +git -C "${D}" config core.worktree "${WORKSPACE}" +git -C "${D}" config core.gitProxy "touch ${WORKSPACE}/proxy-executed" git -C "${D}" config filter.evil.clean "touch ${SENTINEL_FILTER}" cat > "${D}/evil-hooks/pre-commit" </dev/null assert "pre-commit hook was not executed" "[ ! -e '${SENTINEL_HOOK}' ]" assert "clean filter was not executed" "[ ! -e '${SENTINEL_FILTER}' ]" assert "hooks path hardened" "[ \"\$(git -C '${D}' config --default '' core.hooksPath)\" = '/dev/null' ]" +assert "worktree override removed" "! git -C '${D}' config --local --get core.worktree >/dev/null" +assert "git proxy removed" "! git -C '${D}' config --local --get core.gitProxy >/dev/null" assert "filter configuration removed" "! git -C '${D}' config --local --name-only --get-regexp '^filter\\.' >/dev/null 2>&1" assert "agent changes committed" "git -C '${D}' log -1 --format=%s | grep -qx '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" "${SYMLINK_REJECTED}" +assert "symlinked metadata was not replaced" "[ -L '${D}/.git' ]" +assert "existing history was preserved" "[ \"\$(git -C '${D}' rev-parse HEAD)\" = '${INITIAL_COMMIT}' ]" +echo "" + echo "Tests passed: ${TESTS_PASSED}" echo "Tests failed: ${TESTS_FAILED}" From e267d5eb2dbb92a0bfcd325b6008c19ca8b7a811 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Aug 2026 08:56:28 +0000 Subject: [PATCH 4/4] Complete cache commit hardening Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/sh/commit_cache_memory_git.sh | 4 +- .../setup/sh/commit_cache_memory_git_test.sh | 45 ++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/actions/setup/sh/commit_cache_memory_git.sh b/actions/setup/sh/commit_cache_memory_git.sh index 790ed2aafb9..2f4be1bdb0b 100644 --- a/actions/setup/sh/commit_cache_memory_git.sh +++ b/actions/setup/sh/commit_cache_memory_git.sh @@ -52,7 +52,9 @@ if [ -d .git/hooks ]; then 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 @@ -85,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 --no-verify --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 index 65b517173f2..49a57281c9a 100644 --- a/actions/setup/sh/commit_cache_memory_git_test.sh +++ b/actions/setup/sh/commit_cache_memory_git_test.sh @@ -15,8 +15,8 @@ trap cleanup EXIT assert() { local name="$1" - local condition="$2" - if eval "${condition}" 2>/dev/null; then + shift + if "$@" 2>/dev/null; then echo " ✓ ${name}" TESTS_PASSED=$((TESTS_PASSED + 1)) else @@ -33,13 +33,14 @@ echo "Testing commit_cache_memory_git.sh" echo "" echo "Test 1: Script syntax is valid" -assert "script passes bash -n" "bash -n '${SCRIPT}'" +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" @@ -47,25 +48,35 @@ 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 core.hooksPath "${D}/evil-hooks" +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 filter.evil.clean "touch ${SENTINEL_FILTER}" -cat > "${D}/evil-hooks/pre-commit" < "${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 "pre-commit hook was not executed" "[ ! -e '${SENTINEL_HOOK}' ]" -assert "clean filter was not executed" "[ ! -e '${SENTINEL_FILTER}' ]" -assert "hooks path hardened" "[ \"\$(git -C '${D}' config --default '' core.hooksPath)\" = '/dev/null' ]" -assert "worktree override removed" "! git -C '${D}' config --local --get core.worktree >/dev/null" -assert "git proxy removed" "! git -C '${D}' config --local --get core.gitProxy >/dev/null" -assert "filter configuration removed" "! git -C '${D}' config --local --name-only --get-regexp '^filter\\.' >/dev/null 2>&1" -assert "agent changes committed" "git -C '${D}' log -1 --format=%s | grep -qx 'run-test-run'" +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" @@ -86,9 +97,9 @@ if run_script "${D}" >/dev/null; then else SYMLINK_REJECTED=true fi -assert "symlinked metadata was rejected" "${SYMLINK_REJECTED}" -assert "symlinked metadata was not replaced" "[ -L '${D}/.git' ]" -assert "existing history was preserved" "[ \"\$(git -C '${D}' rev-parse HEAD)\" = '${INITIAL_COMMIT}' ]" +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}"