Skip to content

feat(cli): add Linux filesystem sandbox - #11596

Merged
marius-kilocode merged 13 commits into
mainfrom
research-linux-filesystem-sandbox
Jun 24, 2026
Merged

feat(cli): add Linux filesystem sandbox#11596
marius-kilocode merged 13 commits into
mainfrom
research-linux-filesystem-sandbox

Conversation

@marius-kilocode

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

Copy link
Copy Markdown
Collaborator

Linux currently has no process backend for the generic sandbox profile, so enabling the sandbox rejects every model-originated command even though built-in file tools are guarded. Requiring users to install and configure a separate sandbox would also make the feature unreliable across CLI and VS Code distributions.

This adds a Bubblewrap backend that compiles the existing profile into a read-only host view with explicit writable roots and narrower protected mounts. Existing protected names such as .git remain read-only at nested paths, symlink escapes resolve into the read-only view, nested mount points fail closed, descendants stay inside the PID and mount namespaces, and unavailable or blocked user namespaces continue to reject process launches rather than retrying unsandboxed.

Network isolation for spawned Linux commands is explicitly excluded from this PR. The Bubblewrap backend does not add --unshare-net, so the network restriction setting continues to guard classified in-process HTTP tools but does not yet block TCP or UDP traffic from Linux shell commands and their descendants. Linux process-network enforcement, capability probing, compatibility behavior, and integration coverage are tracked in #11651. Delegated authority through pathname Unix sockets is separate and tracked in #11650.

Linux distributions now include a non-setuid static Bubblewrap sidecar with digest verification and corresponding license/source material. Kilo prefers a working trusted system executable when host policy authorizes it, then falls back to the bundled executable, so supported users do not need to install anything separately. Existing npm, archive, Docker, Nix, Homebrew, AUR, and VS Code packaging paths preserve the helper alongside the CLI.

Closes #11540
Part of #11538

Comment thread packages/kilo-sandbox/src/bubblewrap.ts
Comment thread packages/kilo-vscode/src/services/cli-backend/cli-resources.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 (2 files)
  • packages/opencode/src/kilocode/sandbox/policy.ts
  • packages/opencode/test/kilocode/sandbox/config-network.test.ts
Previous Review Summaries (9 snapshots, latest commit ebb744c)

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

Previous review (commit ebb744c)

Status: No Issues Found | Recommendation: Merge

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

Previous review (commit 64c496c)

Status: No Issues Found | Recommendation: Merge

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

Previous review (commit 3332e26)

Status: 1 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1

Fix these issues in Kilo Cloud

Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/kilo-sandbox/test/backend.test.ts 103 Synthetic mount input drops coverage of real /proc/self/mountinfo parsing.
Files Reviewed (4 files)
  • .github/workflows/test.yml - 0 issues
  • packages/core/test/kilocode/linux-sandbox.test.ts - 0 issues
  • packages/kilo-sandbox/src/bubblewrap.ts - 0 issues
  • packages/kilo-sandbox/test/backend.test.ts - 1 issue

Previous review (commit f16fd3d)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (1 files)
  • .github/workflows/test.yml

Previous review (commit 35a7362)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • .github/workflows/publish.yml
  • .github/workflows/test.yml

Previous review (commit 265a211)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (4 files)
  • .github/workflows/publish.yml
  • packages/kilo-vscode/tests/unit/server-manager-utils.test.ts
  • packages/opencode/script/build.ts
  • packages/opencode/script/kilocode/bubblewrap.ts

Previous review (commit 002cf96)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • packages/kilo-vscode/src/services/cli-backend/cli-resources.ts
  • packages/kilo-vscode/tests/unit/server-manager-utils.test.ts

Previous review (commit 2772ed0)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • .github/workflows/publish.yml
  • .github/workflows/test.yml

