feat(sandbox): add Windows restricted runner

This commit is contained in:
coso
2026-08-26 16:59:26 +08:00
parent 22b4262aa8
commit 3ca895af09
24 changed files with 1395 additions and 456 deletions
+2 -2
View File
@@ -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
+29 -4
View File
@@ -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 的结构校验结果;文件缺失、未知字段、版本/账户漂移、空或异常大的密文均
+2
View File
@@ -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 {
+4
View File
@@ -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 = {
+2 -2
View File
@@ -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,
+41 -1
View File
@@ -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;
+23 -1
View File
@@ -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",
+1 -1
View File
@@ -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",