Skip to content

fix(agent-isolation): guard Linux allowAllUnixSockets with denyRead - #1809

Merged
potiuk merged 4 commits into
apache:mainfrom
shahar1:fix/linux-sandbox-socket-exposure
Oct 26, 2026
Merged

potiuk merged 4 commits into
apache:mainfrom
shahar1:fix/linux-sandbox-socket-exposure

Conversation

@shahar1

@shahar1 shahar1 commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Linux allowAllUnixSockets is undocumented and unguarded.
    • On Linux the sandbox's seccomp filter blocks AF_UNIX, unless sandbox.network.allowAllUnixSockets is true. The container gateway and ssh-agent need it, because allowUnixSockets only applies on macOS.
    • With the flag on and deny-only reads, sandboxed code can reach host sockets that run code outside the sandbox: /run/docker.sock, Docker Desktop's /run/docker-cli.sock, all of /run/user/<uid> (D-Bus and systemd/private, i.e. systemd-run --user), /run/dbus, X11, and on WSL /run/WSL interop and /mnt/wslg.
    • This PR documents the flag and pairs it with sandbox.filesystem.denyRead entries for those paths, and names AF_VSOCK as the residual risk denyRead cannot hide. It also makes verify check 12c Linux-consistent and adds a check 12e that requires the socket denyRead coverage whenever the flag is on, even when check 12 is otherwise n/a. Install Step L.3 and tools/sandbox-lint get matching rules and tests.
  • A parent allowRead makes the project read-only. An allowRead entry that is a strict parent of an allowWrite path makes bubblewrap mount the write path read-only. Every Bash call then fails with bwrap: Can't create file … Read-only file system. This PR adds a troubleshooting entry, a sandbox-error-hint.sh pattern, and a sandbox-lint rule.
  • Placeholders show up in git status. A troubleshooting entry explains why write-protected paths that do not exist appear as character devices 1,3 and are listed as untracked.

