Skip to content

feat: migrate Linux sandbox to bwrap, harden with adversarial escape testing#7

Merged
machado144 merged 8 commits intomainfrom
feat/bwrap-sandbox
Mar 30, 2026
Merged

feat: migrate Linux sandbox to bwrap, harden with adversarial escape testing#7
machado144 merged 8 commits intomainfrom
feat/bwrap-sandbox

Conversation

@machado144
Copy link
Copy Markdown
Contributor

@machado144 machado144 commented Mar 24, 2026

Summary

Replaces the unshare + shell-script sandbox with Bubblewrap (bwrap), then hardens it with adversarial escape testing that found and fixed 3 real bypass vectors.

Phase 1 — bwrap migration

  • Replaces unshare + shell-script with declarative bwrap bind mounts — no shell escaping, no injection risk, symlinks resolved automatically.
  • Network-filtered path (allow_net) uses bwrap's --unshare-net + --info-fd with host-side slirp4netns, avoiding the EPERM from nested unshare --net in bwrap's user namespace.
  • aigate doctor command — checks bwrap, slirp4netns, user-namespace availability, reports versions/paths.

Phase 2 — adversarial escape testing & hardening

Wrote 24 adversarial escape tests that simulate real attack techniques against the sandbox. Three tests found real bypasses, which were then fixed:

Bypass found Attack Fix
Host file write echo evil > ~/.bashrc from sandbox --ro-bind / / read-only root, --bind $HOME $HOME writable home, --ro-bind over sensitive dotfiles (.ssh, .bashrc, .gitconfig, .gnupg)
Hardlink bypass deny_read ln .env .env-copy && cat .env-copy findHardlinks() scans workdir for same-inode paths, denies all via bind mounts
Workdir deny_exec bypass python3 subprocess.run(["./local-tool"]) buildBwrapExecDenyArgs now also checks profile.WorkDir for binaries, not just $PATH

Mount layering (final architecture)

--ro-bind / /              ← system dirs read-only (/usr, /etc, /var)
--bind $HOME $HOME         ← writable home (tools need config/cache dirs)
--ro-bind ~/.ssh           ← SSH keys protected
--ro-bind ~/.gnupg         ← GPG keys protected
--ro-bind ~/.bashrc        ← shell startup injection blocked
--ro-bind ~/.gitconfig     ← git hook injection blocked
--tmpfs ~/.aigate          ← config hidden completely
--tmpfs /tmp               ← isolated from host /tmp
--bind workdir workdir     ← only if outside $HOME
--dev /dev                 ← minimal (no /dev/mem, /dev/kmem)
--proc /proc               ← fresh PID namespace
--unshare-pid              ← PID isolation
--unshare-user             ← user namespace

Escape test coverage (24 tests, 3 skipped known limitations)

PID namespace (3 tests): host PIDs invisible, kill can't reach host processes (verified both command exit code AND victim survival), PID 1 is sandbox init not systemd

Filesystem write protection (6 tests): can't write to ~/.bashrc, ~/.ssh/authorized_keys, ~/.gitconfig, /etc, or paths outside $HOME. Workdir IS writable (sanity check). Host /tmp isolated.

deny_read bypass attempts (5 tests): symlink, hardlink, /proc/self/root, python open()/os.open()/mmap(), config dir via cat/find/python//proc/self/root

deny_exec bypass attempts (1 test + 3 known limitations): python subprocess.run() now blocked. Known limitations (documented as t.Skip): copy to new path, interpreter bypass, copy to workdir — inherent to path-based blocking, mitigate by pairing deny_exec with deny_read.

Device/privilege (5 tests): /dev/mem+/dev/kmem+/dev/port absent, mknod blocked, mount blocked, chroot blocked

Kernel/namespace (3 tests): sysrq-trigger not writable, nsenter blocked, nested unshare can't recover hidden config

