anthropics / anthropics/claude-code
[BUG] macOS sandbox: System V shared-memory and sysctl calls are refused with EPERM whenever two sandboxed Bash commands run at the same time — and a refused shmget/shmctl still leaves the segment allocated
- 主要语言
- Python
- 星标
- 145k
- 派生
- 23.1k
- PR 合并指标
- PR 指标待抓取
描述
### Preflight Checklist
- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
### What's Wrong?
A single sandboxed Bash command can call `shmget`, `shmctl(IPC_RMID)` and run `ipcs -mo` (which uses `sysctlbyname`) as often as it likes: 40 calls, 0 refusals.
The moment a **second** sandboxed Bash command is running at the same time — a `run_in_background` Bash call, a subagent's Bash call, or another Claude Code session on the same machine — the same calls are refused with `EPERM` ("Operation not permitted") in **both** commands, for the whole time they overlap. Reproduced three times: 40/40 allowed
alone, 0/40 during overlap.
Two consequences make this worse than a refusal:
1. **A refused create still allocates the segment.** PostgreSQL's `initdb` failed with `FATAL: could not create shared memory segment: Operation not permitted` / `DETAIL: Failed system call was shmget(key=12353002, size=56, 03600)` — and afterwards `ipcs -mo` showed a segment with key `0x00bc7dea` (= 12353002), NATTCH 0, that nothing had created except that refused call. Two refused starts left two orphans with exactly the keys named in the two FATAL lines.
2. **A refused remove leaves the segment behind.** A loop that created a private segment and removed it 40 times, run in two shells at once, left 29 orphan segments (every `shmctl(IPC_RMID)` was refused), after which every `shmget` on the machine returned `ENOSPC` and no PostgreSQL could start — the user's own live server included, until a person removed the orphans from a normal terminal.
So on a machine that runs PostgreSQL-backed tests from more than one agent, the sandbox silently exhausts a scarce kernel resource (32 segments for the whole machine) within minutes.
### What Should Happen?
The sandbox's decision for a system call should not depend on whether another sandboxed command happens to be running.
Either the calls are allowed (the profile does not mention System V IPC) or they are denied consistently — and a denied
`shmget` should not leave an allocated segment.
### Error Messages/Logs
```shell
```
### Steps to Reproduce
## Minimal reproduction
`shm_loop.py` — creates and removes a private segment 40 times, 0.3 s apart, and reports refusals:
```python
import ctypes, ctypes.util, os, errno, time, subprocess
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
ok = refused = 0
for i in range(40):
shmid = libc.shmget(0, 4096, 0o1000 | 0o600) # IPC_PRIVATE, IPC_CREAT | 0600
if shmid < 0:
e = ctypes.get_errno(); refused += 1
ipcs = subprocess.run(["/usr/bin/ipcs", "-mo"], capture_output=True, text=True).returncode
print(f"{time.strftime('%H:%M:%S')} shmget REFUSED {errno.errorcode.get(e)} ipcs_exit={ipcs}")
else:
if libc.shmctl(shmid, 0, None) < 0: # IPC_RMID
e = ctypes.get_errno(); print(f"shmctl RMID REFUSED {errno.errorcode.get(e)} -- segment {shmid} ORPHANED")
ok += 1
time.sleep(0.3)
print(f"ok={ok} refused={refused}")
```
`ipcs_loop.sh` — the listing only, no segment is ever created, so this one cannot leak:
```zsh
#!/bin/zsh
ok=0; refused=0
for i in {1..40}; do
if /usr/bin/ipcs -mo >/dev/null 2>&1; then ok=$((ok+1)); else refused=$((refused+1)); echo "$(date +%H:%M:%S) ipcs REFUSED"; fi
sleep 0.3
done
echo "ok=$ok refused=$refused"
```
Steps, inside Claude Code with the sandbox enabled:
1. Run `python3 shm_loop.py` as one Bash tool call. Result: `ok=40 refused=0`.
2. Run `./ipcs_loop.sh` as a Bash tool call with `run_in_background: true`, and in the same turn run `./ipcs_loop.sh` as a foreground Bash tool call. Result in both: `ok=0 refused=40`, every line `ipcs REFUSED`.
3. (Leaks — clean up afterwards with `ipcrm -m ` on the NATTCH-0 segments from a normal terminal.) Run `python3 shm_loop.py` in the background and again in the foreground at the same time. Result: `shmctl RMID REFUSED EPERM` on every created segment, then `shmget REFUSED ENOSPC` once the 32 slots are full; `ipcs -mo` afterwards lists ~29 segments with key `0x00000000` and NATTCH 0.
4. For the PostgreSQL shape: run `initdb -D /tmp/x` inside a sandboxed Bash call while another sandboxed Bash call is running. `initdb` fails with the FATAL line above, and `ipcs -mo` afterwards shows a NATTCH-0 segment whose key is the one the FATAL line named.
Timestamps from the measured runs: refusals at 08:56:17–08:56:19 and 08:58:46–08:58:54 (concurrent), allowed at 08:55:57, 08:56:08, 08:56:35–08:56:41 (alone) and a further 09:10 solo run at 40/40 followed by a concurrent run at 0/40.
### Claude Model
Other
### Is this a regression?
No, this never worked
### Last Working Version
_No response_
### Claude Code Version
2.1.258
### Platform
Anthropic API
### Operating System
macOS
### Terminal/Shell
Terminal.app (macOS)
### Additional Information
## Workaround in use
`sandbox.excludedCommands` naming the test command (`"*venv/bin/python -m pytest *"`), which runs PostgreSQL-backed
tests outside the sandbox; plus a test fixture that records every segment it creates or is refused on, retries a refused
start, and removes its own segments by identifier with the removal's return value checked — because `ipcs` is refused in
the same window, a cleanup that depends on the listing cannot help.
## Notes for the maintainers
- `ipcs` failing looks exactly like an empty machine: it prints its header row and an empty table and exits 1 with
`ipcs: sysctlbyname: Operation not permitted` on stderr. Tooling that reads a failed listing as "no segments" will make
wrong decisions.
- The refusal windows were not always short. In one session, running one command at a time, `ipcs` and single-segment
scripts were refused for about 17 minutes with brief gaps while a second Claude Code window was open on the same machine.
## Environment
| | |
|---|---|
| Claude Model | Fable 5.1 |
| Claude Code | 2.1.258 |
| macOS | 26.5.2 (build 25F84), Darwin 25.5.0, arm64 |
| Sandbox settings (user) | `"sandbox": {"allowUnsandboxedCommands": false, "strictAllowlist": true, "failIfUnavailable": true, "filesystem": {"disabled": true}}` |
| Sandbox settings (project) | `"sandbox": {"enabled": true, "network": {"deniedDomains": [...], "allowLocalBinding": true}, ...}` |
| Affected program | PostgreSQL 18.4 (Homebrew) — `initdb` and `postmaster` need one System V segment each for the data-directory interlock |
| `sysctl kern.sysv.shmmni` | 32 (the machine-wide limit on segments) |
贡献指南
这个仓库没有索引到贡献指南
调研方向
Start by running shm_loop.py and ipcs_loop.sh concurrently in the macOS sandbox, then compare the solo results and inspect the sandbox entry points handling System V IPC and sysctl calls. Reproduce the PostgreSQL initdb case and verify that denied calls behave consistently without leaving orphaned shared-memory segments.
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- bash, macos, postgresql, python
- 领域
- databases, operating-systems, security
- Issue 类型
- 缺陷
- 难度
- 5/5
- 预计耗时
- 一周以上
- 活跃度
- 活跃
- 描述清晰度
- 基本清楚
- 新手友好度
- 35/100