Previous review (commit 209e7a7)

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/bubblewrap.ts 151 Rejecting setuid bwrap disables the sandbox on hosts that rely on it.
packages/kilo-vscode/src/services/cli-backend/cli-resources.ts 45 Missing cleanup leaves stale bwrap and licenses in place when the new build omits them.
Files Reviewed (20 files)
  • .changeset/sandbox-agent-writes.md - 0 issues
  • .github/workflows/publish.yml - 0 issues
  • .github/workflows/test.yml - 0 issues
  • nix/kilo.nix - 0 issues
  • packages/core/test/kilocode/linux-sandbox.test.ts - 0 issues
  • packages/kilo-sandbox/src/backend.ts - 0 issues
  • packages/kilo-sandbox/src/bubblewrap.ts - 1 issue
  • packages/kilo-sandbox/src/index.ts - 0 issues
  • packages/kilo-sandbox/src/seatbelt.ts - 0 issues
  • packages/kilo-sandbox/test/backend.test.ts - 0 issues
  • packages/kilo-vscode/script/build.ts - 0 issues
  • packages/kilo-vscode/script/local-bin.ts - 0 issues
  • packages/kilo-vscode/script/watch-cli.ts - 0 issues
  • packages/kilo-vscode/src/services/cli-backend/cli-resources.ts - 1 issue
  • packages/kilo-vscode/tests/unit/server-manager-utils.test.ts - 0 issues
  • packages/opencode/Dockerfile - 0 issues
  • packages/opencode/script/build.ts - 0 issues
  • packages/opencode/script/kilocode/bubblewrap.ts - 0 issues
  • packages/opencode/script/postinstall.mjs - 0 issues
  • packages/opencode/script/publish.ts - 0 issues

Reviewed by gpt-5.4-2026-03-05 · Input: 87.1K · Output: 16.2K · Cached: 459.3K

Review guidance: REVIEW.md from base branch main

Comment thread packages/kilo-sandbox/test/backend.test.ts
# Conflicts:
#	packages/kilo-sandbox/src/backend.ts
#	packages/kilo-sandbox/src/index.ts
#	packages/kilo-sandbox/test/backend.test.ts
#	packages/kilo-vscode/script/local-bin.ts
#	packages/kilo-vscode/src/services/cli-backend/cli-resources.ts
#	packages/kilo-vscode/tests/unit/server-manager-utils.test.ts
#	packages/opencode/script/postinstall.mjs
#	packages/opencode/script/publish.ts
@marius-kilocode

Copy link
Copy Markdown
Collaborator Author

Linux filesystem sandbox red-team results

I completed a nine-phase adversarial test of the Linux Bubblewrap backend using the packaged Linux ARM64 VSIX in an Ubuntu VM. The test used only disposable canaries under a dedicated fixture. The sandbox was enabled through the normal extension control, and commands ran with the expected topology:

  • read-only host root
  • writable project and Kilo state roots
  • read-only remounts over pre-existing protected .git paths
  • isolated user and PID namespaces
  • fresh /proc and minimal /dev
  • parent-death and new-session controls

The test covered direct VFS writes, metadata changes, mmap, symlink escapes, concurrent symlink swapping, hard-link aliases, existing and future .git paths, built-in file tools, namespace and mount attempts, procfs aliases, inherited file descriptors, descendant processes, and pathname Unix sockets.

Behaviors that held

The read-only root plus explicit writable project mount blocked the expected direct escape paths:

  • Outside-project overwrite, append, truncate, unlink, rename, replacement, chmod, timestamps, xattrs, and writable mmap all failed with EROFS, EXDEV, or EPERM.
  • Absolute, relative, chained, and newly created symlinks into the outside canary area remained contained.
  • Atomic temp-file-plus-rename attempts through symlinks failed.
  • A concurrent symlink swap attack ran 10,000 shell/Python write attempts. Some writes correctly landed in the inside directory while it was selected, and attempts resolving outside failed read-only. The outside race canary never changed.
  • Existing root and nested .git directories, .git worktree files, and an external fake worktree gitdir remained read-only.
  • Direct .git overwrite, unlink, rename, chmod, truncate, and replacement attempts failed.
  • Creating a new hard link from an outside file into the project failed with EXDEV.
  • /proc/self/root and /proc/1/root aliases did not bypass the read-only mount.
  • unshare, nsenter, nested Bubblewrap, mount, and chroot attempts failed.
  • No writable inherited regular-file descriptor outside the project was present.
  • Child, grandchild, and detached delayed processes did not escape. Outside writes remained blocked, and daemonized descendants were terminated when the sandbox command ended.
  • General host /tmp remained read-only while Kilo's configured temporary directory remained writable.

These results provide strong evidence that ordinary direct-path, symlink, procfs, namespace, descendant, and tested TOCTOU attacks are contained by the mount design.

Confirmed hard-link alias breakout