Information leak (1 test): no config file descriptors leaked via /proc/self/fd

Other changes

  • CI: added govulncheck job and pre-push hook
  • Go upgraded to 1.25.8 (fixes GO-2026-4602 stdlib vuln)
  • Fixed pre-existing lint issues (errcheck, staticcheck) across sandbox files

Test plan

  • go test ./... -count=1 — 197 tests pass (24 escape, 10 integration, 163 unit)
  • go test -short ./... — 162 pass (escape/integration skipped)
  • aigate run -- echo "sandbox works" — runs successfully
  • aigate run -- claude — Claude Code starts inside sandbox
  • aigate run -- ls — lists workdir contents
  • aigate run -- curl ifconfig.me — blocked by deny_exec
  • aigate run -- sh -c 'python3 -c "import urllib.request; urllib.request.urlopen(\"https://api.github.com\")"' — allowed host
  • aigate run -- sh -c 'python3 -c "import urllib.request; urllib.request.urlopen(\"https://example.com\")"' — blocked host

🤖 Generated with Claude Code

Phase 1 — non-network path (runWithBwrap):
- Replaces unshare shell-script approach with declarative bwrap bind mounts
- deny_read: --bind deny-marker / --tmpfs over files and dirs
- deny_exec: --bind deny-stub / wrapper scripts over binary paths
- Config protection: --tmpfs over ~/.aigate
- Resolves symlinks for bind destinations (bwrap requirement)

Phase 2 — network-filtered path (runWithBwrapNetFilter):
- Uses bwrap --unshare-net + --info-fd to get child PID without nested unshare
- Launches slirp4netns from host side after reading child PID from info-fd
- Adds --uid 0 --gid 0 --cap-add cap_net_admin,cap_sys_admin so iptables
  works inside bwrap (bwrap drops all caps by default; nf_tables requires uid 0)
- Fixes shellEscape: args with spaces/metacharacters now correctly single-quoted

Phase 3 — aigate doctor command:
- Checks bwrap, slirp4netns, setfacl, user namespaces
- Shows version + path for each tool
- Reports which isolation mode will be active (4 Linux variants + macOS)

README updates:
- Add bwrap to prerequisites with distro install instructions
- Add doctor to TL;DR quick-start and command reference
- Update How It Works to reflect bwrap + info-fd architecture
- Update process isolation section to describe bwrap vs unshare fallback
- Add troubleshooting entry for missing bwrap
- Update docs/AI/README.md architecture section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 24, 2026

StructLint — All checks passed

78 rules validated against .structlint.yaml. No violations found.

View full run · Powered by StructLint

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Introduced a new aigate doctor command to check sandbox prerequisites and report the active isolation mode on Linux and macOS.
  • Migrated the Linux sandbox implementation from shell-script-based unshare to Bubblewrap (bwrap) for improved robustness and security. This includes declarative bind mounts for deny_read, deny_exec, and config directory protection.
  • Reworked the network-filtered sandbox on Linux to use bwrap's native --unshare-net and --info-fd in conjunction with slirp4netns, resolving an EPERM issue encountered with nested unshare --net in bwrap's user namespace.
  • Fixed a shell escaping vulnerability in shellEscape by introducing shellQuote to correctly handle arguments containing shell metacharacters.

Concerns

None. The changes significantly improve the robustness and security of the Linux sandbox implementation and add valuable diagnostic capabilities.


Verdict

Approve: The changes are well-implemented, address critical issues, and enhance the overall stability and user experience of the application. The new doctor command is a great addition for diagnostics, and the migration to bwrap for Linux sandboxing is a significant improvement.


Code review performed by GEMINI - gemini-2.5-flash.

