fix(exec-approvals): guard Windows rename fallback#77907
fix(exec-approvals): guard Windows rename fallback#77907BradGroux merged 3 commits intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: deaca9a7ac
ℹ️ 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".
| throw err; | ||
| } | ||
| assertSafeExecApprovalsOverwriteFallback(filePath); | ||
| fs.copyFileSync(tempPath, filePath); |
There was a problem hiding this comment.
Avoid deleting approvals file on fallback copy errors
Using fs.copyFileSync(tempPath, filePath) as the overwrite fallback can destroy the existing exec-approvals.json if the copy fails after opening the destination (for example ENOSPC/I/O errors), because Node may remove the destination on copy failure. Before this change, a failed renameSync left the old approvals file intact; now the same failure path can lose the persisted approvals entirely, which can reset policy state and block or weaken expected approval behavior on the next load.
Useful? React with 👍 / 👎.
|
Codex review: needs maintainer review before merge. Summary Reproducibility: yes. source-level: current main persists exec approvals through a temp rename path, and the linked Windows report gives concrete EPERM failures across shipped versions. I did not run a native Windows locked-handle gateway repro in this read-only pass. Real behavior proof Next step before merge Security Review detailsBest possible solution: Land this writer-local fallback after normal maintainer approval so the paired Windows regression can close through the PR merge. Do we have a high-confidence way to reproduce the issue? Yes, source-level: current main persists exec approvals through a temp rename path, and the linked Windows report gives concrete EPERM failures across shipped versions. I did not run a native Windows locked-handle gateway repro in this read-only pass. Is this the best way to solve the issue? Yes. The latest head keeps the fix in the exec approvals writer, adds guarded fallback and restoration behavior, and preserves directory/file permission and symlink/hard-link safety boundaries. What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against ae9f779e5ff4. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 90681bd82d
ℹ️ 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".
| assertSafeExecApprovalsOverwriteFallback(filePath); | ||
| const snapshot = readExecApprovalsFallbackSnapshot(filePath); | ||
| try { | ||
| fs.copyFileSync(tempPath, filePath); |
There was a problem hiding this comment.
Prevent symlink swap between fallback check and copy
The new Windows fallback still has a TOCTOU gap: after assertSafeExecApprovalsOverwriteFallback(filePath) verifies the destination is not a symlink, another process can replace exec-approvals.json with a symlink before fs.copyFileSync(tempPath, filePath) runs. In that race, copyFileSync follows the symlink and overwrites the link target, so an EPERM/EEXIST rename failure can now clobber arbitrary user-writable files outside the approvals path, which defeats the symlink safety boundary this code is trying to enforce.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 153092fab4
ℹ️ 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".
| renameExecApprovalsWithFallback(tempPath, filePath); | ||
| } finally { | ||
| if (tempWritten && fs.existsSync(tempPath)) { | ||
| fs.rmSync(tempPath, { force: true }); | ||
| } |
There was a problem hiding this comment.
Preserve 0600 mode after successful temp-file rename
writeExecApprovalsRaw no longer reapplies permissions after a successful rename, so the final exec-approvals.json mode now depends on the process umask. Because writeFileSync(..., { mode: 0o600 }) is still masked by umask, restrictive service umasks can leave the file unreadable (for example 000), which then makes later reads fall back to defaults and drops persisted approvals; the previous chmodSync step avoided this regression.
Useful? React with 👍 / 👎.
153092f to
b7c0816
Compare
b7c0816 to
f97f4e8
Compare
f97f4e8 to
9bc4a4a
Compare
9bc4a4a to
461bb87
Compare
461bb87 to
6dc0c0f
Compare
|
Maintainer prep update for #77907. What changed:
Prepared head: Local verification:
CI note: earlier runs were blocked by unrelated current-main archive/fs-safe race-test failures in |
6dc0c0f to
4530dd1
Compare
|
Maintainer refresh: rebased again onto current main and pushed prepared head |
7f3d0ee to
28834ad
Compare
|
Maintainer refresh: picked up the extra approvals directory-mode commit, rebased onto current main, resolved the changelog move, and pushed prepared head |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f3d0eebe5
ℹ️ 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".
| } | ||
|
|
||
| function copyExecApprovalsFallback(tempPath: string, filePath: string): void { | ||
| const contents = fs.readFileSync(tempPath); |
There was a problem hiding this comment.
Make the fallback temp file readable before copying
When the process has a restrictive umask (for example 0777) and renameSync takes the new EPERM/EEXIST fallback, the temp file created with { mode: 0o600 } is still umask-masked, so reopening it here by path can throw EACCES before the guarded copy runs. The final chmodSync(filePath, 0o600) only happens after renameExecApprovalsWithFallback returns, so this fallback still fails to persist approvals in those service environments; chmod the temp file before reading it or copy from an already-open descriptor.
Useful? React with 👍 / 👎.
28834ad to
91fe6a6
Compare
* fix exec approvals Windows rename fallback * fix(exec-approvals): restore approvals directory mode * fix(exec-approvals): normalize fallback temp mode --------- Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
Summary
EPERM/EEXISTfallback forexec-approvals.jsonrename-overwrite failuresFixes #77785.
Root cause
writeExecApprovalsRawwrites a temp file and then atomically renames it overexec-approvals.json. On Windows, rename-overwrite can fail withEPERMwhen another process has a transient handle on the target file, which blocks exec approval persistence before the exec call can continue.Real behavior proof
exec-approvals.jsonrename-overwrite is rejected with a Windows-styleEPERM, the approval store falls back to a guarded copy, preserves file mode600, and removes its temp file; hard-linked approval files are still rejected, and a failed fallback copy restores the previous approvals bytes.saveExecApprovalspath against isolated temporaryOPENCLAW_HOMEdirectories under/var/folders/.../T.node --import tsxfrom the patched checkout, created a real approvals file, forcedfs.renameSyncto throwEPERMonly for that approvals target, calledsaveExecApprovals, repeated with a hard-linked approvals target to verify the guard rejects fallback, then forcedfs.copyFileSyncto truncate and throw to verify restore.{ "fallbackHome": "/var/folders/cf/mtzn4vhx6kz_83fmvt5qs60m0000gn/T/openclaw-77785-fallback-tIUOeJ", "fallbackPath": "/var/folders/cf/mtzn4vhx6kz_83fmvt5qs60m0000gn/T/openclaw-77785-fallback-tIUOeJ/.openclaw/exec-approvals.json", "forcedRenameErrors": 1, "fallbackContainsSecurity": true, "fallbackMode": "600", "fallbackTempFiles": [], "hardlinkHome": "/var/folders/cf/mtzn4vhx6kz_83fmvt5qs60m0000gn/T/openclaw-77785-hardlink-fA79BX", "hardlinkErrorIncludesGuard": true, "hardlinkSentinelPreserved": true, "hardlinkTempFiles": [] }Copied live terminal output from the fallback copy-failure restore command:
{ "home": "/var/folders/cf/mtzn4vhx6kz_83fmvt5qs60m0000gn/T/openclaw-77785-restore-proof-VQVbZA", "forcedRenameErrors": 1, "forcedCopyErrors": 1, "errorIncludesCopyFailure": true, "previousApprovalsRestored": true, "finalMode": "600", "tempFiles": [] }EPERMpath persisted the updated approvals file, kept secure permissions, and left no temp files; the hard-linked approvals target threw the hard-link guard and left the sentinel link content unchanged; the simulated destructive copy failure restored the previous approvals content and mode.EPERMand copy-failure boundaries in the actual store writer.Validation
pnpm test src/infra/exec-approvals-store.test.tspnpm test src/agents/bash-tools.exec-host-gateway.test.tspnpm exec oxfmt --check --threads=1 src/infra/exec-approvals.ts src/infra/exec-approvals-store.test.ts CHANGELOG.mdpnpm check:changedgit diff --check