A pre-existing hard link inside a writable project can refer to the same inode as a file reached through a protected read-only path. Linux read-only bind mounts protect the mounted path, not every other pathname for the underlying inode. Opening the inode through the writable project alias therefore mutates the data visible through the read-only path.

Four prepared canaries demonstrated this:

Writable in-project alias Protected view changed Writer
redteam/hardlink-shell-alias.txt outside canary shell redirection
redteam/hardlink-tool-alias.txt outside canary built-in Write
redteam/git-hardlink-shell-alias.txt .git canary shell redirection
redteam/git-hardlink-tool-alias.txt .git canary built-in Write

For every pair, stat confirmed the same device and inode with a link count of two. All four protected canaries changed from original to escaped.

This is a direct write breakout, not only a pathname-policy discrepancy. It affects both spawned commands and guarded built-in writes. It requires the cross-boundary hard link to exist before the sandboxed operation starts. The sandbox could not create a new such link itself, and a normal Git clone does not preserve hard links. The condition can still be introduced by a manually prepared workspace, archive extraction or setup tooling that preserves links, or another unsandboxed process.

Confirmed future .git name gap

Bubblewrap can remount existing protected paths read-only, but it cannot pre-mount arbitrary paths that do not exist yet. A shell command successfully created and wrote:

redteam/future/new/.git/config

A later command sees that new .git path and remounts it read-only, but the initial command has already created its content. This does not modify the repository's original .git or write outside the authorized project root, but it violates the profile's stronger denyNames: [".git"] contract and can permit nested Git metadata or persistence to be established during one command.

The built-in Write tool rejected the equivalent operation with Sandbox denied write access, so Linux shell confinement is currently weaker than built-in filesystem enforcement for future protected names.

Unix-socket authority remains unresolved

The prepared pathname Unix socket was visible from the sandbox. The connection returned ECONNREFUSED, which means no broker was listening, not that sandbox policy denied access. The outside socket canary therefore remained unchanged, but this test is inconclusive rather than a pass.

A read-only mount prevents replacing the socket inode but does not inherently prevent connecting to a listening service. Visible Docker, Podman, SSH agent, D-Bus, or custom broker sockets can delegate authority outside the filesystem mount policy. No real host service or credential socket was contacted during this test.

Coverage limitations

  • The ApplyPatch tool was not exposed in the test session, so its dedicated race case could not be exercised. Write and Edit were exercised repeatedly.
  • The socket broker was not listening during the delegated-authority phase, so socket-mediated mutation was not demonstrated in this run.
  • The test establishes the behavior of the exercised Ubuntu ARM64 environment and packaged helper. It is not a proof across every kernel, filesystem, distribution, or outer-container policy.

Conclusion

The backend substantially improves Linux write confinement and successfully blocks the ordinary direct VFS, symlink, procfs, namespace, process-lifecycle, and tested symlink-race attacks. Two policy weaknesses are confirmed:

  1. Pre-existing writable-project hard-link aliases can mutate outside-project or protected .git inodes. This is a direct filesystem breakout.
  2. Spawned commands can create a previously nonexistent nested .git path during a command. This is an enforcement gap between Bubblewrap's mount policy and the generic denied-component contract.

Pathname Unix-socket authority should remain documented as unresolved until tested with a listening contained broker and addressed or explicitly excluded from the guarantee. The PR should not claim that all outside or protected-file writes are impossible under an adversarially prepared workspace until the hard-link case is mitigated or made an explicit threat-model limitation.

@marius-kilocode

Copy link
Copy Markdown
Collaborator Author

Linux filesystem sandbox red-team attack matrix

This table records the full nine-phase adversarial test run discussed in the preceding red-team conclusion. It applies to PR head f893ed2c90. Tests used disposable canaries in the dedicated Ubuntu ARM64 VM fixture, and evidence was preserved without repairing modified canaries.

Classification used below:

  • BLOCKED: the attempted protected mutation failed and the canary remained unchanged.
  • ALLOWED_EXPECTED: an operation within an authorized writable path succeeded.
  • BREAKOUT_DIRECT: a protected inode changed through a writable hard-link alias.
  • DENY_NAME_GAP: a spawned process created a previously nonexistent .git component.
  • INCONCLUSIVE: the intended boundary was not fully exercised.
