Skip to content

fix(cli): close sandbox filesystem TOCTOU race#11591

Merged
marius-kilocode merged 10 commits into
mainfrom
fix-sandbox-toctou-hardening
Jun 24, 2026
Merged

fix(cli): close sandbox filesystem TOCTOU race#11591
marius-kilocode merged 10 commits into
mainfrom
fix-sandbox-toctou-hardening

Conversation

@marius-kilocode

@marius-kilocode marius-kilocode commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

The macOS sandbox previously validated built-in filesystem mutations in TypeScript and then performed the mutation through the same pathname in the long-lived Kilo process. A concurrent process could replace a validated parent directory with a symlink after validation but before the filesystem syscall, redirecting Write, Edit, or ApplyPatch outside the allowed project roots or into protected .git state. Shell children were already protected because Seatbelt evaluates their actual destination at syscall time, but built-in mutations did not have that kernel-enforced boundary.

This closes the gap by routing every finite Effect filesystem mutation through a one-shot worker launched under the active, replaceable OS sandbox backend. The existing pathname checks remain useful for early diagnostics, but Seatbelt now decides whether the mutation is allowed when the worker performs the actual syscall. Additional realpath or lstat checks are deliberately not treated as a security boundary.

Mitigation design

  • Define a typed request/response protocol for writes, directory creation, copies, links, renames, removals, truncation, timestamps, ownership, permissions, and temporary resources.
  • Launch one worker per sandboxed mutation through the active backend and exact call-local profile. This keeps the backend abstraction usable by future Linux and Windows implementations instead of coupling filesystem code directly to Seatbelt.
  • Preserve binary payloads, string payloads, file flags, modes, copy options, timestamps, temporary-file naming, structured filesystem errors, scoped cleanup, and unsandboxed behavior.
  • Route ensureDir and writeWithDirs parent creation through the decorated filesystem rather than bypassing it with raw fs.mkdir calls.
  • Package the dedicated worker for source execution, standalone CLI binaries, and the Node build without embedding worker source in a string.
  • Continue to fail closed when no OS backend is available. Sandboxed writable file handles also fail closed because a one-shot worker cannot safely transfer a live descriptor; read-only handles remain available.
  • Keep permission approval separate from confinement. Approving external_directory authorizes the tool request but does not add that directory to the sandbox profile, and .git remains denied by name.

Attack vectors considered

The security assessment covered both direct pathname attacks and attempts to avoid pathname checks entirely:

  • Direct writes and edits outside the project and inside .git.
  • Dot segments, parent traversal, repeated separators, /tmp aliases, //private aliases, case variants, and macOS /System/Volumes/Data aliases.
  • One-hop directory symlinks, chained symlinks, direct file symlinks, and validated-parent replacement.
  • Atomic parent symlink swapping while built-in mutations and shell writes run concurrently.
  • Renames, directory renames, hard links, symlink destination creation, APFS clone copies, and archive traversal.
  • Descriptor-relative openat, mkdirat, renameat, and linkat operations, plus relative writes after fchdir into a protected directory.
  • Git object/config writes, formatters, encoders, in-place editors, temporary-file replacement patterns, metadata changes, and patch utilities.
  • Background-process persistence attempts while sandboxing is active.

Regression boundary

The macOS integration harness invokes real /usr/bin/sandbox-exec behavior. Its adversarial hook starts atomic parent swapping after userspace validation and before the confined mutation, so the assertion is not dependent on lucky timing. The harness was also exercised with a vulnerable direct mutation runner to confirm that the protected sentinel is modified without syscall-time confinement.

The suite drives the real shared filesystem capability used by Write, Edit, and ApplyPatch against both external and .git targets. It also covers allowed writes, safe in-project symlinks, shell confinement, permission approval and denial, background-process rejection, UTF-16 LE, Windows-1251, UTF-8 BOM, formatter synchronization, operation options, and temporary-resource cleanup. The real Seatbelt suite is wired only into the macOS CI lane; other platforms explicitly skip OS-specific cases while retaining portable fail-closed coverage.

