Skip to content

feat: migrate Linux sandbox to bwrap, add doctor command#7

Open
machado144 wants to merge 1 commit intomainfrom
feat/bwrap-sandbox
Open

feat: migrate Linux sandbox to bwrap, add doctor command#7
machado144 wants to merge 1 commit intomainfrom
feat/bwrap-sandbox

Conversation

@machado144
Copy link
Contributor

Summary

  • Phase 1: Replaces the unshare + shell-script sandbox with Bubblewrap (bwrap) for the non-network path. deny_read, deny_exec, and config-dir protection are now declarative bind mounts — no shell escaping, no injection risk, symlinks resolved automatically.
  • Phase 2: Network-filtered path (allow_net) no longer uses nested unshare --net inside bwrap's user namespace (fails with EPERM on modern kernels). Instead, bwrap's --unshare-net + --info-fd are used: Go reads the child PID from the info pipe and launches slirp4netns from host-side. Adds --uid 0 --gid 0 --cap-add cap_net_admin,cap_sys_admin so iptables (nf_tables) works inside bwrap (bwrap drops all caps by default; nf_tables requires UID 0 in the user namespace).
  • Phase 3: aigate doctor command — checks bwrap, slirp4netns, setfacl, and user-namespace availability, reports versions/paths, and shows exactly which isolation mode will be active.
  • Fix: shellEscape was concatenating args with raw spaces; fixed with shellQuote() (single-quote escaping).

Root cause details (Phase 2 EPERM)

Inside bwrap's user namespace, unshare --net fails because bwrap drops all effective capabilities and maps the caller as UID 1000 (not root). Creating a network namespace via unshare --net alone requires UID 0 in the user namespace for nf_tables. The fix uses bwrap's own --unshare-net flag so the namespace is created in the correct context, and adds the two minimal capabilities (cap_net_admin, cap_sys_admin) needed for iptables and bind-mount setup inside the inner script.

Test plan

  • go test ./... -count=1 — 157 tests pass
  • go test ./... -short -count=1 — 156 pass (real-exec test correctly skipped)
  • go vet ./... — clean
  • aigate doctor — shows bwrap v0.10.0, slirp4netns v1.3.1, isolation mode: bwrap + slirp4netns
  • aigate run -- true — exits 0, no unshare: unshare failed error
  • aigate run -- sh -c 'python3 -c "import urllib.request; urllib.request.urlopen(\"https://api.github.com\")"' — HTTP 200 (allowed host)
  • aigate run -- sh -c 'python3 -c "import urllib.request; urllib.request.urlopen(\"https://example.com\")"' — connection rejected (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

StructLint — All checks passed

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

View full run · Powered by StructLint

Copy link

@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.

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