Windows sandbox helpers race while updating deny_read_acl_state.json

Open 💬 0 comments Opened Jul 18, 2026 by starriet9

What version of Codex CLI is running?

codex-cli 0.144.5

The failure was captured from a local build based on the exact rust-v0.144.5 source. Its unrelated context-window changes did not modify codex-rs/windows-sandbox-rs. The same unlocked state implementation remains in main at 56395bdda.

What subscription do you have?

Pro

Which model were you using?

gpt-5.6-sol

What platform is your computer?

Microsoft Windows NT 10.0.19045.0 x64

What terminal emulator and version are you using (if applicable)?

Windows Terminal 1.24.11321.0 with PowerShell 7.6.3 (Core, x64). No terminal multiplexer.

Codex doctor report

Relevant results from codex doctor --json are summarized below; unrelated entries are omitted.

{
  "schemaVersion": 1,
  "overallStatus": "warning",
  "codexVersion": "0.144.5",
  "checks": {
    "system.environment": {
      "status": "ok",
      "os": "Windows 10 Pro 10.0.19045, 64-bit"
    },
    "sandbox.helpers": {
      "status": "ok",
      "approvalPolicy": "OnRequest",
      "filesystemSandbox": "restricted",
      "networkSandbox": "restricted"
    },
    "state.paths": {
      "status": "ok",
      "databaseIntegrity": "state, log, goals, and memories checks passed"
    },
    "network.provider_reachability": {
      "status": "ok"
    },
    "network.websocket_reachability": {
      "status": "ok"
    }
  }
}

The overall warning came from 19 old rollout files that contained no parseable rollout items. It is unrelated to the sandbox state-file race.

What issue are you seeing?

When several sandboxed tool calls start in parallel on Windows, their codex-windows-sandbox-setup.exe processes concurrently update the shared file:

%USERPROFILE%\.codex\.sandbox\deny_read_acl_state.json

In the captured run, six setup helpers started within a few milliseconds. Two failed with:

setup error: apply deny-read ACLs

Caused by:
  0: parse deny-read ACL state <CODEX_HOME>\.sandbox\deny_read_acl_state.json
  1: EOF while parsing a value at line 1 column 0

The affected commands failed with helper_unknown_error: apply deny-read ACLs. Retrying the same command by itself succeeded.

The root cause is in codex-rs/windows-sandbox-rs/src/deny_read_state.rs:

  1. sync_persistent_deny_read_acls() loads the shared state.
  2. It applies new ACLs and revokes stale ACLs.
  3. store_state() calls std::fs::write() with no inter-process lock.
  4. std::fs::write() truncates the destination before writing the new JSON.

Another helper can read during that truncate/write interval and receive empty bytes, producing the EOF parse error. Even when every read parses successfully, two helpers updating different principals can both load the same old state and overwrite one another, losing one update.

What steps can reproduce the bug?

Runtime reproduction:

  1. Use the Windows workspace-write sandbox.
  2. Start multiple filesystem tool calls in parallel so several setup helpers run at the same time.
  3. Inspect the failed tool result and the daily file under <CODEX_HOME>\.sandbox\sandbox.YYYY-MM-DD.log.

A deterministic state-layer regression test also reproduces the lost-update form without changing real ACLs:

  1. Synchronize eight state updates so they all load the same initial JSON.
  2. Have every update add a distinct principal and then store the result.
  3. On unmodified main, the final state contains 1 principal instead of 8.

The candidate also launches six child test processes against one state file to verify the fix across real process boundaries.

No thread ID is required; this occurs in the local Windows helper before the sandboxed command starts.

What is the expected behavior?

Concurrent setup helpers should serialize the full persistent state transaction. Every helper should see complete JSON, updates for different principals should not be lost, and parallel shell/file operations should not fail during sandbox setup.

Additional information

Candidate branch:

https://github.com/starriet9/codex/tree/fix/windows-sandbox-deny-read-state-race

Candidate commit:

https://github.com/starriet9/codex/commit/0280e7b8ed1b58f95f2d4da732c601bbf02b7310

The candidate:

  • holds a Windows named mutex across the complete load -> ACL apply/revoke -> store transaction
  • treats an abandoned mutex as acquired, allowing recovery after a process exits
  • writes the JSON to a temporary file in the same directory, flushes it, and atomically replaces the previous state
  • adds thread-level and child-process concurrency regression tests

Validation on Windows:

Original synchronized test: final principal count 1, expected 8
Patched thread test: 8 of 8 principals preserved
Patched process test: 6 of 6 principals preserved
cargo fmt -p codex-windows-sandbox -- --check: passed
cargo clippy --offline -p codex-windows-sandbox --all-targets -- -D warnings: passed
cargo build --offline --release -p codex-windows-sandbox --bins: passed

The process regression test uses actual child processes and a Windows kernel mutex without changing real filesystem ACLs. I did not run a full parallel setup-helper stress against the machine's live sandbox accounts, ACLs, firewall, and Windows Filtering Platform (WFP) state because that would mutate system security configuration.

Related issue #30251 reports a state file that is already malformed or filled with NUL bytes and has a candidate that treats malformed JSON as empty state and uses atomic replacement. Atomic replacement prevents readers from observing a partially written file, but it does not serialize the complete read-modify-write transaction: two helpers can still load the same old state and atomically replace it with different updates, losing one update. The candidate in this report adds that missing transaction lock; it intentionally does not recover a state file that was already corrupt before startup.

Related issue #30540 likewise covers recovery after the JSON has already been corrupted, such as after a crash or power interruption. This report is distinct: it identifies a live concurrency path that can corrupt or lose the state during normal parallel tool execution.

I have not opened a pull request because docs/contributing.md says external pull requests are by invitation only. The focused branch is ready if maintainers want a PR.

View original on GitHub ↗