Problems with WSL Sandbox
Open 💬 2 comments Opened Jul 16, 2026 by roaspeci
What issue are you seeing?
codex-sandbox-process-isolation-support-report-2026-07-15.md
Latest version on WSL with ubuntu
What steps can reproduce the bug?
see md above
What is the expected behavior?
_No response_
Additional information
_No response_
2 Comments
## Retest on Codex CLI 0.144.6
I retested the problem on the latest Codex CLI release, 0.144.6. The nested-process/stdio problem still reproduces.
Support case: 11586908
### Updated environment
Component Value
━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Codex CLI 0.144.6
────────────── ───────────────────────────────────
Installation Standalone CLI
────────────── ───────────────────────────────────
Ubuntu 26.04 LTS
────────────── ───────────────────────────────────
WSL 2.7.3.0
────────────── ───────────────────────────────────
WSL kernel 6.6.114.1-microsoft-standard-WSL2
────────────── ───────────────────────────────────
Windows 10.0.26200.8875
────────────── ───────────────────────────────────
Architecture x86_64
────────────── ───────────────────────────────────
Node.js v24.15.0
────────────── ───────────────────────────────────
Git 2.53.0
────────────── ───────────────────────────────────
bubblewrap 0.11.1
The original report used Codex CLI 0.144.4.
## Independent one-file reproducer
This reproducer does not use our pipeline, TAC configuration, hooks, guard scripts, or private repository code.
Save as codex-wsl-stdio-probe.mjs:
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
if (process.argv[2] === "child") {
const input = readFileSync(0, "utf8");
process.stdout.write(JSON.stringify({ child: "ok", input }));
process.exit(0);
}
const script = fileURLToPath(import.meta.url);
const payload = JSON.stringify({
probe: "codex-wsl-sandbox",
value: 42,
});
const startedAt = Date.now();
const result = spawnSync(process.execPath, [script, "child"], {
encoding: "utf8",
input: payload,
timeout: 5000,
});
console.log(JSON.stringify({
durationMs: Date.now() - startedAt,
status: result.status,
signal: result.signal,
errorCode: result.error?.code ?? null,
errorErrno: result.error?.errno ?? null,
errorSyscall: result.error?.syscall ?? null,
errorMessage: result.error?.message ?? null,
stdout: result.stdout ?? "",
stderr: result.stderr ?? "",
}, null, 2));
## Control run outside the sandbox
Run from a normal WSL shell:
node codex-wsl-stdio-probe.mjs
Observed:
{
"durationMs": 27,
"status": 0,
"signal": null,
"errorCode": null,
"errorErrno": null,
"errorSyscall": null,
"errorMessage": null,
"stdout": "{\"child\":\"ok\",\"input\":\"{\\\"probe\\\":\\\"codex-wsl-sandbox\\\",\\\"value\\\":42}\"}",
"stderr": ""
}
## Run under the built-in workspace profile
From the same normal WSL shell:
codex sandbox \
-P :workspace \
-C "$PWD" \
node "$PWD/codex-wsl-stdio-probe.mjs"
Observed:
{
"durationMs": 5008,
"status": null,
"signal": "SIGTERM",
"errorCode": "ETIMEDOUT",
"errorErrno": -110,
"errorSyscall": "spawnSync /path/to/node",
"errorMessage": "spawnSync /path/to/node ETIMEDOUT",
"stdout": "",
"stderr": ""
}
The direct child command succeeds immediately with the same JSON on stdin. The failure only appears when the child is launched by
the parent process inside the Codex sandbox.
## Isolation matrix
I repeated the test with the following variations:
Test Result
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Outside Codex sandbox Pass
──────────────────────────────────────────────────────────────────────── ────────
Built-in :workspace Fail
──────────────────────────────────────────────────────────────────────── ────────
Built-in :read-only Fail
──────────────────────────────────────────────────────────────────────── ────────
DrvFS workspace under /mnt/d Fail
──────────────────────────────────────────────────────────────────────── ────────
Identical files under native WSL /tmp Fail
──────────────────────────────────────────────────────────────────────── ────────
Standalone codex sandbox launched outside our TAC/host-managed session Fail
──────────────────────────────────────────────────────────────────────── ────────
Built-in :danger-full-access Pass
──────────────────────────────────────────────────────────────────────── ────────
Custom workspace profile with network.enabled=true Pass
This rules out the following as the primary cause:
## Expanded process/stdio observations
A slightly larger matrix tested synchronous and asynchronous Node children.
Under :workspace with restricted networking:
Representative result:
{
"status": 0,
"signal": null,
"errorCode": "EPERM",
"errorErrno": -1,
"stdout": "",
"stderr": ""
}
A successful exit code is therefore not sufficient to establish that the sandbox returned the child result correctly.
## Strong localization result
A temporary custom profile extending :workspace, but enabling network access, makes all tested child-process cases pass:
codex sandbox \
-c 'permissions.workspace_network.extends=":workspace"' \
-c permissions.workspace_network.network.enabled=true \
-P workspace_network \
-C "$PWD" \
node "$PWD/codex-wsl-stdio-probe.mjs"
Observed:
This is a diagnostic result, not an acceptable permanent workaround. It weakens the intended isolation boundary.
The result strongly localizes the failure to the restricted-network enforcement path rather than the bubblewrap filesystem-
isolation path.
In the 0.144.6 source, restricted networking installs an additional seccomp filter in:
codex-rs/linux-sandbox/src/landlock.rs
That filter returns EPERM for several denied operations. I have not identified the exact syscall responsible for the Node
behavior, so this is an evidence-backed localization, not a proven line-level root cause.
Relevant source:
## Legacy fallback observation
I also tried the documented legacy fallback:
codex sandbox \
--enable use_legacy_landlock \
-P :workspace \
-C "$PWD" \
node "$PWD/codex-wsl-stdio-probe.mjs"
Codex terminated with exit code 101 and a Rust panic:
thread 'main' panicked at linux-sandbox/src/linux_run_main.rs:
permission profiles requiring direct runtime enforcement are incompatible
with --use-legacy-landlock
Even if this combination is intentionally unsupported, it would be preferable to return a typed CLI configuration error instead
of panicking.
## Impact on isolated Critic workflows
Our intended use case is an independently isolated, read-only Critic:
The current behavior makes this unreliable:
The codex exec --json lifecycle case has not yet been rerun on 0.144.6; the independent nested-process failure has been
reconfirmed on 0.144.6.
## Expected behavior
Nested processes and stdio forwarding should work under a restricted-network workspace or read-only profile.
If an operation is unsupported, Codex should return a typed and machine-readable failure instead of:
danger-full-access and permanently enabling network access are not acceptable fixes for a Critic whose purpose is to run inside a
genuine least-privilege boundary.
Additional reproducible evidence from WSL2 with Codex CLI 0.145.0
I completed a controlled diagnostic that isolates this issue from the WSL filesystem, DNS, AppArmor, user namespaces, and the system Bubblewrap installation.
Environment
6.18.33.2-microsoft-standard-WSL20.145.0/usr/bin/bwrap, version0.11.1/home/tmp:tmpfs, mode1777, writableThe local CLI exposes the following syntax:
Control tests outside Codex
The following all pass outside the Codex sandbox:
/tmpis mounted as:Minimal reproduction
Create
socket-probe.py:Outside Codex:
Result:
Inside the built-in workspace profile:
Result:
Normal filesystem operations in
/tmpstill work in the same:workspacesandbox:Only the Unix-socket bind fails.
Additional observation
Running the same diagnostic without an explicit permission profile:
mounts
/tmpread-only in this context:There are no relevant
sandbox_mode,default_permissions,writable_roots, or network settings in my userconfig.toml.Analysis
This does not appear to be:
/tmppermissionsThe failure is specific to the Codex sandbox policy applied to
AF_UNIX bind().This appears to be the same underlying class as:
That issue reproduces
listen EPERMfor a dynamically generated IPC pipe under/tmp.Why the current workaround is insufficient
The available permission escape hatch appears to be:
This is too broad as a general solution. It may expose unrelated user-owned local services, including Docker, SSH-agent, credential-agent, database, or other Unix sockets.
Exact socket-path allowlisting is also not practical for common development tools that create dynamically named paths such as:
Users therefore have to choose between:
EPERM, orExpected behavior
One of the following would provide a safer and more usable result:
:workspaceprofile permits creating and binding new Unix sockets inside writable sandbox-owned temporary directories while continuing to block connections to pre-existing host sockets.AF_UNIX bind()and returns an actionable error instead of a genericEPERM.tsx, test workers, browser drivers, and local build daemons.Security distinction
Creating a new socket owned by the sandboxed process under a writable temporary directory is not equivalent to connecting to an existing privileged host socket such as:
A permission model that distinguishes these operations would avoid requiring
dangerously_allow_all_unix_sockets.Relevance
This affects more than one specific tool. Many normal SDLC tools use temporary IPC sockets for:
I can provide the full diagnostic logs and probe script if required.