Skip to content

fix(#605): retry sandbox cleanup with chmod on permission denied - #620

Closed
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/605-force-remove-sandbox-cleanup
Closed

fix(#605): retry sandbox cleanup with chmod on permission denied#620
fullsend-ai-coder[bot] wants to merge 1 commit into
mainfrom
agent/605-force-remove-sandbox-cleanup

Conversation

@fullsend-ai-coder

Copy link
Copy Markdown

When sandboxed processes create files with restrictive permissions (e.g. different UID), os.RemoveAll fails during repo extraction cleanup. This caused the entire run to abort and skip the post-script, losing review results (observed on 4 of 7 review runs for PR fullsend-ai#3193).

Add forceRemoveAll helper that retries removal after making all entries owner-writable (chmod 0700). Change the cleanup at step 9d from a hard failure to a warning so the post-script always has a chance to run.


Closes #605

Post-script verification

  • Branch is not main/master (agent/605-force-remove-sandbox-cleanup)
  • Secret scan passed (gitleaks — f6d95485ad7c634f52e66a32573856b85534daca..HEAD)
  • Pre-commit hooks passed (authoritative run on runner)
  • Tests ran inside sandbox

When sandboxed processes create files with restrictive permissions
(e.g. different UID), os.RemoveAll fails during repo extraction
cleanup. This caused the entire run to abort and skip the post-script,
losing review results (observed on 4 of 7 review runs for PR fullsend-ai#3193).

Add forceRemoveAll helper that retries removal after making all entries
owner-writable (chmod 0700). Change the cleanup at step 9d from a hard
failure to a warning so the post-script always has a chance to run.

Closes #605
@github-actions

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

See E2E testing guide for details.

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 28, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:05 AM UTC · Completed 11:17 AM UTC
Commit: f6d9548 · View workflow run →

@fullsend-ai-review

Copy link
Copy Markdown

Review — PR #620

fix(#605): retry sandbox cleanup with chmod on permission denied

Summary

The PR adds a forceRemoveAll helper that retries directory removal after chmod'ing entries to 0o700 on permission errors, and downgrades cleanup failure from a hard abort to a warning so the post-script always runs. The forceRemoveAll implementation is sound for its intended use case (same-UID restrictive directory permissions). However, the call-site behavior change — downgrading all cleanup failures to warnings — introduces a stale-file contamination risk in the validation loop that needs to be addressed before merge.

Findings

🔴 HIGH — Stale file contamination when cleanup fails in validation loop

File: internal/cli/run.go · ~line 895

The pre-PR code hard-aborted when os.RemoveAll failed, which was intentional: SafeDownload (via openshell sandbox download) overlays onto the target directory without clearing it first. The call to os.RemoveAll at step 9d exists precisely because the download merges.

With this PR, if forceRemoveAll fails and the call site logs a warning and continues, SafeDownload extracts the current sandbox state on top of whatever survived from the previous iteration. Files that the sandbox deleted in iteration N will persist from iteration N-1, potentially causing validation to see ghost artifacts (generated files, build outputs, stale configs) that don't exist in the sandbox's working tree.

Remediation: Extract into a fresh temporary directory and atomically rename into place, so a partial cleanup never contaminates the result. Alternatively, only downgrade to a warning when the cleanup error is a permission error (errors.Is(clearErr, fs.ErrPermission)) and keep the hard abort for other error types — combined with restricting the warn-and-continue path to the final iteration where stale contamination cannot affect subsequent validation runs.

🟡 MEDIUM — All cleanup errors downgraded, not just permission errors

File: internal/cli/run.go · ~line 895

forceRemoveAll correctly distinguishes permission errors (retries with chmod) from other errors (returns immediately). However, the call site treats any error as a warning and continues — including I/O errors, EBUSY, filesystem corruption. The original code hard-aborted on these, which was the safe default. The call site should preserve the hard-abort path for non-permission errors.

🔵 LOW — chmod fallback ineffective for different-UID files

File: internal/cli/run.go · ~line 1308

The issue and doc comment describe the failure as "files created by a sandboxed process with a different UID." os.Chmod on files owned by a different UID requires CAP_FOWNER and will silently fail. The function actually fixes the same-UID, restrictive directory permissions scenario. This is not a defect — it improves the common case — but the doc comment should accurately describe the scope.

🔵 LOW — Test does not cover different-UID scenario

File: internal/cli/run_test.go · ~line 1438

The test exercises same-UID restrictive permissions, which is the scenario the fix actually addresses. A comment noting that the different-UID case is a known limitation (cannot test without root) would clarify intent.

🔵 LOW — Symlink-following in chmod walk

File: internal/cli/run.go · ~line 1313

os.Chmod follows symlinks. While filepath.WalkDir does not descend into symlinked directories, it does report symlink entries. A relative symlink surviving from a prior sanitizeDownload pass could cause chmod on an out-of-tree target. Risk is low (prior sanitization strips dangerous symlinks, chmod only broadens to 0o700 on user-owned files) but adding if d.Type()&fs.ModeSymlink != 0 { return nil } in the walk callback is a one-line defensive improvement.

🔵 LOW — Behavior change broader than stated scope

File: internal/cli/run.go · ~line 895

The PR title says "retry sandbox cleanup with chmod on permission denied" but the call-site change also converts all cleanup failures (not just permission-denied) from hard errors to warnings. This broader behavior change should be explicitly acknowledged.


Labels: PR fixes sandbox cleanup in CLI runtime code

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread internal/cli/run.go
// Use forceRemoveAll to handle files created by sandboxed processes with
// restrictive permissions (#605). Warn on failure instead of aborting so
// the post-script still runs (review results are more valuable than a
// clean temp directory).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[high] stale-file-contamination

SafeDownload overlays onto the target directory without clearing it. When forceRemoveAll fails and the call site continues with a warning, files from the prior iteration that the sandbox deleted persist as ghost artifacts, potentially corrupting validation results in the iteration loop.

Suggested fix: Extract into a fresh temporary directory and atomically rename into place. Alternatively, only downgrade to warning on permission errors (errors.Is(clearErr, fs.ErrPermission)) and keep the hard abort for other error types, restricting warn-and-continue to the final iteration.

Comment thread internal/cli/run.go
// Use forceRemoveAll to handle files created by sandboxed processes with
// restrictive permissions (#605). Warn on failure instead of aborting so
// the post-script still runs (review results are more valuable than a
// clean temp directory).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] error-handling

The call site downgrades ALL forceRemoveAll errors to warnings, not just permission errors. forceRemoveAll correctly distinguishes permission errors from other error types, but the caller discards that distinction. Non-permission errors (I/O errors, EBUSY, filesystem corruption) should still hard-abort as the original code intended.

Suggested fix: Add an errors.Is(clearErr, fs.ErrPermission) check at the call site: warn-and-continue for permission errors, hard-abort for all other error types.

Comment thread internal/cli/run.go
// forceRemoveAll removes a directory tree. If the initial removal fails with a
// permission error (e.g. files created by a sandboxed process with a different
// UID), it makes all entries owner-writable and retries. See #605.
func forceRemoveAll(path string) error {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] incorrect-documentation

The doc comment and issue describe the failure as different-UID files, but os.Chmod on different-UID files requires CAP_FOWNER and will fail. The function fixes same-UID restrictive directory permissions. The doc comment should accurately describe the scope.

Comment thread internal/cli/run_test.go
assert.True(t, os.IsNotExist(err))
})

t.Run("handles restrictive directory permissions", func(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] test-coverage

The test exercises same-UID restrictive directory permissions but does not document that the different-UID scenario is a known limitation that cannot be tested without root.

Comment thread internal/cli/run.go
if err == nil || !errors.Is(err, fs.ErrPermission) {
return err
}
// Best-effort chmod: make every entry owner-rwx so the retry can unlink.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] symlink-following

os.Chmod follows symlinks. While prior sanitizeDownload strips dangerous symlinks, adding a symlink guard (d.Type() and fs.ModeSymlink != 0) in the WalkDir callback is a one-line defensive improvement.

Comment thread internal/cli/run.go
// Use forceRemoveAll to handle files created by sandboxed processes with
// restrictive permissions (#605). Warn on failure instead of aborting so
// the post-script still runs (review results are more valuable than a
// clean temp directory).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] scope-mismatch

The PR title says retry on permission denied but the call-site change converts all cleanup failures to warnings, not just permission-denied. This broader behavior change should be explicitly acknowledged.

@guyoron1

Copy link
Copy Markdown
Owner

Run B invalidated — scaffold was pulling from upstream. Re-running with workflow fix.

@guyoron1 guyoron1 closed this Jul 28, 2026
@guyoron1
guyoron1 deleted the agent/605-force-remove-sandbox-cleanup branch July 29, 2026 15:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sandbox cleanup should handle files with restrictive permissions created by sandboxed processes

1 participant