Tradeoffs

  • Finite sandboxed mutations incur a process launch so the kernel boundary is applied independently to each syscall sequence.
  • Sandboxed sinks buffer their payload before sending it to the one-shot worker.
  • Sandboxed writable open handles are intentionally unavailable until a descriptor-safe cross-process design exists.

Fixes #11554

Parent epic: #11538

Comment thread packages/opencode/src/index.ts Outdated
Comment thread packages/kilo-sandbox/test/filesystem.test.ts
@kilo-code-bot

kilo-code-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • packages/kilo-sandbox/test/kilo-sandbox-mutation-worker.test.ts
Previous Review Summaries (7 snapshots, latest commit e69828e)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit e69828e)

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 101 The worker sidecar is always refreshed, but the cached CLI binary can still be reused by size, allowing an upgraded install to run an old binary with a new worker.
Files Reviewed (27 files)
  • .changeset/secure-sandbox-mutations.md - 0 issues
  • packages/core/src/filesystem.ts - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt - 1 issue
  • packages/kilo-jetbrains/script/build.ts - 0 issues
  • packages/kilo-sandbox/src/backend.ts - 0 issues
  • packages/kilo-sandbox/src/filesystem.ts - 0 issues
  • packages/kilo-sandbox/src/index.ts - 0 issues
  • packages/kilo-sandbox/src/kilo-sandbox-mutation-worker.ts - 0 issues
  • packages/kilo-sandbox/src/mutation-protocol.ts - 0 issues
  • packages/kilo-sandbox/src/mutation.ts - 0 issues
  • packages/kilo-sandbox/test/backend.test.ts - 0 issues
  • packages/kilo-sandbox/test/filesystem.test.ts - 0 issues
  • packages/kilo-sandbox/test/kilo-sandbox-mutation-worker.test.ts - 0 issues
  • packages/kilo-vscode/script/local-bin.ts - 0 issues
  • packages/kilo-vscode/src/services/cli-backend/cli-resources.ts - 0 issues
  • packages/kilo-vscode/tests/unit/server-manager-utils.test.ts - 0 issues
  • packages/opencode/script/build-node.ts - 0 issues
  • packages/opencode/script/build.ts - 0 issues
  • packages/opencode/script/kilocode/kilo-sandbox-worker.ts - 0 issues
  • packages/opencode/script/kilocode/test-profile.ts - 0 issues
  • packages/opencode/script/postinstall.mjs - 0 issues
  • packages/opencode/script/publish.ts - 0 issues
  • packages/opencode/src/kilocode/tool/encoded-io.ts - 0 issues
  • packages/opencode/test/kilocode/cli/install-artifact.test.ts - 0 issues
  • packages/opencode/test/kilocode/sandbox/macos-confinement.test.ts - 0 issues
  • packages/opencode/test/kilocode/sandbox/session-tools.test.ts - 0 issues
  • packages/opencode/test/kilocode/test-profile.test.ts - 0 issues