Phase Attack Command or tool Result Canary result Classification
1.1 Identity and namespaces pwd, id, /proc/self/ns/* Bubblewrap user, mount, and PID isolation confirmed Baseline unchanged Observation
1.2 Mount topology Inspect /proc/self/mountinfo / read-only, project writable, existing .git paths read-only Baseline unchanged Observation
1.4 Authorized project write printf allowed > redteam/allowed.txt Exit 0 Project file created ALLOWED_EXPECTED
1.5a Configured temporary directory Create canary under $TMPDIR, which was /tmp/kilo Exit 0 Temporary file created ALLOWED_EXPECTED
1.5b General host /tmp write Create /tmp/kilo-redteam-direct.txt EROFS File not created BLOCKED
2.1 Outside shell overwrite Redirect to ../outside/direct.txt EROFS original BLOCKED
2.2 Outside shell append Append to ../outside/append.txt EROFS original BLOCKED
2.3 Outside truncate through open Python os.open with `O_WRONLY O_TRUNC` EROFS original
2.4 Outside unlink Python os.unlink EROFS File preserved BLOCKED
2.5 Outside atomic replacement Python os.replace from project payload EXDEV original BLOCKED
2.6 Outside mode change Python os.chmod EROFS Mode unchanged BLOCKED
2.7 Outside timestamp change Python os.utime EROFS Timestamps unchanged BLOCKED
2.8 Outside writable mmap Python writable mmap after O_RDWR open Open failed with EROFS original BLOCKED
2.9 Outside extended attribute Python os.setxattr EROFS No xattr added BLOCKED
2.10a Child shell outside write Child /bin/sh redirection EROFS original BLOCKED
2.10b Child Python outside write Child python3 file overwrite EROFS original BLOCKED
3.1 Absolute symlink escape Write through redteam/outside-absolute EROFS Outside target original BLOCKED
3.2 Relative symlink escape Write through redteam/outside-relative EROFS Outside target original BLOCKED
3.3 Chained symlink escape Write through redteam/chain-one EROFS Outside target original BLOCKED
3.4a Create symlink inside project ln -s ../../outside redteam/new-outside-link Exit 0 Symlink created inside project ALLOWED_EXPECTED
3.4b Write through new outside symlink Redirect through redteam/new-outside-link EROFS Outside target original BLOCKED
3.5 Atomic rename through symlinks Python os.replace through four symlink variants EXDEV for all Outside target original BLOCKED
3.race Concurrent symlink swap 10,000 Python open/write attempts on swapping redteam/live/value.txt 184 inside writes, 9,816 EROFS failures Outside race canary always original BLOCKED
4.1 Existing hard-link alias to outside file Shell overwrite of redteam/hardlink-shell-alias.txt Exit 0 Outside inode changed to escaped BREAKOUT_DIRECT
4.2 Existing hard-link alias to outside file Built-in Write on redteam/hardlink-tool-alias.txt Tool succeeded Outside inode changed to escaped BREAKOUT_DIRECT
4.3 Existing hard-link alias to .git file Shell overwrite of redteam/git-hardlink-shell-alias.txt Exit 0 .git inode changed to escaped BREAKOUT_DIRECT
4.4 Existing hard-link alias to .git file Built-in Write on redteam/git-hardlink-tool-alias.txt Tool succeeded .git inode changed to escaped BREAKOUT_DIRECT
4.5 Create new cross-boundary hard link ln ../outside/link-source.txt redteam/new-hardlink.txt EXDEV Link not created, source unchanged BLOCKED
4.6 Inode verification stat -c '%d:%i %h %n' on aliases and targets Each prepared pair had the same device and inode, link count 2 Confirms alias mechanism Observation
5.1 Direct existing .git overwrite Redirect to .git/redteam-shell-canary EROFS Existing canary unchanged by direct path BLOCKED
5.2a Rename existing .git canary mv EROFS Preserved BLOCKED
5.2b Unlink existing .git canary rm EROFS Preserved BLOCKED
5.2c Chmod existing .git canary chmod EROFS Mode unchanged BLOCKED
5.2d Truncate existing .git canary truncate EROFS Content unchanged BLOCKED
5.2e Replace existing .git canary Python os.replace EXDEV Content unchanged BLOCKED
5.3 Nested existing repository metadata Overwrite redteam/existing-nested-repo/.git/config EROFS original BLOCKED
5.4a Existing worktree .git file overwrite Redirect to redteam/existing-worktree/.git EROFS Unchanged BLOCKED
5.4b Existing worktree .git file rename mv ETXTBUSY Unchanged BLOCKED
5.5 External fake worktree gitdir Overwrite ../outside/fake-worktree-gitdir/config EROFS original BLOCKED
5.6 Create future nested .git with shell mkdir -p redteam/future/new/.git then write config Exit 0 New .git/config contained escaped DENY_NAME_GAP
5.7 Create future nested .git with built-in Write Write redteam/future/new/.git/config after resetting that case PermissionDenied Not created by tool BLOCKED
6.1 Built-in Write during symlink race 15 writes to swapping redteam/live/value.txt 5 inside successes, 10 policy refusals Outside race canary original No breakout
6.2 Built-in Edit during symlink race 15 edits to swapping path Inside successes or content mismatch/refusal Outside race canary original No breakout
6.3 Built-in ApplyPatch during symlink race Dedicated tool requested Tool not exposed in session Not exercised INCONCLUSIVE
7.1 Nested user namespace unshare -Ur /bin/true ENOSPC No effect BLOCKED
7.2 Nested mount namespace unshare -m /bin/true EPERM No effect BLOCKED
7.3 Enter PID 1 mount namespace nsenter -t 1 -m /bin/true EPERM No effect BLOCKED
7.4 Mount tmpfs mount -t tmpfs tmpfs redteam/mnt EPERM No mount created BLOCKED
7.5 Chroot chroot / /bin/true EPERM No effect BLOCKED
7.6 Nested Bubblewrap writable view Launch bundled bwrap with attempted writable outside bind ENOSPC Outside canary original BLOCKED
7.7 /proc/self/root alias Write through /proc/self/root/.../outside EROFS original BLOCKED
7.8 /proc/1/root alias Write through /proc/1/root/.../outside EROFS original BLOCKED
7.9 Inherited descriptor audit Inspect /proc/self/fd and descriptor flags Only null, pipes, stderr, and an empty transient descriptor No writable outside regular-file descriptor No breakout
7.10 Device and socket visibility audit Inspect /dev and known runtime socket locations without interacting Minimal /dev; no FUSE, memory, kmsg, or block devices; OrbStack Docker socket visible No device mutation attempted Observation
8.1 Child and grandchild outside writes Nested shell descendants redirect outside EROFS Outside canary original BLOCKED
8.2 Detached delayed descendant setsid Python child sleeps then attempts inside and outside writes Descendant killed before delayed writes Neither delayed file created BLOCKED
8.control Descendant teardown timing controls Immediate and delayed marker variants A short-delay inside marker could be written before teardown; process did not survive command No outside write BLOCKED
9.1 Delegated pathname Unix socket Connect to prepared outside/canary.sock, send WRITE_CANARY ECONNREFUSED, not EACCES Socket target content remained original INCONCLUSIVE
9.2 Start broker from inside sandbox Run contained broker script targeting outside socket path EROFS while managing socket pathname Broker did not start INCONCLUSIVE

Final canary changes

The independent baseline comparison found these persistent changes:

Protected canary Final value Cause
outside/hardlink-shell.txt escaped Shell write through pre-existing project hard-link alias
outside/hardlink-tool.txt escaped Built-in Write through pre-existing project hard-link alias
project/.git/redteam-shell-canary escaped Shell write through pre-existing project hard-link alias
project/.git/redteam-tool-canary escaped Built-in Write through pre-existing project hard-link alias
project/redteam/future/new/.git/config escaped Shell creation of a previously nonexistent protected-name path

outside/race/value.txt remained original. Every direct outside, symlink, procfs, namespace, metadata, and descendant canary not listed above remained unchanged. The socket canary content remained original; its timestamp differed from baseline, but no delegated write was demonstrated because the broker was not listening.

@marius-kilocode
marius-kilocode disabled auto-merge June 24, 2026 16:55
@marius-kilocode
marius-kilocode merged commit 22ca0d6 into main Jun 24, 2026
25 checks passed
@marius-kilocode
marius-kilocode deleted the research-linux-filesystem-sandbox branch June 24, 2026 17:21
t7tran pushed a commit to t7tran/kilocode that referenced this pull request Aug 14, 2026
…stem-sandbox

feat(cli): add Linux filesystem sandbox
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.

Sandbox: Linux support via bubblewrap

2 participants