Filesystem sandbox preparation scales quadratically with permission entries
What version of Codex CLI is running?
codex-cli 0.144.4
What subscription do you have?
API
Which model were you using?
gpt-5.6-sol
What platform is your computer?
_No response_
What terminal emulator and version are you using (if applicable)?
_No response_
Codex doctor report
What issue are you seeing?
Description
On Linux, filesystem RPCs through codex exec-server become very slow as the number of filesystem permission entries grows. The slowdown happens before the filesystem operation itself and appears to be superlinear in the size of the permission profile.
This is especially noticeable for apply_patch, because one patch may perform several sandboxed filesystem RPCs for verification, metadata, reads, and writes. A profile with a few dozen writable roots can therefore turn a tiny patch into a tens-of-seconds or minutes-long operation.
The reproduction below is self-contained:
- It sets
CODEX_HOMEto an empty temporary directory, so it does not read the user'sconfig.toml. - It creates all writable roots in a temporary directory.
- It calls the exec-server filesystem API directly, so model and API latency are excluded.
Environment
- Codex:
codex-cli - Python:
3.9.2 - Bubblewrap:
0.11.2
Reproduction
Save this as repro.py:
#!/usr/bin/env python3
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
codex = sys.argv[1]
with tempfile.TemporaryDirectory(prefix="codex-fs-repro-") as tmp:
base = Path(tmp)
home = base / "home"
home.mkdir()
roots = [base / f"root-{i}" for i in range(30)]
for root in roots:
root.mkdir()
target = roots[0] / "target.txt"
target.write_text("hello\n")
entries = [{
"path": {"type": "special", "value": {"kind": "root"}},
"access": "read",
}]
for root in roots:
entries.append({
"path": {"type": "path", "path": root.as_uri()},
"access": "write",
})
for name in (".git", ".agents", ".codex"):
entries.append({
"path": {"type": "path", "path": (root / name).as_uri()},
"access": "read",
})
sandbox = {
"permissions": {
"type": "managed",
"file_system": {"type": "restricted", "entries": entries},
"network": "restricted",
},
"cwd": roots[0].as_uri(),
"workspaceRoots": [roots[0].as_uri()],
"windowsSandboxLevel": "disabled",
"windowsSandboxPrivateDesktop": False,
"useLegacyLandlock": False,
}
env = os.environ.copy()
env["CODEX_HOME"] = str(home) # Do not read the user's config.toml.
process = subprocess.Popen(
[codex, "exec-server", "--listen", "stdio"],
cwd=base,
env=env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
def rpc(message):
process.stdin.write(json.dumps(message, separators=(",", ":")) + "\n")
process.stdin.flush()
return json.loads(process.stdout.readline())
try:
rpc({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"clientName": "fs-repro", "resumeSessionId": None},
})
process.stdin.write('{"jsonrpc":"2.0","method":"initialized","params":{}}\n')
process.stdin.flush()
start = time.perf_counter()
response = rpc({
"jsonrpc": "2.0", "id": 2, "method": "fs/getMetadata",
"params": {"path": target.as_uri(), "sandbox": sandbox},
})
assert "result" in response, response
print(f"entries={len(entries)} elapsed={time.perf_counter() - start:.3f}s")
finally:
process.kill()
process.wait()
Run:
$ /path/to/codex --version
codex-cli 0.144.4
$ python3 repro.py /path/to/codex
entries=121 elapsed=10.027s
Scaling observed on v0.144.4
| Writable roots | Permission entries | Elapsed time |
|---:|---:|---:|
| 1 | 5 | 0.121s |
| 5 | 21 | 0.515s |
| 10 | 41 | 1.432s |
| 15 | 61 | 2.898s |
| 20 | 81 | 4.775s |
| 25 | 101 | 7.294s |
| 30 | 121 | 10.185s |
Expected behavior
Sandbox preparation should scale approximately linearly with the number of permission entries. A metadata lookup on a local temporary file should not spend roughly 10 seconds preparing a 121-entry policy.
Likely cause and high-level fix
The writable-root calculation repeatedly resolves and scans the same permission entries from nested root-by-entry loops. In particular, effective path normalization and write-access checks are recomputed many times for identical paths, and each write-access check reconstructs/scans the resolved policy again.
A local fix that preserves the existing permission semantics does the following:
- Resolve the permission entries once for each writable-root calculation.
- Precompute each resolved path's normalized effective path and writability once.
- Reuse that resolved data for access-precedence and protected-metadata checks instead of rebuilding and rescanning it inside nested loops.
With that change, the same self-contained reproduction in an unoptimized debug build produced:
| Writable roots | Permission entries | v0.144.4 | Local build with fix |
|---:|---:|---:|---:|
| 1 | 5 | 0.121s | 0.145s |
| 10 | 41 | 1.432s | 0.343s |
| 20 | 81 | 4.775s | 0.596s |
| 30 | 121 | 10.185s | 0.804s |
No protocol or sandbox-policy behavior change is intended; this only removes repeated derivation of the same resolved policy data.
What steps can reproduce the bug?
See above
What is the expected behavior?
_No response_
Additional information
Codex generates the description above. I have a fix locally and happy to send a PR if you need.