Repository navigation
Replies: 1 comment 2 replies
|
Your reading matches two places in the code. There is also a one-step experiment that separates it from the other causes.
The experiment: launch the desktop from an existing console (cmd or PowerShell) instead of Explorer, then run one confined One more that applies only to a particular account, and is easy to rule in or out: if the process runs as the built-in Administrator (SID ending What I did not verify: whether your Electron host really has no console in the launch shape you used, and what Windows does for the host's own children when it has none. Source read at |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Summary
On Windows,
@deepseek-ai/dsh-sandbox-windows-aclspawns its restricted child withCreateProcessAsUserW. The child is a console-subsystem process. When the process that creates it owns no console, the child cannot finish loader initialization and dies beforemain():The desktop app launches the runner through
process.execPath— the Electron GUI binary — which owns no console. Result: thepwshtool never works in the default sandbox mode, whiledanger-full-accessworks (it bypasses the sandbox entirely), so the failure looks like "the sandboxed tool is broken" rather than "the sandbox runner needs a console".Two mechanisms produce this same exit code and they are independent — see the cross matrix below. One is this console absence; the other is a restricting list missing the keep-alive group, already documented in the package README.
Environment
0.2.1-rc.3(DeepSeek Harness.exeFileVersion),resources/app.asar182,434,783 B@deepseek-ai/dsh-sandbox-local→windows-aclrunner;dsh-sandbox-windows-acl;dsh-win32-process(koffi 3.1.1)Root cause
dsh-sandbox-locallaunches the runner as[process.execPath, runner.js], i.e. the Electron binary withELECTRON_RUN_AS_NODE=1.process.execPathfor the desktop app is a GUI-subsystem executable: the process has no console (GetConsoleWindow() === NULL,GetConsoleCP() === 0).The runner then creates a
WRITE_RESTRICTED+ Low-integrity token and callsCreateProcessAsUserWfor the console-subsystem child. A console-subsystem child of a console-less parent would normally allocate a fresh console itself, but the Low-integrity restricted token cannot — so the child dies in loader initialization beforemain(), surfacing only as0xC0000142.The keep-alive group (logon SID + Everyone) is necessary but not sufficient: without it early DLL init dies (and CNG crashes
pwshwith0xE0434352when it is absent at runtime); with it, the child still needs a console it can share with its parent. The README documents this as "console isolation is unavailable — children share the host console", but the shipped desktop host has no console at all, so there is nothing to share.Verified facts that narrow it down — each measured, not inferred:
whoami /all: same 60 groups, High mandatory level in both parents)nodewith the full Electron env still succeeds)GetConsoleWindow() != NULL→ works,== NULL→0xC0000142CREATE_NO_WINDOW:packages/subprocess/win32-process/src/process.ts:232and:455set onlySTARTF_USESTDHANDLES | STARTF_USESHOWWINDOWwithwShowWindow: SW_HIDE; the only creation flags areCREATE_SUSPENDED/CREATE_UNICODE_ENVIRONMENT(:542,:567). The file states the intent at:454: "Preserve console inheritance: CREATE_NO_WINDOW can fail restricted-token DLL initialization."LUA_TOKENand the built-in Administrator are not involved on current builds: local0.2.1-rc.3app.asar callscreateRestrictedToken(currentToken, 13, …)=0xD(includesLUA_TOKEN), while npm0.2.2-alpha.2(lib/types-DFdfvhLN.js) calls it with9=0x9, andmaster@f3803c9src/token.ts:232passesDISABLE_MAX_PRIVILEGE | WRITE_RESTRICTEDwith no-500branch anywhere. Consistent with that, processes do start inside the confined shell on the affected account.Evidence: parent console (all reproduced locally; runner binary unchanged between rows)
GetConsoleWindow/GetConsoleCP)node.exefrom a consoleokDeepSeek Harness.exe(ELECTRON_RUN_AS_NODE=1), the real host path0xC0000142node.exespawned withdetached: true(DETACHED_PROCESS⇒ no console)0xC0000142node.exestarted withwindowsHide: trueAllocConsole()in the runner beforesandbox.spawn()okTwo qualifications on those rows, so the numbers are not over-read:
windowsHidehides the window; it says nothing about how the confined child is created.DETACHED_PROCESS(0x8) on the child returned 0 but lost all output capture, so it is retracted: "exit 0 with no output" cannot be distinguished from "the command never ran".Child creation flags tested separately (console-less parent):
CREATE_NEW_CONSOLE(0x10) → still0xC0000142, consistent with the README's known limitation.Evidence: restricting list (the second, independent mechanism)
Bisect with all other factors held (Low integrity + default DACL, console-less parent):
[logonsid, EVERYONE, workspaceSid, tempSid][]/[logonsid]/[EVERYONE]/[workspaceSid]0xC0000142Refined on the token side with the upstream parameters and a console present (8 repetitions per cell):
0xC00001420xC00001420xC0000142So both sides of the keep-alive group are required. Earlier notes claimed "
LUA_TOKEN/ the built-in Administrator explains the-500failures"; that does not reproduce on0.2.1-rc.3here (see the token bullet above).Evidence: stdio is not the variable
Measured with creator, token and console state fixed, varying only the child stdio shape (8 repetitions per cell; the child runs
exit 7, so "survived" cannot be confused with "silently did nothing"):0xC0000142STARTF_USESTDHANDLES0xC0000142NULhandles +STARTF_USESTDHANDLES0xC0000142STARTF_USESTDHANDLES0xC0000142STARTF_USESTDHANDLES0xC0000142This rules out "valid std handles can substitute for a console", and matches the README's "pipe-based stdio redirection is unaffected".
Evidence: the two mechanisms are independent
Built the restricted token directly (
CreateRestrictedTokenwithWRITE_RESTRICTED) and spawned withCreateProcessAsUserW, crossing parent-console state with the restricting list:0x00xC00001420xC0000142AllocConsole()in the runner0x0AllocConsole()0xC0000142(decisive)0xC0000142M4shows the console fix works;M5shows it does not rescue a token whose restricting list lacks the keep-alive group.Rejected alternatives (all measured)
CREATE_NEW_CONSOLE(0x10) on the child0xC0000142CREATE_NO_WINDOWon the childProposed fix
Layer 1 (required): give the runner a console when it has none, before spawning, and hide it so GUI users never see a console flash.
Placement: in
main(), immediately after theSetConsoleCtrlHandlercheck and beforenew AclSandbox(...). Failure must be fail-open (one warning line, keep going): a setup without a console then behaves exactly as it does today.Layer 2 (required, regression guard): assert before
CreateRestrictedTokenthat the restricting list contains both keep-alive members, and fail with a message naming the missing side instead of letting the user see a bare0xC0000142.Layer 3 (cheap, high value): when the mirrored child exit code equals
3221225794, write one stderr line stating console presence and pointing at the README section — so the next report carries a real code plus a hint instead of empty output.Verified locally: with this shim in front of the shipped
runner.jsandDeepSeek Harness.exeas the parent, the confinedpowershell.exeruns and returns0x0(row A5).Patch (git diff, +56 lines, applies to master@f3803c9)
If a visible console window is unacceptable, an alternative is a dedicated hidden console host plus
AttachConsole;AllocConsole+ShowWindow(SW_HIDE)is the shortest path and is what this report verified.Limits
0xC0000142" is measured, not explained at the loader level: no debugger was attached to the dying child.README:189"tree carrying another sandbox's package SID" shape. A directCreateProcessAsUserWcall from inside the confined shell would settle it.System32; that is a different layer and both could be real.All reactions