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

オープン
#91,555 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る
area:sandbox bug has repro platform:macos
主要言語
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
見積もり時間
1週間以上
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
35/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。