Previous review (commit 61c7f97)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-sandbox/src/mutation.ts 230 state.closed flips only after the final flush completes, so a retained runner invoked during shutdown can append late operations that are never flushed or delegated upstream.
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 101 The worker sidecar is always refreshed, but the cached CLI binary can still be reused by size, allowing an upgraded install to run an old binary with a new worker.
Files Reviewed (25 files)
  • .github/workflows/test.yml - 0 issues
  • packages/core/src/filesystem.ts - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt - 1 issue
  • packages/kilo-jetbrains/script/build.ts - 0 issues
  • packages/kilo-sandbox/src/filesystem.ts - 0 issues
  • packages/kilo-sandbox/src/index.ts - 0 issues
  • packages/kilo-sandbox/src/kilo-sandbox-mutation-worker.ts - 0 issues
  • packages/kilo-sandbox/src/mutation.ts - 1 issue
  • packages/kilo-sandbox/test/filesystem.test.ts - 0 issues
  • packages/kilo-sandbox/test/kilo-sandbox-mutation-worker.test.ts - 0 issues
  • packages/kilo-vscode/script/local-bin.ts - 0 issues
  • packages/kilo-vscode/src/services/cli-backend/cli-resources.ts - 0 issues
  • packages/kilo-vscode/tests/unit/server-manager-utils.test.ts - 0 issues
  • packages/opencode/script/build-node.ts - 0 issues
  • packages/opencode/script/build.ts - 0 issues
  • packages/opencode/script/kilocode/kilo-sandbox-worker.ts - 0 issues
  • packages/opencode/script/kilocode/test-profile.ts - 0 issues
  • packages/opencode/script/postinstall.mjs - 0 issues
  • packages/opencode/script/publish.ts - 0 issues
  • packages/opencode/src/kilocode/sandbox/mutation.ts - 0 issues
  • packages/opencode/src/kilocode/tool/encoded-io.ts - 0 issues
  • packages/opencode/src/tool/apply_patch.ts - 0 issues
  • packages/opencode/test/kilocode/cli/install-artifact.test.ts - 0 issues
  • packages/opencode/test/kilocode/sandbox/macos-confinement.test.ts - 0 issues
  • packages/opencode/test/kilocode/test-profile.test.ts - 0 issues

Previous review (commit 10b06b9)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/tool/apply_patch.ts 246 Batched raw writes can succeed for multiple files before a formatter failure aborts the later event/LSP notifications, leaving downstream state stale for already-modified files.
packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt 101 The worker sidecar is always refreshed, but the cached CLI binary can still be reused by size, allowing an upgraded install to run an old binary with a new worker.
Files Reviewed (21 files)
  • packages/core/src/filesystem.ts - 0 issues
  • packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt - 1 issue
  • packages/kilo-jetbrains/script/build.ts - 0 issues
  • packages/kilo-sandbox/package.json - 0 issues
  • packages/kilo-sandbox/src/index.ts - 0 issues
  • packages/kilo-sandbox/src/mutation-protocol.ts - 0 issues
  • packages/kilo-sandbox/src/mutation-worker.ts - 0 issues
  • packages/kilo-sandbox/src/mutation.ts - 0 issues
  • packages/kilo-sandbox/test/filesystem.test.ts - 0 issues
  • packages/kilo-sandbox/test/mutation-worker.test.ts - 0 issues
  • packages/kilo-vscode/script/local-bin.ts - 0 issues
  • packages/kilo-vscode/src/services/cli-backend/cli-resources.ts - 0 issues
  • packages/kilo-vscode/tests/unit/server-manager-utils.test.ts - 0 issues
  • packages/opencode/script/build.ts - 0 issues
  • packages/opencode/script/postinstall.mjs - 0 issues
  • packages/opencode/script/publish.ts - 0 issues
  • packages/opencode/src/index.ts - 0 issues
  • packages/opencode/src/kilocode/sandbox/mutation.ts - 0 issues
  • packages/opencode/src/tool/apply_patch.ts - 1 issue
  • packages/opencode/test/cli/install-artifact.test.ts - 0 issues
  • packages/opencode/test/kilocode/sandbox/macos-confinement.test.ts - 0 issues

Previous review (commit e4e8d43)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/kilo-sandbox/src/mutation.ts
  • packages/kilo-sandbox/test/backend.test.ts

Previous review (commit 06fac86)

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-sandbox/src/mutation.ts 139 Early stdin failures can hide the worker's real stderr/exit status and report only a pipe error.
Files Reviewed (2 files)
  • packages/kilo-sandbox/src/mutation.ts - 1 issue
  • packages/kilo-sandbox/test/backend.test.ts - 0 issues

