-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(autofix): pass CI=true through the gate's env -i launches #9649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8db672e
0fcdd5a
4d87d79
0fbc379
cde480e
3b52b95
b3cb25c
001d1cc
ae63e71
6aaf14f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,3 @@ | ||
| name: 'Qwen Autofix' | ||
|
|
||
| # One workflow for the whole autonomous-fix lifecycle: | ||
|
|
@@ -3860,6 +3860,12 @@ | |
| echo "${_upsert_delim}" | ||
| } >> "${GITHUB_OUTPUT}" | ||
| echo "trusted_path=${PATH}" >> "${GITHUB_OUTPUT}" | ||
| # HOME is captured on the same doctrine: the verification gates | ||
| # pin it at step level, so a $GITHUB_ENV plant after staging | ||
| # cannot reach npm's userconfig inside the gate child — a planted | ||
| # HOME's .npmrc script-shell wraps every verdict-determining | ||
| # `npm run` in an attacker shell (R8-3). | ||
| echo "trusted_home=${HOME}" >> "${GITHUB_OUTPUT}" | ||
| # The agent step runs AFTER prepare checks out the PR branch, so | ||
| # invoking the runner from the working tree would execute | ||
| # branch-controlled code on the host with the model key in env | ||
|
|
@@ -5189,8 +5195,38 @@ | |
| # clean child, so its bash inherits nothing at all. | ||
| BASH_ENV: '' | ||
| SHELLOPTS: '' | ||
| # LD_* are likewise mapped by ld.so at process STARTUP, before the | ||
| # body's unset can run: the unset clears them for children but | ||
| # cannot unload a library already mapped into THIS step's bash, | ||
| # whose execve hooks would forge the pre-launch digest check | ||
| # below; ld.so ignores empty values (R6-2). | ||
| LD_PRELOAD: '' | ||
| LD_AUDIT: '' | ||
| LD_LIBRARY_PATH: '' | ||
| TRUSTED_PATH: '${{ steps.stage.outputs.trusted_path }}' | ||
| VERIFY_RUNNER_SHA256: '${{ steps.stage.outputs.verify_runner_sha256 }}' | ||
| # RUNNER_TEMP, WORKDIR, and BRANCH re-enter the digest check and | ||
| # the allowlisted child below from the step environment: a | ||
| # $GITHUB_ENV plant points the digest oracle at a decoy runner | ||
| # (the hash is expression-context, the checked path is not) and | ||
| # swaps the tree the gate builds/tests. Pin them from trusted | ||
| # expression context, the TRUSTED_PATH doctrine above (R6-3). | ||
| RUNNER_TEMP: '${{ runner.temp }}' | ||
| WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}' | ||
| BRANCH: '${{ matrix.target.branch }}' | ||
| # CI re-enters the allowlisted child through the step | ||
| # environment: the child's `:-true` default only covers an UNSET | ||
| # CI, so a $GITHUB_ENV plant of CI=false survives the expansion | ||
| # and inverts the gate's CI semantics. Pin it at step level, the | ||
| # FOOTPRINT_ENFORCE doctrine below (R1-1). | ||
| CI: 'true' | ||
| # HOME re-enters the allowlisted child below from the step | ||
| # environment: a $GITHUB_ENV-planted HOME points npm's userconfig | ||
| # at a .npmrc whose script-shell wraps every verdict-determining | ||
| # `npm run` in an attacker shell — a red branch reports green. | ||
| # Pin it from the stage-time capture, the TRUSTED_PATH doctrine | ||
| # above (R8-3). | ||
| HOME: '${{ steps.stage.outputs.trusted_home }}' | ||
| # Step-level env outranks $GITHUB_ENV: an earlier shell-capable | ||
| # step (the agent runs branch code on the host) must not be able | ||
| # to downgrade a repo-variable 'reject' back to 'advisory'. | ||
|
|
@@ -5201,13 +5237,22 @@ | |
| run: |- | ||
| # The gate decides whether the PAT push runs, and the first pass | ||
| # executes the branch's own build/test on the host before the | ||
| # second — so pin PATH to the staged trusted value, drop the | ||
| # preload channels, and verify the staged runner's digest (recorded | ||
| # in GITHUB_OUTPUT, unreachable from a disk write) before executing, | ||
| # second — so verify the staged runner's digest (recorded in | ||
| # GITHUB_OUTPUT, unreachable from a disk write) before executing, | ||
| # or a mid-run overwrite lets the branch define its own verdict. | ||
| export PATH="${TRUSTED_PATH}" | ||
| unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH | ||
| echo "${VERIFY_RUNNER_SHA256} ${RUNNER_TEMP}/run-autofix-review-verification.sh" | sha256sum -c - > /dev/null | ||
| # Every command word below is called by absolute path: bare names | ||
| # — even builtins like echo, export, or unset — are shadowed by | ||
| # $GITHUB_ENV-planted BASH_FUNC_<name>%% functions, imported at | ||
| # bash STARTUP even under --norc, ahead of builtins and PATH | ||
| # (R6-4; a shadowed echo prints any digest line, blinding the | ||
| # check to a mid-run overwrite of the staged runner, and a | ||
| # shadowed export/unset arms a DEBUG trap that swaps the staged | ||
| # runner AFTER the digest passes and BEFORE the launch executes | ||
| # it, R8-1). The body therefore carries no in-shell pin of its | ||
| # own: PATH reaches the child through the allowlist below, and | ||
| # the preload channels are closed by the step-level LD_* pins | ||
| # above, the env execve prefix, and env -i. | ||
| /usr/bin/echo "${VERIFY_RUNNER_SHA256} ${RUNNER_TEMP}/run-autofix-review-verification.sh" | /usr/bin/sha256sum -c - > /dev/null | ||
| # Launch the gate through the workflow's env -i clean-child | ||
| # pattern: the step environment inherits every $GITHUB_ENV plant | ||
| # earlier steps left (verdict-variable plants, BITE_RUNNER | ||
|
|
@@ -5222,6 +5267,7 @@ | |
| WORKDIR="${WORKDIR}" \ | ||
| BRANCH="${BRANCH}" \ | ||
| GITHUB_OUTPUT="${GITHUB_OUTPUT}" \ | ||
|
Comment on lines
5267
to
5269
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] The allowlist this PR's new pin canonizes as the SANCTIONED chain re-injects path/state variables ( Failure: branch code writes Witness: Fix: pin the path variables from trusted expression context at step level, mirroring RUNNER_TEMP: '${{ runner.temp }}'
WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}'and extend the new pins to assert the step-env entries. 中文说明[Critical] 本 PR 新 pin 宣告为 SANCTIONED 链的允许列表,把路径/状态变量( 失败路径:分支代码向 见证:未在真实 GitHub runner 上运行——合并前提依据 actions/runner 实现确认(JobExtension.cs InitializeJob 先播下 runner 默认值再以后写覆盖合并 workflow/job env 到 Global.EnvironmentVariables;FileCommandManager.cs 在两者之后应用 $GITHUB_ENV,屏蔽名单仅 {NODE_OPTIONS};ExecutionContext.SetEnvContext 仅检查 NotNullOrEmpty(name));给定该前提,本 workflow 内的取值源追踪是确定性的。 修复:仿照 — qwen3.8-max via Qwen Code /review (v0.21.15) |
||
| CI="${CI:-true}" \ | ||
|
doudouOUC marked this conversation as resolved.
|
||
| KISS_AUDIT="${KISS_AUDIT:-false}" \ | ||
| FOOTPRINT_ENFORCE="${FOOTPRINT_ENFORCE:-advisory}" \ | ||
| bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh" | ||
|
|
@@ -5396,8 +5442,38 @@ | |
| # clean child, so its bash inherits nothing at all. | ||
| BASH_ENV: '' | ||
| SHELLOPTS: '' | ||
| # LD_* are likewise mapped by ld.so at process STARTUP, before the | ||
| # body's unset can run: the unset clears them for children but | ||
| # cannot unload a library already mapped into THIS step's bash, | ||
| # whose execve hooks would forge the pre-launch digest check | ||
| # below; ld.so ignores empty values (R6-2). | ||
| LD_PRELOAD: '' | ||
| LD_AUDIT: '' | ||
| LD_LIBRARY_PATH: '' | ||
| TRUSTED_PATH: '${{ steps.stage.outputs.trusted_path }}' | ||
| VERIFY_RUNNER_SHA256: '${{ steps.stage.outputs.verify_runner_sha256 }}' | ||
| # RUNNER_TEMP, WORKDIR, and BRANCH re-enter the digest check and | ||
| # the allowlisted child below from the step environment: a | ||
| # $GITHUB_ENV plant points the digest oracle at a decoy runner | ||
| # (the hash is expression-context, the checked path is not) and | ||
| # swaps the tree the gate builds/tests. Pin them from trusted | ||
| # expression context, the TRUSTED_PATH doctrine above (R6-3). | ||
| RUNNER_TEMP: '${{ runner.temp }}' | ||
| WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}' | ||
| BRANCH: '${{ matrix.target.branch }}' | ||
| # CI re-enters the allowlisted child through the step | ||
| # environment: the child's `:-true` default only covers an UNSET | ||
| # CI, so a $GITHUB_ENV plant of CI=false survives the expansion | ||
| # and inverts the gate's CI semantics. Pin it at step level, the | ||
| # FOOTPRINT_ENFORCE doctrine below (R1-1). | ||
| CI: 'true' | ||
| # HOME re-enters the allowlisted child below from the step | ||
| # environment: a $GITHUB_ENV-planted HOME points npm's userconfig | ||
| # at a .npmrc whose script-shell wraps every verdict-determining | ||
| # `npm run` in an attacker shell — a red branch reports green. | ||
| # Pin it from the stage-time capture, the TRUSTED_PATH doctrine | ||
| # above (R8-3). | ||
| HOME: '${{ steps.stage.outputs.trusted_home }}' | ||
| # Step-level env outranks $GITHUB_ENV: an earlier shell-capable | ||
| # step (the agent runs branch code on the host) must not be able | ||
| # to downgrade a repo-variable 'reject' back to 'advisory'. | ||
|
|
@@ -5410,13 +5486,22 @@ | |
| run: |- | ||
| # The gate decides whether the PAT push runs, and the first pass | ||
| # executes the branch's own build/test on the host before the | ||
| # second — so pin PATH to the staged trusted value, drop the | ||
| # preload channels, and verify the staged runner's digest (recorded | ||
| # in GITHUB_OUTPUT, unreachable from a disk write) before executing, | ||
| # second — so verify the staged runner's digest (recorded in | ||
| # GITHUB_OUTPUT, unreachable from a disk write) before executing, | ||
| # or a mid-run overwrite lets the branch define its own verdict. | ||
| export PATH="${TRUSTED_PATH}" | ||
| unset LD_PRELOAD LD_AUDIT LD_LIBRARY_PATH | ||
| echo "${VERIFY_RUNNER_SHA256} ${RUNNER_TEMP}/run-autofix-review-verification.sh" | sha256sum -c - > /dev/null | ||
| # Every command word below is called by absolute path: bare names | ||
| # — even builtins like echo, export, or unset — are shadowed by | ||
| # $GITHUB_ENV-planted BASH_FUNC_<name>%% functions, imported at | ||
| # bash STARTUP even under --norc, ahead of builtins and PATH | ||
| # (R6-4; a shadowed echo prints any digest line, blinding the | ||
| # check to a mid-run overwrite of the staged runner, and a | ||
| # shadowed export/unset arms a DEBUG trap that swaps the staged | ||
| # runner AFTER the digest passes and BEFORE the launch executes | ||
| # it, R8-1). The body therefore carries no in-shell pin of its | ||
| # own: PATH reaches the child through the allowlist below, and | ||
| # the preload channels are closed by the step-level LD_* pins | ||
| # above, the env execve prefix, and env -i. | ||
| /usr/bin/echo "${VERIFY_RUNNER_SHA256} ${RUNNER_TEMP}/run-autofix-review-verification.sh" | /usr/bin/sha256sum -c - > /dev/null | ||
| # Launch the gate through the workflow's env -i clean-child | ||
| # pattern: the step environment inherits every $GITHUB_ENV plant | ||
| # earlier steps left (verdict-variable plants, BITE_RUNNER | ||
|
|
@@ -5431,6 +5516,7 @@ | |
| WORKDIR="${WORKDIR}" \ | ||
| BRANCH="${BRANCH}" \ | ||
| GITHUB_OUTPUT="${GITHUB_OUTPUT}" \ | ||
| CI="${CI:-true}" \ | ||
| KISS_AUDIT="${KISS_AUDIT:-false}" \ | ||
| FOOTPRINT_ENFORCE="${FOOTPRINT_ENFORCE:-advisory}" \ | ||
| bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh" | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10544,7 +10544,7 @@ exit 1 | |||||||||||||||
| // runs its own build/test between them), with PATH pinned first. | ||||||||||||||||
| expect( | ||||||||||||||||
| workflow.match( | ||||||||||||||||
| /echo "\$\{VERIFY_RUNNER_SHA256\} {2}\$\{RUNNER_TEMP\}\/run-autofix-review-verification\.sh" \| sha256sum -c - > \/dev\/null/g, | ||||||||||||||||
| /\/usr\/bin\/echo "\$\{VERIFY_RUNNER_SHA256\} {2}\$\{RUNNER_TEMP\}\/run-autofix-review-verification\.sh" \| \/usr\/bin\/sha256sum -c - > \/dev\/null/g, | ||||||||||||||||
| ) ?? [], | ||||||||||||||||
| ).toHaveLength(2); | ||||||||||||||||
| expect( | ||||||||||||||||
|
|
@@ -11201,6 +11201,104 @@ exit 1 | |||||||||||||||
| expect(reviewVerificationGateStep).not.toContain( | ||||||||||||||||
| 'bash .github/scripts/run-autofix-review-verification.sh', | ||||||||||||||||
| ); | ||||||||||||||||
| // The gate launches through an env -i clean child with a SANCTIONED | ||||||||||||||||
| // allowlist (R5-1): every variable the gate's own build/test checks need | ||||||||||||||||
| // must be passed, and the step-pinned CI=true is one of them — without | ||||||||||||||||
| // it the gate's checks run with inverted CI semantics and the 18 | ||||||||||||||||
| // deliberately-skipped TUI-input tests un-skip inside the gate (one flakes | ||||||||||||||||
| // ~5s, reject_fix fires retryable on a fix the PR's own CI passes green). | ||||||||||||||||
| // Pin the launch STRUCTURALLY — one verbatim adjacency chain from the | ||||||||||||||||
| // LD_* prefix through the digest-verified script, every entry in order | ||||||||||||||||
| // with its exact value — not as text tokens: shell edits that preserve | ||||||||||||||||
| // token text (a commented-out entry, a dropped `\`, an =-less operand, a | ||||||||||||||||
| // quote suffix, an entry smuggled behind a `bash --norc` value, the | ||||||||||||||||
| // launch head moved into a comment) each broke the child's isolation | ||||||||||||||||
| // while every token-level pin stayed green (R2-1). Anchoring the chain | ||||||||||||||||
| // on the LD_* prefix pins the one channel env -i cannot block; the | ||||||||||||||||
| // body-side unset and PATH export are pinned with it (R3-1, R2-2). By | ||||||||||||||||
| // themselves they do NOT protect the pre-launch digest check, which | ||||||||||||||||
| // runs in this step's own bash: startup-time channels — an LD_* | ||||||||||||||||
| // library mapped before line 1, a BASH_FUNC function import, a | ||||||||||||||||
| // path-variable redirection of the checked file — are closed by the | ||||||||||||||||
| // step-level env pins and the absolute digest path below (R6-2, R6-3, | ||||||||||||||||
| // R6-4). The shapes AROUND the chain | ||||||||||||||||
| // are closed by pinning the run body's whole statement list with | ||||||||||||||||
| // comments stripped: a prefix command word that demotes the whole chain | ||||||||||||||||
| // to one command's argv (the gate never executes and a forged outcome | ||||||||||||||||
| // survives), a command appended or inserted around the launch, a | ||||||||||||||||
| // demotion of the pinned block into a never-run arm, a commented-out or | ||||||||||||||||
| // relocated statement — each adds, drops, reorders, or renames a | ||||||||||||||||
| // statement here (R4-2, R4-3, R4-4). Within the chain, separators allow | ||||||||||||||||
| // only bash whitespace — space/tab after the continuation newline: JS | ||||||||||||||||
| // `\s` also matches a blank line, which splits the chain into two | ||||||||||||||||
| // commands (the orphaned env -i prints and exits 0 while the rest runs | ||||||||||||||||
| // with the FULL step environment), and NBSP/U+2028, which glue into the | ||||||||||||||||
| // next operand and rename it. Blank lines filter out of the statement | ||||||||||||||||
| // list too, so only this pin closes the blank-line split; lines that | ||||||||||||||||
| // carry NBSP/U+2028 instead fail the statement list's exact match, | ||||||||||||||||
| // whose ASCII-only strip keeps them visible (R4-1, R6-1). | ||||||||||||||||
| const gateLaunchTokens = [ | ||||||||||||||||
| 'LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH=', | ||||||||||||||||
| '/usr/bin/env -i', | ||||||||||||||||
| 'PATH="${TRUSTED_PATH}"', | ||||||||||||||||
| 'HOME="${HOME}"', | ||||||||||||||||
| 'RUNNER_TEMP="${RUNNER_TEMP}"', | ||||||||||||||||
| 'WORKDIR="${WORKDIR}"', | ||||||||||||||||
| 'BRANCH="${BRANCH}"', | ||||||||||||||||
| 'GITHUB_OUTPUT="${GITHUB_OUTPUT}"', | ||||||||||||||||
| 'CI="${CI:-true}"', | ||||||||||||||||
| 'KISS_AUDIT="${KISS_AUDIT:-false}"', | ||||||||||||||||
| 'FOOTPRINT_ENFORCE="${FOOTPRINT_ENFORCE:-advisory}"', | ||||||||||||||||
| 'bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh"', | ||||||||||||||||
| ]; | ||||||||||||||||
| const gateLaunchPin = new RegExp( | ||||||||||||||||
| gateLaunchTokens | ||||||||||||||||
| .map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) | ||||||||||||||||
| .join(' \\\\\n[ \\t]*'), | ||||||||||||||||
| ); | ||||||||||||||||
| // The digest check executes in the PARENT shell before the clean child | ||||||||||||||||
| // exists. Its binaries are absolute paths and its inputs step-level | ||||||||||||||||
| // pins, because ANY bare command word in the body — echo, export, or | ||||||||||||||||
| // unset alike — is shadowed by $GITHUB_ENV-planted BASH_FUNC functions | ||||||||||||||||
| // imported at bash startup (R6-4), and a shadowed in-shell pin arms a | ||||||||||||||||
| // DEBUG trap that swaps the staged runner AFTER the digest passes and | ||||||||||||||||
| // BEFORE the launch executes it (R8-1): the body carries no pin | ||||||||||||||||
| // statement of its own, so the pinned list is exactly the digest line | ||||||||||||||||
| // and the launch. The digest line is pinned whole and per step — a | ||||||||||||||||
| // workflow-wide count accepts relocation out of the gates, and | ||||||||||||||||
| // `|| true` accepts a digest mismatch under bash -e (R4-3, the | ||||||||||||||||
| // resanitize sibling's doctrine). | ||||||||||||||||
| const gateDigestCheck = | ||||||||||||||||
| '/usr/bin/echo "${VERIFY_RUNNER_SHA256} ${RUNNER_TEMP}/run-autofix-review-verification.sh" | /usr/bin/sha256sum -c - > /dev/null'; | ||||||||||||||||
| const gateBodyStatements = [ | ||||||||||||||||
| gateDigestCheck, | ||||||||||||||||
| ...gateLaunchTokens.map((token, index) => | ||||||||||||||||
| index < gateLaunchTokens.length - 1 ? `${token} \\` : token, | ||||||||||||||||
| ), | ||||||||||||||||
| ]; | ||||||||||||||||
| // Bash breaks words only on ASCII space/tab/newline: strip ASCII | ||||||||||||||||
| // whitespace only, so a line carrying any other "whitespace" (NBSP, | ||||||||||||||||
| // U+2000–U+200A, U+2028, ...) keeps it and fails the exact match. | ||||||||||||||||
| // JS trim() stripped those too, classifying `\u00a0# x` as a comment | ||||||||||||||||
| // while bash executed it — a smuggled statement invisible to | ||||||||||||||||
| // every other pin here (R6-1). | ||||||||||||||||
| const gateBodyStatementsOf = (stepText) => | ||||||||||||||||
| stepText | ||||||||||||||||
| .slice(stepText.indexOf('run: |-') + 'run: |-'.length) | ||||||||||||||||
| .split('\n') | ||||||||||||||||
| .map((line) => line.replace(/^[ \t]+|[ \t]+$/g, '')) | ||||||||||||||||
| .filter((line) => line !== '' && !line.startsWith('#')); | ||||||||||||||||
| expect(gateBodyStatementsOf('run: |-\n \u00a0# x')).toEqual(['\u00a0# x']); | ||||||||||||||||
| for (const step of [ | ||||||||||||||||
| reviewVerificationGateStep, | ||||||||||||||||
| repairVerificationGateStep, | ||||||||||||||||
| ]) { | ||||||||||||||||
| expect(step).toMatch(gateLaunchPin); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This block pins the chain "through the digest-verified script", but the pre-launch digest check itself is pinned only by a workflow-wide Witness:
Suggested change
中文说明本块把链钉到「digest 校验过的脚本」,但启动前的 digest 检查本身只由一个全 workflow 的 — qwen3.8-max via Qwen Code /review (v0.21.15) |
||||||||||||||||
| expect(gateBodyStatementsOf(step)).toEqual(gateBodyStatements); | ||||||||||||||||
| // Exactly one launch per step: a second, unpinned `bash --norc` (the | ||||||||||||||||
| // pinned block demoted into a never-run arm) must fail here (R2-1). | ||||||||||||||||
| expect((step.match(/bash --norc/g) ?? []).length).toBe(1); | ||||||||||||||||
| } | ||||||||||||||||
| expect( | ||||||||||||||||
| reviewVerifyGate.indexOf( | ||||||||||||||||
| 'bash "${RUNNER_TEMP}/check-autofix-contracts.sh"', | ||||||||||||||||
|
|
@@ -12793,7 +12891,7 @@ exit 1 | |||||||||||||||
| // Delimited tokens, not substrings: match `NAME=value` up to the line | ||||||||||||||||
| // continuation, so a value swap or an extra entry is visible. | ||||||||||||||||
| const assignments = ( | ||||||||||||||||
| argList.match(/[A-Z_][A-Z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? [] | ||||||||||||||||
| argList.match(/[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|[^\s\\]*)/g) ?? [] | ||||||||||||||||
| ).map((m) => m.trim()); | ||||||||||||||||
| const passed = assignments.map((m) => m.split('=')[0]); | ||||||||||||||||
| // Sorted multiset, not a Set: a symmetric duplicate entry is exactly | ||||||||||||||||
|
|
@@ -19321,10 +19419,34 @@ describe('growth-audit hardening: park wake set and verdict pipeline (round 3)', | |||||||||||||||
| // level, which outranks any $GITHUB_ENV plant; the gate itself then | ||||||||||||||||
| // runs through the workflow's env -i clean-child pattern, so its bash | ||||||||||||||||
| // inherits nothing at all (enumerating plants is the failure mode the | ||||||||||||||||
| // verdict pipeline kept hitting). | ||||||||||||||||
| // verdict pipeline kept hitting). LD_* load at startup the same way, so | ||||||||||||||||
| // they are pinned empty at step level too — an in-body unset cannot | ||||||||||||||||
| // unload a library already mapped into the parent running the digest | ||||||||||||||||
| // check, and a bare unset is itself a BASH_FUNC shadow target (R6-2, | ||||||||||||||||
| // R8-1) — and RUNNER_TEMP/WORKDIR/BRANCH steer that digest check and | ||||||||||||||||
| // the child's tree, so they are pinned from trusted expression context | ||||||||||||||||
| // too (R6-3). CI reaches the child's `CI="${CI:-true}"` expansion from | ||||||||||||||||
| // the step environment, and `:-true` only covers an UNSET CI — a | ||||||||||||||||
| // $GITHUB_ENV plant of CI=false survives the expansion and inverts the | ||||||||||||||||
| // gate's CI semantics — so CI is pinned at step level too (R1-1). | ||||||||||||||||
| // HOME reaches the child's allowlist from the step environment, and | ||||||||||||||||
| // npm resolves its userconfig from HOME — a planted HOME's .npmrc | ||||||||||||||||
| // script-shell wraps every verdict-determining `npm run`, so a red | ||||||||||||||||
| // branch reports green; HOME is pinned from the stage-time capture | ||||||||||||||||
| // (R8-3). | ||||||||||||||||
| for (const step of [verificationGateSteps[1], repairVerificationGateStep]) { | ||||||||||||||||
| expect(step).toContain("BASH_ENV: ''"); | ||||||||||||||||
| expect(step).toContain("SHELLOPTS: ''"); | ||||||||||||||||
| expect(step).toContain("LD_PRELOAD: ''"); | ||||||||||||||||
| expect(step).toContain("LD_AUDIT: ''"); | ||||||||||||||||
| expect(step).toContain("LD_LIBRARY_PATH: ''"); | ||||||||||||||||
| expect(step).toContain("RUNNER_TEMP: '${{ runner.temp }}'"); | ||||||||||||||||
| expect(step).toContain( | ||||||||||||||||
| "WORKDIR: '/tmp/autofix-review-${{ matrix.target.pr }}'", | ||||||||||||||||
| ); | ||||||||||||||||
| expect(step).toContain("BRANCH: '${{ matrix.target.branch }}'"); | ||||||||||||||||
| expect(step).toContain("CI: 'true'"); | ||||||||||||||||
| expect(step).toContain("HOME: '${{ steps.stage.outputs.trusted_home }}'"); | ||||||||||||||||
| expect(step).toContain('/usr/bin/env -i'); | ||||||||||||||||
| expect(step).toContain( | ||||||||||||||||
| 'bash --norc "${RUNNER_TEMP}/run-autofix-review-verification.sh"', | ||||||||||||||||
|
|
@@ -19335,6 +19457,10 @@ describe('growth-audit hardening: park wake set and verdict pipeline (round 3)', | |||||||||||||||
| 'FOOTPRINT_ENFORCE="${FOOTPRINT_ENFORCE:-advisory}"', | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
| // The review stage step records HOME before any branch code runs — the | ||||||||||||||||
| // trusted_path doctrine — and only it: the issue job's stage has no | ||||||||||||||||
| // gate child re-injecting HOME. | ||||||||||||||||
| expect(workflow.match(/trusted_home=\$\{HOME\}/g) ?? []).toHaveLength(1); | ||||||||||||||||
| }); | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical] This pin block closes every
$GITHUB_ENV→gate channel except HOME — the only allowlisted child variable (HOME="${HOME}"in the launch below, both gates) whose value still flows from the plantable step environment. Git is defended in the gate child, but npm resolves its userconfig from$HOMEand has no defense: a planted HOME whose.npmrcsetsscript-shellwraps everynpm run build|typecheck|lint|testinvocation in an attacker-controlled shell, so a genuinely failing check exits 0 — every verdict-determining check reports green for a red branch, and the PAT-bearing push runs. actions/runner accepts the plant (FileCommandManagerblocklist isNODE_OPTIONSonly;ScriptHandlerre-injects onlyRUNNER_*/allowlistedGITHUB_*, so HOME carries the plant through), which is exactly the doctrine this block applies to RUNNER_TEMP/WORKDIR/BRANCH/CI.Witness (probe, node 22.23.2 / npm 10.9.8):
Fix: capture HOME at stage time with the existing trusted-value doctrine — add
echo "trusted_home=${HOME}" >> "${GITHUB_OUTPUT}"to 'Stage trusted schema gate and agent runner' (it runs before any branch code), then pinHOME: '${{ steps.stage.outputs.trusted_home }}'in both gates' env blocks here and extend the pin assertions to it.中文说明
[Critical] 本 pin 块关闭了所有
$GITHUB_ENV→gate 通道,唯独剩 HOME——它是允许列表中唯一一个(下方启动的HOME="${HOME}",两个 gate 皆然)取值仍来自可植入步骤环境的变量。gate 子进程中 git 已有防御,但 npm 从$HOME解析 userconfig 且无防御:植入的 HOME 目录里.npmrc设置script-shell后,每一次npm run build|typecheck|lint|test都会被攻击者控制的 shell 包裹,真正失败的检查也会以 0 退出——红分支的所有 verdict 判定检查全绿,PAT push 随之执行。actions/runner 接受该植入(FileCommandManager屏蔽名单仅NODE_OPTIONS;ScriptHandler只重新注入RUNNER_*/允许列表内的GITHUB_*,HOME 带着植入值透传)——这正是本块对 RUNNER_TEMP/WORKDIR/BRANCH/CI 所应用的学说。修复:按既有的受信任值学说在 stage 阶段捕获 HOME——在 'Stage trusted schema gate and agent runner'(先于任何分支代码运行)中加
echo "trusted_home=${HOME}" >> "${GITHUB_OUTPUT}",然后在此处两个 gate 的 env 块中钉住HOME: '${{ steps.stage.outputs.trusted_home }}',并扩展 pin 断言覆盖它。— qwen3.8-max via Qwen Code /review (v0.21.15)