Skip to content

Preserve newer trusted base config during agent restoration - #68817

Closed
pelikhan wants to merge 3 commits into
mainfrom
pelikhan-fix-stale-base-config-restoration-race
Closed

pelikhan wants to merge 3 commits into
mainfrom
pelikhan-fix-stale-base-config-restoration-race

Conversation

@pelikhan

Copy link
Copy Markdown
Collaborator

Summary

Fixes #55318.

  • Keep the checked-out configuration when HEAD exactly matches the workflow repository's local default-branch remote-tracking ref, using the existing compiler-provided DEFAULT_BRANCH environment variable.
  • Preserve activation-snapshot restoration for PR/feature heads, missing refs, and workflows without the trusted default-branch input. An ancestry relationship alone never permits bypassing restoration. .mcp.json removal remains unchanged.
  • Add a real-Git regression covering activation at A, concurrent config additions/modifications/deletions at B, and an agent commit whose parent is B and whose only changed path is the intended output file. Cover PR-descendant and missing-input/ref fallback behavior.
  • Document the trusted-checkout exception. No new fetches, credentials, compiler output, generated locks, or transport changes are required.

Validation

  • make build passed.
  • Focused restoration, checkout, dynamic-workflow, and main-job environment Go tests passed.
  • shellcheck --severity=warning actions/setup/sh/restore_base_github_folders.sh passed.
  • Impacted Go tests from make agent-report-progress passed. Its initial workflow-drift failure was eliminated by reusing the existing environment variable rather than changing generated YAML.
  • Final make agent-report-progress-no-test passed with modern Bash and an isolated TMPDIR: Go/custom lint, shell lint, schema freshness, and all 344 workflow locks in sync.
  • make fmt ran Go formatting successfully but hit a pre-existing JavaScript formatter failure (convertToJson is not defined in testdata/copilot_sdk_web_fetch_contract.json); unrelated formatting edits were removed.

This fix does not touch the trusted PR bundle baseline changes in #68717 or validated transport-byte changes in #68732.

pelikhan and others added 3 commits October 14, 2026 13:16
Avoid overwriting a verified default-branch checkout with stale activation config. Retain snapshot restoration for other checkouts and cover concurrent base updates with a real-Git regression.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment on lines +47 to +54
if [ -n "${DEFAULT_BRANCH:-}" ] &&
HEAD_SHA=$(git -C "${WORKSPACE}" rev-parse --verify 'HEAD^{commit}' 2>/dev/null) &&
BASE_SHA=$(git -C "${WORKSPACE}" rev-parse --verify "refs/remotes/origin/${DEFAULT_BRANCH}^{commit}" 2>/dev/null) &&
[ "${HEAD_SHA}" = "${BASE_SHA}" ]; then
echo "Keeping agent config folders from the checked-out trusted base commit ${HEAD_SHA}"
FOLDERS=()
ROOT_FILES=()
fi
@pelikhan
pelikhan marked this pull request as ready for review October 14, 2026 21:29
Copilot AI max review requested due to automatic review settings October 14, 2026 21:29
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ PR Code Quality Reviewer completed the code quality review.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Test Quality Sentinel

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Test Quality Sentinel · sonnet50 · 15.8 AIC · ⌖ 14.8 AIC · ◇ 6.53 AIC · ⊞ 12.0K · ◷

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Ponytail Reviewer

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Ponytail Reviewer · gpt56 · 6.14 AIC · ⌖ 6.16 AIC · ⊞ 14.2K · ◷

Generated by Ponytail Reviewer for #68817

@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ Design Decision Gate 🏗️ completed the design decision gate check. See the comment below for the result and any generated ADR draft.

Design Decision Gate 🏗️

Outcome: reported_no_action

The agent completed and reported no action. A no-op output alone does not verify that all required work was completed or that publication was possible.

Agent messages are omitted from this comment. The category uses structured run status, not interpretation of agent prose.

Generated from Design Decision Gate 🏗️ · opus50 · 15.5 AIC · ⌖ 41 AIC · ◇ 43.2 AIC · ⊞ 1.9K · ◷

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