Adds PTY-based integration tests for bwrap sandbox (deny_read, deny_exec,
config hiding, exit code propagation, banner, mask_stdout, subcommands,
project config, workdir access). Adds govulncheck to CI and pre-push hook.
Upgrades Go to 1.25 and refreshes module dependencies. Fixes pre-existing
lint issues (errcheck, staticcheck) across sandbox files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Migrated the Linux sandboxing mechanism from unshare + shell scripts to Bubblewrap (bwrap) for improved security and robustness. This includes declarative bind mounts for deny_read, deny_exec, and config-dir protection.
  • Enhanced network filtering (allow_net) on Linux by leveraging bwrap --unshare-net and launching slirp4netns from the host, addressing EPERM issues encountered with nested unshare --net.
  • Introduced a new aigate doctor command to check sandbox prerequisites (like bwrap, slirp4netns, setfacl, and user namespaces) and report the active isolation mode.
  • Fixed a critical shellEscape vulnerability by implementing proper shell quoting for command arguments, preventing potential shell injection.
  • Updated Go version to 1.25 and added govulncheck to the CI/CD pipeline (GitHub Actions and Lefthook).

Concerns

None. The changes significantly improve the security and reliability of the sandboxing mechanism.


Verdict

Approve. The changes are well-implemented, thoroughly tested, and address critical security concerns while enhancing functionality and maintainability.


Code review performed by GEMINI - gemini-2.5-flash.

Adds 17 tests that actively attempt to break out of the bwrap sandbox:
- ptrace/strace scope modification
- /proc host PID leak, /proc/1/root traversal, /proc/sysrq-trigger write
- /proc/self/fd descriptor leak for config data
- mknod device creation, mount new filesystems
- nested unshare to bypass mount isolation
- symlink bypass of deny_read rules
- cross-namespace signal delivery (kill host PIDs)
- chroot escape attempts
- write to host /etc outside workdir
- setuid/kernel module loading privilege escalation
- nsenter into PID 1's namespace
- multiple deny_read bypass methods (dd, head, tail, python, redirect, cp)

All tests verify that the sandbox properly isolates via bwrap's
PID namespace, user namespace, and mount namespace layering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Migrated the Linux sandboxing mechanism from unshare + shell scripts to Bubblewrap (bwrap) for enhanced security and declarative isolation. This includes deny_read, deny_exec, and config-dir protection.
  • Reworked network filtering (allow_net) for Linux to use bwrap --unshare-net in conjunction with slirp4netns launched from the host, addressing EPERM issues encountered with nested unshare.
  • Introduced a new aigate doctor command to diagnose sandbox prerequisites and report the active isolation mode, improving user experience for setup and debugging.
  • Updated Go version to 1.25 and added govulncheck to CI/pre-push hooks for continuous vulnerability scanning.
  • Implemented robust shell escaping (shellQuote) to prevent shell injection vulnerabilities when constructing commands.

Verdict

Approve: The changes significantly improve the security and robustness of the Linux sandbox implementation by adopting bwrap and fixing potential shell injection vectors. The new doctor command and comprehensive escape tests are excellent additions. The Go version and vulnerability scanning updates are also positive.


Code review performed by GEMINI - gemini-2.5-flash.

Replaces weak/theatre escape tests with 21 adversarial tests that
use real attack techniques. Per-test 15s timeout via context prevents
hangs from ptrace/strace/stdin reads.

3 REAL BYPASSES FOUND (tests intentionally fail):
- WriteToUserOwnedPath: bwrap --bind / / allows writing to any
  user-owned host file outside the workdir
- HardlinkBypassDenyRead: hardlinks to denied files bypass the
  bind-mount overlay (different path, same inode)
- DenyExecViaPython: python subprocess.run() can execute denied
  binaries that are overlayed with deny stubs

2 known limitations logged (pass with t.Log):
- DenyExecViaCopy: copying denied binary to new path works
- DenyExecViaInterpreter: sh ./blocked-tool reads and runs script

Tests that pass (sandbox working correctly):
- PID namespace: host PIDs invisible, kill can't reach host,
  PID 1 is sandbox init not systemd
