feat: migrate Linux sandbox to bwrap, add doctor command#7
Open
machado144 wants to merge 1 commit intomainfrom
Open
feat: migrate Linux sandbox to bwrap, add doctor command#7machado144 wants to merge 1 commit intomainfrom
machado144 wants to merge 1 commit intomainfrom
Conversation
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>
StructLint — All checks passed76 rules validated against
|
There was a problem hiding this comment.
Core Changes
- Introduced a new
aigate doctorcommand to check sandbox prerequisites and report the active isolation mode on Linux and macOS. - Migrated the Linux sandbox implementation from shell-script-based
unshareto Bubblewrap (bwrap) for improved robustness and security. This includes declarative bind mounts fordeny_read,deny_exec, and config directory protection. - Reworked the network-filtered sandbox on Linux to use
bwrap's native--unshare-netand--info-fdin conjunction withslirp4netns, resolving anEPERMissue encountered with nestedunshare --netin bwrap's user namespace. - Fixed a shell escaping vulnerability in
shellEscapeby introducingshellQuoteto 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
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.allow_net) no longer uses nestedunshare --netinside bwrap's user namespace (fails with EPERM on modern kernels). Instead, bwrap's--unshare-net+--info-fdare used: Go reads the child PID from the info pipe and launchesslirp4netnsfrom host-side. Adds--uid 0 --gid 0 --cap-add cap_net_admin,cap_sys_adminsoiptables(nf_tables) works inside bwrap (bwrap drops all caps by default; nf_tables requires UID 0 in the user namespace).aigate doctorcommand — checksbwrap,slirp4netns,setfacl, and user-namespace availability, reports versions/paths, and shows exactly which isolation mode will be active.shellEscapewas concatenating args with raw spaces; fixed withshellQuote()(single-quote escaping).Root cause details (Phase 2 EPERM)
Inside bwrap's user namespace,
unshare --netfails because bwrap drops all effective capabilities and maps the caller as UID 1000 (not root). Creating a network namespace viaunshare --netalone requires UID 0 in the user namespace for nf_tables. The fix uses bwrap's own--unshare-netflag 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 passgo test ./... -short -count=1— 156 pass (real-exec test correctly skipped)go vet ./...— cleanaigate doctor— shows bwrap v0.10.0, slirp4netns v1.3.1, isolation mode: bwrap + slirp4netnsaigate run -- true— exits 0, nounshare: unshare failederroraigate 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