No additional blocking issues found in the trusted-base restore guard or its regression coverage.

Notes

I checked the exact-commit bypass, the PR-head and missing-ref fallback paths, and the new real-Git test. The change stays narrowly scoped to the one case it is trying to preserve and keeps the previous restoration behavior everywhere else.

🔎 Code quality review by PR Code Quality Reviewer · copilot · gpt54 · 33.1 AIC · ⌖ 5.55 AIC · ⊞ 19.9K
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /diagnosing-bugs — requesting changes due to a confirmed, reproducible security hole in the fix itself.

📋 Key Themes & Highlights

Key Themes

  • Security regression in the new guard (critical): The base-ref lookup uses git rev-parse --verify on a refs/remotes/origin/<branch>^{commit} expression, which does not require an exact ref-path match — it falls back through git's standard ref disambiguation order, including refs/heads/<name>. A fork PR branch literally named refs/remotes/origin/main becomes refs/heads/refs/remotes/origin/main after the agent-job checkout (git checkout -B <headRef> ...), and in setups without a real local refs/remotes/origin/<default> tracking ref (pull_request triggers via actions/checkout's shallow refs/pull/N/merge fetch, or non-default-branch bases), this resolves to the attacker's own commit. Since HEAD_SHA then equals this spoofed BASE_SHA, the script treats the fork PR as trusted and skips config restoration entirely — the exact class of attack (.github/, .claude/, AGENTS.md injection) this PR exists to prevent. I reproduced this locally in a throwaway repo and confirmed the vulnerable match, then confirmed git show-ref --verify --hash on the exact ref path correctly rejects it (no fallback). See inline comment for the concrete fix and a suggested regression test to add to the existing table-driven TestRestoreConfigWhenBaseAdvances in pr_test.go.
  • This matches a finding GitHub Advanced Security already flagged on this PR (comment id 4276470596) that has not yet been addressed in the current diff.

Positive Highlights

  • ✅ Good threat-model framing in the script comment: explicitly calling out that an ancestry relationship alone must never bypass restoration, and that only an exact commit match should.
  • ✅ The new TestRestoreConfigWhenBaseAdvances table test is well structured (real Git repos, four realistic modes: trusted base, PR head, missing base ref, missing default-branch input) and gives good coverage of the intended semantics — it just doesn't yet cover the ref-namespace-confusion case above.
  • ✅ Docs updates in copilot.md and safe-outputs-pull-requests.md clearly explain the new trusted-checkout exception and its boundaries for users.

@copilot please address the review comments above.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 63 AIC · ⌖ 14.9 AIC · ⊞ 11.1K
Comment /matt to run again

echo "Keeping agent config folders from the checked-out trusted base commit ${HEAD_SHA}"
FOLDERS=()
ROOT_FILES=()
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnosing-bugs] 🔴 Critical: git rev-parse --verify "refs/remotes/origin/${DEFAULT_BRANCH}^{commit}" does not do an exact-path ref lookup — it falls back through git's standard ref search order (including refs/heads/<name>). This reopens the exact attack this PR is meant to close.

💡 Exploit and fix