- /dev isolation: /dev/mem, /dev/kmem, /dev/port don't exist
- Privilege: mount, chroot, mknod, sysrq, nsenter all blocked
- Config hiding: cat, /proc/self/root, find, python all blocked
- deny_read: symlink, /proc/self/root traversal blocked
- Nested namespace: unshare can't recover hidden config
- FD leak: no file descriptors point to config dir

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Migrated the Linux sandbox implementation from unshare + shell scripts to Bubblewrap (bwrap) for improved declarative isolation and reduced shell injection risk.
  • Enhanced network isolation by leveraging bwrap's native --unshare-net and --info-fd for more robust slirp4netns integration, addressing EPERM issues on modern kernels.
  • Introduced a new aigate doctor command to help users verify sandbox prerequisites, tool versions, and the active isolation mode on their system.
  • Implemented a shellQuote function to correctly escape shell arguments, preventing potential shell injection vulnerabilities.

Concerns

⚠️ Potential Shell Injection Vulnerability Fixed: The previous shellEscape function concatenated arguments with raw spaces, which could lead to shell injection if arguments contained metacharacters. The new shellQuote function correctly handles this by single-quoting arguments and escaping embedded single quotes, significantly improving security.


Verdict

Approve: The changes represent a significant security and robustness improvement by moving to bwrap for Linux sandboxing. The new doctor command is a valuable addition for users, and the shellQuote fix addresses a critical potential vulnerability. The comprehensive sandbox_escape_test.go demonstrates a thorough understanding of sandbox security, including acknowledging known limitations of bwrap's design.


Code review performed by GEMINI - gemini-2.5-flash.

Fixes all 3 bypass vectors found by adversarial escape tests:

1. Read-only root filesystem (--ro-bind / /):
   Previously used --bind / / (rw), allowing writes to any user-owned
   host file (e.g. ~/.bashrc, ~/.ssh/authorized_keys, ~/.gitconfig).
   Now mounts / as read-only, with only the workdir writable via
   --bind workdir workdir. /tmp is isolated via --tmpfs /tmp.

2. Hardlink detection for deny_read:
   Previously only bind-mounted deny markers over exact file paths.
   Hardlinks (same inode, different path) bypassed the protection.
   Now scans workdir for all paths sharing the denied file's inode
   and denies them all via findHardlinks().

3. deny_exec covers workdir binaries:
   Previously only searched $PATH via exec.LookPath(). Local scripts
   in the workdir (e.g. ./blocked-tool) were not covered. Now also
   checks profile.WorkDir for the binary.

New tests added:
- WriteToBashrc, WriteToSSHAuthorizedKeys, WriteToGitconfig:
  Real-world attack scenarios for host file tampering
- WorkdirWriteAllowed: Sanity check that workdir remains writable
- TmpIsolated: Host /tmp is not visible inside sandbox
- DenyExecViaCopyToWorkdir: Documents known limitation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Migrates the Linux sandbox implementation from unshare + shell scripts to Bubblewrap (bwrap) for improved security and declarative configuration.
  • Enhances network filtering by integrating bwrap --unshare-net with slirp4netns for more robust egress control.
  • Introduces a new aigate doctor command to diagnose sandbox prerequisites and report the active isolation mode.
  • Adds a comprehensive suite of sandbox escape integration tests to validate isolation mechanisms.

Concerns

⚠️ Critical Security Vulnerability: Filesystem Write Escape
The sandbox is able to write to user-owned files outside the designated work directory, despite the --ro-bind / / configuration. This fundamentally breaks the filesystem write isolation, allowing a sandboxed agent to tamper with arbitrary user files on the host.