Type of change

  • Skill change (.claude/skills/<name>/) — eval fixtures updated below
  • Python package (tools/*/ with pyproject.toml)
  • Cross-cutting (RFC, AGENTS.md, sandbox, privacy-LLM)
  • Documentation (docs/, README.md, CONTRIBUTING.md)

Test plan

  • prek run --all-files --skip workspace-pytest passes
  • prek run workspace-pytest --from-ref upstream/main --to-ref HEAD passes; tools/sandbox-lint pytest and the agent-isolation test_sandbox_error_hint.py pass. New tests cover:
    • the socket rule: uid matching against os.getuid(), plus ~ and relative entries;
    • the parent rule: ~, a trailing slash, and .;
    • the hint pattern: a path with a space, and precedence over the /tmp hint.
  • Eval suite, skill-eval --cli "claude -p" tools/skill-evals/evals/setup-isolated-setup-verify/: 23/28 pass.
    • 4 of the 5 failures also fail on unmodified upstream/main (step-1 case-7; step-2 case-3, case-5, case-6).
    • The new step-1 case-23-linux-all-unix-sockets-exposed passed 3 of 4 runs.
  • Checked on a WSL2 host:
    • With the flag on and the denyRead entries added, the host sockets are no longer sockets inside the sandbox (test -S is false).
    • The gateway sockets and an ssh-agent under /tmp keep working: docker version through the gateway and ssh-keygen -Y sign both succeed.
    • The parent-allowRead failure was reproduced and fixed by listing the specific directories.
    • Claude Code skips absent denyRead paths, so listing paths that a host lacks is harmless.
  • Reviewed by an independent agent. Its requested changes are in the second commit.

RFC-AI-0004 compliance

  • Sandbox — closes host-socket reach from the sandbox on Linux; no new host access
  • Vendor neutrality — placeholders only (check-placeholders passes)

Notes for reviewers (optional)

  • Not verified: on Linux, whether a single-socket allowRead inside a denied /run/user/<uid> re-opens an agent socket that lives there. The doc marks it as unverified.
  • The root cause of the parent-allowRead trap is Claude Code's mount ordering, so it may also be worth an upstream Claude Code report. The lint rule fails closed, including outside denyRead.
  • Companion PR fix(setup): make isolated-setup-verify checks catch false passes #1808 also adds step-1 eval cases (up to case-22, so this one is case-23) and touches the same README case counts. Whichever merges second needs a trivial rebase.

Generative AI: drafted with Claude Code (Claude Opus 5.5) and checked by an independent Claude Opus review agent; opened as a draft for the author's line-by-line review before it is marked ready.

🤖 Generated with Claude Code

@github-actions github-actions Bot added family:setup setup-* skills family:tools tools/* family:docs Docs, MISSION.md, READMEs capability:platform Framework / agent substrate skills (install, verify, doctor, override, status, setup bootstrap) substrate:sandbox Tool substrate: agent isolation, egress control, settings audit substrate:framework-dev Tool substrate: build / validate / eval the framework itself labels Oct 25, 2026
@shahar1
shahar1 requested review from andreahlert and potiuk October 25, 2026 06:53
@shahar1
shahar1 marked this pull request as ready for review October 25, 2026 20:23
shahar1 and others added 3 commits October 26, 2026 01:36
On Linux, Claude Code ignores sandbox.network.allowUnixSockets and its
seccomp filter rejects every AF_UNIX socket, so the container gateway
and ssh-agent signing only work with allowAllUnixSockets: true. The
setup docs never said so, and nothing warned that the flag lets a
sandboxed command connect to every socket it can see: the Docker and
Podman daemons, the user D-Bus (systemd-run --user runs commands
outside the sandbox), the system bus, X11, and on WSL the interop and
WSLg sockets. The reference denyRead of ~/ hides none of them.

Document the Linux requirement and the denyRead entries that must go
with it, make verify check 12c accept allowAllUnixSockets as the Linux
socket half and check 12d require denyRead cover for each host socket
path that exists, mirror both in the verification prompt and install
Step L, and add a sandbox-lint invariant for the daemon sockets and
the per-user runtime directory, with an eval case for 12d.

Also catalogue two Linux traps met on the same setup: an allowRead
parent of an allowWrite path makes bubblewrap mount it read-only so
every Bash call fails with "bwrap: Can't create file ... Read-only
file system" (troubleshooting entry, hint-hook pattern, lint check),
and denied paths showing up in git status as /dev/null placeholders.

Generated-by: Claude Code (Claude Opus 5.5)
The systemd user manager also listens on /run/user/<uid>/systemd/private,
so denying only bus and podman under the runtime directory still lets a
sandboxed command run systemctl --user; the directory also holds editor
askpass and systemd ssh-agent sockets. Require the whole directory of
the current uid in sandbox-lint, verify and the docs, and drop the advice
to deny single sockets in it.

Move the socket-cover requirement into its own verify check 12e, which
runs whenever allowAllUnixSockets is set, even with no container runtime
(the flag may be there only for ssh-agent signing). Make sandbox-lint
list the same paths as the docs and 12e, accept only the current uid,
and note it reads only project layers.

Correct the ssh-agent caveat (allow the socket or its own subdirectory,
never /run/user/<uid>; unverified on Linux), name AF_VSOCK as residual
risk, fix the /dev/null placeholder explanation (write-denied paths
only), let the bwrap hint match paths with spaces, and add the missing
tests. The verify eval case moves to case-23 to avoid a numbering clash.

Generated-by: Claude Code (Claude Opus 5.5)
Pasted as-is, "/run/user/<uid>" names a path that does not exist, so the per-user runtime directory, the entry that stops systemd-run --user from escaping the sandbox, stays reachable.

Generated-by: Claude Opus 5
@potiuk
potiuk force-pushed the fix/linux-sandbox-socket-exposure branch from b39042d to 2ac2869 Compare October 26, 2026 00:52
@potiuk
potiuk enabled auto-merge (squash) October 26, 2026 01:06
@potiuk
potiuk merged commit b9e1400 into apache:main Oct 26, 2026
16 checks passed

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, this closes a real hole: on Linux the flag silently turns every visible socket into a way out, and the docs, verify 12e and sandbox-lint now say so in the same terms.

I rebased it onto main (conflicts in the troubleshooting table, test_validator.py, and the generated stamps/fingerprint) and added one small fixup: the jsonc example now says to replace <uid> with id -u. Pasted literally, /run/user/<uid> would deny nothing, and it's the entry that stops systemd-run --user.

One non-blocking question for a follow-up: the example names ~/.claude/settings.local.json as a per-machine file. I'm not sure Claude Code reads a user-scope settings.local.json (the documented local file is the project's .claude/settings.local.json). If it doesn't, the Linux half would silently not apply from there.


Drafted-by: Claude Code (Opus 5.5); reviewed by @potiuk before posting

@shahar1
shahar1 deleted the fix/linux-sandbox-socket-exposure branch October 26, 2026 03:54
potiuk added a commit that referenced this pull request Oct 28, 2026
* docs(spec-loop): sync specs with contributed functionality

Back-fill the specs for the 46 commits since the last sync that changed
behaviour without touching a spec: the public-surface scrub mod (#1676),
adapter hardening (#1862), the hook managers with install strategy
`none` (#1838), Linux `allowAllUnixSockets` and its socket cover (#1809),
the touch-overlay dismissal and matcher (#1832, #1833), the per-family
Codex manifests (#1836), Vulnogram reviewer comments (#1840), the verify
false-pass fixes (#1808), gateway refusal hints (#1810), adversarial-review
output and agy fixes (#1852, #1854, #1871), and code-review inline comment
text (#1853).

Also correct drift found on the way: the threat-model pointer now names
Appendix B after the #1859 restructure, the troubleshooting catalogue has
fourteen entries, and the overview's Mentoring and Pairing skill counts
match docs/modes.md.

Generated-by: Claude Opus 5

* docs(marketplace): magpie-security ships its own Codex manifest too

#1676 gave magpie-security a .codex-plugin/plugin.json and an empty
hooks/codex-hooks.json, because its hooks.json now carries a Claude Code
mod, but the "Which manifest Codex reads" table and the Hooks paragraph
still named only magpie-setup and magpie-utilities.

Generated-by: Claude Opus 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

capability:platform Framework / agent substrate skills (install, verify, doctor, override, status, setup bootstrap) family:docs Docs, MISSION.md, READMEs family:setup setup-* skills family:tools tools/* substrate:framework-dev Tool substrate: build / validate / eval the framework itself substrate:sandbox Tool substrate: agent isolation, egress control, settings audit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants