Replies: 5 comments 2 replies
Defect 3 —
|
| Mechanism | What breaks | |
|---|---|---|
| Defect 1 | AllocConsole inside a restricted token |
any console-subsystem child of the GUI-subsystem entry binary |
| Defect 2 | token default DACL has no SID that is also enabled | creation of unnamed objects (CreatePipe) |
| Defect 3 | mandatory integrity label = Low | creation of objects in Medium-integrity namespaces (\BaseNamedObjects) |
They are independent. Applying the Defect 1 fix lets the shell start, and the Defect 2
fix restores pipes, but MSYS2 applets still die on Defect 3.
Suggested fix
Either of:
- Do not downgrade the integrity level for shells that require global named objects,
or expose it as a knob on the windows-acl backend (e.g.
integrityLevel: 'low' | 'medium'), so a plugin whose shell depends on
\BaseNamedObjectscan opt out. This is a small conditional. - If Low integrity must remain for security reasons, redirect those objects into a
namespace the Low token can write — a per-session
\Sessions\<n>\BaseNamedObjects-style container. That needs cooperation from the
shell runtime (MSYS2), so option 1 is the pragmatic path.
For reference, read-only avoids the visible failure here only because MSYS2 applets
still cannot start — the mode is simply less commonly exercised with this shell.
Note: this cannot be fixed from the plugin layer
The plugin ships a native guard (msys-token-guard.exe + a Detours hook DLL) that hooks
RtlAddAccessAllowedAce, NtSetInformationToken and CreateProcessW specifically to
let MSYS2 survive WRITE_RESTRICTED. It cannot help with Defect 3 because:
- the integrity level is committed before the guard is spawned (the guard only ever
runs inside the already-restricted token), and NtCreateDirectoryObjectis not among the hooked APIs.
So a fix has to happen in dsh-sandbox-windows-acl or in the spawn path, not in a
third-party shell plugin.
For completeness, here is the exact probe output from that session:
=== A. shell identity ===
shell = /usr/bin/bash
version = 5.3.15(1)-release
msystem = MSYS
cwd = /d/workspace/<...>
=== B. write INSIDE workspace === INSIDE: OK
=== C. write OUTSIDE workspace === OUTSIDE: DENIED <- sandbox boundary is correct
=== D. native binaries === git -> 0, cmd -> 0
=== E. MSYS2 binaries === all -> 127 (0xC0000022, NtCreateDirectoryObject)
=== F. guard marker === CYGWIN_TESTING=1 <- guard did run
|
patch_dsh.sh
Result: |
|
I cross-checked a few earlier Windows "workspace-write" reports because several of them look identical at the symptom level but appear to be different failure paths. My current map is:
So the broader symptom “Windows "workspace-write" is unusable” currently seems to contain at least four different mechanisms, rather than one root cause. Would it be useful to keep future reports routed into these separate buckets and cross-link duplicates to the corresponding thread? I can help map new reports when the mechanism is clear. |
|
Thanks for the report. |