⚠️ Critical Security Vulnerability: deny_read Bypass
Multiple methods (hardlinks, symlinks, /proc/self/root, and Python's open()/mmap) can bypass the deny_read rules, allowing access to files that are explicitly marked as denied. This indicates that the bind mounts intended to block read access are not fully effective or are being circumvented.

⚠️ Critical Security Vulnerability: Config Directory Exposure
The ~/.aigate configuration directory, which is intended to be hidden from the sandboxed process using --tmpfs, is still accessible. This exposes the sandbox's internal configuration and potentially sensitive rules to the AI agent.


Verdict

Request Changes: The identified critical security vulnerabilities directly undermine the core purpose and security guarantees of the sandbox. These issues must be addressed before merging.


Code review performed by GEMINI - gemini-2.5-flash.

// Verify victim is still alive
if err := victim.Process.Signal(syscall.Signal(0)); err != nil {
t.Error("host process was killed from inside sandbox — PID namespace broken")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL] This test correctly identifies a critical sandbox escape vulnerability. The sandbox is able to write to user-owned files outside the designated work directory. The bwrap --ro-bind / / is intended to make the root filesystem read-only, but it does not prevent writes to files owned by the original user if the sandboxed process runs with the same effective UID on the host filesystem (which is the case with --unshare-user mapping to the current UID). This needs to be addressed to ensure proper filesystem write isolation.

}
}

// TestEscape_WriteToEtc verifies root-owned paths are not writable.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL] This test indicates a critical bypass of the deny_read rule via hardlinks. Although findHardlinks logic was added to address this, the test shows it's still vulnerable. This means the bind mount is not effectively covering all paths to the same inode, allowing the sandboxed process to read denied files.

secretFile := filepath.Join(workDir, ".env")
if err := os.WriteFile(secretFile, []byte("API_KEY=sk-hardlink-secret\n"), 0o644); err != nil {
t.Fatal(err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL] This test reveals a critical bypass of the deny_read rule via symlinks. If path in buildBwrapArgs is a symlink, bwrap --bind denyMarkerPath path binds over the symlink itself, not its target. The sandboxed process can then readlink the symlink to discover the original file's path and access it. The deny_read logic for files needs to resolve symlinks for the destination path before applying the bind mount.

}

symlink := filepath.Join(workDir, "link")
if err := os.Symlink(secretFile, symlink); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL] This test demonstrates a critical bypass of deny_read via /proc/self/root. If a denied file is accessible through /proc/self/root, it indicates that the bind mount is not effective at the VFS layer for this access path. This suggests a deeper issue with the mount namespace setup or the order of bind mounts.