Previous review (commit e232ca9)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • packages/kilo-sandbox/src/mutation-worker.ts
  • packages/kilo-sandbox/test/filesystem.test.ts
  • packages/opencode/test/kilocode/sandbox/session-tools.test.ts

Previous review (commit 76a9d9b)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/index.ts 50 Forcing process.exit(0) after importing the mutation worker can terminate before the JSON response drains to stdout.

SUGGESTION

File Line Issue
packages/kilo-sandbox/test/filesystem.test.ts 157 The preserve-timestamps test stats source, so it never verifies the copied tree kept its timestamps.
Files Reviewed (15 files)
  • .changeset/secure-sandbox-mutations.md - 0 issues
  • .github/workflows/test.yml - 0 issues
  • packages/core/src/filesystem.ts - 0 issues
  • packages/kilo-sandbox/package.json - 0 issues
  • packages/kilo-sandbox/src/backend.ts - 0 issues
  • packages/kilo-sandbox/src/filesystem.ts - 0 issues
  • packages/kilo-sandbox/src/index.ts - 0 issues
  • packages/kilo-sandbox/src/mutation-protocol.ts - 0 issues
  • packages/kilo-sandbox/src/mutation-worker.ts - 0 issues
  • packages/kilo-sandbox/src/mutation.ts - 0 issues
  • packages/kilo-sandbox/test/filesystem.test.ts - 1 issue
  • packages/opencode/script/build-node.ts - 0 issues
  • packages/opencode/script/build.ts - 0 issues
  • packages/opencode/src/index.ts - 1 issue
  • packages/opencode/test/kilocode/sandbox/macos-confinement.test.ts - 0 issues

Reviewed by gpt-5.4-20260305 · Input: 53.5K · Output: 4.4K · Cached: 197.4K

Review guidance: REVIEW.md from base branch main

Comment thread packages/kilo-sandbox/src/mutation.ts Outdated
Comment thread packages/kilo-sandbox/src/mutation.ts Outdated
@marius-kilocode

Copy link
Copy Markdown
Collaborator Author

I profiled the filesystem mutation path before and after this change in the same disposable macOS repository through an isolated VS Code extension instance.

The repository had experimental.sandbox: true in kilo.json, so backend sandbox enforcement was active in both runs. The internal VS Code setting kilo-code.new.internal.sandboxControls was not enabled; that setting only exposes the UI toggle and does not control backend enforcement.

Tool durations come from persisted tool-part timestamps. Parallel envelope measures the interval from the earliest call start to the latest call completion.

Scenario Before Hardened Increase
Single Write 17 ms 2,602 ms +2,585 ms
Parallel Write, average per call 23 ms 1,434 ms +1,411 ms
Parallel Write, six-call envelope 138 ms 1,846 ms +1,708 ms
Parallel Edit, average per call 21 ms 1,078 ms +1,056 ms
Parallel Edit, six-call envelope 118 ms 2,294 ms +2,176 ms
ApplyPatch, two files 19 ms 2,321 ms +2,302 ms

I also measured the compiled one-shot worker directly over 10 runs:

  • Minimum: 728 ms
  • Median: 804 ms
  • Average: 848 ms
  • Maximum: 1,133 ms

The VS Code renderer profiles showed that frontend CPU was not the bottleneck. Most of the regression is process startup: each finite mutation launches the full compiled Kilo binary under sandbox-exec. Calls can overlap, but parallel worker startup adds contention. A Write that also creates its parent directory can require multiple confined mutations, while ApplyPatch pays roughly one worker startup per changed file.

These are single-run before/after tool measurements plus a 10-run direct worker sample, so they are directional rather than statistically complete. The result is still clear: the security boundary works, but the current full-binary one-shot worker has a material interactive performance cost that should be optimized before treating this design as final.

@marius-kilocode

Copy link
Copy Markdown
Collaborator Author

Updated performance results after replacing full CLI initialization with the bundled sandbox-mutation-worker.js and batching ordered mutations.

