Windows: sandbox broker wedges conversation thread across client restarts (deny-read ACLs -> silent hangs, spinning codex-windows-sandbox-setup); evidence of ACE accumulation (TokenDefaultDacl error 1344)
Summary
On Windows, the interactive Codex client's sandbox execution broker can enter a wedged state in which every spawned operation (cmd, PowerShell, Node, git, filesystem writes, browser startup) hangs indefinitely and ignores timeouts. The wedge is conversation-thread-scoped and persists across a full client restart: restarting Codex and resuming the affected conversation re-attaches to the same broken broker, and subagents spawned inside that thread inherit it. Fresh codex exec processes and brand-new conversations on the same machine work normally during the entire incident.
Environment
- Codex CLI:
codex-cli 0.144.6 - OS: Windows 11 Home, build 10.0.26200
- Sandbox modes affected: default sandbox in interactive sessions (workspace-write)
Original error
Before the hang state, the client surfaced:
windows sandbox: helper_unknown_error: apply deny-read ACLs
After that, all spawns hung silently.
Minimal reproduction (inside an affected thread)
- Command:
cmd.exe /d /c echo sandbox-health-ok - Directory:
C:\tmp(any directory reproduces) - Expected: immediate output
- Actual: no output; the process timeout is ignored; the outer tool has to terminate it
Isolation evidence
Probes run inside the affected conversation after a full client restart + thread resume:
| Probe | Spawns a process? | Result |
|---|---|---|
| Root shell canary (cmd.exe /d /c echo ...) | yes | hung |
| Fresh subagent running the same canary | yes (inherits thread broker) | hung |
| Non-mutating apply_patch parser | no (in-process) | responded normally (~0.2 s) |
| External codex exec dispatches, same machine, same hour | yes (new process tree) | all completed (multiple long multi-hundred-k-token runs incl. git push, pytest, cargo) |
Additional system-level evidence: each hung canary left behind an orphaned codex-windows-sandbox-setup process spinning CPU indefinitely (~6–7 CPU-minutes each at ~19 MB RSS before being killed manually). Three such processes accumulated, their start times matching the three hung probes to the second.
Follow-up evidence (same day, after recovery)
In a brand-new conversation on the same machine (after killing the orphaned helpers), the very first spawn failed fast with:
windows sandbox: runner failed during SpawnChild: SetTokenInformation(TokenDefaultDacl) failed: 1344
Win32 error 1344 is ERROR_ALLOTTED_SPACE_EXCEEDED ("No more memory is available for security information updates") — the token's fixed-size default-DACL buffer overflowed. An immediate retry of the same command succeeded. Together with the original apply deny-read ACLs failure, this suggests the sandbox setup accumulates ACEs (in the token default DACL and/or object ACLs) across runs until security-information writes start failing, and that the helper's handling of that failure is inconsistent: sometimes a fast error (recoverable), sometimes the indefinite busy-loop described above (unrecoverable for the thread). A reboot/fresh logon session appears to reset the accumulated state.
Interpretation
The conversation thread appears to hold a persistent binding to a sandbox/execution broker that crashed or deadlocked after the ACL error. Client restart does not recycle that binding when the thread is resumed; subagents inherit it. The per-spawn codex-windows-sandbox-setup helper then spins forever (busy-loop, not a blocked wait, given the CPU burn) instead of failing fast, which is why timeouts are never honored.
Impact
- An affected conversation can still converse (in-process operations work) but cannot execute anything, which is confusing to diagnose.
- Timeout settings are silently ignored, so automation on top of the client stalls indefinitely.
- Orphaned sandbox-setup helpers leak CPU until killed manually.
Workaround
- Abandon the affected conversation; start a brand-new one (or use
codex exec). Do not resume the wedged thread — resuming re-attaches the broken binding even after client restart/reinstall. - Kill leftover
codex-windows-sandbox-setupprocesses (identifiable by abnormal cumulative CPU). - Health-check any new context with a cheap canary first:
cmd.exe /d /c echo canary-ok.
Suggested fixes
- Recycle/re-create the sandbox broker binding on client restart rather than persisting it with the resumed thread.
- Make
codex-windows-sandbox-setupfail fast (propagatehelper_unknown_errorinstead of busy-looping) and honor the caller's timeout. - Avoid unbounded ACE accumulation in the token default DACL / object ACLs across sandbox runs (root cause of error 1344).
- Surface a visible "execution broker unhealthy" state instead of silent hangs.
4 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Analysis (community)
Sharing a static code read against
mainthat lines up with the reporter’s symptoms. I have not reproduced this on a Windows host; confidence is medium-high on the hang/timeout path (code-verified), and medium on the exact ACE-growth mechanics that produce helper CPU spin vs. error1344(consistent with design + issue evidence, not runtime-confirmed here).Environment / versions (from report + related)
codex-cli 0.144.6, Windows 1110.0.26200, interactiveworkspace-writecodex-windows-sandbox-setup.exe; unelevated works (0.145.0-alpha.18/ Desktop)Repro status
cmd.exe /d /c echo …), isolation table (in-thread hang vs freshcodex execOK), timestamp-matched high-CPU setup helpers, exact error strings, post-recoveryTokenDefaultDacl/1344.Observed vs expected
| Observed | Expected |
| --- | --- |
| After
helper_unknown_error: apply deny-read ACLs, every sandboxed spawn in the resumed thread hangs; tool timeouts appear ignored | Fail fast with an actionable setup/runner error; honor tool/timeout cancellation || Each hung probe leaves a spinning
codex-windows-sandbox-setuporphan | Setup helper exits (success or structured failure); no unbounded CPU spin || Resume re-breaks the conversation; fresh
codex exec/ new threads work | Setup failures are process-local and recoverable without abandoning the thread || Later:
SetTokenInformation(TokenDefaultDacl) failed: 1344(ERROR_ALLOTTED_SPACE_EXCEEDED) | Default-DACL / object ACL growth stays within Windows allotment, or maps to a clear retryable setup error |Root-cause hypothesis
Primary hang mechanism (high confidence from code): every elevated sandboxed spawn goes through
require_logon_sandbox_creds→ alwaysrun_setup_refresh_…before the runner starts (identity.rs, comment: “Always refresh ACLs … via the setup binary”). The orchestrator then waits oncodex-windows-sandbox-setupwith no bound:run_setup_refresh_payload→Command::status()(unbounded child wait) —setup.rsWaitForSingleObject(…, INFINITE)—setup.rs/run_setup_exe_payloadrun_setup_singleflightand block on aCondvaruntil the leader completes — also no timeout (SetupFlight::wait)If the helper stalls (reporter: indefinite CPU after/around deny-read / ACL work; #33732: “setup binary completed” never logged after ACL phase), the blocking setup path never returns. Tool
timeout_ms/ cancellation appear to cover the post-setup capture path rather than racing the setup wait, so timeouts look “never honored” and each probe can leak another setup process.Secondary durability factor (medium confidence): sandbox security state is intentionally persistent across runs:
sync_persistent_deny_read_acls(deny_read_state.rs) — keeps deny-read ACEs for descendants that may outlive the launcher; state in.sandbox/deny_read_acl_state.json; per-principal revoke onlytoken.rsset_default_dacl/create_token_with_caps_from) grantsGENERIC_ALLto logon + Everyone + every capability SID — error string matches the report exactly:SetTokenInformation(TokenDefaultDacl) failed: {err}cap.rs) grow without an obvious global GC in the paths reviewedThat design can bloat object DACLs and enlarge default DACLs until Windows returns
1344, and maps untyped helper failures throughHelperUnknownError— matching the first surfaced stringhelper_unknown_error: apply deny-read ACLsaroundsync_persistent_deny_read_acls(...).context("apply deny-read ACLs")in the setup Full path.On “conversation-thread-scoped broker binding across restart”: I did not find a serialized broker IPC handle that would reattach after a full process death. More consistent explanations with the code:
SETUP_FLIGHTSsingleflight while a long-lived client/app-server process is still alive;Happy to be corrected if there is a durable binding I’m missing.
High-level fix outline (not a PR)
Small, layered changes (each independently shippable). Analysis only — not opening a PR unless the team invites one.
Command::status/WaitForSingleObject(INFINITE)with a budget (and cancel on tool expiration); kill the setup child on timeout; map failures to structuredSetupErrorCode(including a dedicated deny-read / ACL code instead ofHelperUnknownError).TokenDefaultDaclcannot exceed allotment (e.g. grantGENERIC_ALLonly to a small fixed set if capabilities are not required on the default DACL — note the PowerShell pipeline/IPC comment intoken.rs); (b) GC unused principals fromdeny_read_acl_state.jsonand revoke their ACEs carefully; (c) prune unusedwritable_root_by_path/workspace_by_cwdafter inactivity; (d) surface1344as a retryable setup error with cleanup guidance.Non-goals / risks
Test ideas
Failing-first, if the team (or an invitee) implements:
run_setup_refresherrors within timeoutTand does not leave a permanentSETUP_FLIGHTSentry blocking a second call with the same key.TokenDefaultDaclsize: largeNcapability SIDs either succeeds under a reduced default-DACL policy or returns a structured error containingTokenDefaultDacl/1344(not a hang).Questions for maintainers
SETUP_FLIGHTS/ helper processes outlive the UI?SetEntriesInAclW/GetNamedSecurityInfoon bloated DACLs (stack/ETL / sandbox log would settle this)?1344from too many ACEs in one default DACL, or from a corrupted/oversized ACL buffer on the base token?.sandboxsetup logs +deny_read_acl_state.json+ rough capability-SID counts if still available?---
Happy to refine this analysis with any corrections from the team. If maintainers later decide an external PR would help and invite one, I’d be glad to help implement a minimal slice (e.g. bounded setup wait + singleflight cleanup) — no expectation of that unless useful.
Building on the root-cause analysis above with a finer blame pass on the wait origins and the current fix status. Traced against
mainas of5dd992a(2026-07-24).#32864 is an amplifier, not the origin of setup hangs.
git log -S "SetupFlight" -- codex-rs/windows-sandbox-rs/src/setup.rsreturns3370181ec/ #32864 (2026-07-13, "Coalesce concurrent Windows sandbox setup requests"), a single-file PR addingSetupFlight/Condvar/run_setup_singleflightwith no timeout. Before #32864, each setup spawn ran its own unbounded wait, so one stuck helper hung only that tool call. #32864 made concurrent identical-payload spawns share one leader's unbounded wait - amplifying a per-spawn stall into a shared-payload wedge. I am not calling the whole hang class a "recent regression": the underlying unbounded waits predate #32864 by months.Wait origins (finer blame pass). The setup helper waits (refresh
Command::status()and elevatedWaitForSingleObject(INFINITE)) originate in #7792 ("Elevated Sandbox 2",13c0919bf, which createdsetup_orchestrator.rs, later refactored intosetup.rs), not #24831. The parent of #24831 (cb9178e^) already has both.status()on the refresh/setup paths andWaitForSingleObject(sei.hProcess, INFINITE)on elevated setup. #24831 (2026-05-29, "Add Windows sandbox provisioning setup command") added provisioning /refresh_onlyplumbing, not the waits. I am not claiming #4905 as the elevated-setup-wait origin -git log -S "WaitForSingleObject"hits #4905 on a different path (sandbox process capture), not the setup-helper wait.Wedge mechanic (process-local, payload-keyed).
setup.rs:174-179removes theSETUP_FLIGHTSentry only after the leader'srun()returns; the leader'srun()callsrun_setup_refresh_payload->Command::status()(setup.rs:362, unbounded) / elevatedWaitForSingleObject(…, INFINITE). Concurrent same-payload waiters sharerun_setup_singleflightand block on aCondvaruntil the leader completes - also no timeout (SetupFlight::wait). So one stuck leader occupies the flight for that payload key until it returns. The singleflight is process-local + payload-keyed (it may manifest as a conversation wedge if the app-server process lives across turns, but the mechanism is not a durable conversation-broker binding).Not fixed as of
5dd992a. No commits touchsetup.rsafter the 2026-07-22 issue date.mainstill has unboundedcmd.status(),WaitForSingleObject(INFINITE), and the timeout-lessSetupFlight::wait. The two post-singleflight commits tosetup.rs(dfd2d81#34612,999a715#34613) only addedstdin(Stdio::null())and proxy routing - no timeout. #34629 "Harden Windows elevated sandbox startup" (2026-07-21) and #34624 "Terminate process trees" did not touch the wait/singleflight paths.Existing test + its specific hole.
setup.rs:1339 identical_setup_requests_share_one_in_flight_run(also added by #32864) covers only happy-path dedup (waiter joins leader,runs == 1). It does not exercise the stuck-leader / timeout-recovery path. A useful failing test would inject a same-key stuck leader (whosecmd.status()never returns) and assert a bounded wait recovers and the flight is cleaned up - distinct-payload keys already do not share a flight today, so that is not the right recovery criterion.Hi @Lut3ce, this subagent persistence/history issue aligns with some boundary anomalies observed in multi-agent rollouts. Codex Rescue Alpha5 provides read-only lifecycle and subagent boundary diagnostics, cleanly separating historical start markers from current live execution state without altering the source rollout.
If you have access to the local session, you can run a non-destructive check:
No raw session data is required, and please redact private paths if you share any output.