fix(interp): restore prior value for same-name inline-command vars#262
Conversation
vuln-hunt F-2 (inline_var, P3): `A=v1 A=v2 cmd` left `A=v1` in the parent shell instead of restoring A's true pre-command value. Each loop iteration recaptured `prev` after the previous assignment took effect, so the second restore entry held an intermediate, and the forward-order defer applied it last. Dedupe the restore list by name so only the first prev per name is recorded, matching bash semantics. Adds a regression scenario covering the 2- and 3-assignment cases.
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Self-review (iteration 1)
Overall assessment: safe to merge — small, focused fix that moves rshell behaviour closer to bash. No security concerns. Only nice-to-have improvements to test coverage.
Summary table
Coverage
| Code path | Scenario test | Status |
|---|---|---|
A=v1 A=v2 cmd restores prior set value |
same_name_multi_assign_restores_prior_value.yaml |
Covered |
A=v1 A=v2 A=v3 cmd restores prior set value |
same_name_multi_assign_restores_prior_value.yaml |
Covered |
| Same-name dedup with interleaved different names | — | Missing (P3) |
A=v1 A=v2 cmd when A is unset before |
— | Missing (P3) |
| Distinct-name multi-assign (existing) | multi_assign_all_restored.yaml |
Covered (no regression) |
Positive observations
- Fix is minimal and matches the finding's preferred Option 1 (dedup by name).
- Inline comment explains why only the first
previs the true pre-command value — good for future readers. cdPWD/OLDPWD skip is preserved (the dedup happens before defer; the loop's restore filter still applies).- Restore semantics are unchanged for the readonly-target case (
FOO=ok RO_VAR=evil cmd): existing pentestTestVulnHuntShellFeatureReadonlyBypass_MultiVarInlineWithReadonlystill passes. - No filesystem, no new imports, no concurrency surface.
|
Iteration 1 self-review: 0 P0 / 0 P1 / 0 P2 / 2 P3 — no blocking findings. P0_P1_P2_COUNT=0. The two P3 findings are nice-to-have test-coverage extensions (interleaved same/different name case, and unset-before case). See the review at #262 (review). |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f89a95915b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Add interleaved same/different-name case (`A=v1 B=vb A=v2 cmd`) and a never-set-before case (`C=v1 C=v2 cmd`) to the regression scenario. Both verified against debian:bookworm-slim bash.
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Self-review (iteration 2)
Overall assessment: safe to merge — no new findings. All P3 items from iteration 1 have been addressed (interleaved + never-set cases added to scenario; expect.stderr uses |+ block scalar per AGENTS.md).
Summary table
No findings.
Coverage
| Code path | Scenario test | Status |
|---|---|---|
A=v1 A=v2 cmd restores prior set value |
same_name_multi_assign_restores_prior_value.yaml |
Covered |
A=v1 A=v2 A=v3 cmd restores prior set value |
same_name_multi_assign_restores_prior_value.yaml |
Covered |
| Same-name dedup interleaved with distinct name | same_name_multi_assign_restores_prior_value.yaml |
Covered |
| Same-name dedup when var was never set before | same_name_multi_assign_restores_prior_value.yaml |
Covered |
| Distinct-name multi-assign (existing) | multi_assign_all_restored.yaml |
Covered (no regression) |
P0_P1_P2_COUNT=0 for this iteration.
|
Iteration 2 self-review: 0 P0 / 0 P1 / 0 P2 / 0 P3 — clean. P0_P1_P2_COUNT=0. See #262 (review). |
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
|
Codex Review: Didn't find any major issues. Bravo. ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
AlexandreYang
left a comment
There was a problem hiding this comment.
Self-review (iteration 3)
No new commits since iteration 2 (HEAD still at 3969b86b). Re-ran the same review checks: security (no fs/exec/import deltas), bash compatibility (verified against debian:bookworm-slim), correctness (dedup logic preserves the existing cd PWD/OLDPWD restore filter), coverage (4 scenario cases for the same-name path).
No findings.
P0_P1_P2_COUNT=0 for this iteration.
|
Iteration 3 self-review: 0 P0 / 0 P1 / 0 P2 / 0 P3 — clean (no new commits since iter 2). P0_P1_P2_COUNT=0. |
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Self-review (iteration 4)
No new commits since iteration 3 (HEAD still at 3969b86b). No findings.
P0_P1_P2_COUNT=0.
|
Iteration 4 self-review: 0 P0 / 0 P1 / 0 P2 / 0 P3 — clean (no new commits since iter 3). P0_P1_P2_COUNT=0. |
|
@codex review this PR Please use the following severity format for all findings:
Prefix each finding title with its priority label, e.g. "P0: ...", "P1: ...", etc. |
AlexandreYang
left a comment
There was a problem hiding this comment.
Self-review (iteration 5)
No new commits since iteration 4 (HEAD still at 3969b86b). No findings.
P0_P1_P2_COUNT=0.
|
Iteration 5 self-review: 0 P0 / 0 P1 / 0 P2 / 0 P3 — clean (no new commits since iter 4). P0_P1_P2_COUNT=0. |
Review-Fix Loop Summary
Iteration log
Final state
Remaining issuesNone. |
| vr expand.Variable | ||
| } | ||
| var restores []restoreVar | ||
| seenRestore := map[string]bool{} |
There was a problem hiding this comment.
nit: this could be a set. I don't think Go has one in the std impl thouhg?
There was a problem hiding this comment.
indeed, there is no set in Go 😭
Summary
inline_var, P3):A=v1 A=v2 cmdwas leavingA=v1in the parent shell instead of restoring A's true pre-command value.prevafter the previous assignment took effect, so the 2nd restore entry held the intermediate. The forward-order defer then applied the stale restore last.prevper name is recorded, matching bash semantics.Test plan
go test ./interp/... ./tests/passes locallysame_name_multi_assign_restores_prior_value.yamlpasses (and is asserted against bash by default in CI)debian:bookworm-slimRSHELL_BASH_TEST=1) greenReference