Uh oh!
There was an error while loading. Please reload this page.
DSH Windows ACL sandbox: two defects make
workspace-writeunusableEnvironment
0.2.0-rc.2, Windows 10/11 x64@deepseek-ai/dsh-sandbox-windows-acl→dsh-sandbox→dsh-bash-sandbox→dsh-tool-bashbash.exeis a byte copy ofbusybox.exe)read-onlyworks.workspace-writedoes not.danger-full-accessworks.Impact
Every bash command under
workspace-writefails withexit code 3221225794(
0xC0000142) and no output, so the Bash tool is completely unusable in the defaultmutating mode on Windows.
Defect 1 — sandboxed children die in
conhost.exe(0xC0000142)Symptom
ProcMon evidence
The child's own image and DLLs load fine:
Then it tries to allocate a console, and the console host dies inside the restricted token:
conhost.exenever even reacheskernel32.dll. ProcMon logs noACCESS DENIEDfor thechild itself; the denial is inside the console host.
Root cause
dsh-win32-processspawns restricted-token children throughspawnInheritedJobProcess()with:
There is no
DETACHED_PROCESSand noCREATE_NO_WINDOW.The Electron entry binary is
IMAGE_SUBSYSTEM_WINDOWS_GUI. A console-subsystem child of aprocess with no console makes the Windows loader call
AllocConsole()at process start.That allocates a console inside the sandboxed token, which cannot host
conhost.exe, soAllocConsolefails and the loader reports0xC0000142before any user code runs.Because the runner is GUI-subsystem it never inherits the launching console, so this fires
on every sandboxed spawn, not just some.
Why this is easy to miss when testing outside DSH
A standalone repro that spawns the same restricted token from
powershell.exealwayssucceeds:
powershell.exehas a console, the child inherits it, andconhost.exeistherefore never created. The bug only appears when the parent has no console.
Fix
Make the entry binary console-subsystem so the whole process tree inherits a console:
and launch DSH from a console. Confirmed: after this, direct children and their
fork()descendants run normally, with
app.asarleft completely stock.Equivalent alternative (asar patch): add
DETACHED_PROCESSto the restricted-token spawnflags. That fixes only the direct child — busybox's own
fork()children still allocate aconsole and still die with
0xC0000142— so the subsystem fix is strictly better.Defect 2 —
CreatePipefails under theworkspace-writetokenOnce the console problem is fixed,
workspace-writestill cannot use pipes:Both are the same failure seen with different errno. Nested shells fail identically
(
bash -c 'echo x | cat'from inside the sandbox), so this is not a CRT fd-table orlpReserved2issue — under this token, processes at any depth fail to create a pipe.A direct Win32 probe under the sandboxed token gives the precise error:
It is object creation that fails, not pipe I/O.
read-onlymode works(
echo a | cat→a;printf 'p\nq\n' | wc -l→2), so this is not a platformlimitation.
Evidence — the sandbox token's default DACL
The logon SID is session-specific and the two
S-1-4-*capability SIDs are derived from ahash of the workspace path; both are redacted here.
Note that
Everyone(S-1-1-0) is not in the default DACL, and neither is the user SID.Root cause
New objects created without an explicit security descriptor take their DACL from the token's
default DACL. For a
WRITE_RESTRICTEDtoken the object manager requires both passes ofthe access check:
Administrators,Everyone,logon, …)logon,Everyone, workspace cap SID, temp cap SID)In the DACL above the only ACE with a real, object-specific mask is
[0], and itstrustee
S-1-4-*exists only in the restricting list, never in the enabled groups.ACEs
[1]and[2]carryGENERIC_ALL(0x10000000) and[3]carries0xA0000000(
GENERIC_READ|GENERIC_EXECUTE); those are generic bits that do not map tofile/pipe-specific rights, so they grant nothing for a pipe object. Pass 1 therefore fails
and creation is refused with
ERROR_ACCESS_DENIED.read-onlyworks becausecreateRestrictedToken()callsand in read-only the fallback
worldSid(Everyone) is used, merging a realEveryone:FILE_ALL_ACCESSACE.Everyoneis simultaneously an enabled group (pass 1 ✓)and a restricting SID (pass 2 ✓), so the object is usable. In
workspace-writethe cap SIDreplaces it and neither pass succeeds on its own.
Fix
In
workspace-write, merge a SID that the token actually has enabled —Everyone, thelogon SID, or the user SID — into the default DACL in addition to the cap SID. Concretely,
call
setTokenDefaultDaclGrantforworldSidas well astempWriteSidPtr ?? writeSidPtr,so newly created pipes and other unnamed objects satisfy both passes.
Why files are unaffected but pipes are not
Files created inside the workspace inherit the workspace directory's inheritable ACEs (DSH
sets those explicitly, including
Everyone), so pass 1 succeeds. Pipes have no parentdirectory and must rely on the token default DACL, which is why they are the visible
casualty.
Reproduction
read-onlymode: everything works.workspace-writemode with a stockapp.asar:0xC0000142, no output (Defect 1).Subsystemto03 00and relaunch from a console: simplecommands and
fork()applets work, pipes still fail (Defect 2).TokenDefaultDacl, then callCreatePipeunder that token: itreturns
err=5.Defect 2 can be isolated without a full run. Build a token with
flags = 0xD | WRITE_RESTRICTEDand the four restricting SIDs listed above, impersonate it,and call
CreatePipewhile varying only what gets merged into the default DACL:All reactions