Repository navigation
Replies: 4 comments 5 replies
感谢这份报告 —— 两个请求你好, 先说结论:这份报告的「发现 C」直接证伪了我们三天前那条 请求一:那份插件的源码你写「可按需附完整源码」—— 我们想要。原因很实在:我们想在本机复现一遍,而不是照抄结论。
请求二(可答可不答)你报告里提到一个新问题:「工作区属主是 最后一句你开头那段声明(「全部内容由 DeepSeek 生成,发帖人只负责执行命令」)写得很好 —— |
第二个样本:原版在我们这台机器上是失败的 —— 需要三处「打包布局」补丁
⇒ 任何需要插进机器级配置的补丁,都应该配一个这样的自检。这点是我们最应该学的。 下面是我们的结果。结论先说:在我们这台机器上,你的原版直接失败,需要三处补丁才能跑通;打完补丁
我们的环境(和你的差别不小)
⇒ 顺带补一个数据点:这条报告在 原版失败的直接原因:
|
| # | 改动 | 为什么这台机器必须改 |
|---|---|---|
| ① | koffi 从 LINKED_PACKAGES 挪进 COPIED_PACKAGES |
unpacked 下没有它,只能从 asar 复制 |
| ② | COPY_RULES 加 'src/' |
koffi 的入口 index.cjs 会 require('./src/koffi/index.cjs') |
| ③ | @koromix/koffi-win32-x64 也改成 COPIED,COPY_RULES 加 'win32_x64/' |
unpacked 里那个目录只有 native 那一半 |
改完 LINKED_PACKAGES = [],linkPackages() 自然成了空操作。
其余文件逐字节未动 —— index.js、lib/asar.js、lib/fs-original.js、lib/runner-console.mjs、lib/selftest.mjs、package.json、README.md 全部 SHA-256 相同。
打完补丁后的 selftest
runtime dir: C:\Users\<user>\.dsh\cache\sandbox-console-fix\win32-121348951-1792047035641
reused: true
node.exe: F:\DeepSeek Harness\resources\runtime\primary-runtime\dependencies\node\bin\node.exe
wrapper: ...\cache\sandbox-console-fix\win32-...\runner-console.mjs
runner: ...\node_modules\@deepseek-ai\dsh-sandbox-windows-acl\lib\runner.js
patch row name: file:///C:/Users/<user>/.dsh/plugins/sandbox-console-fix/index.js
status: 0
stdout: "WRAPPER_OK\r\n"
stderr: ""
SELFTEST OK
(reused: true 是因为我们已经在另一个目录跑过一次解包;reused: false 是干净机器第一次跑的样子。)
两个小问题(不影响结论,但会影响别人排查)
1)plugin.log 的路径:selftest 打印的和插件实际写的不一致。
lib/selftest.mjs:27打印的是join(runtime.dir, '..', '..', 'plugin.log')⇒%DSH_HOME%\cache\plugin.logindex.js的makeLogger()实际写的是join(stateDir(), 'plugin.log')⇒%DSH_HOME%\cache\sandbox-console-fix\plugin.log
差一级目录。README 里写的是后者。⇒ 建议 selftest 直接打印 stateDir() 拼出来的路径 —— 否则照着 selftest 的提示去 cache\plugin.log 找日志会找不到文件,而「日志是空的」和「日志在别处」在排查时长得一模一样。
(我们实测:%DSH_HOME%\cache\plugin.log 不存在,真正的日志在 %DSH_HOME%\cache\sandbox-console-fix\plugin.log。)
2)缓存 key 里没有插件目录 —— 不是 bug,但值得写进 README。
ensureRuntime() 的 key 是 platform + asar.size + asar.mtimeMs,与插件放在哪个目录无关。我们照建议把插件从审计目录搬到了 ~/.dsh/plugins/ 之后,selftest 仍然输出 reused: true —— 这是正确的(缓存里装的解包产物和包装脚本由缓存目录自己承载,与插件位置无关)。
但如果有人误以为「缓存里存的是旧插件代码」,就会白删一次缓存、白等一次解包。⇒ README 里加一句「移动插件目录不会让缓存失效,这是预期行为」就够了。
挂载之后:patch 层被热重载了,不需要重启
这一条你可能会感兴趣,因为 README 里写的是「完全退出并重新打开 DSH」。
我们的观察是:把 $DSH_HOME/cordis.patch.yml 写下去的两秒后,插件日志就出现了 [active]:
cordis.patch.yml 创建于 14:24:13(UTC+8)
plugin.log [active] 于 14:24:15(UTC+8)
2026-10-18T06:24:15.779Z [active] windowsAclRunnerArgs =
["F:\\...\\resources\\runtime\\primary-runtime\\dependencies\\node\\bin\\node.exe",
"C:\\Users\\<user>\\.dsh\\cache\\sandbox-console-fix\\win32-121348951-1792047035641\\runner-console.mjs"]
(runtime C:\Users\<user>\.dsh\cache\sandbox-console-fix\win32-121348951-1792047035641)
⇒ 在 0.2.0-rc.2 上,home patch 层的变化会被 profile reconcile 拾取(app-boot/config-reload)。我们没有继续追它的触发条件,只记录观察结果。
实测结果:workspace-write 下通过(按你的验证标准逐条跑)
| 探测 | 你的标准 | 我们的结果 |
|---|---|---|
cmd /c echo |
有输出、无 0xC0000142 |
✅ WW_ALIVE,status 0 |
| 工作区内写文件 | 成功 | ✅ WS_WRITE_OK,读回 hello |
工作区外(C:\Windows\Temp)写 |
拒绝 | ✅ OUTSIDE_WRITE_DENIED,文件未创建 |
%TEMP% |
重写到私有 dsh-XXXXXX |
✅ ...\<tests>\temp\dsh-VQmmBg |
whoami |
仍拒绝访问(Low 令牌) | ✅ Access is denied |
| 事后 ACL | 收回 | ✅ 测试目录可被普通权限删除 |
又补了最后一块:真实会话里也测过了
上面那组是直接调用 runner 做的(参数与 DSH 内部 confine() 完全一致,但不经过 sandbox provider)。为了不留这个缺口,我们后来把这个会话的权限预设真的切到 workspace-write,再跑一遍 —— 这一档下如果 confine() 没换上新的 runner 前缀,第一条命令就该是 0xC0000142:
探测(真实会话 · workspace-write 档) |
结果 |
|---|---|
Write-Output |
✅ 有输出 |
$env:TEMP |
✅ C:\Users\<user>\AppData\Local\Temp\dsh-f20x6u(重写到私有目录) |
| 工作区内写文件 | ✅ 成功 |
工作区外(C:\Windows\Temp)写 |
✅ UnauthorizedAccessException |
cmd /c 子进程 |
✅ 正常 |
whoami |
✅ 拒绝访问(Low 令牌) |
⇒ confine() 确实用上了新前缀。 至此三件事都齐了:插件被加载 → runner 前缀被设置 → 真实命令走的就是它。
更正 + 一个新发现:能执行,但抓不到输出
上一版这里写过「node.exe 被拒、我们没解释清楚」。那个判断是错的,先更正。
补做四轮对照实验后,真正的分界线是「要不要新建管道去捕获子进程的输出」,与 exe 本身能不能执行无关:
| 方式 | 结果 |
|---|---|
Start-Process node.exe <script> -NoNewWindow -Wait |
✅ NODE_RAN v24.21.0 pid=20080(node 确实跑起来了) |
Start-Process cmd.exe /c "hostname > file" |
✅ 文件里写出 Saika |
Start-Process hostname.exe(不重定向) |
✅ 正常输出、exit 0 |
& node.exe --version(要捕获输出) |
❌ StandardOutputEncoding is only supported when standard output is redirected |
& cmd /c hostname(要捕获输出) |
❌ 同上 |
[Diagnostics.Process]::Start + RedirectStandardOutput |
❌ 拒绝访问(卡在 CreatePipe 那一侧) |
⇒ node.exe 在 F:\ 上能跑;复制到工作区内也能跑。 之前看到的「拒绝访问」来自「建管道 / 继承句柄」那一步,与目标 exe 的位置、签名、依赖都无关。
机理大概就是你的「发现 A」:workspace-write 的受限令牌,其默认 DACL 只被合并了一条能力 SID 的 ACE(不含 Everyone)⇒ 这个令牌新建的对象(管道)默认 DACL 不允许通用受托人访问 ⇒ 管道建起来之后拿不回可继承的句柄。
⇒ 这给「建议 3」加了一条理由:在 workspace-write 下也合并一条通用受托人,不只是安全权衡,也是可用性修复 —— 否则任何「跑一条命令并读它的输出」的用法(git status、构建、测试)都得退化成「写到文件再读」。
谢谢你更正那个「工作区属主检查」
这一条对我们价值很大,而且方向很干脆:你核出 Workspace is not owned by the current user; refusing automatic ACL preparation 是第三方插件 dsh-ops(lib/workspace-preparation.js:57 → bin/prepare-workspace.ps1:14-16)抛的,不是 DSH 核心。
我们这台机器恰好没装 dsh-ops —— 所以在读到你更正之前,我们差点把这段描述当成核心缺陷记进自己的笔记。
⇒ 把「插件行为」和「核心行为」分开写,避免了一个会顺着文档传下去的错误结论。 这比多一条复现数据更重要。
最后一条建议(不算挑错)
LINKED_PACKAGES 这个默认值,可能换成「默认全复制」更稳。
理由:「unpacked 里有什么」取决于打包器配置,不同安装方式(默认位置 / 非默认盘 / 未来某个版本)都可能不同;而 junction 失败的表现是运行期才炸(require 解析失败,报错点离原因很远),复制则只是慢一点、多占约 0.4 MB。
⇒ 如果只是本机用的插件,怎么都行;如果要向官方提,建议把「不要假设 app.asar.unpacked 里有什么」写进理由 —— 我们这台就是现成的反例。
独立复现与一点补充(另一个环境类别的数据点;不涉及结论,仅供排查)
环境:Windows 10 Home China 25H2(10.0.26200.9457,x64)、DSH Desktop nightly 0.2.1-rc.2( 直接复现:用与 harness 完全相同的 runner 承载方式(
补充:用公开的 影响面统计(解压本机 我们目前的自救方式(不是修复建议,只是说明现状):把该会话的权限预设切到 |
谢谢致谢 + 两条修复路径的对照(换宿主 vs 补通用受托人)
我们这边走的是另一条路(换宿主),已经跑通,正好能和你们的结论拼上。两件事想补给你们。 1. 我们用的不是「补通用受托人」,而是「换宿主」环境:Windows 11 26300 / DSH 做法与主帖一致:把 runner 前缀换成自带 按主帖的验证标准实测(
⇒ 换宿主能让它跑起来,而且不削弱隔离(ACL 那部分完全没动)。 2. 但换宿主不解决「抓不到输出」—— 这正好接上你们的发现主帖新增的附录(PowerShell 在该会话里建管道失败)我们复现过,四轮对照的结果是:
关键点:这条限制在换宿主修好之后依然存在。 ⇒ 而你们的实验说明「补 Everyone / Users 就能活」—— 那么它很可能同时解决两件事,因为管道也是「该令牌新建的对象」,同一个 DACL 问题。 3. 一个建议做、但我们可以先猜的实验如果你们那边方便,把
我们这台是 一点边界声明
|
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
[Windows] workspace-write 下所有受限子进程死于
0xC0000142:宿主镜像没有控制台 + 受限令牌默认 DACL 不含通用受托人(0.2.1-rc.2仍复现,附最小复现与修复建议)TL;DR
workspace-write下每条命令都是(no output)+exit 3221225794(0xC0000142),read-only与danger-full-access正常。process.execPath(GUI 子系统的DeepSeek Harness.exe)当作 runner 解释器,该进程没有可继承的控制台;② 受限令牌的默认 DACL 只被追加了一条能力 SID 的 ACE,不含 Everyone/Authenticated Users 这类通用受托人。受限子进程的 DLL 初始化两者缺一即死。[process.execPath, runner.js]换成「自带的控制台子系统node.exe+ 一个先AllocConsole()再import官方 runner 的包装脚本」。这份修复已以本地插件形式跑通(见文末),无需改app.asar,实测区内可写、区外被拒、TEMP被重写,隔离未被削弱。0.2.1-rc.2仍未修。环境
0.2.1-rc.2(另在0.2.0-rc.2上得到同样结果),安装于%LOCALAPPDATA%\Programs\DeepSeek Harness\ELECTRON_RUN_AS_NODE=1,process.execPath=DeepSeek Harness.exe(GUI 子系统)C:\Program Files\PowerShell\7\pwsh.exe);DSH 自带 node v24.21.0现象
read-onlyworkspace-write(no output)+[exit code: 3221225794],重试恒定danger-full-access3221225794=0xC0000142=STATUS_DLL_INIT_FAILED:子进程死在加载器初始化,因而没有任何 stdout/stderr。发现 A:从受限内部读出的默认 DACL(无需打补丁)
在受限子进程里用
GetTokenInformation(TokenDefaultDacl)读回自己的令牌(探针:原生node.exe+ koffi,不需要改任何 DSH 文件):read-only0x1F01FF→S-1-1-0(Everyone)workspace-write0x1F01FF→S-1-4-…-1(能力/temp SID)0xC0000142两种模式的其余 ACE 完全相同(
BUILTIN\Administrators、SYSTEM、logon SID),完整性均为S-1-16-4096(Low),组里Administrators/Users已降为 use-only(0x10)——说明受限是生效的。源码依据:
packages/sandbox/sandbox-windows-acl/src/index.ts把令牌默认 DACL 合并(不是替换)一条 ACE:read-only没有 write SID/temp SID,于是取worldSid(Everyone);workspace-write取能力 SID。这正是两种模式表现相反的原因。发现 B:控制台是另一个充分变量(因果实验)
同一个 runner(
lib/runner.js)、同一组 argv、同一个受限令牌,只改变「宿主进程有没有控制台」:exit -1073741502=0xC0000142,标记文件未创建AllocConsole()exit 0,受限子进程执行并写入工作区AllocConsole()之后FreeConsole()0xC0000142→ 必须在「创建子进程时」控制台存在,而不是调用副作用OK、区外写DENIED、TEMP被重写到私有dsh-XXXXXX、完整性 Low → 隔离未被削弱发现 C:与「是否 .NET」无关(纠正一处流行猜测)
同一 GUI 无控制台宿主、同一
workspace-write配置下:pwsh0xC0000142node.exe -e "…"0xC0000142cmd.exe /c "echo …"0xC0000142→ 原生可执行文件同样死亡。「.NET 初始化失败 / 缺
LoadUserProfile」不是本缺陷的必要条件(补LoadUserProfile不会修好它)。与既有讨论的关系
[process.execPath, builtEntry])与本报告一致,并给出 19045 上的臂表。本报告补的是:受限令牌默认 DACL 的实测(从受限内读出,无需补丁)、原生 exe 也死、以及 26200 /0.2.1-rc.2上的复现。CreateProcessAsUserW;但仓库内另一条CreateProcessW路径是以当前令牌运行(等于不再受限),且创建函数不解释子进程侧的 DLL 初始化失败。.NET/LoadUserProfile假设被发现 C 实验证伪。0.2.1-rc.2仍未修复,且新增一个前置检查问题windowsAclRunnerInvocation()里仍是return [process.execPath, builtEntry](其上方注释却写着 “The prefix stays[node, runner, ...]”),且整个app.asar内AllocConsole零命中。BUILTIN\Administrators而调用者是Administrator时直接拒绝(Workspace is not owned by the current user; refusing automatic ACL preparation)。dsh-ops0.3.1 的lib/workspace-preparation.js:57调用bin/prepare-workspace.ps1:14-16抛出的;仅在workspace-write+ shell 类工具执行时触发,判定是(Get-Acl <root>).GetOwner(SecurityIdentifier) ≠ 当前用户 SID。修复:icacls "<root>" /setowner "%COMPUTERNAME%\%USERNAME%" /T /C(本机 593 个文件,0 失败)。workspace-write下运行。此前记录「单次命令提权仍被拦」的准确解释是——工具级单次提权不改变会话策略,钩子解析到的仍是workspace-write;把会话策略整体切到 full access 是否即绕过,未验证。这属于另一个问题,建议单列(并报给dsh-ops)。修复建议(按侵入性排序)
process.execPath当作 runner 解释器;改用安装自带的控制台子系统 node:<install>\resources\runtime\primary-runtime\dependencies\node\bin\node.exe。注意普通
node.exe读不了app.asar,runner 及其依赖闭包需要以 asar 外可读的形式提供(或解包到app.asar.unpacked)。参考实现:下节「用户侧已验证的规避」的本地插件——只替换 runner 前缀、复用官方 runner,已在本机
0.2.1-rc.2+ 26200 上跑通。spawn前若GetConsoleWindow() === null则AllocConsole()(可ShowWindow(SW_HIDE)隐藏,独占控制台时)。需要给dsh-win32-process增加allocConsole/getConsoleWindow两个绑定。setTokenDefaultDaclGrant在workspace-write下也合并一条Everyone(即read-only现在的行为)——可用但会放宽新建对象的默认 DACL,属于安全权衡,建议至少作为可配置项。验证标准:
workspace-write下Write-Output ok有输出;区外写入被拒;TEMP被重写到dsh-*私有目录;whoami仍拒绝访问(Low 令牌)。用户侧已验证的规避:一个本地 DSH 插件(不改
app.asar)app.asar开了EnableEmbeddedAsarIntegrityValidation,直接改归档不安全;但LocalSandboxProvider.internals(官方代码注释为 “Test hook”)每次 confine 都会重读,所以插件只需要设置一次即可,等价于:
挂载方式
$DSH_HOME/cordis.patch.yml是机器级 patch 层(对所有 profile 生效,在 profile 自己的cordis.patch.yml之后合成)。新建该文件并写入:insert里的name若是绝对路径,dsh-app-boot的anchorInsertedPluginNames会自动转成file://URL,所以插件目录不必放进 profile 的node_modules。patch 层通常几秒内自动热加载(实测约 2 秒),但重启一定生效;patch 写错时 DSH 启动会直接报错,不会静默忽略。
插件做了什么
ctx.sandbox,并确认internals.windowsAclRunnerArgs/windowsAclRunnerEntry没被占用——被占用就什么都不做,因此与上游修复天然兼容。app.asar解包到缓存目录(普通node.exe读不了asar):
dsh-sandbox-windows-acl(含lib/runner.js与lib/types-*.js)、dsh-win32-process、dsh-lazy-require、dsh-subprocess、yaml。koffi与@koromix/koffi-win32-x64是整个包从 asar 复制的——不能假设resources/app.asar.unpacked里有什么(unpacked 的内容取决于打包器配置,不同构建/安装方式不一样,实测两种构建就不同);只有在判定 unpacked 副本完整时才用 junction 作快速
路径。
dsh-skill用只导出BUNDLED_SKILL_RANK = 0的桩(runner 不需要它)。缓存 key 里带
asarSize+asar mtimeMs,DSH 升级后自动重新解包,不会用旧代码(key 不含插件目录,所以移动插件目录不会让缓存失效——缓存里是 runner 依赖,不是插件代码)。
runner-console.mjs,保持官方 CLI 契约不变,再设置 runner 前缀。前提与边界
ctx.sandbox存在时激活;其他平台 no-op。不需管理员权限,不写系统目录。runnerCommand配置:那条路用 bwrap 方言、并把 enforcement 误报成full;插件改的是 runner 前缀,confine()仍走windows-acl分支,partial与拒绝签名都不变。index.js+lib/asar.js、lib/fs-original.js、lib/runtime.js、lib/runner-console.mjs、lib/selftest.mjs);实现细节如需核对,正文里可以补充说明。lib/selftest.mjs:在安装/升级后单独跑一次WRAPPER_OK即可确认包装链在当前app.asar上仍然成立。包装脚本的核心只有这几行(其余是解包与缓存管理):
runner.js不会因为多了这一层而改变行为:它仍然从process.argv.slice(2)解析同一套 CLI 参数,仍然自己铸造受限令牌、自己收回 ACL。两个实现坑(上游自己修时也会撞到)
fs.readFileSync('<install>/resources/app.asar')会被当成“在归档里找一个叫 app.asar 的文件”而抛
ENOENT。要读归档字节必须用require('original-fs').readFileSync(未打 asar 补丁的原始 fs)。node.exe是 Console 子系统,父进程没有控制台时会新分配一个;包装脚本再补一次
AllocConsole()(当GetConsoleWindow()为空)作为兜底,并在独占控制台时
ShowWindow(SW_HIDE)避免闪窗。顺序是「先有控制台,再 spawn」。真实 DSH 中实测(
0.2.1-rc.2/ Windows 11 26200 / 默认workspace-write/ 未提权)Write-Output ok0xC0000142C:\Windows\Temp)写UnauthorizedAccessException(拒绝)$env:TEMP...\Temp\dsh-XXXXXXcmd /c子进程ok+ 区外仍被拒,排除单会话偶发0.2.0-rc.2/ Windows 11 26300 / 非默认盘安装)0xC0000142;装本插件后同样通过——与 DSH 版本、安装位置无关隔离强度与官方
windows-acl路径一致:拒绝分类、partialenforcement 声明、ACL 授予与撤销都走上游逻辑,插件只换宿主解释器,不复制任何隔离代码。
卸载
删掉
$DSH_HOME/cordis.patch.yml里那一行(或整个文件),重启 DSH。插件目录与解包缓存留着不影响启动;怀疑缓存坏了就删掉
$DSH_HOME/cache/sandbox-console-fix/再重启。附:另一个独立现象(与本文 bug 无关,供参考)
在
workspace-write会话里,PowerShell 对原生 exe的启动会因“是否捕获输出”而结果不同(同一受限令牌、同一个
node.exe):& <node.exe> --version(不捕获输出)v24.21.0$v = & <node.exe> --version(捕获/重定向)Program 'node.exe' failed to run: StandardOutputEncoding is only supported when standard output is redirected.$h = & hostname.exe(捕获任意原生 exe)cmd /c "<node.exe>" --version > filev24.21.0Start-Process ... -RedirectStandardOutput filev24.21.0即:不是某个 exe 被沙箱禁止执行(后两条已经用同一令牌把同一个 exe 跑起来),而是
PowerShell 7.6 在该会话里替子进程建管道/重定向输出时失败。猜测与该 shell“有控制台、但
stdout 实际是管道”有关(
Console.IsOutputRedirected与实际不符),未在 PS 源码中定位。最小复现脚本如需核对可以补充。
复现材料
node.exe+ koffi 读TokenDefaultDacl/TokenGroups/TokenIntegrityLevel(约 120 行,无第三方依赖)。runner.js,分别用node.exe与DeepSeek Harness.exe(ELECTRON_RUN_AS_NODE=1)承载,其余不变。AllocConsole()/AllocConsole()+FreeConsole()两个对照。TEMP、whoami、pwsh版本。index.js+lib/asar.js(纯 JS 读app.asar)、lib/fs-original.js、lib/runtime.js(解包缓存;koffi 整包复制,unpacked 副本完整时用 junction 快速路径)、lib/runner-console.mjs、lib/selftest.mjs——约 300 行,无第三方依赖。致谢
本报告的许多关键信息来自社区,尤其要感谢 #9858 的作者 @Void-LifeStream:
0.2.0-rc.2+ 非默认盘安装 + Windows 11 26300 的环境下独立复现了0xC0000142,并逐项列出实验条件与输出——这证明该缺陷与安装位置、PowerShell 版本、安装方式都无关。.NET/LoadUserProfile假设后来被本报告的「发现 C」修正:原生 exe(node.exe、cmd.exe)同样失败,所以运行时不是变量。但这个假设是关键的排除步骤——正是为了检验它才做了原生 exe 对照,从而把变量收窄到「控制台」这一项。app.asar.unpacked里有什么(不同构建的内容不同);②selftest打印的日志路径少了一级。这两个缺陷都已修复并写入正文。dsh-ops,从而帮助把「工作区属主前置检查」从 DSH 核心问题里分离出来(见「0.2.1-rc.2仍未修复」一节的更正)。最后再次说明:发帖人没有任何计算机背景,本报告的定位过程、全部实验与结论都是 DeepSeek 生成的,发帖人只负责执行命令和回传原始输出。请不要把本文当作“作者本人的技术判断”;如果结论与官方排查不符,以官方判断为准,并欢迎直接指出错误。
All reactions