This supersedes the performance conclusion in my earlier benchmark comment. These measurements use the final packaged kilo binary and its adjacent bundled worker. Each scenario has 20 samples.

Scenario Original implementation Full-CLI worker Bundled worker + batching
Single Write (mkdir + write) 17 ms 2,602 ms 138 ms median
Two-file ApplyPatch 19 ms 2,321 ms 135 ms median
Six parallel Writes, batch envelope 138 ms 1,846 ms 184 ms median

Final packaged-worker distributions:

Scenario Min Median Average Max
Single Write 129 ms 138 ms 148 ms 297 ms
Two-file ApplyPatch 128 ms 135 ms 140 ms 188 ms
Six parallel Writes 161 ms 184 ms 194 ms 261 ms

Relative to the full-CLI worker, this is approximately:

  • 94.7% faster for a single Write.
  • 94.2% faster for a two-file ApplyPatch.
  • 90.0% faster for the six-parallel-Write envelope.

The optimized design keeps the one-shot process boundary and call-local OS sandbox profile. The change is that the child now loads only a roughly 4 KB bundled mutation worker instead of initializing the complete CLI application graph. Ordered batching also lets directory creation, writes, mode changes, and multi-file ApplyPatch operations share one confined worker launch while the kernel still checks every filesystem syscall.

Compared with the original unconfined implementation, the remaining measured median cost is approximately 116-121 ms for sequential tool operations and 46 ms across the six-write parallel envelope.

Measurement limitation: the latest 20-sample numbers isolate the final packaged worker and batch protocol rather than including model latency or VS Code rendering. The earlier original/full-worker values are persisted real tool durations from the isolated VS Code benchmark. The real macOS confinement suite remains the functional security validation for the optimized path.

Comment thread packages/opencode/src/tool/apply_patch.ts Outdated
Comment thread packages/kilo-sandbox/src/mutation.ts Outdated
@marius-kilocode marius-kilocode merged commit 3063fde into main Jun 24, 2026
27 checks passed
@marius-kilocode marius-kilocode deleted the fix-sandbox-toctou-hardening branch June 24, 2026 08:20
marius-kilocode added a commit that referenced this pull request Jun 29, 2026
PR #11591 rewrote build.ts during the sandbox refactor, which recreated the file with default 644 permissions and dropped the executable bit (100755 -> 100644). The same change stripped the bit from build-node.ts and publish.ts.

The pre-release publish workflow invokes ./packages/opencode/script/build.ts directly, relying on its #!/usr/bin/env bun shebang. On Linux CI the non-executable mode fails with Permission denied before the build starts.

Restore mode 100755 on all three scripts. Content is unchanged.
vkeerthivikram pushed a commit to vkeerthivikram/kilocode that referenced this pull request Jun 30, 2026
PR Kilo-Org#11591 rewrote build.ts during the sandbox refactor, which recreated the file with default 644 permissions and dropped the executable bit (100755 -> 100644). The same change stripped the bit from build-node.ts and publish.ts.

The pre-release publish workflow invokes ./packages/opencode/script/build.ts directly, relying on its #!/usr/bin/env bun shebang. On Linux CI the non-executable mode fails with Permission denied before the build starts.

Restore mode 100755 on all three scripts. Content is unchanged.
TahsinArafat pushed a commit to TahsinArafat/sleepy-vscode that referenced this pull request Jul 5, 2026
PR Kilo-Org#11591 rewrote build.ts during the sandbox refactor, which recreated the file with default 644 permissions and dropped the executable bit (100755 -> 100644). The same change stripped the bit from build-node.ts and publish.ts.

The pre-release publish workflow invokes ./packages/opencode/script/build.ts directly, relying on its #!/usr/bin/env bun shebang. On Linux CI the non-executable mode fails with Permission denied before the build starts.

Restore mode 100755 on all three scripts. Content is unchanged.
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.

fix(cli): close built-in file-tool symlink TOCTOU race

2 participants