You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Windows: first provisioning of a new workspace can freeze the whole host for minutes
#11545
Windows: first provisioning of a new workspace can freeze the whole host for minutes
Summary
On Windows, when the sandbox provisions a workspace directory for the first time, SetNamedSecurityInfoW runs synchronously on the host event loop and eagerly propagates the descriptor across the whole subtree. On a large tree this blocks the host for minutes (observed >10 min in one incident; a standalone provisioning run over a large root took ~75 min). The entire harness becomes unresponsive: the web UI spins and every local port times out, while host CPU and disk IO stay near zero, because the process is waiting inside a synchronous FFI call rather than being busy.
The same code is present in 0.2.1-rc.5 and 0.2.2-alpha.2 (lib/types-*.js, 64,308 / 64,309 bytes, zero occurrences of INHERITED or child_process), so upgrading does not change this behaviour.
How it triggers
grantWrite() has an idempotent fast path that skips the write when the directory already carries the exact capability ACE, the Everyone deny and the Low label. hasExactGrant / hasExactDeny / hasExactLabel compare the ACE inheritance flags for exact equality (3 / 2 / 3).
workspaceWriteSid() derives the capability SID from the canonical directory path:
A child directory therefore inherits the PARENT's SID, never its own, so the fast path can never hit for a new workspace root. Note that merely masking INHERITED_ACE (0x10) in the comparison is not enough: the SID itself differs. Observed on a fresh workspace root: its ACL carried only the parent's S-1-4-... ACE marked inherited, and the check reported grant=false.
The write itself is one SetNamedSecurityInfoW call with SECURITY_INFORMATION covering the DACL and the mandatory label, which propagates the descriptor across the existing subtree. Its cost is proportional to the tree, and it runs inside the host process.
Why the whole host freezes, not just one request
withPathLock() wraps the get-merge-set sequence in a synchronous Koffi FFI call:
There is no LOCKFILE_FAIL_IMMEDIATELY (4) and no timeout, so a caller blocks in kernel32 until the holder releases. Together with step 3, the host's single JS thread sits inside the propagation and every other request queues behind it.
Controlled experiment
Symptom: both local ports dead at the same time; host CPU and IO deltas near zero.
The held lock file (sha256(lowercase path)[0:16].lock under %TEMP%\dsh-acl-locks) reverse-maps to the workspace directory being provisioned.
Killing the sandbox child process chain does not release the lock and does not recover the host.
Killing the host process releases the lock immediately and both ports come back.
Suggested fixes, in priority order
Do the ACL apply off the host main thread (child process or worker) and have the host wait with a timeout. This is the only change that removes the freeze; the propagation cost itself is inherent to the operation.
Make the lock acquisition non-blocking (LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY) with bounded backoff and a fail-closed error, so no caller can block indefinitely.
Make the fast path reachable for new workspaces: a new workspace root always needs its own ACE, so either provision the directory when it is created, or document that a workspace should be provisioned while it is still empty. The propagation is instant on an empty directory (measured 0.8 s) versus minutes on a populated tree.
Workaround we use meanwhile
We provision a new workspace root before its first use (user WRITE_OWNER + Low label + explicit capability ACE + deny) so the fast path hits, and we avoid opening a session directly on a large directory that was never provisioned.
Happy to provide additional traces or run instrumentation if useful.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Windows: first provisioning of a new workspace can freeze the whole host for minutes
Summary
On Windows, when the sandbox provisions a workspace directory for the first time,
SetNamedSecurityInfoWruns synchronously on the host event loop and eagerly propagates the descriptor across the whole subtree. On a large tree this blocks the host for minutes (observed >10 min in one incident; a standalone provisioning run over a large root took ~75 min). The entire harness becomes unresponsive: the web UI spins and every local port times out, while host CPU and disk IO stay near zero, because the process is waiting inside a synchronous FFI call rather than being busy.Environment
@deepseek-ai/dsh-sandbox-windows-acl0.2.0-rc.2 (packages/sandbox/sandbox-windows-acl)lib/types-*.js, 64,308 / 64,309 bytes, zero occurrences ofINHERITEDorchild_process), so upgrading does not change this behaviour.How it triggers
grantWrite()has an idempotent fast path that skips the write when the directory already carries the exact capability ACE, the Everyone deny and the Low label.hasExactGrant/hasExactDeny/hasExactLabelcompare the ACE inheritance flags for exact equality (3/2/3).workspaceWriteSid()derives the capability SID from the canonical directory path:A child directory therefore inherits the PARENT's SID, never its own, so the fast path can never hit for a new workspace root. Note that merely masking
INHERITED_ACE(0x10) in the comparison is not enough: the SID itself differs. Observed on a fresh workspace root: its ACL carried only the parent'sS-1-4-...ACE marked inherited, and the check reportedgrant=false.The write itself is one
SetNamedSecurityInfoWcall withSECURITY_INFORMATIONcovering the DACL and the mandatory label, which propagates the descriptor across the existing subtree. Its cost is proportional to the tree, and it runs inside the host process.Why the whole host freezes, not just one request
withPathLock()wraps the get-merge-set sequence in a synchronous Koffi FFI call:There is no
LOCKFILE_FAIL_IMMEDIATELY(4) and no timeout, so a caller blocks in kernel32 until the holder releases. Together with step 3, the host's single JS thread sits inside the propagation and every other request queues behind it.Controlled experiment
sha256(lowercase path)[0:16].lockunder%TEMP%\dsh-acl-locks) reverse-maps to the workspace directory being provisioned.Suggested fixes, in priority order
LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY) with bounded backoff and a fail-closed error, so no caller can block indefinitely.Workaround we use meanwhile
We provision a new workspace root before its first use (user WRITE_OWNER + Low label + explicit capability ACE + deny) so the fast path hits, and we avoid opening a session directly on a large directory that was never provisioned.
Happy to provide additional traces or run instrumentation if useful.
All reactions