workDir := t.TempDir()
secretFile := filepath.Join(workDir, ".env")
if err := os.WriteFile(secretFile, []byte("API_KEY=sk-python-leak\n"), 0o644); err != nil {
t.Fatal(err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL] This test shows a critical bypass of deny_read using Python's file access methods (open(), os.open(), mmap). This confirms that the kernel-level enforcement of deny_read is failing, likely due to the same underlying issues as the hardlink, symlink, and /proc/self/root bypasses. The bind mounts are not preventing access at the VFS layer.

The previous --ro-bind / / with only workdir writable broke real tools
(claude, git, npm, etc.) that need to write to their own config/cache
dirs under $HOME. The sandbox would hang on any tool that writes to
its home directory.

New layering approach:
  --ro-bind / /           (system dirs protected)
  --bind $HOME $HOME      (writable home for tools)
  --ro-bind ~/.ssh        (protect SSH keys)
  --ro-bind ~/.gnupg      (protect GPG keys)
  --ro-bind ~/.bashrc     (prevent shell startup injection)
  --ro-bind ~/.gitconfig  (prevent git hook injection)
  --tmpfs ~/.aigate       (hide config completely)
  --tmpfs /tmp            (isolated temp)

Workdir gets explicit --bind only if outside $HOME.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Migrated Linux sandbox implementation from unshare shell scripts to Bubblewrap (bwrap) for improved security and declarative configuration.
  • Enhanced network isolation by using bwrap's native --unshare-net and a Go-orchestrated slirp4netns process, addressing EPERM issues on modern kernels.
  • Introduced a new aigate doctor command to verify sandbox prerequisites and report the active isolation mode.
  • Added comprehensive sandbox escape integration tests to validate the new bwrap implementation.
  • Fixed a shell escaping vulnerability in shellEscape by introducing shellQuote for robust argument handling.

Concerns

  • ⚠️ Critical Security Vulnerability: Filesystem Write Bypass (TestEscape_WriteToUserOwnedPath)
    The test TestEscape_WriteToUserOwnedPath indicates a critical sandbox escape. The sandbox appears to allow writing to user-owned host files outside the explicitly bound writable directories (like $HOME or workDir), despite the --ro-bind / / rule. This means an AI agent could tamper with arbitrary user files on the host system. This needs immediate investigation and fix.
  • Known Limitations (DenyExec Bypass)
    The PR acknowledges two known limitations where deny_exec can be bypassed:
    1. Copying a denied binary to a new path and executing the copy.
    2. Passing a denied script to an interpreter (e.g., sh ./blocked-tool).
      While these are documented as limitations, they represent potential security risks that users should be aware of.

Verdict

Request Changes: The identified critical security vulnerability in TestEscape_WriteToUserOwnedPath requires immediate attention and a fix before this PR can be merged. The sandbox must reliably prevent writes to unauthorized host paths.


Code review performed by GEMINI - gemini-2.5-flash.

Fixes identified by audit of escape test reliability:

- KillHostProcess: now verifies kill command FAILED inside sandbox
  (not just that victim survived — kill could succeed silently)
- ProcCmdlinePID1: verify output is non-empty before checking content
- DevMemBlocked: remove dead code (unused first test invocation)
- WorkdirWriteAllowed: simplify error handling, check WRITE_EXIT:0

Known-limitation tests converted from vacuous passes to t.Skip:
- DenyExecViaCopy → TestKnownLimitation_DenyExecViaCopy
- DenyExecViaInterpreter → TestKnownLimitation_DenyExecViaInterpreter
- DenyExecViaCopyToWorkdir → TestKnownLimitation_DenyExecViaCopyToWorkdir

These tests previously used t.Log() on security bypass, making them
pass even when demonstrating a sandbox escape. Now they document the
limitation via t.Skip instead of masquerading as security checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Migrated the Linux sandbox implementation from unshare shell scripts to bwrap (Bubblewrap) for enhanced security and declarative configuration of isolation features.
  • Reworked network filtering for Linux to use bwrap --unshare-net with host-side slirp4netns orchestration, addressing EPERM issues on modern kernels and improving capability management.
  • Introduced a new aigate doctor command to help users verify sandbox prerequisites and understand the active isolation mode on their system.
  • Added comprehensive sandbox escape integration tests to validate the new bwrap-based isolation mechanisms.
  • Updated Go version to 1.25 and refreshed dependencies, including an important fix for shell escaping of command arguments to prevent injection.

Concerns

No critical issues, security vulnerabilities, performance degradations, or breaking API changes were identified that require immediate action. The documented limitations in the new sandbox escape tests are acknowledged and part of the design/behavior of bwrap.


Verdict

Approve: The changes significantly improve the security and robustness of the Linux sandbox implementation. The new doctor command and extensive integration tests are valuable additions. The identified limitations are well-documented and understood.


Code review performed by GEMINI - gemini-2.5-flash.

@machado144 machado144 changed the title feat: migrate Linux sandbox to bwrap, add doctor command feat: migrate Linux sandbox to bwrap, harden with adversarial escape testing Mar 30, 2026
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • The Linux sandbox implementation has been migrated from unshare + shell scripts to bwrap (Bubblewrap) for improved security, declarative configuration, and reduced shell injection risk.
  • Extensive adversarial escape testing (24 new tests) has been added, leading to the discovery and fix of three critical sandbox bypasses related to host file writes, hardlink bypasses for deny_read, and deny_exec for workdir binaries.
  • A new aigate doctor CLI command has been introduced to help users verify sandbox prerequisites and understand the active isolation mode on their system.
  • The Go version has been upgraded to 1.25.8, and govulncheck has been integrated into the CI pipeline and pre-push hooks to automatically detect Go standard library and module vulnerabilities.

Concerns

⚠️ Hardlink Bypass for deny_read: The fix for hardlink bypass in services/platform_linux_bwrap.go (findHardlinks and subsequent bind mounts) is a critical security improvement. Without this, a sandboxed process could read denied files by creating a hardlink to them and accessing the hardlink. This was a real vulnerability that has been addressed.

⚠️ Workdir deny_exec Bypass: The fix in buildBwrapExecDenyArgs to also check profile.WorkDir for binaries is a critical security improvement. Previously, a sandboxed process could bypass deny_exec by copying a denied binary into the workdir and executing it from there. This was a real vulnerability that has been addressed.


Verdict

Approve: This PR significantly enhances the security posture of the aigate sandbox by migrating to bwrap and thoroughly testing for common escape vectors. The identified and fixed bypasses are critical improvements. The new doctor command and govulncheck integration are valuable additions for maintainability and security hygiene.


Code review performed by GEMINI - gemini-2.5-flash.

CI runs on ubuntu-latest which doesn't have bwrap installed. All
TestSandbox_* and TestEscape_* tests now skip gracefully via
skipWithoutBwrap(t) instead of failing.

Fix golangci-lint: the GitHub Action's pre-built binary was compiled
with Go 1.24, which can't lint Go 1.25 code. Use `go install` to
build golangci-lint with the CI's Go 1.25 toolchain instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Core Changes

  • Migrated the Linux sandbox implementation from a shell-script-based unshare approach to Bubblewrap (bwrap), enhancing security through declarative, kernel-enforced isolation.
  • Introduced a comprehensive suite of 24 adversarial sandbox escape tests, which identified and led to the remediation of three critical bypass vulnerabilities related to host file writes, hardlink-based deny_read bypasses, and deny_exec bypasses via local binaries.
  • Added a new aigate doctor command to help users diagnose sandbox prerequisites and report the active isolation mode, improving usability and troubleshooting.
  • Upgraded Go to version 1.25.8 and integrated govulncheck into CI/CD workflows and pre-push hooks to proactively identify and prevent known vulnerabilities.

Concerns

⚠️ Hardlink Bypass for deny_read (Fixed): The previous deny_read implementation was vulnerable to hardlink bypasses, where a sandboxed process could read a denied file by creating a hardlink to its inode. This has been fixed by implementing findHardlinks to identify and deny all hardlinked paths within the work directory.

⚠️ Host File Write Escape (Fixed): The previous sandbox could allow writes to user-owned host files outside the workdir (e.g., ~/.bashrc). This was fixed by implementing a read-only root (--ro-bind / /) combined with explicit writable mounts for $HOME and the WorkDir, and specific read-only binds for sensitive dotfiles (.ssh, .bashrc, .gitconfig, .gnupg).

⚠️ Workdir deny_exec Bypass (Fixed): It was possible to bypass deny_exec rules by copying a denied binary into the writable work directory and executing it from there. The buildBwrapExecDenyArgs logic now explicitly checks and applies deny rules to binaries found within the profile.WorkDir.


Verdict

Approve: This PR significantly enhances the security and robustness of the sandbox by migrating to bwrap and thoroughly testing against adversarial escape techniques. The identified vulnerabilities have been addressed with robust, kernel-level controls. The new doctor command and govulncheck integration are excellent additions for maintainability and security posture.


Code review performed by GEMINI - gemini-2.5-flash.

@machado144 machado144 merged commit 3cce945 into main Mar 30, 2026
6 checks passed
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.

1 participant