mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-10-16 07:16:36 +08:00
feat(sandbox): add Windows restricted runner
This commit is contained in:
@@ -375,8 +375,8 @@ jobs:
|
||||
- name: Prepare sherpa-onnx runtime
|
||||
run: node scripts/prepare-sherpa-onnx-runtime.mjs --target x86_64-pc-windows-msvc
|
||||
|
||||
- name: Build Windows app-server, code-mode-host, and sandbox setup sidecars
|
||||
run: cargo build --manifest-path lime-rs/Cargo.toml --target x86_64-pc-windows-msvc -p app-server --bin app-server -p tool-runtime --bin code-mode-host --bin windows-sandbox-setup
|
||||
- name: Build Windows app-server, code-mode-host, and sandbox sidecars
|
||||
run: cargo build --manifest-path lime-rs/Cargo.toml --target x86_64-pc-windows-msvc -p app-server --bin app-server -p tool-runtime --bin code-mode-host --bin windows-sandbox-setup --bin windows-sandbox-runner
|
||||
|
||||
- name: Test Windows shell runtime fallback
|
||||
run: cargo test --manifest-path lime-rs/Cargo.toml -p lime-agent --test windows_shell_runtime -- --test-threads=1
|
||||
|
||||
@@ -2594,7 +2594,7 @@ Settings/Claw evidence。Responsible developer confirmation: root, 2026-08-10。
|
||||
## 42. Windows Sandbox Readiness Owner
|
||||
|
||||
Windows sandbox readiness 是 Desktop 控制面能力;Windows restricted-token runner 的当前基础也已归入
|
||||
`tool-runtime`,但 readiness 仍必须以真实平台 enforcement 证据为准。唯一数据流为:
|
||||
`tool-runtime`,但 readiness 仍必须以真实平台 enforcement 证据为准。控制面唯一数据流为:
|
||||
|
||||
```text
|
||||
Settings execution policy
|
||||
@@ -2606,11 +2606,32 @@ Settings execution policy
|
||||
```
|
||||
|
||||
非 Windows 平台或未启用 workspace sandbox 返回 `notConfigured`;Windows 已启用但 backend 不是
|
||||
`Ready + enforced=true` 返回 `updateRequired`;只有真实 enforcement 才能返回 `ready`。`tool-runtime`
|
||||
当前拥有 target-gated restricted-token runner:受限 token、sandbox account user SID、capability SID、workspace/explicit write-root
|
||||
`Ready + enforced=true` 返回 `updateRequired`;只有真实 enforcement 才能返回 `ready`。Windows 执行面唯一数据流为:
|
||||
|
||||
```text
|
||||
App Server -> tool-runtime::execution_process host
|
||||
-> ACL lease + sandbox account/network policy selection
|
||||
-> scoped named pipes + DPAPI credential
|
||||
-> CreateProcessWithLogonW(windows-sandbox-runner.exe as sandbox account)
|
||||
-> runner current primary token -> restricted token + TokenDefaultDacl
|
||||
-> CreateProcessAsUserW + STARTUPINFOEX
|
||||
-> job + handle list + pipe child
|
||||
-> job + pseudoconsole + TTY child
|
||||
-> framed output/exit/error -> existing bounded ExecutionProcess projection
|
||||
<- framed stdin/close/resize/terminate
|
||||
```
|
||||
|
||||
runner executable、宿主 transport、restricted token 和 child supervisor 都属于 `tool-runtime::execution_process` 同一
|
||||
current owner;它不是 Electron backend、App Server method 或平级 process supervisor。Electron 只将 runner 作为 App Server
|
||||
相邻的 Windows runtime resource 打包并校验 manifest digest。宿主创建的 named pipe DACL 只允许被选中的 sandbox account,
|
||||
连接后校验 client PID;framing、单帧大小、启动握手和 cleanup 必须有界。宿主直接用 sandbox-account token 调用
|
||||
`CreateProcessAsUserW` 以及 `CreateProcessWithTokenW` fallback 均为 `dead / forbidden-to-restore`:前者在普通宿主缺少
|
||||
`SeAssignPrimaryTokenPrivilege`,后者无法可靠承接 pipe child 且不支持当前 ConPTY attribute contract。
|
||||
|
||||
`tool-runtime` 当前拥有 target-gated restricted-token runner:受限 token、sandbox account user SID、capability SID、workspace/explicit write-root
|
||||
双重访问检查、workspace/explicit read/write-root ACL lease、`.git/.codex/.agents` 写入拒绝、Job Object 进程树、显式继承句柄列表、stdout/stderr reader
|
||||
和既有有界 retained output 均在同一 owner;ACL audit、ConPTY 与 Job/supervisor 分别由
|
||||
`windows_audit`、`windows_conpty`、`windows_job` 子模块承接,主 runner 只保留 token/spawn 编排。TTY 请求由同一 restricted-token owner 创建 ConPTY,使用
|
||||
`windows_audit`、`windows_runner*`、`windows_conpty`、`windows_job` 子模块承接,主入口只保留 policy/ACL/account 编排。TTY 请求由同一 restricted-token owner 创建 ConPTY,使用
|
||||
`PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE + PROC_THREAD_ATTRIBUTE_JOB_LIST` 原子附着,合并输出并承接 stdin/resize/terminate,
|
||||
不回退普通 `portable-pty` 非沙箱进程。Windows 环境块继承父环境并按大小写不敏感应用请求覆盖;正常根进程
|
||||
退出时遵循 Codex 语义关闭 `KILL_ON_JOB_CLOSE`,由 reaper 持有 Job 与 ACL lease 到 Job 为空,取消、超时、控制断开或
|
||||
@@ -2633,6 +2654,10 @@ group 与 capability SID 的全部访问,与 Codex restricted access check 和
|
||||
因平台 evidence 尚未具备,当前 `SandboxBackendStatus::Planned`、`enforced=false` 保持不变,不能由 runner 源码、
|
||||
Settings 或 setup 文案推断 ready。
|
||||
|
||||
Architecture impact: major; Windows restricted execution now uses a sandbox-account runner sidecar inside the existing
|
||||
`tool-runtime` owner, and Windows packages must carry that runtime resource beside App Server. Responsible developer
|
||||
confirmation: coso, 2026-08-26.
|
||||
|
||||
setup artifact 的结构事实源同样归 `tool-runtime::windows_setup`:Lime AgentRoot 下的 `.sandbox/setup_marker.json`
|
||||
与 `.sandbox-secrets/sandbox_users.json` 使用单一版本号,固定 offline/online account identity,并只保存 DPAPI 密文的
|
||||
base64 表达。App Server 只能消费该 owner 的结构校验结果;文件缺失、未知字段、版本/账户漂移、空或异常大的密文均
|
||||
|
||||
@@ -92,6 +92,8 @@ npm run test:rust:integration -- -p <crate> --test <target>
|
||||
|
||||
Agent/runtime 核心行为需要跨 owner 时,不把所有 case 堆进实现文件的 inline unit tests;使用专用测试模块或 integration target。`cargo nextest` 只在工具链、archive 和 CI shard 稳定后作为执行加速器,不改变本地 related-first 规则。
|
||||
|
||||
Windows restricted execution 改动必须把 `windows-sandbox-runner.exe` 与 `app-server.exe`、`code-mode-host.exe`、`windows-sandbox-setup.exe` 成组构建。Electron Windows resources 和 `app-server.release.json` 必须包含 runner 及其 `windowsSandboxRunnerSha256`,`verify-package-resources` 对缺失或 digest 漂移 fail closed。源码/静态门禁不替代 `windows-restricted-execution-evidence-v3` 七项真实 Windows/MSVC 矩阵;矩阵通过前 readiness 保持 `Planned/enforced=false`。
|
||||
|
||||
## Gate A 与 Gate B
|
||||
|
||||
Gate A 使用可控数据验证 Renderer projection、DOM、交互、错误文案和五语言资源。它可以使用显式 fixture,但证据必须标明 `test-only`。
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
> upstream: `/Users/coso/Documents/dev/rust/codex`(当前观测 HEAD:`99660ab3c7b861c916e467581fa9b8723504d66b`)
|
||||
> checklist progress: `44/46 (95.7%)`;这是未加权任务数,不等于交付完成度,剩余项包含 Windows 平台安全、live/verifier 与正式分发硬门槛。
|
||||
|
||||
> 当前主目标(2026-08-26):关闭 C-3 Windows restricted execution 的真机证据。Codex current 的 per-user Windows Firewall 非 loopback/loopback 出站阻断、offline/online sandbox identity、`LimeSandboxUsers` 普通 token ACL、短生命周期 capability SID restricted ACL、clean-runner setup、read-back、原始 DACL rollback 与七项 evidence collector 已进入唯一 current owner;下一刀是在真实 Windows/MSVC runner 编译并执行矩阵,在证据通过前保持 `Planned/enforced=false`。
|
||||
> 当前主目标(2026-08-26):关闭 C-3 Windows restricted execution 的真机证据。Codex current 的 per-user Windows Firewall 非 loopback/loopback 出站阻断、offline/online sandbox identity、`LimeSandboxUsers` 普通 token ACL、短生命周期 capability SID restricted ACL、clean-runner setup、read-back、原始 DACL rollback 与七项 evidence collector 已进入唯一 current owner;Windows 真机已证明宿主账户直接 `CreateProcessAsUserW` 缺少 `SeAssignPrimaryTokenPrivilege`,`CreateProcessWithTokenW` fallback 无法可靠启动 pipe 子进程且不能承接 ConPTY,因此下一刀是在 `tool-runtime` 内按 Codex current 建立 sandbox-account command runner,再重跑真实 Windows/MSVC 矩阵。在证据通过前保持 `Planned/enforced=false`。
|
||||
> 参考原则:`/Users/coso/Documents/dev/rust/codex` 是协议、Thread/Turn/Item、runtime lifecycle、tool/approval 和 GUI 语义的直接实现参考源;Lime 只在产品范围、provider 和桌面宿主边界做显式适配,不以旧矩阵或同名 legacy API 代替对齐。
|
||||
> 目标:把 Lime 的产品主链、协议字段、GUI 工作区、执行安全和真实 Desktop 证据推进到当前 Codex Desktop 语义;不恢复已退役 runtime、v0 或 `agentSession/*` 兼容主链。
|
||||
|
||||
@@ -198,7 +198,17 @@ Renderer typed gateway
|
||||
- [x] C-3 当前工作树 macOS signed packaged candidate:Developer ID identity、主/Helper bundle identity、`codesign --verify --deep --strict`、资源 verifier 与包内 executable Shell-01 21/21 已通过;证据目录 `.tmp/codex-alignment-packaged-current-20260826/` 与 `.lime/qc/project-gates/standalone-shell-01-20260825171233-4797/shell-01-electron-smoke/summary.json`。该证据只证明当前候选包,不能替代 notarization/stapling 或正式发布。
|
||||
- [ ] C-3 Windows restricted execution 真机 evidence:Codex `windows-sandbox-rs` 对齐的 restricted-token、sandbox account user SID、capability SID、TokenDefaultDacl、ACL lease/rollback、STARTUPINFOEX handle allowlist、Job Object descendant cleanup、bounded pipe reader 与 restricted ConPTY 已存在于 current target-gated owner;account user SID 只进入 restricting SID access check,不进入默认 DACL 充当 capability。ConPTY 使用 pseudoconsole + Job Object 原子属性,承接 stdin/resize/combined-output,不回退非沙箱 PTY。elevated setup helper、offline/online local account、machine-scope DPAPI artifacts、按 offline SID 生效的 Windows Firewall 非 loopback/loopback 全出站阻断与 12 条 WFP 补充过滤器已经进入 current owner;runner 按 network permission 选择 offline/online identity,readiness 对 account login、Firewall 属性和 WFP conditions 全量 read-back。Quality 通过 `scripts/lib/windows-restricted-execution-evidence.mjs --provision` 在 clean runner 的隔离 AgentRoot 先执行 setup,再执行 schema v3 七项逐 case 矩阵,并用 `actions/upload-artifact@v4` 保留 setup/test summary/stdout/stderr。证据入口固定 setup/Cargo 超时,ACL audit 对每目录超过 1,000 项、总计超过 50,000 项、超过 2 秒和 reparse-point 元数据读取失败均 fail-visible,避免不完整扫描被当作 clean。真实 Windows runner 尚未执行,因此仍缺 workspace write、外部/metadata path denial、ACL rollback、offline identity/firewall enforcement、ConPTY、timeout/descendant kill、large output 和 Everyone-write audit 的真实矩阵。production plan 继续 `SandboxBackendStatus::Planned/enforced=false`;`windowsSandbox/setupStart` 仅在真实 plan 为 `Ready + enforced` 时报告成功。
|
||||
|
||||
本轮 C-3 窄写集(进行中):`lime-rs/crates/tool-runtime/{Cargo.toml,src/windows_setup.rs,src/windows_firewall.rs,src/execution_process/windows.rs,tests/windows_restricted_execution.rs}`、`lime-rs/{Cargo.toml,Cargo.lock}`、`scripts/lib/windows-restricted-execution-evidence*`、`.github/workflows/quality.yml`、本计划与 `internal/aiprompts/architecture.md`。退出条件:setup 原子提交前安装并 read-back 两条 per-user Firewall block rule 与既有 WFP filters;runtime 只在 setup artifacts/firewall/WFP 都有效时选择 offline identity,network-enabled 选择 online identity;clean Windows evidence 显式 provision 隔离 AgentRoot 后证明进程实际以预期账户运行且 offline loopback 连接被阻断;本机只能完成静态/非 Windows 回归时继续记录 Windows/MSVC blocker,不提升 readiness。
|
||||
当前失败证据与替换方向:真实 Windows run `32947104967`(SHA `22b4262aa`)只有 2/7 通过。宿主 `CreateProcessAsUserW` 固定返回 `ERROR_PRIVILEGE_NOT_HELD`;`CreateProcessWithTokenW` 对 pipe 返回成功但子进程不进入有效执行,对 ConPTY/attribute list 返回 `ERROR_INVALID_PARAMETER`。该 fallback 归类为 `dead / forbidden-to-restore`。current 目标结构是宿主使用 DPAPI 凭证和 `CreateProcessWithLogonW` 启动 sandbox-account runner,runner 从自己的 primary token 派生 restricted token,再以 `CreateProcessAsUserW + STARTUPINFOEX` 原子附着 Job/handle list/ConPTY;有界 named-pipe framed IPC 承接 spawn、stdin/close/resize/terminate、output、exit/error。runner 仍属于 `tool-runtime::execution_process` 唯一 owner,Electron 只把其可执行文件与 App Server 资源一起打包。七项 matrix 全绿、packaged resource verifier 覆盖 runner且临时诊断清理后,才允许提升 readiness。
|
||||
|
||||
本轮 C-3 窄写集(进行中):`lime-rs/crates/tool-runtime/{Cargo.toml,src/bin/windows-sandbox-runner.rs,src/execution_process/windows*.rs,tests/windows_restricted_execution.rs}`、`lime-rs/crates/app-server-daemon/src/lib.rs`、`packages/app-server-client/src/sidecar-types.ts`、`scripts/check-app-server-client-contract.mjs`、`scripts/prepare-sherpa-onnx-runtime.test.mjs`、`scripts/lib/{electron-app-server-assets*,electron-dev-sidecar*,windows-restricted-execution-evidence*}`、`scripts/electron/{prepare-app-server-assets.mjs,verify-package-resources*}`、`.github/workflows/quality.yml`、本计划与 `internal/aiprompts/{architecture,quality-workflow}.md`。退出条件:宿主不再直接为 sandbox account token 启动用户命令;runner named-pipe ACL 只允许目标 sandbox identity,连接后校验 client PID,单帧和启动等待有界;runner 内 restricted token、Job、handle list/ConPTY、stdin/resize/terminate、output 和 ACL lease 终态语义全部通过 clean Windows 七项矩阵;Windows packaged manifest/resource verifier 必须包含 runner;本机只能完成静态/非 Windows 回归时继续记录 Windows/MSVC blocker,不提升 readiness。
|
||||
|
||||
架构影响:重大;Windows restricted execution 从宿主直接 token spawn 改为同一 `tool-runtime` owner 下的 sandbox-account runner sidecar,且 Windows Electron package 增加必需 runtime resource。
|
||||
|
||||
架构图已更新:`internal/aiprompts/architecture.md#42-windows-sandbox-readiness-owner`。
|
||||
|
||||
责任开发者确认:coso,2026-08-26。
|
||||
|
||||
确认内容:已核对目录归属、数据流、依赖方向、协议边界和验证门禁。
|
||||
|
||||
### D:模型/MCP/Environment/Multi-Agent evidence
|
||||
|
||||
@@ -327,6 +337,8 @@ Renderer typed gateway
|
||||
|
||||
### 本轮进度(2026-08-26)
|
||||
|
||||
- 2026-08-26:真实 Windows run `32947104967`(SHA `22b4262aa`)完成 2/7、失败 5/7。pipe 路径诊断为 `CreateProcessWithTokenW=success`、`ResumeThread=success`、进程持续 `STILL_ACTIVE` 但不执行 workspace marker/stdin/output;ConPTY 路径固定 `ERROR_INVALID_PARAMETER`。结合宿主 `CreateProcessAsUserW=ERROR_PRIVILEGE_NOT_HELD`,已排除 plain/extended `STARTUPINFO`、desktop、error mode、profile load、stdio allowlist 与命令行重建等单点原因,停止继续修补 fallback。下一实现按 Codex current 改为 `CreateProcessWithLogonW` 启动 sandbox-account runner,再由 runner 的当前 token 派生 restricted token并通过 framed named pipe 交接 I/O/control;本条仅记录根因与方向,C-3/readiness 仍未完成。
|
||||
|
||||
- 关闭 A-Queue 视觉重复:`ThreadQueueStatus` 删除 submission preview/`thread-queue-items`,只保留标题、数量、loading/error;五语言删除无消费者的 preview/more key。补充组件负向回归,Gate B 改为断言 canonical 时间线 marker 唯一、Queue 状态条不含 marker、旧列表节点不存在。
|
||||
- 验证证据:Queue 定向组件/Gate B/Sherpa 25/25,真实 `smoke:thread-queue-electron-gate-b` 通过(47 次 electron-ipc、`timelineMarkerCount=1`、`queueStatusContainsMarker=false`、`queueItemsPresent=false`、mock/console/page/invoke error 0);`npm run i18n:check` 100%;`smoke:agent-runtime-current-fixture` 完整通过;`npm run verify:gui-smoke` 21/21;`npm run test:contracts` 299 checks;renderer/node typecheck、scripts governance、legacy governance(2063 文件,0 违规/漂移)、Rust fmt/diff check 通过。
|
||||
- 修复 CI 可观测性:根 `package.json` 与 lock importer 增加直接 `ws@8.18.3`;`.github/workflows/quality.yml` 的 `windows_shell_runtime` 从 20 分钟调为 60 分钟;Sherpa 旧目录清理、tar 解压前后加入阶段日志;新增 workflow/source 阶段顺序回归。`npm run test:rust:unit -- -p app-server processor::windows_sandbox::tests` 实际运行 4/4,通过 backend readiness/enforced fail-closed 边界。
|
||||
@@ -374,6 +386,7 @@ Renderer typed gateway
|
||||
- 2026-08-26:继续对照 Codex current `windows-sandbox-rs::token` 完成 C-3 restricting identity 收口。Lime 从 `LogonUserW` base token 读取实际 `TokenUser` SID,并按 capability、account user、logon、Everyone 顺序加入 `CreateRestrictedToken` restricting SID;account user SID 明确不进入 `TokenDefaultDacl`,避免 identity 被当成 capability。同步收敛 active 计划中的重复/过时 Firewall/WFP 状态。验证:`npm run test:rust:related -- lime-rs/crates/tool-runtime/src/execution_process/windows.rs lime-rs/crates/tool-runtime/src/windows_firewall.rs lime-rs/crates/tool-runtime/src/windows_wfp.rs lime-rs/crates/tool-runtime/src/windows_setup.rs lime-rs/crates/tool-runtime/tests/windows_restricted_execution.rs` 退出码 `0`,`tool-runtime` `373/373`;evidence Vitest `7/7`;`npm run test:contracts` `299 checks`;`docs:boundary`、`governance:scripts`、`governance:legacy-report`(2063 files、0/0/0)、rustfmt 与 `git diff --check` 均通过。macOS collector 以退出码 `1` 刷新 schema v3 evidence,明确记录 `platform=darwin`、`result=evidence-pending`、`failedStage=windows-runner` 和七项全部 missing。本机未安装 Windows/MSVC target,Windows-only `TokenUser`/COM/WFP/ConPTY API 编译和七项真机矩阵仍无证据,计划保持 `active`、`44/46 (95.7%)` 与 `Planned/enforced=false`。
|
||||
- 2026-08-26:继续对照 Codex current `windows-sandbox-rs::setup_main::sandbox_users` 与 write-root ACL 语义,关闭 restricted token 双重访问检查缺口。`tool-runtime::windows_setup` 现在幂等创建 `LimeSandboxUsers` 本地组,将 offline/online 账户加入组;默认 Windows readiness 与每次 runner 启动均验证组 SID 和目标账户成员关系,组缺失、成员漂移或 NetAPI 错误 fail closed。`windows_acl` 将 cwd/显式 permission 拆成 read/write/write-deny/access-deny plan:普通 token 与 restricted token 两侧分别由 sandbox group 和短生命周期 capability SID 获得 read/write ACE,metadata/read-only carveout 只拒绝 capability 写,显式 `Deny` 同时拒绝 group/capability 全部访问。lease 不再通过 `/remove:g` 删除稳定主体 ACE,而是在每个路径第一次修改前保存原始 DACL security descriptor,终态逆序恢复。Windows integration matrix 在 workspace process 存活时断言新增 SID 同时包含 sandbox group 与另一条 capability SID,并继续覆盖 online/offline identity、workspace write、metadata/external deny 和全部目标原始 SDDL rollback。本机 `npm run test:rust:related -- ...` 退出码 `0`,反向依赖链与 `tool-runtime 373/373` 通过;Windows-only NetAPI/GetFileSecurityW/SetFileSecurityW 与增强矩阵仍必须由真实 MSVC runner 编译执行,因此 checklist 保持 `44/46 (95.7%)`、状态 `active`、readiness `Planned/enforced=false`。
|
||||
- 2026-08-26:收口 Windows setup 文件规模与职责边界。将本地账户创建/更新、本地组与成员关系、组 SID/readiness 校验及 account SID 解析从 1,086 行 `windows_setup.rs` 原样迁入唯一 current 子 owner `windows_setup/accounts.rs`;主编排文件降至 817 行,子模块 281 行,没有新增 backend、compat API 或行为分支。`npm run test:rust:related -- ...` 退出码 `0`,反向依赖链与 `tool-runtime 373/373` 通过;`npm run test:contracts` 完成 App Server client `299 checks` 及 command/harness/modality/scripts/release/docs guards;`npm run governance:legacy-report` 扫描 2063 文件且零引用候选、分类漂移、边界违规均为 `0`;evidence parser `7/7`,rustfmt 与 `git diff --check` 通过。macOS collector 继续以退出码 `1` 生成 schema v3 `evidence-pending`,七项全部 missing。该拆分只消除仓库 1,000 行硬约束,不改变安全完成度:真实 Windows/MSVC restricted matrix、live provider/Pier/DeepSWE verifier artifacts 与覆盖当前工作树的正式 notarized/package 仍未完成,计划保持 `active`、`44/46 (95.7%)` 与 `SandboxBackendStatus::Planned/enforced=false`。
|
||||
- 2026-08-26:继续 C-3,删除已由真机证据判定无效的 `CreateProcessWithTokenW` fallback、suspended/resume 与后置 Job 分配,改为 `CreateProcessWithLogonW -> sandbox-account runner -> current token 派生 restricted token -> CreateProcessAsUserW + STARTUPINFOEX` 唯一 current 链;有界双 named-pipe framing 承接 spawn/stdin/close/resize/terminate/output/exit/error,pipe ACL 只允许目标 account SID且连接后校验 runner PID,pipe 与 ConPTY 均通过 job-list attribute 原子入 Job,宿主 reaper 持有控制 pipe 与 ACL lease 到 runner 退出。`windows-sandbox-runner.exe` 已进入 dev/Quality 成组构建、Electron Windows resource、release manifest `windowsSandboxRunnerSha256` 和 packaged verifier;Everyone-write case 的 `icacls` 输出改为进程内捕获,避免污染 evidence parser 的稳定 test 行。本地 `cargo fmt --check`、Prettier、`git diff --check`、packaging/sidecar/evidence 定向 Vitest `66/66`、Rust related 反向依赖链、`tool-runtime 373/373`、`app-server-daemon 38/38`、contracts `299 checks`、scripts/release/docs/legacy governance 全部通过,legacy 扫描仍为 2063 files、0/0/0。当前主机未安装 Windows target,因此这些结果不替代 MSVC 编译和 schema v3 七项真机矩阵;计划继续保持 `active`、`44/46 (95.7%)` 与 `Planned/enforced=false`,下一刀是推送该窄提交并监控 Windows Quality evidence。
|
||||
|
||||
## 7. 阻塞与避让
|
||||
|
||||
|
||||
@@ -427,6 +427,8 @@ pub struct AppServerReleaseArtifact {
|
||||
pub sha256: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub windows_sandbox_setup_sha256: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub windows_sandbox_runner_sha256: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -1016,6 +1018,7 @@ mod tests {
|
||||
url: "https://example/app-server-darwin-arm64.tar.gz".to_string(),
|
||||
sha256: "abc".to_string(),
|
||||
windows_sandbox_setup_sha256: None,
|
||||
windows_sandbox_runner_sha256: None,
|
||||
}],
|
||||
};
|
||||
let options = SidecarBinaryPathOptions {
|
||||
@@ -1081,6 +1084,7 @@ mod tests {
|
||||
url: "https://example/app-server-darwin-arm64.tar.gz".to_string(),
|
||||
sha256: "abc".to_string(),
|
||||
windows_sandbox_setup_sha256: None,
|
||||
windows_sandbox_runner_sha256: None,
|
||||
}],
|
||||
};
|
||||
let options = SidecarBinaryPathOptions {
|
||||
@@ -1126,6 +1130,7 @@ mod tests {
|
||||
url: "https://example/app-server-darwin-arm64.tar.gz".to_string(),
|
||||
sha256: "abc".to_string(),
|
||||
windows_sandbox_setup_sha256: None,
|
||||
windows_sandbox_runner_sha256: None,
|
||||
}],
|
||||
};
|
||||
let settings = DaemonSettings {
|
||||
|
||||
@@ -18,6 +18,10 @@ path = "src/bin/code-mode-host.rs"
|
||||
name = "windows-sandbox-setup"
|
||||
path = "src/bin/windows-sandbox-setup.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "windows-sandbox-runner"
|
||||
path = "src/bin/windows-sandbox-runner.rs"
|
||||
|
||||
[dependencies]
|
||||
agent-protocol.workspace = true
|
||||
app-server-protocol.workspace = true
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
fn main() {
|
||||
if let Err(error) = tool_runtime::execution_process::run_windows_sandbox_runner() {
|
||||
eprintln!("Windows sandbox runner failed: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,23 @@ mod pty;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
/// Runs the Windows sandbox-account command runner sidecar.
|
||||
///
|
||||
/// The executable is packaged beside App Server, while all protocol and child
|
||||
/// execution semantics remain owned by this module.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn run_windows_sandbox_runner() -> io::Result<()> {
|
||||
windows::run_windows_sandbox_runner()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn run_windows_sandbox_runner() -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Windows sandbox runner is only available on Windows",
|
||||
))
|
||||
}
|
||||
|
||||
/// Bounded Windows ACL audit result used by the App Server setup warning.
|
||||
///
|
||||
/// The audit is intentionally a diagnostic only. It never changes ACLs and it
|
||||
|
||||
@@ -8,9 +8,8 @@ use std::ptr;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use windows_sys::Win32::Foundation::{
|
||||
CloseHandle, GetLastError, LocalFree, SetHandleInformation, ERROR_PRIVILEGE_NOT_HELD,
|
||||
ERROR_SUCCESS, HANDLE, HANDLE_FLAG_INHERIT, HLOCAL, INVALID_HANDLE_VALUE, LUID, WAIT_OBJECT_0,
|
||||
WAIT_TIMEOUT,
|
||||
CloseHandle, GetLastError, LocalFree, SetHandleInformation, ERROR_SUCCESS, HANDLE,
|
||||
HANDLE_FLAG_INHERIT, HLOCAL, INVALID_HANDLE_VALUE, LUID,
|
||||
};
|
||||
use windows_sys::Win32::Security::Authorization::{
|
||||
ConvertStringSidToSidW, SetEntriesInAclW, EXPLICIT_ACCESS_W, GRANT_ACCESS, TRUSTEE_IS_SID,
|
||||
@@ -18,27 +17,23 @@ use windows_sys::Win32::Security::Authorization::{
|
||||
};
|
||||
use windows_sys::Win32::Security::{
|
||||
AdjustTokenPrivileges, CopySid, CreateRestrictedToken, CreateWellKnownSid, GetLengthSid,
|
||||
GetTokenInformation, LogonUserW, LookupPrivilegeValueW, SetTokenInformation, TokenDefaultDacl,
|
||||
TokenGroups, TokenUser, ACL, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
|
||||
SECURITY_ATTRIBUTES, SID_AND_ATTRIBUTES, TOKEN_ADJUST_DEFAULT, TOKEN_ADJUST_PRIVILEGES,
|
||||
TOKEN_ADJUST_SESSIONID, TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_PRIVILEGES, TOKEN_QUERY,
|
||||
TOKEN_USER,
|
||||
GetTokenInformation, LookupPrivilegeValueW, SetTokenInformation, TokenDefaultDacl, TokenGroups,
|
||||
TokenUser, ACL, SECURITY_ATTRIBUTES, SID_AND_ATTRIBUTES, TOKEN_ADJUST_DEFAULT,
|
||||
TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_SESSIONID, TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE,
|
||||
TOKEN_PRIVILEGES, TOKEN_QUERY, TOKEN_USER,
|
||||
};
|
||||
use windows_sys::Win32::Storage::FileSystem::{ReadFile, WriteFile};
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
|
||||
use windows_sys::Win32::Storage::FileSystem::WriteFile;
|
||||
use windows_sys::Win32::System::JobObjects::{
|
||||
AssignProcessToJobObject, CreateJobObjectW, JobObjectBasicAccountingInformation,
|
||||
JobObjectExtendedLimitInformation, QueryInformationJobObject, SetInformationJobObject,
|
||||
TerminateJobObject, JOBOBJECT_BASIC_ACCOUNTING_INFORMATION,
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_BREAKAWAY_OK,
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
CreateJobObjectW, JobObjectBasicAccountingInformation, JobObjectExtendedLimitInformation,
|
||||
QueryInformationJobObject, SetInformationJobObject, TerminateJobObject,
|
||||
JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
|
||||
JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
};
|
||||
use windows_sys::Win32::System::Pipes::CreatePipe;
|
||||
use windows_sys::Win32::System::Threading::{
|
||||
CreateProcessAsUserW, CreateProcessWithTokenW, GetCurrentProcess, GetExitCodeProcess,
|
||||
OpenProcessToken, ResumeThread, WaitForSingleObject, CREATE_NO_WINDOW, CREATE_SUSPENDED,
|
||||
CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, LOGON_WITH_PROFILE,
|
||||
PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOEXW, STARTUPINFOW,
|
||||
CreateProcessAsUserW, GetCurrentProcess, OpenProcessToken, CREATE_NO_WINDOW,
|
||||
CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, PROCESS_INFORMATION,
|
||||
STARTF_USESTDHANDLES, STARTUPINFOEXW,
|
||||
};
|
||||
|
||||
#[path = "windows_acl.rs"]
|
||||
@@ -51,18 +46,25 @@ mod windows_audit;
|
||||
mod windows_conpty;
|
||||
#[path = "windows_job.rs"]
|
||||
mod windows_job;
|
||||
#[path = "windows_runner_child.rs"]
|
||||
mod windows_runner_child;
|
||||
#[path = "windows_runner_host.rs"]
|
||||
mod windows_runner_host;
|
||||
#[path = "windows_runner_protocol.rs"]
|
||||
mod windows_runner_protocol;
|
||||
#[path = "windows_runner_supervisor.rs"]
|
||||
mod windows_runner_supervisor;
|
||||
use crate::windows_setup::{
|
||||
read_windows_sandbox_password, verify_windows_sandbox_group_membership,
|
||||
windows_sandbox_users_group_sid, WINDOWS_SANDBOX_OFFLINE_USERNAME,
|
||||
WINDOWS_SANDBOX_ONLINE_USERNAME,
|
||||
verify_windows_sandbox_group_membership, windows_sandbox_users_group_sid,
|
||||
WINDOWS_SANDBOX_OFFLINE_USERNAME, WINDOWS_SANDBOX_ONLINE_USERNAME,
|
||||
};
|
||||
use windows_acl::{build_acl_plan, AclLease};
|
||||
use windows_attr::ProcessAttributeList;
|
||||
pub(crate) use windows_audit::audit_world_writable;
|
||||
use windows_conpty::RestrictedConpty;
|
||||
use windows_job::create_kill_on_close_job;
|
||||
#[cfg(test)]
|
||||
use windows_job::preserve_job_descendants;
|
||||
use windows_job::{create_kill_on_close_job, read_pipe_stream, supervise_restricted_process};
|
||||
|
||||
const DISABLE_MAX_PRIVILEGE: u32 = 0x01;
|
||||
const LUA_TOKEN: u32 = 0x04;
|
||||
@@ -71,8 +73,10 @@ const GENERIC_ALL: u32 = 0x1000_0000;
|
||||
const WIN_WORLD_SID: i32 = 1;
|
||||
const SE_GROUP_LOGON_ID: u32 = 0xC000_0000;
|
||||
const CONTROL_POLL_MILLIS: u32 = 25;
|
||||
const JOB_REAPER_POLL_MILLIS: u64 = 250;
|
||||
const CHILD_ERROR_MODE_FLAGS: u32 = 0x0001 | 0x0002;
|
||||
|
||||
pub(super) fn run_windows_sandbox_runner() -> io::Result<()> {
|
||||
windows_runner_child::run()
|
||||
}
|
||||
pub(super) fn start_windows_restricted_execution_process(
|
||||
mut request: LocalExecutionRequest,
|
||||
sandbox: LocalExecutionSandbox,
|
||||
@@ -111,10 +115,12 @@ pub(super) fn start_windows_restricted_execution_process(
|
||||
let sandbox_group_sid = windows_sandbox_users_group_sid()?;
|
||||
let capability_sid = capability_sid();
|
||||
let acl_lease = AclLease::acquire(&sandbox_group_sid, &capability_sid, acl_plan)?;
|
||||
let token = create_restricted_token(&capability_sid, sandbox_account)?;
|
||||
enable_process_creation_privileges();
|
||||
let spawned = spawn_restricted_process(&request, &cwd, token.raw())?;
|
||||
drop(token);
|
||||
let transport = windows_runner_host::spawn_runner_transport(
|
||||
&request,
|
||||
&cwd,
|
||||
sandbox_account,
|
||||
capability_sid,
|
||||
)?;
|
||||
|
||||
let start = ExecutionProcessStart {
|
||||
process_id: request.process_id.clone(),
|
||||
@@ -128,66 +134,11 @@ pub(super) fn start_windows_restricted_execution_process(
|
||||
let process = Arc::new(Mutex::new(process_state));
|
||||
let (output_tx, output_rx) = mpsc::unbounded_channel();
|
||||
let (control_tx, control_rx) = std::sync::mpsc::channel();
|
||||
let (reader_done_tx, reader_done_rx) = std::sync::mpsc::channel();
|
||||
let (state_tx, state_rx) = watch::channel(initial_snapshot);
|
||||
let (final_tx, final_rx) = oneshot::channel();
|
||||
|
||||
let SpawnedRestrictedProcess {
|
||||
process: spawned_process,
|
||||
thread: spawned_thread,
|
||||
job,
|
||||
stdin_write,
|
||||
stdout_read,
|
||||
stderr_read,
|
||||
pseudoconsole,
|
||||
} = spawned;
|
||||
let output_kind = if pseudoconsole.is_some() {
|
||||
ExecutionOutputKind::Combined
|
||||
} else {
|
||||
ExecutionOutputKind::Stdout
|
||||
};
|
||||
let stdout_process = Arc::clone(&process);
|
||||
let stdout_state = state_tx.clone();
|
||||
let stdout_output = output_tx.clone();
|
||||
let stdout_reader_done = reader_done_tx.clone();
|
||||
let _stdout_reader = thread::spawn(move || {
|
||||
read_pipe_stream(
|
||||
stdout_read,
|
||||
output_kind,
|
||||
stdout_process,
|
||||
stdout_output,
|
||||
stdout_state,
|
||||
);
|
||||
let _ = stdout_reader_done.send(());
|
||||
});
|
||||
let _stderr_reader = stderr_read.map(|stderr_read| {
|
||||
let stderr_process = Arc::clone(&process);
|
||||
let stderr_state = state_tx.clone();
|
||||
let stderr_reader_done = reader_done_tx.clone();
|
||||
thread::spawn(move || {
|
||||
read_pipe_stream(
|
||||
stderr_read,
|
||||
ExecutionOutputKind::Stderr,
|
||||
stderr_process,
|
||||
output_tx,
|
||||
stderr_state,
|
||||
);
|
||||
let _ = stderr_reader_done.send(());
|
||||
})
|
||||
});
|
||||
thread::spawn(move || {
|
||||
supervise_restricted_process(
|
||||
spawned_process,
|
||||
spawned_thread,
|
||||
job,
|
||||
stdin_write,
|
||||
pseudoconsole,
|
||||
acl_lease,
|
||||
process,
|
||||
state_tx,
|
||||
final_tx,
|
||||
control_rx,
|
||||
reader_done_rx,
|
||||
windows_runner_supervisor::supervise(
|
||||
transport, acl_lease, process, output_tx, state_tx, final_tx, control_rx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -250,6 +201,12 @@ impl OwnedHandle {
|
||||
fn raw(&self) -> HANDLE {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn into_raw(mut self) -> HANDLE {
|
||||
let handle = self.0;
|
||||
self.0 = 0;
|
||||
handle
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OwnedHandle {
|
||||
@@ -296,31 +253,23 @@ struct TokenDefaultDaclInfo {
|
||||
default_dacl: *mut ACL,
|
||||
}
|
||||
|
||||
fn create_restricted_token(capability_sid: &str, account: &str) -> io::Result<OwnedHandle> {
|
||||
fn current_process_token_for_restriction() -> io::Result<OwnedHandle> {
|
||||
let desired = TOKEN_DUPLICATE
|
||||
| TOKEN_QUERY
|
||||
| TOKEN_ASSIGN_PRIMARY
|
||||
| TOKEN_ADJUST_DEFAULT
|
||||
| TOKEN_ADJUST_SESSIONID
|
||||
| TOKEN_ADJUST_PRIVILEGES;
|
||||
let mut token = 0;
|
||||
if unsafe { OpenProcessToken(GetCurrentProcess(), desired, &mut token) } == 0 {
|
||||
return Err(last_os_error("OpenProcessToken(runner)"));
|
||||
}
|
||||
OwnedHandle::new(token, "OpenProcessToken(runner)")
|
||||
}
|
||||
|
||||
fn create_restricted_token(capability_sid: &str) -> io::Result<OwnedHandle> {
|
||||
unsafe {
|
||||
let desired = TOKEN_DUPLICATE
|
||||
| TOKEN_QUERY
|
||||
| TOKEN_ASSIGN_PRIMARY
|
||||
| TOKEN_ADJUST_DEFAULT
|
||||
| TOKEN_ADJUST_SESSIONID
|
||||
| TOKEN_ADJUST_PRIVILEGES;
|
||||
let password = read_windows_sandbox_password(account)?;
|
||||
let username = to_wide(account);
|
||||
let domain = to_wide(".");
|
||||
let password = to_wide(password);
|
||||
let mut base = 0;
|
||||
if LogonUserW(
|
||||
username.as_ptr(),
|
||||
domain.as_ptr(),
|
||||
password.as_ptr(),
|
||||
LOGON32_LOGON_INTERACTIVE,
|
||||
LOGON32_PROVIDER_DEFAULT,
|
||||
&mut base,
|
||||
) == 0
|
||||
{
|
||||
return Err(last_os_error("LogonUserW"));
|
||||
}
|
||||
let base = OwnedHandle::new(base, "LogonUserW")?;
|
||||
let base = current_process_token_for_restriction()?;
|
||||
let capability = LocalSid::parse(capability_sid)?;
|
||||
let mut user = token_user_sid(base.raw())?;
|
||||
let mut logon = logon_sid(base.raw())?;
|
||||
@@ -438,28 +387,6 @@ unsafe fn enable_privilege(token: HANDLE, name: &str) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enable_process_creation_privileges() {
|
||||
unsafe {
|
||||
let mut token = 0;
|
||||
if OpenProcessToken(
|
||||
GetCurrentProcess(),
|
||||
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
|
||||
&mut token,
|
||||
) == 0
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Ok(token) = OwnedHandle::new(token, "OpenProcessToken") else {
|
||||
return;
|
||||
};
|
||||
for privilege in ["SeIncreaseQuotaPrivilege", "SeAssignPrimaryTokenPrivilege"] {
|
||||
if let Err(error) = enable_privilege(token.raw(), privilege) {
|
||||
tracing::debug!(%error, privilege, "Windows process creation privilege unavailable");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn token_user_sid(token: HANDLE) -> io::Result<Vec<u8>> {
|
||||
let mut needed = 0;
|
||||
GetTokenInformation(token, TokenUser, ptr::null_mut(), 0, &mut needed);
|
||||
@@ -614,8 +541,9 @@ fn spawn_restricted_pipe_process(
|
||||
let mut env_block = environment_block(&request.env)?;
|
||||
let cwd = to_wide(cwd.as_os_str());
|
||||
let mut desktop = to_wide("winsta0\\default");
|
||||
let mut attributes = ProcessAttributeList::new(1)?;
|
||||
let mut attributes = ProcessAttributeList::new(2)?;
|
||||
attributes.set_handle_list(&[stdin_read.raw(), stdout_write.raw(), stderr_write.raw()])?;
|
||||
attributes.set_job(job.raw())?;
|
||||
let mut startup: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
|
||||
startup.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
@@ -625,10 +553,7 @@ fn spawn_restricted_pipe_process(
|
||||
startup.StartupInfo.lpDesktop = desktop.as_mut_ptr();
|
||||
startup.lpAttributeList = attributes.as_mut_ptr();
|
||||
let mut process_info: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
// Prevent restricted child initialization failures from opening a hidden
|
||||
// Windows error dialog and leaving the process alive indefinitely.
|
||||
let previous_error_mode = unsafe { SetErrorMode(CHILD_ERROR_MODE_FLAGS) };
|
||||
let mut created = unsafe {
|
||||
let created = unsafe {
|
||||
CreateProcessAsUserW(
|
||||
token,
|
||||
ptr::null(),
|
||||
@@ -636,78 +561,18 @@ fn spawn_restricted_pipe_process(
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
1,
|
||||
CREATE_UNICODE_ENVIRONMENT
|
||||
| CREATE_SUSPENDED
|
||||
| CREATE_NO_WINDOW
|
||||
| EXTENDED_STARTUPINFO_PRESENT,
|
||||
CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT,
|
||||
env_block.as_mut_ptr() as *mut c_void,
|
||||
cwd.as_ptr(),
|
||||
&startup.StartupInfo,
|
||||
&mut process_info,
|
||||
)
|
||||
};
|
||||
if created == 0 && unsafe { GetLastError() } == ERROR_PRIVILEGE_NOT_HELD {
|
||||
// Process creation may modify lpCommandLine even when it fails. Rebuild
|
||||
// it before retrying or PowerShell can start without its arguments.
|
||||
command_line = to_wide(&command_line_text);
|
||||
let mut fallback_startup: STARTUPINFOW = unsafe { std::mem::zeroed() };
|
||||
fallback_startup.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||
fallback_startup.dwFlags = STARTF_USESTDHANDLES;
|
||||
fallback_startup.hStdInput = stdin_read.raw();
|
||||
fallback_startup.hStdOutput = stdout_write.raw();
|
||||
fallback_startup.hStdError = stderr_write.raw();
|
||||
// CreateProcessWithTokenW grants the token access to the inherited
|
||||
// window station/desktop only when lpDesktop is null. The explicit
|
||||
// CreateProcessAsUserW path keeps the current desktop contract.
|
||||
fallback_startup.lpDesktop = ptr::null_mut();
|
||||
created = unsafe {
|
||||
CreateProcessWithTokenW(
|
||||
token,
|
||||
LOGON_WITH_PROFILE,
|
||||
ptr::null(),
|
||||
command_line.as_mut_ptr(),
|
||||
CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED | CREATE_NO_WINDOW,
|
||||
env_block.as_mut_ptr() as *mut c_void,
|
||||
cwd.as_ptr(),
|
||||
&fallback_startup,
|
||||
&mut process_info,
|
||||
)
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"CreateProcessWithTokenW pipe result={} error={} pid={}",
|
||||
created,
|
||||
unsafe { GetLastError() },
|
||||
process_info.dwProcessId
|
||||
);
|
||||
}
|
||||
unsafe {
|
||||
SetErrorMode(previous_error_mode);
|
||||
}
|
||||
if created == 0 {
|
||||
return Err(last_os_error("CreateProcessAsUserW"));
|
||||
}
|
||||
let process = OwnedHandle::new(process_info.hProcess, "CreateProcessAsUserW process")?;
|
||||
let thread = OwnedHandle::new(process_info.hThread, "CreateProcessAsUserW thread")?;
|
||||
if unsafe { AssignProcessToJobObject(job.raw(), process.raw()) } == 0 {
|
||||
return Err(last_os_error("AssignProcessToJobObject"));
|
||||
}
|
||||
let resume_result = unsafe { ResumeThread(thread.raw()) };
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let mut exit_code = 0;
|
||||
let exit_result = unsafe { GetExitCodeProcess(process.raw(), &mut exit_code) };
|
||||
eprintln!(
|
||||
"restricted pipe process pid={} resume={} exit_result={} exit_code={}",
|
||||
process_info.dwProcessId, resume_result, exit_result, exit_code
|
||||
);
|
||||
}
|
||||
if resume_result == u32::MAX {
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
return Err(last_os_error("ResumeThread"));
|
||||
}
|
||||
drop(stdin_read);
|
||||
drop(stdout_write);
|
||||
drop(stderr_write);
|
||||
@@ -736,8 +601,9 @@ fn spawn_restricted_conpty_process(
|
||||
let mut env_block = environment_block(&request.env)?;
|
||||
let cwd = to_wide(cwd.as_os_str());
|
||||
let mut desktop = to_wide("winsta0\\default");
|
||||
let mut attributes = ProcessAttributeList::new(1)?;
|
||||
let mut attributes = ProcessAttributeList::new(2)?;
|
||||
attributes.set_pseudoconsole(pseudoconsole.raw())?;
|
||||
attributes.set_job(job.raw())?;
|
||||
let mut startup: STARTUPINFOEXW = unsafe { std::mem::zeroed() };
|
||||
startup.StartupInfo.cb = std::mem::size_of::<STARTUPINFOEXW>() as u32;
|
||||
startup.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
@@ -747,10 +613,7 @@ fn spawn_restricted_conpty_process(
|
||||
startup.StartupInfo.lpDesktop = desktop.as_mut_ptr();
|
||||
startup.lpAttributeList = attributes.as_mut_ptr();
|
||||
let mut process_info: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
// Keep restricted child initialization fail-visible instead of allowing a
|
||||
// hidden Windows error dialog to hold the process open.
|
||||
let previous_error_mode = unsafe { SetErrorMode(CHILD_ERROR_MODE_FLAGS) };
|
||||
let mut created = unsafe {
|
||||
let created = unsafe {
|
||||
CreateProcessAsUserW(
|
||||
token,
|
||||
ptr::null(),
|
||||
@@ -758,58 +621,18 @@ fn spawn_restricted_conpty_process(
|
||||
ptr::null_mut(),
|
||||
ptr::null_mut(),
|
||||
0,
|
||||
CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT,
|
||||
CREATE_UNICODE_ENVIRONMENT | EXTENDED_STARTUPINFO_PRESENT,
|
||||
env_block.as_mut_ptr() as *mut c_void,
|
||||
cwd.as_ptr(),
|
||||
&startup.StartupInfo,
|
||||
&mut process_info,
|
||||
)
|
||||
};
|
||||
if created == 0 && unsafe { GetLastError() } == ERROR_PRIVILEGE_NOT_HELD {
|
||||
// Process creation may modify lpCommandLine even when it fails.
|
||||
command_line = to_wide(&command_line_text);
|
||||
// CreateProcessWithTokenW grants the token access to the inherited
|
||||
// desktop only when lpDesktop is null. The AsUser path above retains
|
||||
// the explicit desktop contract.
|
||||
startup.StartupInfo.lpDesktop = ptr::null_mut();
|
||||
created = unsafe {
|
||||
CreateProcessWithTokenW(
|
||||
token,
|
||||
LOGON_WITH_PROFILE,
|
||||
ptr::null(),
|
||||
command_line.as_mut_ptr(),
|
||||
CREATE_UNICODE_ENVIRONMENT | CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT,
|
||||
env_block.as_mut_ptr() as *mut c_void,
|
||||
cwd.as_ptr(),
|
||||
&startup.StartupInfo,
|
||||
&mut process_info,
|
||||
)
|
||||
};
|
||||
#[cfg(debug_assertions)]
|
||||
eprintln!(
|
||||
"CreateProcessWithTokenW conpty result={} error={} pid={}",
|
||||
created,
|
||||
unsafe { GetLastError() },
|
||||
process_info.dwProcessId
|
||||
);
|
||||
}
|
||||
unsafe {
|
||||
SetErrorMode(previous_error_mode);
|
||||
}
|
||||
if created == 0 {
|
||||
return Err(last_os_error("CreateProcessAsUserW ConPTY"));
|
||||
}
|
||||
let process = OwnedHandle::new(process_info.hProcess, "CreateProcessAsUserW process")?;
|
||||
let thread = OwnedHandle::new(process_info.hThread, "CreateProcessAsUserW thread")?;
|
||||
if unsafe { AssignProcessToJobObject(job.raw(), process.raw()) } == 0 {
|
||||
return Err(last_os_error("AssignProcessToJobObject ConPTY"));
|
||||
}
|
||||
if unsafe { ResumeThread(thread.raw()) } == u32::MAX {
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
return Err(last_os_error("ResumeThread ConPTY"));
|
||||
}
|
||||
let stdin_write = request.stdin.then_some(stdin_write);
|
||||
Ok(SpawnedRestrictedProcess {
|
||||
process,
|
||||
|
||||
@@ -34,7 +34,7 @@ pub(super) fn preserve_job_descendants(job: &OwnedHandle) -> io::Result<()> {
|
||||
set_job_limit_flags(job.raw(), JOB_OBJECT_LIMIT_BREAKAWAY_OK)
|
||||
}
|
||||
|
||||
fn active_job_processes(job: &OwnedHandle) -> io::Result<u32> {
|
||||
pub(super) fn active_job_processes(job: &OwnedHandle) -> io::Result<u32> {
|
||||
let mut accounting: JOBOBJECT_BASIC_ACCOUNTING_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
if unsafe {
|
||||
QueryInformationJobObject(
|
||||
@@ -52,147 +52,7 @@ fn active_job_processes(job: &OwnedHandle) -> io::Result<u32> {
|
||||
}
|
||||
}
|
||||
|
||||
fn reap_preserved_job(job: OwnedHandle, acl_lease: AclLease) {
|
||||
loop {
|
||||
match active_job_processes(&job) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => thread::sleep(Duration::from_millis(JOB_REAPER_POLL_MILLIS)),
|
||||
Err(_) => {
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(acl_lease);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn supervise_restricted_process(
|
||||
process_handle: OwnedHandle,
|
||||
thread_handle: OwnedHandle,
|
||||
job: OwnedHandle,
|
||||
mut stdin: Option<OwnedHandle>,
|
||||
pseudoconsole: Option<RestrictedConpty>,
|
||||
acl_lease: AclLease,
|
||||
process: Arc<Mutex<ExecutionProcess>>,
|
||||
state_tx: watch::Sender<ExecutionProcessSnapshot>,
|
||||
final_tx: oneshot::Sender<ExecutionProcessSnapshot>,
|
||||
control_rx: std::sync::mpsc::Receiver<LocalExecutionControl>,
|
||||
reader_done_rx: std::sync::mpsc::Receiver<()>,
|
||||
) {
|
||||
let mut wait_error = None;
|
||||
loop {
|
||||
let wait = unsafe { WaitForSingleObject(process_handle.raw(), CONTROL_POLL_MILLIS) };
|
||||
if wait == WAIT_OBJECT_0 {
|
||||
break;
|
||||
}
|
||||
drain_controls(
|
||||
&job,
|
||||
pseudoconsole.as_ref(),
|
||||
&mut stdin,
|
||||
&process,
|
||||
&state_tx,
|
||||
&control_rx,
|
||||
);
|
||||
if wait != WAIT_TIMEOUT {
|
||||
wait_error = Some(last_os_error("WaitForSingleObject").to_string());
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
stdin.take();
|
||||
let preserve_descendants =
|
||||
should_preserve_windows_job(wait_error.is_some(), process.blocking_lock().status());
|
||||
if preserve_descendants && preserve_job_descendants(&job).is_ok() {
|
||||
thread::spawn(move || reap_preserved_job(job, acl_lease));
|
||||
} else {
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
drop(job);
|
||||
drop(acl_lease);
|
||||
}
|
||||
let reader_count = if pseudoconsole.is_some() { 1 } else { 2 };
|
||||
drop(pseudoconsole);
|
||||
// A descendant can retain an inherited pipe handle after the root exits.
|
||||
// Bound the drain window so cleanup and the terminal snapshot cannot hang.
|
||||
for _ in 0..reader_count {
|
||||
let _ = reader_done_rx.recv_timeout(Duration::from_secs(5));
|
||||
}
|
||||
drop(thread_handle);
|
||||
|
||||
let mut exit_code = 1;
|
||||
let exit_result = unsafe { GetExitCodeProcess(process_handle.raw(), &mut exit_code) };
|
||||
let final_snapshot = {
|
||||
let mut guard = process.blocking_lock();
|
||||
if !guard.status().is_terminal() {
|
||||
if let Some(error) = wait_error {
|
||||
guard.fail(error);
|
||||
} else if exit_result == 0 {
|
||||
guard.fail(last_os_error("GetExitCodeProcess").to_string());
|
||||
} else {
|
||||
guard.exit(i32::try_from(exit_code).unwrap_or(-1));
|
||||
}
|
||||
}
|
||||
guard.snapshot()
|
||||
};
|
||||
let _ = state_tx.send(final_snapshot.clone());
|
||||
let _ = final_tx.send(final_snapshot);
|
||||
}
|
||||
|
||||
fn drain_controls(
|
||||
job: &OwnedHandle,
|
||||
pseudoconsole: Option<&RestrictedConpty>,
|
||||
stdin: &mut Option<OwnedHandle>,
|
||||
process: &Arc<Mutex<ExecutionProcess>>,
|
||||
state_tx: &watch::Sender<ExecutionProcessSnapshot>,
|
||||
control_rx: &std::sync::mpsc::Receiver<LocalExecutionControl>,
|
||||
) {
|
||||
loop {
|
||||
match control_rx.try_recv() {
|
||||
Ok(LocalExecutionControl::WriteStdin(bytes)) => {
|
||||
if let Some(stdin) = stdin.as_ref() {
|
||||
let _ = write_pipe(stdin.raw(), &bytes);
|
||||
}
|
||||
}
|
||||
Ok(LocalExecutionControl::CloseStdin) => {
|
||||
stdin.take();
|
||||
}
|
||||
Ok(LocalExecutionControl::Resize { rows, cols }) => {
|
||||
if let Some(pseudoconsole) = pseudoconsole {
|
||||
let _ = pseudoconsole.resize(rows, cols);
|
||||
}
|
||||
}
|
||||
Ok(LocalExecutionControl::Interrupt) => {
|
||||
update_status_blocking(process, state_tx, ExecutionProcessStatus::Interrupted);
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
}
|
||||
Ok(LocalExecutionControl::Terminate) => {
|
||||
update_status_blocking(process, state_tx, ExecutionProcessStatus::Terminated);
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => break,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||
update_status_blocking(process, state_tx, ExecutionProcessStatus::Terminated);
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write_pipe(handle: HANDLE, mut bytes: &[u8]) -> io::Result<()> {
|
||||
pub(super) fn write_pipe(handle: HANDLE, mut bytes: &[u8]) -> io::Result<()> {
|
||||
while !bytes.is_empty() {
|
||||
let mut written = 0;
|
||||
if unsafe {
|
||||
@@ -217,55 +77,3 @@ fn write_pipe(handle: HANDLE, mut bytes: &[u8]) -> io::Result<()> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn read_pipe_stream(
|
||||
handle: OwnedHandle,
|
||||
kind: ExecutionOutputKind,
|
||||
process: Arc<Mutex<ExecutionProcess>>,
|
||||
output_tx: mpsc::UnboundedSender<ExecutionOutputDelta>,
|
||||
state_tx: watch::Sender<ExecutionProcessSnapshot>,
|
||||
) {
|
||||
let mut buffer = vec![0u8; PROCESS_OUTPUT_CHUNK_BYTES];
|
||||
loop {
|
||||
let mut read = 0;
|
||||
let ok = unsafe {
|
||||
ReadFile(
|
||||
handle.raw(),
|
||||
buffer.as_mut_ptr(),
|
||||
buffer.len() as u32,
|
||||
&mut read,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read == 0 {
|
||||
break;
|
||||
}
|
||||
let (delta, snapshot) = {
|
||||
let mut guard = process.blocking_lock();
|
||||
let delta = guard.append_output(kind, &buffer[..read as usize]);
|
||||
let snapshot = guard.snapshot();
|
||||
(delta, snapshot)
|
||||
};
|
||||
let _ = output_tx.send(delta);
|
||||
let _ = state_tx.send(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
fn update_status_blocking(
|
||||
process: &Arc<Mutex<ExecutionProcess>>,
|
||||
state_tx: &watch::Sender<ExecutionProcessSnapshot>,
|
||||
status: ExecutionProcessStatus,
|
||||
) {
|
||||
let snapshot = {
|
||||
let mut guard = process.blocking_lock();
|
||||
match status {
|
||||
ExecutionProcessStatus::Interrupted => guard.interrupt(),
|
||||
ExecutionProcessStatus::Terminated => guard.terminate(),
|
||||
ExecutionProcessStatus::Failed => guard.fail("process failed"),
|
||||
ExecutionProcessStatus::Exited => guard.exit(-1),
|
||||
ExecutionProcessStatus::Starting | ExecutionProcessStatus::Running => {}
|
||||
}
|
||||
guard.snapshot()
|
||||
};
|
||||
let _ = state_tx.send(snapshot);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
use super::windows_job::write_pipe;
|
||||
use super::windows_job::{active_job_processes, preserve_job_descendants};
|
||||
use super::windows_runner_protocol::{
|
||||
decode_bytes, encode_bytes, read_frame, write_frame, RunnerMessage, RunnerSpawnRequest,
|
||||
};
|
||||
use super::{
|
||||
create_restricted_token, last_os_error, spawn_restricted_process, to_wide,
|
||||
LocalExecutionRequest, LocalSid, OwnedHandle, RestrictedConpty,
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::ptr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use windows_sys::Win32::Foundation::{EqualSid, WAIT_OBJECT_0};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
CreateFileW, ReadFile, FILE_GENERIC_READ, FILE_GENERIC_WRITE, OPEN_EXISTING,
|
||||
};
|
||||
use windows_sys::Win32::System::JobObjects::TerminateJobObject;
|
||||
use windows_sys::Win32::System::Threading::{
|
||||
GetExitCodeProcess, GetProcessId, WaitForSingleObject, INFINITE,
|
||||
};
|
||||
|
||||
const OUTPUT_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const JOB_REAPER_POLL: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(super) fn run() -> io::Result<()> {
|
||||
let (pipe_in, pipe_out) = parse_pipe_arguments()?;
|
||||
let pipe_read = open_pipe(&pipe_in, FILE_GENERIC_READ)?;
|
||||
let pipe_write = open_pipe(&pipe_out, FILE_GENERIC_WRITE)?;
|
||||
let mut pipe_read = unsafe { File::from_raw_handle(pipe_read.into_raw() as _) };
|
||||
let pipe_write = Arc::new(Mutex::new(unsafe {
|
||||
File::from_raw_handle(pipe_write.into_raw() as _)
|
||||
}));
|
||||
|
||||
let request = match read_frame(&mut pipe_read) {
|
||||
Ok(Some(RunnerMessage::Spawn { payload })) => payload,
|
||||
Ok(Some(_)) => {
|
||||
return send_start_error(
|
||||
&pipe_write,
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"runner expected a spawn request",
|
||||
),
|
||||
)
|
||||
}
|
||||
Ok(None) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"runner pipe closed before spawn request",
|
||||
))
|
||||
}
|
||||
Err(error) => return send_start_error(&pipe_write, error),
|
||||
};
|
||||
|
||||
if let Err(error) = run_spawned_process(request, pipe_read, Arc::clone(&pipe_write)) {
|
||||
let _ = write_locked(
|
||||
&pipe_write,
|
||||
RunnerMessage::Error {
|
||||
windows_error_code: error.raw_os_error().map(|value| value as u32),
|
||||
message: error.to_string(),
|
||||
},
|
||||
);
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_spawned_process(
|
||||
request: RunnerSpawnRequest,
|
||||
pipe_read: File,
|
||||
pipe_write: Arc<Mutex<File>>,
|
||||
) -> io::Result<()> {
|
||||
verify_runner_identity(&request.expected_account_sid)?;
|
||||
let token = create_restricted_token(&request.capability_sid)?;
|
||||
let local_request = LocalExecutionRequest {
|
||||
process_id: "windows-sandbox-runner-child".to_string(),
|
||||
tool_id: "windows-sandbox-runner-child".to_string(),
|
||||
tool_name: "exec_command".to_string(),
|
||||
command: request.command,
|
||||
cwd: Some(request.cwd.clone()),
|
||||
env: request.env,
|
||||
tty: request.tty,
|
||||
stdin: request.stdin_open,
|
||||
env_clear: true,
|
||||
pty_size: request.pty_size,
|
||||
sandbox: None,
|
||||
};
|
||||
let spawned = spawn_restricted_process(&local_request, &request.cwd, token.raw())?;
|
||||
drop(token);
|
||||
|
||||
let super::SpawnedRestrictedProcess {
|
||||
process,
|
||||
thread: process_thread,
|
||||
job,
|
||||
stdin_write,
|
||||
stdout_read,
|
||||
stderr_read,
|
||||
pseudoconsole,
|
||||
} = spawned;
|
||||
let process_id = unsafe { GetProcessId(process.raw()) };
|
||||
write_locked(&pipe_write, RunnerMessage::Ready { process_id })?;
|
||||
|
||||
let (reader_done_tx, reader_done_rx) = mpsc::channel();
|
||||
let output_kind = if pseudoconsole.is_some() {
|
||||
super::ExecutionOutputKind::Combined
|
||||
} else {
|
||||
super::ExecutionOutputKind::Stdout
|
||||
};
|
||||
spawn_output_reader(
|
||||
stdout_read,
|
||||
output_kind,
|
||||
Arc::clone(&pipe_write),
|
||||
reader_done_tx.clone(),
|
||||
);
|
||||
if let Some(stderr) = stderr_read {
|
||||
spawn_output_reader(
|
||||
stderr,
|
||||
super::ExecutionOutputKind::Stderr,
|
||||
Arc::clone(&pipe_write),
|
||||
reader_done_tx,
|
||||
);
|
||||
}
|
||||
|
||||
let job = Arc::new(job);
|
||||
let stdin = Arc::new(Mutex::new(stdin_write));
|
||||
let pseudoconsole = pseudoconsole.map(Arc::new);
|
||||
let terminated = Arc::new(AtomicBool::new(false));
|
||||
spawn_control_reader(
|
||||
pipe_read,
|
||||
Arc::clone(&job),
|
||||
Arc::clone(&stdin),
|
||||
pseudoconsole.as_ref().map(Arc::clone),
|
||||
Arc::clone(&terminated),
|
||||
);
|
||||
|
||||
let wait = unsafe { WaitForSingleObject(process.raw(), INFINITE) };
|
||||
if wait != WAIT_OBJECT_0 {
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
return Err(last_os_error("WaitForSingleObject(restricted child)"));
|
||||
}
|
||||
stdin.lock().ok().and_then(|mut value| value.take());
|
||||
let preserve_descendants = !terminated.load(Ordering::Acquire);
|
||||
if preserve_descendants {
|
||||
preserve_job_descendants(&job)?;
|
||||
} else {
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
drop(pseudoconsole);
|
||||
let reader_count = if request.tty { 1 } else { 2 };
|
||||
for _ in 0..reader_count {
|
||||
let _ = reader_done_rx.recv_timeout(OUTPUT_DRAIN_TIMEOUT);
|
||||
}
|
||||
|
||||
let mut exit_code = 1;
|
||||
if unsafe { GetExitCodeProcess(process.raw(), &mut exit_code) } == 0 {
|
||||
return Err(last_os_error("GetExitCodeProcess(restricted child)"));
|
||||
}
|
||||
drop(process_thread);
|
||||
drop(process);
|
||||
write_locked(
|
||||
&pipe_write,
|
||||
RunnerMessage::Exit {
|
||||
exit_code: i32::try_from(exit_code).unwrap_or(-1),
|
||||
},
|
||||
)?;
|
||||
|
||||
if preserve_descendants {
|
||||
while active_job_processes(&job)? != 0 {
|
||||
thread::sleep(JOB_REAPER_POLL);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_output_reader(
|
||||
handle: OwnedHandle,
|
||||
kind: super::ExecutionOutputKind,
|
||||
pipe_write: Arc<Mutex<File>>,
|
||||
done: mpsc::Sender<()>,
|
||||
) {
|
||||
thread::spawn(move || {
|
||||
let mut buffer = vec![0u8; super::PROCESS_OUTPUT_CHUNK_BYTES];
|
||||
loop {
|
||||
let mut read = 0;
|
||||
let ok = unsafe {
|
||||
ReadFile(
|
||||
handle.raw(),
|
||||
buffer.as_mut_ptr(),
|
||||
buffer.len() as u32,
|
||||
&mut read,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if ok == 0 || read == 0 {
|
||||
break;
|
||||
}
|
||||
if write_locked(
|
||||
&pipe_write,
|
||||
RunnerMessage::Output {
|
||||
kind,
|
||||
data_base64: encode_bytes(&buffer[..read as usize]),
|
||||
},
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = done.send(());
|
||||
});
|
||||
}
|
||||
|
||||
fn spawn_control_reader(
|
||||
mut pipe_read: File,
|
||||
job: Arc<OwnedHandle>,
|
||||
stdin: Arc<Mutex<Option<OwnedHandle>>>,
|
||||
pseudoconsole: Option<Arc<RestrictedConpty>>,
|
||||
terminated: Arc<AtomicBool>,
|
||||
) {
|
||||
thread::spawn(move || loop {
|
||||
match read_frame(&mut pipe_read) {
|
||||
Ok(Some(RunnerMessage::Stdin { data_base64 })) => {
|
||||
let Ok(bytes) = decode_bytes(&data_base64) else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(guard) = stdin.lock() {
|
||||
if let Some(handle) = guard.as_ref() {
|
||||
let _ = write_pipe(handle.raw(), &bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(RunnerMessage::CloseStdin)) => {
|
||||
if let Ok(mut guard) = stdin.lock() {
|
||||
guard.take();
|
||||
}
|
||||
}
|
||||
Ok(Some(RunnerMessage::Resize { rows, cols })) => {
|
||||
if let Some(pseudoconsole) = pseudoconsole.as_ref() {
|
||||
let _ = pseudoconsole.resize(rows, cols);
|
||||
}
|
||||
}
|
||||
Ok(Some(RunnerMessage::Terminate)) => {
|
||||
terminated.store(true, Ordering::Release);
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
}
|
||||
Ok(Some(_)) => {}
|
||||
Ok(None) | Err(_) => {
|
||||
terminated.store(true, Ordering::Release);
|
||||
unsafe {
|
||||
TerminateJobObject(job.raw(), 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_pipe_arguments() -> io::Result<(String, String)> {
|
||||
let mut pipe_in = None;
|
||||
let mut pipe_out = None;
|
||||
for argument in std::env::args().skip(1) {
|
||||
if let Some(value) = argument.strip_prefix("--pipe-in=") {
|
||||
pipe_in = Some(value.to_string());
|
||||
} else if let Some(value) = argument.strip_prefix("--pipe-out=") {
|
||||
pipe_out = Some(value.to_string());
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("unknown windows sandbox runner argument: {argument}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
match (pipe_in, pipe_out) {
|
||||
(Some(pipe_in), Some(pipe_out)) => Ok((pipe_in, pipe_out)),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"windows sandbox runner requires --pipe-in and --pipe-out",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_pipe(name: &str, access: u32) -> io::Result<OwnedHandle> {
|
||||
let name = to_wide(name);
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
name.as_ptr(),
|
||||
access,
|
||||
0,
|
||||
ptr::null_mut(),
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
OwnedHandle::new(handle, "CreateFileW(windows sandbox runner pipe)")
|
||||
}
|
||||
|
||||
fn verify_runner_identity(expected_sid: &str) -> io::Result<()> {
|
||||
let token = super::current_process_token_for_restriction()?;
|
||||
let actual_sid = unsafe { super::token_user_sid(token.raw())? };
|
||||
let expected_sid = LocalSid::parse(expected_sid)?;
|
||||
if unsafe { EqualSid(actual_sid.as_ptr() as *mut _, expected_sid.raw()) } == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
"Windows sandbox runner identity does not match the requested account",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_locked(writer: &Arc<Mutex<File>>, message: RunnerMessage) -> io::Result<()> {
|
||||
let mut writer = writer
|
||||
.lock()
|
||||
.map_err(|_| io::Error::other("Windows sandbox runner writer lock poisoned"))?;
|
||||
write_frame(&mut *writer, message)
|
||||
}
|
||||
|
||||
fn send_start_error(writer: &Arc<Mutex<File>>, error: io::Error) -> io::Result<()> {
|
||||
let _ = write_locked(
|
||||
writer,
|
||||
RunnerMessage::Error {
|
||||
windows_error_code: error.raw_os_error().map(|value| value as u32),
|
||||
message: error.to_string(),
|
||||
},
|
||||
);
|
||||
Err(error)
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
use super::windows_runner_protocol::{read_frame, write_frame, RunnerMessage, RunnerSpawnRequest};
|
||||
use super::{argv_to_command_line, last_os_error, to_wide, LocalExecutionRequest, OwnedHandle};
|
||||
use crate::windows_setup::{read_windows_sandbox_password, resolve_windows_account_sid};
|
||||
use std::ffi::c_void;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::windows::io::FromRawHandle;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ptr;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
use windows_sys::Win32::Foundation::{
|
||||
GetLastError, LocalFree, ERROR_PIPE_CONNECTED, ERROR_PIPE_LISTENING, HANDLE, HLOCAL,
|
||||
WAIT_OBJECT_0, WAIT_TIMEOUT,
|
||||
};
|
||||
use windows_sys::Win32::Security::Authorization::{
|
||||
ConvertSidToStringSidW, ConvertStringSecurityDescriptorToSecurityDescriptorW,
|
||||
};
|
||||
use windows_sys::Win32::Security::{PSECURITY_DESCRIPTOR, SECURITY_ATTRIBUTES};
|
||||
use windows_sys::Win32::System::Diagnostics::Debug::SetErrorMode;
|
||||
use windows_sys::Win32::System::Pipes::{
|
||||
ConnectNamedPipe, CreateNamedPipeW, GetNamedPipeClientProcessId, PeekNamedPipe,
|
||||
SetNamedPipeHandleState, PIPE_NOWAIT, PIPE_READMODE_BYTE, PIPE_REJECT_REMOTE_CLIENTS,
|
||||
PIPE_TYPE_BYTE, PIPE_WAIT,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::{
|
||||
CreateProcessWithLogonW, TerminateProcess, WaitForSingleObject, CREATE_NO_WINDOW,
|
||||
CREATE_UNICODE_ENVIRONMENT, PROCESS_INFORMATION, STARTUPINFOW,
|
||||
};
|
||||
|
||||
const PIPE_ACCESS_INBOUND: u32 = 0x0000_0001;
|
||||
const PIPE_ACCESS_OUTBOUND: u32 = 0x0000_0002;
|
||||
const RUNNER_START_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
const RUNNER_POLL_INTERVAL: Duration = Duration::from_millis(5);
|
||||
const RUNNER_ERROR_MODE_FLAGS: u32 = 0x0001 | 0x0002;
|
||||
|
||||
pub(super) struct RunnerTransport {
|
||||
pub(super) pipe_write: File,
|
||||
pub(super) pipe_read: File,
|
||||
pub(super) process: OwnedHandle,
|
||||
}
|
||||
|
||||
pub(super) fn spawn_runner_transport(
|
||||
request: &LocalExecutionRequest,
|
||||
cwd: &Path,
|
||||
account: &str,
|
||||
capability_sid: String,
|
||||
) -> io::Result<RunnerTransport> {
|
||||
let account_sid = resolve_windows_account_sid(account).map_err(io::Error::other)?;
|
||||
let account_sid = sid_string(&account_sid)?;
|
||||
let nonce = uuid::Uuid::new_v4().simple();
|
||||
let pipe_in_name = format!(r"\\.\pipe\lime-sandbox-runner-{nonce}-in");
|
||||
let pipe_out_name = format!(r"\\.\pipe\lime-sandbox-runner-{nonce}-out");
|
||||
let pipe_in = create_named_pipe(&pipe_in_name, PIPE_ACCESS_OUTBOUND, &account_sid)?;
|
||||
let pipe_out = create_named_pipe(&pipe_out_name, PIPE_ACCESS_INBOUND, &account_sid)?;
|
||||
|
||||
let runner = resolve_runner_executable()?;
|
||||
let runner_args = vec![
|
||||
runner.to_string_lossy().into_owned(),
|
||||
format!("--pipe-in={pipe_in_name}"),
|
||||
format!("--pipe-out={pipe_out_name}"),
|
||||
];
|
||||
let mut command_line = to_wide(argv_to_command_line(&runner_args));
|
||||
let runner_wide = to_wide(runner.as_os_str());
|
||||
let cwd_wide = to_wide(cwd.as_os_str());
|
||||
let username = to_wide(account);
|
||||
let domain = to_wide(".");
|
||||
let password = to_wide(read_windows_sandbox_password(account)?);
|
||||
let mut startup: STARTUPINFOW = unsafe { std::mem::zeroed() };
|
||||
startup.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
|
||||
let mut process_info: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
let previous_error_mode = unsafe { SetErrorMode(RUNNER_ERROR_MODE_FLAGS) };
|
||||
let created = unsafe {
|
||||
CreateProcessWithLogonW(
|
||||
username.as_ptr(),
|
||||
domain.as_ptr(),
|
||||
password.as_ptr(),
|
||||
0,
|
||||
runner_wide.as_ptr(),
|
||||
command_line.as_mut_ptr(),
|
||||
CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT,
|
||||
ptr::null(),
|
||||
cwd_wide.as_ptr(),
|
||||
&startup,
|
||||
&mut process_info,
|
||||
)
|
||||
};
|
||||
unsafe {
|
||||
SetErrorMode(previous_error_mode);
|
||||
}
|
||||
if created == 0 {
|
||||
return Err(last_os_error(
|
||||
"CreateProcessWithLogonW(windows-sandbox-runner)",
|
||||
));
|
||||
}
|
||||
let process = OwnedHandle::new(process_info.hProcess, "windows sandbox runner process")?;
|
||||
drop(OwnedHandle::new(
|
||||
process_info.hThread,
|
||||
"windows sandbox runner thread",
|
||||
)?);
|
||||
|
||||
if let Err(error) = connect_named_pipe(&pipe_in, process.raw(), process_info.dwProcessId)
|
||||
.and_then(|_| connect_named_pipe(&pipe_out, process.raw(), process_info.dwProcessId))
|
||||
{
|
||||
unsafe {
|
||||
TerminateProcess(process.raw(), 1);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let mut transport = RunnerTransport {
|
||||
pipe_write: unsafe { File::from_raw_handle(pipe_in.into_raw() as _) },
|
||||
pipe_read: unsafe { File::from_raw_handle(pipe_out.into_raw() as _) },
|
||||
process,
|
||||
};
|
||||
let startup_result = (|| {
|
||||
write_frame(
|
||||
&mut transport.pipe_write,
|
||||
RunnerMessage::Spawn {
|
||||
payload: RunnerSpawnRequest {
|
||||
command: request.command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: request.env.clone(),
|
||||
capability_sid,
|
||||
expected_account_sid: account_sid,
|
||||
tty: request.tty,
|
||||
stdin_open: request.stdin,
|
||||
pty_size: request.pty_size,
|
||||
},
|
||||
},
|
||||
)?;
|
||||
wait_for_complete_frame(
|
||||
&transport.pipe_read,
|
||||
transport.process.raw(),
|
||||
RUNNER_START_TIMEOUT,
|
||||
)?;
|
||||
match read_frame(&mut transport.pipe_read)? {
|
||||
Some(RunnerMessage::Ready { .. }) => Ok(()),
|
||||
Some(RunnerMessage::Error {
|
||||
message,
|
||||
windows_error_code,
|
||||
}) => Err(io::Error::other(format_runner_error(
|
||||
&message,
|
||||
windows_error_code,
|
||||
))),
|
||||
Some(_) => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Windows sandbox runner sent an unexpected startup frame",
|
||||
)),
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"Windows sandbox runner closed before ready",
|
||||
)),
|
||||
}
|
||||
})();
|
||||
if let Err(error) = startup_result {
|
||||
unsafe {
|
||||
TerminateProcess(transport.process.raw(), 1);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
fn resolve_runner_executable() -> io::Result<PathBuf> {
|
||||
let current = std::env::current_exe()?;
|
||||
let name = "windows-sandbox-runner.exe";
|
||||
let mut candidates = Vec::new();
|
||||
if let Some(parent) = current.parent() {
|
||||
candidates.push(parent.join(name));
|
||||
if parent.file_name().is_some_and(|value| value == "deps") {
|
||||
if let Some(target_profile) = parent.parent() {
|
||||
candidates.push(target_profile.join(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
candidates
|
||||
.into_iter()
|
||||
.find(|candidate| candidate.is_file())
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!(
|
||||
"windows-sandbox-runner.exe was not found beside {}",
|
||||
current.display()
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_named_pipe(name: &str, access: u32, account_sid: &str) -> io::Result<OwnedHandle> {
|
||||
let sddl = to_wide(format!("D:(A;;GA;;;{account_sid})"));
|
||||
let mut descriptor: PSECURITY_DESCRIPTOR = ptr::null_mut();
|
||||
if unsafe {
|
||||
ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
sddl.as_ptr(),
|
||||
1,
|
||||
&mut descriptor,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
return Err(last_os_error(
|
||||
"ConvertStringSecurityDescriptorToSecurityDescriptorW(runner pipe)",
|
||||
));
|
||||
}
|
||||
let mut attributes = SECURITY_ATTRIBUTES {
|
||||
nLength: std::mem::size_of::<SECURITY_ATTRIBUTES>() as u32,
|
||||
lpSecurityDescriptor: descriptor,
|
||||
bInheritHandle: 0,
|
||||
};
|
||||
let name = to_wide(name);
|
||||
let handle = unsafe {
|
||||
CreateNamedPipeW(
|
||||
name.as_ptr(),
|
||||
access,
|
||||
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_NOWAIT | PIPE_REJECT_REMOTE_CLIENTS,
|
||||
1,
|
||||
64 * 1024,
|
||||
64 * 1024,
|
||||
0,
|
||||
&mut attributes,
|
||||
)
|
||||
};
|
||||
unsafe {
|
||||
LocalFree(descriptor as HLOCAL);
|
||||
}
|
||||
OwnedHandle::new(handle, "CreateNamedPipeW(windows sandbox runner)")
|
||||
}
|
||||
|
||||
fn connect_named_pipe(pipe: &OwnedHandle, process: HANDLE, expected_pid: u32) -> io::Result<()> {
|
||||
let deadline = Instant::now() + RUNNER_START_TIMEOUT;
|
||||
loop {
|
||||
let connected = unsafe { ConnectNamedPipe(pipe.raw(), ptr::null_mut()) };
|
||||
let error = if connected == 0 {
|
||||
unsafe { GetLastError() }
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if connected != 0 || error == ERROR_PIPE_CONNECTED {
|
||||
break;
|
||||
}
|
||||
if error != ERROR_PIPE_LISTENING {
|
||||
return Err(io::Error::from_raw_os_error(error as i32));
|
||||
}
|
||||
if unsafe { WaitForSingleObject(process, 0) } == WAIT_OBJECT_0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"Windows sandbox runner exited before connecting",
|
||||
));
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"timed out connecting Windows sandbox runner pipe",
|
||||
));
|
||||
}
|
||||
thread::sleep(RUNNER_POLL_INTERVAL);
|
||||
}
|
||||
let wait_mode = PIPE_READMODE_BYTE | PIPE_WAIT;
|
||||
if unsafe { SetNamedPipeHandleState(pipe.raw(), &wait_mode, ptr::null(), ptr::null()) } == 0 {
|
||||
return Err(last_os_error("SetNamedPipeHandleState(runner pipe)"));
|
||||
}
|
||||
let mut client_pid = 0;
|
||||
if unsafe { GetNamedPipeClientProcessId(pipe.raw(), &mut client_pid) } == 0 {
|
||||
return Err(last_os_error("GetNamedPipeClientProcessId"));
|
||||
}
|
||||
if client_pid != expected_pid {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
format!("runner pipe client pid {client_pid} did not match {expected_pid}"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_for_complete_frame(pipe: &File, process: HANDLE, timeout: Duration) -> io::Result<()> {
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
|
||||
let handle = pipe.as_raw_handle() as HANDLE;
|
||||
let deadline = Instant::now() + timeout;
|
||||
let mut length = [0u8; 4];
|
||||
loop {
|
||||
let mut bytes_read = 0;
|
||||
let mut available = 0;
|
||||
if unsafe {
|
||||
PeekNamedPipe(
|
||||
handle,
|
||||
length.as_mut_ptr().cast::<c_void>(),
|
||||
length.len() as u32,
|
||||
&mut bytes_read,
|
||||
&mut available,
|
||||
ptr::null_mut(),
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
return Err(last_os_error("PeekNamedPipe(runner ready)"));
|
||||
}
|
||||
if bytes_read == length.len() as u32 {
|
||||
let frame_bytes = u32::from_le_bytes(length) as usize + length.len();
|
||||
if available as usize >= frame_bytes {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
match unsafe { WaitForSingleObject(process, 0) } {
|
||||
WAIT_OBJECT_0 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::BrokenPipe,
|
||||
"Windows sandbox runner exited before ready",
|
||||
))
|
||||
}
|
||||
WAIT_TIMEOUT => {}
|
||||
_ => return Err(last_os_error("WaitForSingleObject(runner ready)")),
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"timed out waiting for Windows sandbox runner ready frame",
|
||||
));
|
||||
}
|
||||
thread::sleep(RUNNER_POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
fn sid_string(sid: &[u8]) -> io::Result<String> {
|
||||
let mut value = ptr::null_mut();
|
||||
if unsafe { ConvertSidToStringSidW(sid.as_ptr() as *mut c_void, &mut value) } == 0 {
|
||||
return Err(last_os_error("ConvertSidToStringSidW(runner account)"));
|
||||
}
|
||||
let mut length = 0;
|
||||
while unsafe { *value.add(length) } != 0 {
|
||||
length += 1;
|
||||
}
|
||||
let result = String::from_utf16_lossy(unsafe { std::slice::from_raw_parts(value, length) });
|
||||
unsafe {
|
||||
LocalFree(value as HLOCAL);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(super) fn format_runner_error(message: &str, code: Option<u32>) -> String {
|
||||
match code {
|
||||
Some(code) => format!("{message} (Windows error {code})"),
|
||||
None => message.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
use super::ExecutionOutputKind;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use base64::Engine as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(super) const RUNNER_PROTOCOL_VERSION: u8 = 1;
|
||||
const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct RunnerSpawnRequest {
|
||||
pub command: Vec<String>,
|
||||
pub cwd: PathBuf,
|
||||
pub env: HashMap<String, String>,
|
||||
pub capability_sid: String,
|
||||
pub expected_account_sid: String,
|
||||
pub tty: bool,
|
||||
pub stdin_open: bool,
|
||||
pub pty_size: Option<(u16, u16)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub(super) enum RunnerMessage {
|
||||
Spawn {
|
||||
payload: RunnerSpawnRequest,
|
||||
},
|
||||
Ready {
|
||||
process_id: u32,
|
||||
},
|
||||
Output {
|
||||
kind: ExecutionOutputKind,
|
||||
data_base64: String,
|
||||
},
|
||||
Stdin {
|
||||
data_base64: String,
|
||||
},
|
||||
CloseStdin,
|
||||
Resize {
|
||||
rows: u16,
|
||||
cols: u16,
|
||||
},
|
||||
Terminate,
|
||||
Exit {
|
||||
exit_code: i32,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
windows_error_code: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(super) struct RunnerFrame {
|
||||
pub version: u8,
|
||||
#[serde(flatten)]
|
||||
pub message: RunnerMessage,
|
||||
}
|
||||
|
||||
impl RunnerFrame {
|
||||
pub(super) fn new(message: RunnerMessage) -> Self {
|
||||
Self {
|
||||
version: RUNNER_PROTOCOL_VERSION,
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn encode_bytes(bytes: &[u8]) -> String {
|
||||
BASE64_STANDARD.encode(bytes)
|
||||
}
|
||||
|
||||
pub(super) fn decode_bytes(value: &str) -> io::Result<Vec<u8>> {
|
||||
BASE64_STANDARD
|
||||
.decode(value)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
|
||||
}
|
||||
|
||||
pub(super) fn write_frame(writer: &mut impl Write, message: RunnerMessage) -> io::Result<()> {
|
||||
let payload = serde_json::to_vec(&RunnerFrame::new(message))
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||
if payload.len() > MAX_FRAME_BYTES {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Windows sandbox runner frame exceeds {MAX_FRAME_BYTES} bytes"),
|
||||
));
|
||||
}
|
||||
let length = u32::try_from(payload.len())
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "runner frame is too large"))?;
|
||||
writer.write_all(&length.to_le_bytes())?;
|
||||
writer.write_all(&payload)?;
|
||||
writer.flush()
|
||||
}
|
||||
|
||||
pub(super) fn read_frame(reader: &mut impl Read) -> io::Result<Option<RunnerMessage>> {
|
||||
let mut length = [0u8; 4];
|
||||
match reader.read_exact(&mut length) {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
let length = u32::from_le_bytes(length) as usize;
|
||||
if length > MAX_FRAME_BYTES {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Windows sandbox runner frame exceeds {MAX_FRAME_BYTES} bytes"),
|
||||
));
|
||||
}
|
||||
let mut payload = vec![0u8; length];
|
||||
reader.read_exact(&mut payload)?;
|
||||
let frame: RunnerFrame = serde_json::from_slice(&payload)
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
||||
if frame.version != RUNNER_PROTOCOL_VERSION {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"unsupported Windows sandbox runner protocol version {}",
|
||||
frame.version
|
||||
),
|
||||
));
|
||||
}
|
||||
Ok(Some(frame.message))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn framed_output_round_trips_raw_bytes() {
|
||||
let mut bytes = Vec::new();
|
||||
write_frame(
|
||||
&mut bytes,
|
||||
RunnerMessage::Output {
|
||||
kind: ExecutionOutputKind::Stdout,
|
||||
data_base64: encode_bytes(&[0, 1, 2, 255]),
|
||||
},
|
||||
)
|
||||
.expect("frame should encode");
|
||||
|
||||
let message = read_frame(&mut bytes.as_slice())
|
||||
.expect("frame should decode")
|
||||
.expect("frame should be present");
|
||||
let RunnerMessage::Output { data_base64, .. } = message else {
|
||||
panic!("unexpected runner message")
|
||||
};
|
||||
assert_eq!(decode_bytes(&data_base64).unwrap(), [0, 1, 2, 255]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_frame_is_rejected_before_allocation() {
|
||||
let mut encoded = ((MAX_FRAME_BYTES + 1) as u32).to_le_bytes().to_vec();
|
||||
encoded.extend_from_slice(b"{}");
|
||||
let error = read_frame(&mut encoded.as_slice()).expect_err("oversized frame must fail");
|
||||
assert_eq!(error.kind(), io::ErrorKind::InvalidData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
use super::windows_acl::AclLease;
|
||||
use super::windows_runner_host::RunnerTransport;
|
||||
use super::windows_runner_protocol::{
|
||||
decode_bytes, encode_bytes, read_frame, write_frame, RunnerMessage,
|
||||
};
|
||||
use super::{
|
||||
ExecutionOutputDelta, ExecutionProcess, ExecutionProcessSnapshot, ExecutionProcessStatus,
|
||||
LocalExecutionControl, OwnedHandle, CONTROL_POLL_MILLIS,
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::sync::mpsc::{self, Receiver, TryRecvError};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc as tokio_mpsc, oneshot, watch, Mutex};
|
||||
use windows_sys::Win32::Foundation::WAIT_OBJECT_0;
|
||||
use windows_sys::Win32::System::Threading::{TerminateProcess, WaitForSingleObject};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) fn supervise(
|
||||
transport: RunnerTransport,
|
||||
acl_lease: AclLease,
|
||||
process: Arc<Mutex<ExecutionProcess>>,
|
||||
output_tx: tokio_mpsc::UnboundedSender<ExecutionOutputDelta>,
|
||||
state_tx: watch::Sender<ExecutionProcessSnapshot>,
|
||||
final_tx: oneshot::Sender<ExecutionProcessSnapshot>,
|
||||
control_rx: Receiver<LocalExecutionControl>,
|
||||
) {
|
||||
let RunnerTransport {
|
||||
mut pipe_write,
|
||||
mut pipe_read,
|
||||
process: runner_process,
|
||||
} = transport;
|
||||
let (event_tx, event_rx) = mpsc::channel();
|
||||
thread::spawn(move || loop {
|
||||
let event = read_frame(&mut pipe_read);
|
||||
let terminal = matches!(
|
||||
event,
|
||||
Ok(Some(
|
||||
RunnerMessage::Exit { .. } | RunnerMessage::Error { .. }
|
||||
)) | Ok(None)
|
||||
| Err(_)
|
||||
);
|
||||
if event_tx.send(event).is_err() || terminal {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let mut terminal_exit = None;
|
||||
let mut failure = None;
|
||||
let mut control_disconnected = false;
|
||||
while terminal_exit.is_none() && failure.is_none() {
|
||||
if !control_disconnected {
|
||||
match control_rx.recv_timeout(Duration::from_millis(CONTROL_POLL_MILLIS as u64)) {
|
||||
Ok(control) => {
|
||||
if let Err(error) =
|
||||
forward_control(control, &mut pipe_write, &process, &state_tx)
|
||||
{
|
||||
failure = Some(error.to_string());
|
||||
}
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
control_disconnected = true;
|
||||
update_status(&process, &state_tx, ExecutionProcessStatus::Terminated);
|
||||
if let Err(error) = write_frame(&mut pipe_write, RunnerMessage::Terminate) {
|
||||
failure = Some(error.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
thread::sleep(Duration::from_millis(CONTROL_POLL_MILLIS as u64));
|
||||
}
|
||||
|
||||
loop {
|
||||
match event_rx.try_recv() {
|
||||
Ok(Ok(Some(RunnerMessage::Output { kind, data_base64 }))) => {
|
||||
match decode_bytes(&data_base64) {
|
||||
Ok(bytes) => append_output(&process, &output_tx, &state_tx, kind, &bytes),
|
||||
Err(error) => {
|
||||
failure = Some(error.to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Ok(Some(RunnerMessage::Exit { exit_code }))) => {
|
||||
terminal_exit = Some(exit_code);
|
||||
break;
|
||||
}
|
||||
Ok(Ok(Some(RunnerMessage::Error {
|
||||
message,
|
||||
windows_error_code,
|
||||
}))) => {
|
||||
failure = Some(super::windows_runner_host::format_runner_error(
|
||||
&message,
|
||||
windows_error_code,
|
||||
));
|
||||
break;
|
||||
}
|
||||
Ok(Ok(Some(_))) => {
|
||||
failure = Some("Windows sandbox runner sent an unexpected frame".to_string());
|
||||
break;
|
||||
}
|
||||
Ok(Ok(None)) => {
|
||||
failure = Some("Windows sandbox runner closed before exit".to_string());
|
||||
break;
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
failure = Some(format!("Windows sandbox runner IPC failed: {error}"));
|
||||
break;
|
||||
}
|
||||
Err(TryRecvError::Empty) => break,
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
failure = Some("Windows sandbox runner event reader closed".to_string());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let final_snapshot = {
|
||||
let mut guard = process.blocking_lock();
|
||||
if !guard.status().is_terminal() {
|
||||
if let Some(error) = failure.as_ref() {
|
||||
guard.fail(error.clone());
|
||||
} else if let Some(exit_code) = terminal_exit {
|
||||
guard.exit(exit_code);
|
||||
}
|
||||
}
|
||||
guard.snapshot()
|
||||
};
|
||||
let _ = state_tx.send(final_snapshot.clone());
|
||||
let _ = final_tx.send(final_snapshot);
|
||||
|
||||
if failure.is_some() {
|
||||
unsafe {
|
||||
TerminateProcess(runner_process.raw(), 1);
|
||||
}
|
||||
}
|
||||
thread::spawn(move || reap_runner(runner_process, pipe_write, acl_lease));
|
||||
}
|
||||
|
||||
fn forward_control(
|
||||
control: LocalExecutionControl,
|
||||
pipe_write: &mut File,
|
||||
process: &Arc<Mutex<ExecutionProcess>>,
|
||||
state_tx: &watch::Sender<ExecutionProcessSnapshot>,
|
||||
) -> io::Result<()> {
|
||||
let message = match control {
|
||||
LocalExecutionControl::WriteStdin(bytes) => RunnerMessage::Stdin {
|
||||
data_base64: encode_bytes(&bytes),
|
||||
},
|
||||
LocalExecutionControl::CloseStdin => RunnerMessage::CloseStdin,
|
||||
LocalExecutionControl::Resize { rows, cols } => RunnerMessage::Resize { rows, cols },
|
||||
LocalExecutionControl::Interrupt => {
|
||||
update_status(process, state_tx, ExecutionProcessStatus::Interrupted);
|
||||
RunnerMessage::Terminate
|
||||
}
|
||||
LocalExecutionControl::Terminate => {
|
||||
update_status(process, state_tx, ExecutionProcessStatus::Terminated);
|
||||
RunnerMessage::Terminate
|
||||
}
|
||||
};
|
||||
write_frame(pipe_write, message)
|
||||
}
|
||||
|
||||
fn append_output(
|
||||
process: &Arc<Mutex<ExecutionProcess>>,
|
||||
output_tx: &tokio_mpsc::UnboundedSender<ExecutionOutputDelta>,
|
||||
state_tx: &watch::Sender<ExecutionProcessSnapshot>,
|
||||
kind: super::ExecutionOutputKind,
|
||||
bytes: &[u8],
|
||||
) {
|
||||
let (delta, snapshot) = {
|
||||
let mut guard = process.blocking_lock();
|
||||
let delta = guard.append_output(kind, bytes);
|
||||
(delta, guard.snapshot())
|
||||
};
|
||||
let _ = output_tx.send(delta);
|
||||
let _ = state_tx.send(snapshot);
|
||||
}
|
||||
|
||||
fn update_status(
|
||||
process: &Arc<Mutex<ExecutionProcess>>,
|
||||
state_tx: &watch::Sender<ExecutionProcessSnapshot>,
|
||||
status: ExecutionProcessStatus,
|
||||
) {
|
||||
let snapshot = {
|
||||
let mut guard = process.blocking_lock();
|
||||
match status {
|
||||
ExecutionProcessStatus::Interrupted => guard.interrupt(),
|
||||
ExecutionProcessStatus::Terminated => guard.terminate(),
|
||||
ExecutionProcessStatus::Failed => guard.fail("process failed"),
|
||||
ExecutionProcessStatus::Exited => guard.exit(-1),
|
||||
ExecutionProcessStatus::Starting | ExecutionProcessStatus::Running => {}
|
||||
}
|
||||
guard.snapshot()
|
||||
};
|
||||
let _ = state_tx.send(snapshot);
|
||||
}
|
||||
|
||||
fn reap_runner(process: OwnedHandle, _pipe_write: File, _acl_lease: AclLease) {
|
||||
let _ = unsafe { WaitForSingleObject(process.raw(), u32::MAX) } == WAIT_OBJECT_0;
|
||||
}
|
||||
@@ -407,9 +407,14 @@ async fn world_writable_audit_reports_everyone_write_acl() {
|
||||
"/grant",
|
||||
"Everyone:(OI)(CI)(M)",
|
||||
])
|
||||
.status()
|
||||
.output()
|
||||
.expect("icacls should start");
|
||||
assert!(grant.success(), "icacls grant should succeed");
|
||||
assert!(
|
||||
grant.status.success(),
|
||||
"icacls grant should succeed: stdout={}, stderr={}",
|
||||
String::from_utf8_lossy(&grant.stdout),
|
||||
String::from_utf8_lossy(&grant.stderr)
|
||||
);
|
||||
|
||||
let environment = env::vars().collect::<HashMap<_, _>>();
|
||||
let audit = audit_windows_world_writable(fixture.path(), &environment);
|
||||
|
||||
@@ -145,6 +145,7 @@ export type AppServerReleaseArtifact = {
|
||||
sha256: string;
|
||||
codeModeHostSha256?: string;
|
||||
windowsSandboxSetupSha256?: string;
|
||||
windowsSandboxRunnerSha256?: string;
|
||||
};
|
||||
|
||||
export type AppServerReleaseManifest = {
|
||||
|
||||
@@ -7520,8 +7520,8 @@ const checks = [
|
||||
"cargo clean --manifest-path lime-rs/Cargo.toml -p v8",
|
||||
"Prepare sherpa-onnx runtime",
|
||||
"node scripts/prepare-sherpa-onnx-runtime.mjs --target x86_64-pc-windows-msvc",
|
||||
"Build Windows app-server, code-mode-host, and sandbox setup sidecars",
|
||||
"cargo build --manifest-path lime-rs/Cargo.toml --target x86_64-pc-windows-msvc -p app-server --bin app-server -p tool-runtime --bin code-mode-host --bin windows-sandbox-setup",
|
||||
"Build Windows app-server, code-mode-host, and sandbox sidecars",
|
||||
"cargo build --manifest-path lime-rs/Cargo.toml --target x86_64-pc-windows-msvc -p app-server --bin app-server -p tool-runtime --bin code-mode-host --bin windows-sandbox-setup --bin windows-sandbox-runner",
|
||||
],
|
||||
absentSnippets: [
|
||||
"cargo check --manifest-path lime-rs/Cargo.toml -p app-server --bin app-server -p tool-runtime --bin code-mode-host",
|
||||
|
||||
@@ -5,5 +5,5 @@ import { prepareElectronAppServerAssets } from "../lib/electron-app-server-asset
|
||||
const result = await prepareElectronAppServerAssets();
|
||||
|
||||
console.log(
|
||||
`[electron-assets] prepared app-server sidecar ${result.binaryPath}, code-mode host ${result.codeModeHostBinaryPath}, and ${result.manifestPath}`,
|
||||
`[electron-assets] prepared app-server sidecar ${result.binaryPath}, code-mode host ${result.codeModeHostBinaryPath}, Windows sandbox runner ${result.windowsSandboxRunnerBinaryPath || "n/a"}, and ${result.manifestPath}`,
|
||||
);
|
||||
|
||||
@@ -119,6 +119,10 @@ function windowsSandboxSetupBinaryName(platform = process.platform) {
|
||||
return platform === "win32" ? "windows-sandbox-setup.exe" : null;
|
||||
}
|
||||
|
||||
function windowsSandboxRunnerBinaryName(platform = process.platform) {
|
||||
return platform === "win32" ? "windows-sandbox-runner.exe" : null;
|
||||
}
|
||||
|
||||
function sha256(filePath) {
|
||||
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
||||
}
|
||||
@@ -204,6 +208,8 @@ export function verifyResourceRoot(
|
||||
});
|
||||
let windowsSandboxSetupPath = null;
|
||||
let windowsSandboxSetupIntegrity = null;
|
||||
let windowsSandboxRunnerPath = null;
|
||||
let windowsSandboxRunnerIntegrity = null;
|
||||
if (platform === "win32") {
|
||||
windowsSandboxSetupPath = path.join(
|
||||
root,
|
||||
@@ -221,6 +227,22 @@ export function verifyResourceRoot(
|
||||
execFileSyncImpl,
|
||||
},
|
||||
);
|
||||
windowsSandboxRunnerPath = path.join(
|
||||
root,
|
||||
"app-server",
|
||||
key,
|
||||
windowsSandboxRunnerBinaryName(platform),
|
||||
);
|
||||
assertFile(windowsSandboxRunnerPath, "Windows sandbox runner");
|
||||
windowsSandboxRunnerIntegrity = verifySidecarIntegrity(
|
||||
windowsSandboxRunnerPath,
|
||||
artifact.windowsSandboxRunnerSha256,
|
||||
{
|
||||
platform,
|
||||
label: "Windows sandbox runner",
|
||||
execFileSyncImpl,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (const name of [
|
||||
@@ -247,6 +269,8 @@ export function verifyResourceRoot(
|
||||
codeModeHostIntegrity: codeModeHost,
|
||||
windowsSandboxSetupPath,
|
||||
windowsSandboxSetupIntegrity,
|
||||
windowsSandboxRunnerPath,
|
||||
windowsSandboxRunnerIntegrity,
|
||||
sha256: appServer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,9 +56,11 @@ function createResourceRoot({
|
||||
appServer = "signed app-server",
|
||||
codeModeHost = "signed code-mode-host",
|
||||
windowsSandboxSetup = "signed windows-sandbox-setup",
|
||||
windowsSandboxRunner = "signed windows-sandbox-runner",
|
||||
manifestAppServer = "unsigned app-server",
|
||||
manifestCodeModeHost = "unsigned code-mode-host",
|
||||
manifestWindowsSandboxSetup = "unsigned windows-sandbox-setup",
|
||||
manifestWindowsSandboxRunner = "unsigned windows-sandbox-runner",
|
||||
platformKey = "darwin-arm64",
|
||||
} = {}) {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "lime-electron-resources-"));
|
||||
@@ -81,6 +83,10 @@ function createResourceRoot({
|
||||
path.join(sidecarDir, "windows-sandbox-setup.exe"),
|
||||
windowsSandboxSetup,
|
||||
);
|
||||
writeFileSync(
|
||||
path.join(sidecarDir, "windows-sandbox-runner.exe"),
|
||||
windowsSandboxRunner,
|
||||
);
|
||||
}
|
||||
for (const name of [
|
||||
"icon.png",
|
||||
@@ -102,7 +108,12 @@ function createResourceRoot({
|
||||
sha256: sha256(manifestAppServer),
|
||||
codeModeHostSha256: sha256(manifestCodeModeHost),
|
||||
...(platformKey.startsWith("win32-")
|
||||
? { windowsSandboxSetupSha256: sha256(manifestWindowsSandboxSetup) }
|
||||
? {
|
||||
windowsSandboxSetupSha256: sha256(manifestWindowsSandboxSetup),
|
||||
windowsSandboxRunnerSha256: sha256(
|
||||
manifestWindowsSandboxRunner,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
],
|
||||
@@ -238,15 +249,17 @@ describe("verify-electron-package-resources sidecar integrity", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("Windows 资源必须包含 sandbox setup helper 并校验哈希", () => {
|
||||
it("Windows 资源必须包含 sandbox setup helper 与 command runner 并校验哈希", () => {
|
||||
const root = createResourceRoot({
|
||||
platformKey: "win32-x64",
|
||||
appServer: "app-server",
|
||||
codeModeHost: "code-mode-host",
|
||||
windowsSandboxSetup: "windows-sandbox-setup",
|
||||
windowsSandboxRunner: "windows-sandbox-runner",
|
||||
manifestAppServer: "app-server",
|
||||
manifestCodeModeHost: "code-mode-host",
|
||||
manifestWindowsSandboxSetup: "windows-sandbox-setup",
|
||||
manifestWindowsSandboxRunner: "windows-sandbox-runner",
|
||||
});
|
||||
|
||||
const result = verifyResourceRoot(root, {
|
||||
@@ -255,6 +268,7 @@ describe("verify-electron-package-resources sidecar integrity", () => {
|
||||
});
|
||||
|
||||
expect(result.windowsSandboxSetupIntegrity.acceptedBecause).toBe("sha256");
|
||||
expect(result.windowsSandboxRunnerIntegrity.acceptedBecause).toBe("sha256");
|
||||
});
|
||||
|
||||
it("拒绝哈希变化且严格 codesign 失败的 macOS code-mode host", () => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
appServerBinaryName,
|
||||
codeModeHostBinaryName,
|
||||
resolveDevAppServerBinary,
|
||||
windowsSandboxRunnerBinaryName,
|
||||
} from "./electron-dev-sidecar.mjs";
|
||||
import {
|
||||
ensureMacBinaryRpath,
|
||||
@@ -47,6 +48,12 @@ export function windowsSandboxSetupResourceBinaryName(
|
||||
return platform === "win32" ? "windows-sandbox-setup.exe" : null;
|
||||
}
|
||||
|
||||
export function windowsSandboxRunnerResourceBinaryName(
|
||||
platform = process.platform,
|
||||
) {
|
||||
return windowsSandboxRunnerBinaryName(platform);
|
||||
}
|
||||
|
||||
export function appServerResourcePlatformKey(
|
||||
platform = process.platform,
|
||||
arch = process.arch,
|
||||
@@ -117,10 +124,24 @@ export function electronWindowsSandboxSetupBinaryDestination({
|
||||
return path.resolve(outputRoot, "app-server", platformKey, name);
|
||||
}
|
||||
|
||||
export function electronWindowsSandboxRunnerBinaryDestination({
|
||||
outputRoot = electronAppServerResourcesRoot(),
|
||||
platform = process.platform,
|
||||
arch = process.arch,
|
||||
} = {}) {
|
||||
const name = windowsSandboxRunnerResourceBinaryName(platform);
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
const platformKey = appServerResourcePlatformKey(platform, arch);
|
||||
return path.resolve(outputRoot, "app-server", platformKey, name);
|
||||
}
|
||||
|
||||
export async function buildElectronAppServerReleaseManifest({
|
||||
binaryPath,
|
||||
codeModeHostBinaryPath,
|
||||
windowsSandboxSetupBinaryPath,
|
||||
windowsSandboxRunnerBinaryPath,
|
||||
version,
|
||||
platform = appServerResourcePlatformKey(),
|
||||
sha256File = hashFile,
|
||||
@@ -150,6 +171,14 @@ export async function buildElectronAppServerReleaseManifest({
|
||||
),
|
||||
),
|
||||
);
|
||||
artifact.windowsSandboxRunnerSha256 = await sha256File(
|
||||
path.resolve(
|
||||
requiredValue(
|
||||
windowsSandboxRunnerBinaryPath,
|
||||
"windowsSandboxRunnerBinaryPath",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -167,6 +196,7 @@ export async function prepareElectronAppServerAssets({
|
||||
sourceBinary,
|
||||
sourceCodeModeHostBinary,
|
||||
sourceWindowsSandboxSetupBinary,
|
||||
sourceWindowsSandboxRunnerBinary,
|
||||
resolveBinary = resolveDevAppServerBinary,
|
||||
env = process.env,
|
||||
readPackageJson = readJsonFile,
|
||||
@@ -200,6 +230,12 @@ export async function prepareElectronAppServerAssets({
|
||||
platform,
|
||||
arch,
|
||||
});
|
||||
const windowsSandboxRunnerDestination =
|
||||
electronWindowsSandboxRunnerBinaryDestination({
|
||||
outputRoot,
|
||||
platform,
|
||||
arch,
|
||||
});
|
||||
const manifestPath = electronAppServerManifestPath({ outputRoot });
|
||||
const resolvedSourceBinary = path.resolve(
|
||||
sourceBinary ??
|
||||
@@ -236,6 +272,16 @@ export async function prepareElectronAppServerAssets({
|
||||
),
|
||||
)
|
||||
: null;
|
||||
const resolvedSourceWindowsSandboxRunnerBinary =
|
||||
windowsSandboxRunnerDestination
|
||||
? path.resolve(
|
||||
sourceWindowsSandboxRunnerBinary ??
|
||||
path.join(
|
||||
path.dirname(resolvedSourceBinary),
|
||||
windowsSandboxRunnerResourceBinaryName(platform),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
if (
|
||||
windowsSandboxSetupDestination &&
|
||||
resolvedSourceWindowsSandboxSetupBinary === windowsSandboxSetupDestination
|
||||
@@ -244,6 +290,14 @@ export async function prepareElectronAppServerAssets({
|
||||
`Windows sandbox setup asset source must not equal packaged destination: ${windowsSandboxSetupDestination}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
windowsSandboxRunnerDestination &&
|
||||
resolvedSourceWindowsSandboxRunnerBinary === windowsSandboxRunnerDestination
|
||||
) {
|
||||
throw new Error(
|
||||
`Windows sandbox runner asset source must not equal packaged destination: ${windowsSandboxRunnerDestination}`,
|
||||
);
|
||||
}
|
||||
|
||||
await makeDir(path.dirname(destination), { recursive: true });
|
||||
await rm(destination, { force: true });
|
||||
@@ -251,6 +305,9 @@ export async function prepareElectronAppServerAssets({
|
||||
if (windowsSandboxSetupDestination) {
|
||||
await rm(windowsSandboxSetupDestination, { force: true });
|
||||
}
|
||||
if (windowsSandboxRunnerDestination) {
|
||||
await rm(windowsSandboxRunnerDestination, { force: true });
|
||||
}
|
||||
await copy(resolvedSourceBinary, destination);
|
||||
await copy(resolvedSourceCodeModeHostBinary, codeModeHostDestination);
|
||||
if (windowsSandboxSetupDestination) {
|
||||
@@ -259,6 +316,12 @@ export async function prepareElectronAppServerAssets({
|
||||
windowsSandboxSetupDestination,
|
||||
);
|
||||
}
|
||||
if (windowsSandboxRunnerDestination) {
|
||||
await copy(
|
||||
resolvedSourceWindowsSandboxRunnerBinary,
|
||||
windowsSandboxRunnerDestination,
|
||||
);
|
||||
}
|
||||
await clearLaunchBlockingXattrs(destination, platform);
|
||||
await clearLaunchBlockingXattrs(codeModeHostDestination, platform);
|
||||
const sourceStat = await getStat(resolvedSourceBinary);
|
||||
@@ -268,6 +331,9 @@ export async function prepareElectronAppServerAssets({
|
||||
const windowsSandboxSetupSourceStat = windowsSandboxSetupDestination
|
||||
? await getStat(resolvedSourceWindowsSandboxSetupBinary)
|
||||
: null;
|
||||
const windowsSandboxRunnerSourceStat = windowsSandboxRunnerDestination
|
||||
? await getStat(resolvedSourceWindowsSandboxRunnerBinary)
|
||||
: null;
|
||||
await changeMode(destination, sourceStat.mode);
|
||||
await changeMode(codeModeHostDestination, codeModeHostSourceStat.mode);
|
||||
if (windowsSandboxSetupDestination) {
|
||||
@@ -276,6 +342,12 @@ export async function prepareElectronAppServerAssets({
|
||||
windowsSandboxSetupSourceStat.mode,
|
||||
);
|
||||
}
|
||||
if (windowsSandboxRunnerDestination) {
|
||||
await changeMode(
|
||||
windowsSandboxRunnerDestination,
|
||||
windowsSandboxRunnerSourceStat.mode,
|
||||
);
|
||||
}
|
||||
prepareRuntimeBinary({ binaryPath: destination, platform });
|
||||
prepareRuntimeBinary({ binaryPath: codeModeHostDestination, platform });
|
||||
if (windowsSandboxSetupDestination) {
|
||||
@@ -284,6 +356,12 @@ export async function prepareElectronAppServerAssets({
|
||||
platform,
|
||||
});
|
||||
}
|
||||
if (windowsSandboxRunnerDestination) {
|
||||
prepareRuntimeBinary({
|
||||
binaryPath: windowsSandboxRunnerDestination,
|
||||
platform,
|
||||
});
|
||||
}
|
||||
const runtimeLibraries = await copyRuntimeLibraries({
|
||||
repoRoot,
|
||||
platform,
|
||||
@@ -298,6 +376,7 @@ export async function prepareElectronAppServerAssets({
|
||||
binaryPath: destination,
|
||||
codeModeHostBinaryPath: codeModeHostDestination,
|
||||
windowsSandboxSetupBinaryPath: windowsSandboxSetupDestination,
|
||||
windowsSandboxRunnerBinaryPath: windowsSandboxRunnerDestination,
|
||||
version,
|
||||
platform: appServerResourcePlatformKey(platform, arch),
|
||||
sha256File,
|
||||
@@ -308,9 +387,11 @@ export async function prepareElectronAppServerAssets({
|
||||
sourceBinary: resolvedSourceBinary,
|
||||
sourceCodeModeHostBinary: resolvedSourceCodeModeHostBinary,
|
||||
sourceWindowsSandboxSetupBinary: resolvedSourceWindowsSandboxSetupBinary,
|
||||
sourceWindowsSandboxRunnerBinary: resolvedSourceWindowsSandboxRunnerBinary,
|
||||
binaryPath: destination,
|
||||
codeModeHostBinaryPath: codeModeHostDestination,
|
||||
windowsSandboxSetupBinaryPath: windowsSandboxSetupDestination,
|
||||
windowsSandboxRunnerBinaryPath: windowsSandboxRunnerDestination,
|
||||
manifestPath,
|
||||
manifest,
|
||||
runtimeLibraries,
|
||||
|
||||
@@ -24,6 +24,10 @@ export function windowsSandboxSetupBinaryName(platform = process.platform) {
|
||||
return platform === "win32" ? "windows-sandbox-setup.exe" : null;
|
||||
}
|
||||
|
||||
export function windowsSandboxRunnerBinaryName(platform = process.platform) {
|
||||
return platform === "win32" ? "windows-sandbox-runner.exe" : null;
|
||||
}
|
||||
|
||||
export function localAppServerBinaryPath({
|
||||
repoRoot = process.cwd(),
|
||||
platform = process.platform,
|
||||
@@ -53,6 +57,15 @@ export function localWindowsSandboxSetupBinaryPath({
|
||||
return binaryName ? path.resolve(targetDirectory, "debug", binaryName) : null;
|
||||
}
|
||||
|
||||
export function localWindowsSandboxRunnerBinaryPath({
|
||||
repoRoot = process.cwd(),
|
||||
platform = process.platform,
|
||||
targetDirectory = resolveCargoTargetDirectory({ repoRoot, platform }),
|
||||
} = {}) {
|
||||
const binaryName = windowsSandboxRunnerBinaryName(platform);
|
||||
return binaryName ? path.resolve(targetDirectory, "debug", binaryName) : null;
|
||||
}
|
||||
|
||||
export function resolveCargoTargetDirectory({
|
||||
env = process.env,
|
||||
platform = process.platform,
|
||||
@@ -170,11 +183,18 @@ export function resolveDevAppServerBinary({
|
||||
platform,
|
||||
...(targetDirectory ? { targetDirectory } : {}),
|
||||
});
|
||||
const windowsSandboxRunnerPath = localWindowsSandboxRunnerBinaryPath({
|
||||
repoRoot,
|
||||
platform,
|
||||
...(targetDirectory ? { targetDirectory } : {}),
|
||||
});
|
||||
const hasUsableBinaries = () =>
|
||||
isUsableBinary(binaryPath, { exists }) &&
|
||||
isUsableBinary(codeModeHostPath, { exists }) &&
|
||||
(!windowsSandboxSetupPath ||
|
||||
isUsableBinary(windowsSandboxSetupPath, { exists }));
|
||||
isUsableBinary(windowsSandboxSetupPath, { exists })) &&
|
||||
(!windowsSandboxRunnerPath ||
|
||||
isUsableBinary(windowsSandboxRunnerPath, { exists }));
|
||||
if (forceBuild || !hasUsableBinaries()) {
|
||||
build({ repoRoot, platform, env });
|
||||
}
|
||||
@@ -185,6 +205,7 @@ export function resolveDevAppServerBinary({
|
||||
binaryPath,
|
||||
codeModeHostPath,
|
||||
windowsSandboxSetupPath,
|
||||
windowsSandboxRunnerPath,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(", ")}`,
|
||||
@@ -196,6 +217,9 @@ export function resolveDevAppServerBinary({
|
||||
if (windowsSandboxSetupPath) {
|
||||
prepareBinary({ binaryPath: windowsSandboxSetupPath, platform });
|
||||
}
|
||||
if (windowsSandboxRunnerPath) {
|
||||
prepareBinary({ binaryPath: windowsSandboxRunnerPath, platform });
|
||||
}
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
@@ -280,6 +304,8 @@ export function cargoBuildAppServerArgs({ repoRoot = process.cwd() } = {}) {
|
||||
"code-mode-host",
|
||||
"--bin",
|
||||
"windows-sandbox-setup",
|
||||
"--bin",
|
||||
"windows-sandbox-runner",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -325,6 +351,13 @@ export function buildLocalAppServer({
|
||||
if (windowsSetupBinaryPath) {
|
||||
prepareBinary({ binaryPath: windowsSetupBinaryPath, platform });
|
||||
}
|
||||
const windowsRunnerBinaryPath = localWindowsSandboxRunnerBinaryPath({
|
||||
repoRoot,
|
||||
platform,
|
||||
});
|
||||
if (windowsRunnerBinaryPath) {
|
||||
prepareBinary({ binaryPath: windowsRunnerBinaryPath, platform });
|
||||
}
|
||||
}
|
||||
|
||||
export function buildLocalAppServerAsync({
|
||||
@@ -367,6 +400,13 @@ export function buildLocalAppServerAsync({
|
||||
if (windowsSetupBinaryPath) {
|
||||
prepareBinary({ binaryPath: windowsSetupBinaryPath, platform });
|
||||
}
|
||||
const windowsRunnerBinaryPath = localWindowsSandboxRunnerBinaryPath({
|
||||
repoRoot,
|
||||
platform,
|
||||
});
|
||||
if (windowsRunnerBinaryPath) {
|
||||
prepareBinary({ binaryPath: windowsRunnerBinaryPath, platform });
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
localAppServerBinaryPath,
|
||||
localCodeModeHostBinaryPath,
|
||||
localWindowsSandboxSetupBinaryPath,
|
||||
localWindowsSandboxRunnerBinaryPath,
|
||||
resolveDevAppServerBackendEnv,
|
||||
resolveDevAppServerBinary,
|
||||
resolveCargoTargetDirectory,
|
||||
@@ -48,6 +49,17 @@ describe("electron dev sidecar", () => {
|
||||
).toBe(
|
||||
path.resolve("/repo/lime/lime-rs/target/debug/windows-sandbox-setup.exe"),
|
||||
);
|
||||
expect(
|
||||
localWindowsSandboxRunnerBinaryPath({
|
||||
repoRoot: "/repo/lime",
|
||||
platform: "win32",
|
||||
targetDirectory: path.resolve("/repo/lime/lime-rs/target"),
|
||||
}),
|
||||
).toBe(
|
||||
path.resolve(
|
||||
"/repo/lime/lime-rs/target/debug/windows-sandbox-runner.exe",
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("读取仓库 cargo target-dir 配置", () => {
|
||||
@@ -204,9 +216,17 @@ describe("electron dev sidecar", () => {
|
||||
expect(builds).toEqual([
|
||||
{ repoRoot: "/repo/lime", platform: "win32", env: {} },
|
||||
]);
|
||||
expect(prepared.at(-1)).toEqual([
|
||||
expect(prepared).toContainEqual([
|
||||
{ binaryPath: setupPath, platform: "win32" },
|
||||
]);
|
||||
expect(prepared).toContainEqual([
|
||||
{
|
||||
binaryPath: path.resolve(
|
||||
"/repo/lime/lime-rs/target/debug/windows-sandbox-runner.exe",
|
||||
),
|
||||
platform: "win32",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("空 app-server 文件不算可用二进制", () => {
|
||||
@@ -332,6 +352,8 @@ describe("electron dev sidecar", () => {
|
||||
"code-mode-host",
|
||||
"--bin",
|
||||
"windows-sandbox-setup",
|
||||
"--bin",
|
||||
"windows-sandbox-runner",
|
||||
],
|
||||
options: {
|
||||
cwd: "/repo/lime",
|
||||
|
||||
@@ -110,7 +110,7 @@ version = "1.13.0"
|
||||
);
|
||||
const sherpaOffset = windowsJob.indexOf("Prepare sherpa-onnx runtime");
|
||||
const sidecarBuildOffset = windowsJob.indexOf(
|
||||
"Build Windows app-server, code-mode-host, and sandbox setup sidecars",
|
||||
"Build Windows app-server, code-mode-host, and sandbox sidecars",
|
||||
);
|
||||
const timeoutContractOffset = windowsJob.indexOf(
|
||||
"Test Windows command timeout contract",
|
||||
|
||||
Reference in New Issue
Block a user