A fork PR can name its branch refs/remotes/origin/main. git checkout -B <headRef> origin/pr-head then creates a local branch literally at refs/heads/refs/remotes/origin/main. When the workspace has no real refs/remotes/origin/<default> (e.g. pull_request runs where actions/checkout only fetched refs/pull/N/merge, or the PR base isn't the default branch), rev-parse resolves BASE_SHA to the attacker's commit, which equals HEAD_SHA — so FOLDERS/ROOT_FILES are emptied and restoration is skipped, leaving attacker-controlled .github/, .claude/, skills, and AGENTS.md in place.

I reproduced this locally: creating a branch named refs/remotes/origin/main and checking it out causes the current script to treat the attacker commit as the trusted base and skip restoration.

Fix: replace the rev-parse call with an exact-path lookup that can't fall back to other namespaces:

BASE_SHA=$(git -C "${WORKSPACE}" show-ref --verify --hash "refs/remotes/origin/${DEFAULT_BRANCH}" 2>/dev/null)

I verified this rejects the attacker-branch scenario (exits non-zero, no match) while still resolving a genuine remote-tracking ref. Please add a regression test for this exact scenario (PR head named refs/remotes/origin/<default-branch>) alongside the existing table-driven cases in pr_test.go.

@copilot please address this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The security-sensitive bypass has unresolved checkout and restoration edge cases that require human review.

4 open findings
What changed in this PR

Adds a trusted-default-branch exception to agent configuration restoration, aiming to prevent stale activation snapshots from causing unintended PR reverts (#55318).

Changes:

  • Preserve checkout configuration when HEAD exactly matches the local default-branch ref.
  • Add real-Git regressions for advancing bases and restoration fallbacks.
  • Document the exception while retaining .mcp.json cleanup.
File Description
pkg/​workflow/​pr_test.go Adds advancing-base and fallback regression coverage.
docs/​src/​content/​docs/​reference/​safe-outputs-pull-requests.md Explains newer-base configuration preservation.
docs/​src/​content/​docs/​engines/​copilot.md Documents the engine-restoration exception.
actions/​setup/​sh/​restore_base_github_folders.sh Adds the exact-default-branch-match bypass.

🧠 Review effort: Max

# with the old snapshot would turn concurrent base changes into agent-authored
# reverts. Only an exact commit match is sufficient; a PR head can also descend
# from the base. Missing refs (including checkout-disabled runs) retain restoration.
if [ -n "${DEFAULT_BRANCH:-}" ] &&
# reverts. Only an exact commit match is sufficient; a PR head can also descend
# from the base. Missing refs (including checkout-disabled runs) retain restoration.
if [ -n "${DEFAULT_BRANCH:-}" ] &&
HEAD_SHA=$(git -C "${WORKSPACE}" rev-parse --verify 'HEAD^{commit}' 2>/dev/null) &&
if [ -n "${DEFAULT_BRANCH:-}" ] &&
HEAD_SHA=$(git -C "${WORKSPACE}" rev-parse --verify 'HEAD^{commit}' 2>/dev/null) &&
BASE_SHA=$(git -C "${WORKSPACE}" rev-parse --verify "refs/remotes/origin/${DEFAULT_BRANCH}^{commit}" 2>/dev/null) &&
[ "${HEAD_SHA}" = "${BASE_SHA}" ]; then
BASE_SHA=$(git -C "${WORKSPACE}" rev-parse --verify "refs/remotes/origin/${DEFAULT_BRANCH}^{commit}" 2>/dev/null) &&
[ "${HEAD_SHA}" = "${BASE_SHA}" ]; then
echo "Keeping agent config folders from the checked-out trusted base commit ${HEAD_SHA}"
FOLDERS=()
@gh-aw-bot

Copy link
Copy Markdown
Collaborator

@copilot address the following outstanding work in one pass:

  1. Update this branch with the latest main using make merge-main, resolving any conflicts and preserving the intended changes.
  2. Review (actions/setup/sh/restore_base_github_folders.sh:54): git rev-parse --verify "refs/remotes/origin/${DEFAULT_BRANCH}^{commit}" does not require the name to resolve as a remote-tracking ref: rev-parse falls back through the standard ref search order, including refs/heads/(name). In the agent job the PR head is checked out with git checkout -B (headRef) origin/pr-head, where headRef is fully attacker-controlled for fork PRs. An attacker can name their fork branch refs/remotes/origin/main, which creates the local branch refs/heads/refs/remotes/origin/main<... - Preserve newer trusted base config during agent restoration #68817 (comment)
  3. Review (actions/setup/sh/restore_base_github_folders.sh:54): [/diagnosing-bugs] 🔴 Critical: git rev-parse --verify "refs/remotes/origin/${DEFAULT_BRANCH}^{commit}" does not do an exact-path ref lookup — it falls back through git's standard ref search order (including refs/heads/<name>). This reopens the exact attack this PR is meant to close. - Preserve newer trusted base config during agent restoration #68817 (comment)
  4. Review (actions/setup/sh/restore_base_github_folders.sh:47): If the default branch is named pr-head, actions/setup/js/checkout_pr_branch.cjs:506–510 force-fetches the PR into refs/remotes/origin/pr-head and checks it out, including for fork PRs. Both lookups then resolve to the untrusted PR commit, so restoration is skipped and PR-controlled instructions, hooks, and extensions remain. Exclude this reserved ref from the shortcut, or move the PR staging ref outside the remote-branch namespace. Add a regression for this branch-name collision; the suggested guard conservatively retains snapshot restoration. - Preserve newer trusted base config during agent restoration #68817 (comment)
  5. Review (actions/setup/sh/restore_base_github_folders.sh:48): git -C "${WORKSPACE}" can discover a parent repository when the workspace itself has no checkout. With checkout: false and dynamic workflows, a self-hosted runner can have such a workspace inside another clone. If that parent's HEAD equals its default-branch tracking ref, this shortcut skips restoration and leaves workspace configuration untouched, contrary to the intended checkout-disabled fallback. Require the workspace to be the repository root before trusting these refs, and add nested-workspace and no-repository regressions. - Preserve newer trusted base config during agent restoration #68817 (comment)
  6. Review (actions/setup/sh/restore_base_github_folders.sh:50): For ambient-folders paths such as .github/agents, matching HEAD does not mean the working tree contains the newer base's configuration. The compiler restores activation content before this script (pkg/workflow/compiler_yaml_runtime_setup.go:304) and again afterward (pkg/workflow/compiler_yaml_ai_execution.go:580), replacing directories without moving HEAD. This check therefore accepts stale files, and the later copy would overwrite newer files anyway. Concurrent base changes can still become agent-authored reverts. Make both ambient restore points honor the trusted-checkout exception, ... - Preserve newer trusted base config during agent restoration #68817 (comment)
  7. Review (actions/setup/sh/restore_base_github_folders.sh:52): With a supported sparse checkout such as ref: main and sparse-checkout: src, HEAD can equal origin/main while .github and .claude are absent. Activation checks out these configuration paths separately, and restoration previously supplied them. Clearing FOLDERS now leaves the agent without trusted extensions, skills, or settings even when the base has not advanced. Materialize the configured paths from the matched commit, respecting files deleted there, before skipping the snapshot, and add a sparse-checkout regression. - Preserve newer trusted base config during agent restoration #68817 (comment)
  8. Fix failing check github-advanced-security (FAILURE): https://github.com/github/gh-aw/actions/runs/38639812149/job/116112159959.

Push the necessary fixes, reply to each listed review thread and resolve it when addressed. Ignore feedback already answered or resolved. Use the pr-finisher skill and stop when only human review or CI remains; do not trigger CI.

Sous-chef head: 127b8396071a35a97d721ea45e6a683e994fb422
Sous-chef work: 100fcc80bf8337fc59ffffd48d4a170ff2c3eb9d843b094ea1b47b69b8db65e8 59a6a583a37592e747bf1abce329f47a320670a98846aa86197ddc789ae2f061 65c877c9b36b3f695d75cfbc0272dde08f374da79cccfea768e1b9ec2f022792 960a6fe297781e0f1e1c8c03fa3740110735e1332e99a040b5aa7b1b6d14a3f2 a6ed89ebb572824a7bc3dcd2cbcdf14b369def9c06b9a8690eaa69acd5b99497 bcf87d164a2ec4742694384254083696e34777fe26e64497e8b68a1b7fe337a7 d105bd71a478aed99a017862e51889c5e164c202a8a692ffa26d4ef9a8a845c6
Sous-chef state: 85c080eae570085f47b01cf53dbed792a3abf996435bd180ea7127bd5e3d3d9d

Generated by 👨‍🍳 PR Sous Chef · pi · haiku45 · 6.02 AIC · ⌖ 6.61 AIC · ⊞ 1.1K · ◷
Comment /souschef to run again

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Base-branch config-folder restore silently reverts changes that race the agent run (trusted triggers)

4 participants