@prettier-ai/dsh-win32-process 0.1.2-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +85 -0
- package/README.zh.md +85 -0
- package/lib/index.js +547 -0
- package/lib/invariant.js +10 -0
- package/lib/types/abi.d.ts +38 -0
- package/lib/types/errors.d.ts +9 -0
- package/lib/types/ffi.d.ts +137 -0
- package/lib/types/index.d.ts +8 -0
- package/lib/types/invariant.d.ts +6 -0
- package/lib/types/process.d.ts +80 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/subprocess/win32-process/README.md
|
|
5
|
+
README.md: c2859996c5b5b3a82a5f78e12628613089eab3e6
|
|
6
|
+
README.zh.md: 738d27f2671647921c80007f2cf7a7d974f211ce
|
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Low-level Win32 process primitives for maintainers implementing or debugging the Windows ACL sandbox."
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-win32-process
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Low-level Win32 process library consumed by the Windows ACL sandbox. It owns the repository's one Koffi binding table for reusable restricted-process, stdio, and Job Object operations; it is not a Cordis service and does not choose sandbox policy or public child behavior. Read this page when maintaining the sandbox's native process path or checking its handle-lifetime limits.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Behavior](#behavior)
|
|
17
|
+
- [Header verification](#header-verification)
|
|
18
|
+
- [Model Experience](#model-experience)
|
|
19
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
20
|
+
- [Dev Note](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="behavior"></a>
|
|
25
|
+
## Behavior
|
|
26
|
+
|
|
27
|
+
- **One reusable ABI owner** — `abi.ts` owns the Win32 constants and x64 layout values consumed by the sandbox process paths. `ffi.ts` lazily loads `kernel32.dll` and `advapi32.dll`, verifies `STARTUPINFOW` and `PROCESS_INFORMATION`, exposes typed operations and error formatting, and lets sandbox policy bind its remaining APIs through the same loaded libraries.
|
|
28
|
+
- **Restricted-token creation** — `RestrictedProcessSpawnOptions` requires the sandbox's primary token and uses `CreateProcessAsUserW`. Piped and inherited-stdio paths share command-line quoting, cwd, the inherited environment block, checked return values, and handle cleanup.
|
|
29
|
+
- **Piped process primitive** — `spawnPipedProcess()` creates anonymous stdin/stdout/stderr pipes, closes stdin immediately, returns the two read ends, and leaves process waiting and pipe draining to the caller. Every partial failure closes the handles already owned by the operation, and every Koffi out-parameter or struct allocation is freed after its Win32 lifetime.
|
|
30
|
+
- **Inherited-stdio Job primitive** — `spawnInheritedJobProcess()` creates one kill-on-close Job, temporarily marks the current stdio handles inheritable, creates the restricted child suspended, assigns it to the Job, and then resumes its initial thread. Target code cannot run before Job assignment; controlled assignment or resume failures terminate the suspended child or close the assigned Job before releasing every owned handle.
|
|
31
|
+
- **Explicit settlement ownership** — `waitForProcessExit()` waits and closes the process handle. `drainPipe()` reuses one native count slot while draining, frees it, and closes the pipe read handle. The sandbox retains its existing scheduling, result composition, and caller-owned Job closure.
|
|
32
|
+
|
|
33
|
+
The Windows ACL sandbox adds SID, DACL, grant, workspace, and public child policy above these primitives.
|
|
34
|
+
|
|
35
|
+
<a id="header-verification"></a>
|
|
36
|
+
|
|
37
|
+
<a id="header-verification"></a>
|
|
38
|
+
## Header verification
|
|
39
|
+
|
|
40
|
+
The process, stdio, and Job constants plus selected structure sizes and offsets are checked against the MinGW Windows headers by [`verify/abi-probe.cpp`](verify/abi-probe.cpp):
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
g++ -std=c++20 -municode -O2 -o abi-probe.exe verify/abi-probe.cpp && ./abi-probe.exe
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The Koffi `STARTUPINFOW` and `PROCESS_INFORMATION` definitions also assert their 64-bit sizes at module load. The probe remains the evidence for the other recorded offsets and constants.
|
|
47
|
+
|
|
48
|
+
<a id="model-experience"></a>
|
|
49
|
+
## Model Experience
|
|
50
|
+
|
|
51
|
+
### Process primitives
|
|
52
|
+
|
|
53
|
+
#### What the model sees
|
|
54
|
+
|
|
55
|
+
Nothing directly. The package exposes `Win32ProcessBindings` and process primitives to the sandbox, which owns all model-visible tools, output, and diagnostics; this package contributes no prompt text or tool schema.
|
|
56
|
+
|
|
57
|
+
#### Token effect
|
|
58
|
+
|
|
59
|
+
None directly. Consumers decide whether process output enters a tool result or later model request.
|
|
60
|
+
|
|
61
|
+
#### KV Cache effect
|
|
62
|
+
|
|
63
|
+
The package contributes no stable request prefix, so it does not invalidate model KV caches.
|
|
64
|
+
|
|
65
|
+
## Known Limitations and Deferred Work
|
|
66
|
+
|
|
67
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
68
|
+
|
|
69
|
+
- **Windows-only native loading** — importing the generic types is portable, but resolving the binding table loads Windows DLLs and fails on other hosts. Cross-platform tests inject a binding table instead of loading native APIs.
|
|
70
|
+
- **No public process service** — the package intentionally does not wrap its primitives in Cordis or Node streams. A consumer must own its policy, async scheduling, output limits, cancellation, and final handle closure.
|
|
71
|
+
- **Inherited environment only** — process creation passes a null environment block. The sandbox establishes changes through `SetEnvironmentVariableW` first because passing an explicit block through Koffi makes `CreateProcessAsUserW` fail with `ERROR_INVALID_PARAMETER`. Other callers that need environment changes must establish them before invoking the primitive or use their own runner process.
|
|
72
|
+
- **Restricted-token consumer only** — ordinary `CreateProcessW`, exact `applicationName`, parent-stdio release, and whole-Job settlement are absent until an ordinary process consumer requires them.
|
|
73
|
+
- **Create-to-assignment interruption** — the target starts suspended and cannot execute before Job assignment, but an external termination of the runner in the narrow interval between process creation and assignment can leave the suspended target behind. The package does not claim atomic Job attachment.
|
|
74
|
+
- **Header evidence is architecture-specific** — the committed ABI probe and layout constants cover the repository's current 64-bit Windows targets. A new pointer width or incompatible Windows ABI requires updating the probe before support is claimed.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
<a id="dev-note"></a>
|
|
78
|
+
### Dev Note
|
|
79
|
+
|
|
80
|
+
<details>
|
|
81
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
82
|
+
|
|
83
|
+
None.
|
|
84
|
+
|
|
85
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "面向实现或排查 Windows ACL 沙箱的维护者,说明底层 Win32 进程原语。"
|
|
3
|
+
kind: "package-library"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-win32-process
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
供 Windows ACL 沙箱消费的底层 Win32 进程库。它唯一拥有仓库中可复用 restricted-process、stdio 与 Job Object 操作的 Koffi 绑定表;它不是 Cordis 服务,也不决定沙箱策略或公共 child 行为。维护沙箱原生进程路径或检查 handle 生命周期限制时,请阅读本页。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [Behavior](#behavior)
|
|
17
|
+
- [头部验证](#header-verification)
|
|
18
|
+
- [Model Experience](#model-experience)
|
|
19
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
20
|
+
- [开发备注](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="behavior"></a>
|
|
25
|
+
## Behavior
|
|
26
|
+
|
|
27
|
+
- **唯一可复用 ABI owner** — `abi.ts` 拥有 sandbox process 路径消费的 Win32 常量与 x64 布局值。`ffi.ts` 懒加载 `kernel32.dll` 与 `advapi32.dll`,核验 `STARTUPINFOW` 和 `PROCESS_INFORMATION`,提供带类型的操作与错误格式化,并让 sandbox policy 通过同一组已加载库绑定剩余 API。
|
|
28
|
+
- **restricted-token 创建** — `RestrictedProcessSpawnOptions` 要求 sandbox 的 primary token,并使用 `CreateProcessAsUserW`。pipe 与 inherited-stdio 路径共用命令行引用、cwd、继承环境块、返回值检查与句柄清理。
|
|
29
|
+
- **管道进程原语** — `spawnPipedProcess()` 创建匿名 stdin/stdout/stderr 管道,立即关闭 stdin,并返回两个读取端;调用方负责等待进程与排空管道。任一局部失败都会关闭该操作已经拥有的句柄,并在各自 Win32 生命周期结束后释放每个 Koffi 输出槽与结构体分配。
|
|
30
|
+
- **继承 stdio 的 Job 原语** — `spawnInheritedJobProcess()` 创建一个 kill-on-close Job,临时把当前 stdio 句柄设为可继承,以 suspended 状态创建 restricted child,把它分配给 Job,再恢复初始线程。目标代码不会在 Job 分配前运行;受控的分配或恢复失败会终止 suspended child,或在释放全部已拥有句柄前关闭已分配的 Job。
|
|
31
|
+
- **显式结算归属** — `waitForProcessExit()` 等待并关闭进程句柄。`drainPipe()` 在排空期间复用一个 native count slot,释放该分配并关闭管道读取句柄。sandbox 保留既有调度、result 组合与调用方拥有的 Job 关闭行为。
|
|
32
|
+
|
|
33
|
+
Windows ACL 沙箱在这些原语上增加 SID、DACL、grant、workspace 与公共 child policy。
|
|
34
|
+
|
|
35
|
+
<a id="header-verification"></a>
|
|
36
|
+
|
|
37
|
+
<a id="header-verification"></a>
|
|
38
|
+
## 头部验证
|
|
39
|
+
|
|
40
|
+
process、stdio 与 Job 的常量以及选定结构体的大小和偏移由 [`verify/abi-probe.cpp`](verify/abi-probe.cpp) 对照 MinGW Windows 头文件检查:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
g++ -std=c++20 -municode -O2 -o abi-probe.exe verify/abi-probe.cpp && ./abi-probe.exe
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Koffi 的 `STARTUPINFOW` 与 `PROCESS_INFORMATION` 定义还会在模块加载时断言各自的 64 位大小;其余已记录偏移和常量由该探针提供证据。
|
|
47
|
+
|
|
48
|
+
<a id="model-experience"></a>
|
|
49
|
+
## Model Experience
|
|
50
|
+
|
|
51
|
+
### 进程原语
|
|
52
|
+
|
|
53
|
+
#### 模型看到什么
|
|
54
|
+
|
|
55
|
+
没有直接内容。本包向 sandbox 提供 `Win32ProcessBindings` 与进程原语;sandbox 拥有全部模型可见工具、输出与诊断,本包不贡献提示词或工具 schema。
|
|
56
|
+
|
|
57
|
+
#### Token 影响
|
|
58
|
+
|
|
59
|
+
没有直接影响。消费方决定进程输出是否进入工具结果或后续模型请求。
|
|
60
|
+
|
|
61
|
+
#### KV Cache 影响
|
|
62
|
+
|
|
63
|
+
本包不贡献稳定请求前缀,因此不会使模型 KV Cache 失效。
|
|
64
|
+
|
|
65
|
+
## Known Limitations and Deferred Work
|
|
66
|
+
|
|
67
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
68
|
+
|
|
69
|
+
- **仅在 Windows 原生加载** — 导入通用类型可跨平台进行,但解析绑定表会加载 Windows DLL,并在其他宿主失败。跨平台测试注入绑定表,不加载原生 API。
|
|
70
|
+
- **没有公共进程服务** — 本包刻意不把原语包装成 Cordis 或 Node streams。消费方必须拥有自己的策略、异步调度、输出上限、取消与最终句柄关闭。
|
|
71
|
+
- **只继承环境** — 进程创建传入空环境块。sandbox 会先通过 `SetEnvironmentVariableW` 建立改动,因为经 Koffi 传入显式环境块会使 `CreateProcessAsUserW` 以 `ERROR_INVALID_PARAMETER` 失败。其他需要改写环境的调用方必须在调用原语前建立环境,或使用自己的 runner 进程。
|
|
72
|
+
- **只有 restricted-token 消费方** — ordinary `CreateProcessW`、精确 `applicationName`、parent-stdio release 与 whole-Job settlement 在 ordinary process 消费方出现前均不提供。
|
|
73
|
+
- **创建到分配之间的中断** — 目标以 suspended 状态启动,不能在 Job 分配前执行,但 runner 若在进程创建到分配之间的极窄区间被外力终止,可能留下 suspended target。本包不声明原子 Job 附加保证。
|
|
74
|
+
- **header 证据限定架构** — 已提交的 ABI probe 与布局常量覆盖仓库当前 64 位 Windows 目标。支持新的指针宽度或不兼容 Windows ABI 前,必须先更新 probe。
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
<a id="dev-note"></a>
|
|
78
|
+
### 开发备注
|
|
79
|
+
|
|
80
|
+
<details>
|
|
81
|
+
<summary>维护者工作上下文——点击展开</summary>
|
|
82
|
+
|
|
83
|
+
无。
|
|
84
|
+
|
|
85
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import koffi from "koffi";
|
|
2
|
+
/** Win32 code reporting a caller-provided buffer is too small. */
|
|
3
|
+
const ERROR_INSUFFICIENT_BUFFER = 122;
|
|
4
|
+
/** Job limit that terminates every member when the final Job handle closes. */
|
|
5
|
+
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 8192;
|
|
6
|
+
//#endregion
|
|
7
|
+
//#region lib/types/errors.js
|
|
8
|
+
/** Win32 call failure with the exact API name and error code. */
|
|
9
|
+
var Win32Error = class extends Error {
|
|
10
|
+
/** Win32 function whose checked result failed. */
|
|
11
|
+
api;
|
|
12
|
+
/** Exact GetLastError value or direct Win32 API error code. */
|
|
13
|
+
win32Code;
|
|
14
|
+
constructor(api, win32Code, detail) {
|
|
15
|
+
super(`${api} failed (Win32 ${win32Code})${detail === void 0 ? "" : `: ${detail}`}`);
|
|
16
|
+
this.name = "Win32Error";
|
|
17
|
+
this.api = api;
|
|
18
|
+
this.win32Code = win32Code;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region lib/types/ffi.js
|
|
23
|
+
/** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */
|
|
24
|
+
const PVOID = koffi.pointer("void");
|
|
25
|
+
const PPVOID = koffi.pointer(PVOID);
|
|
26
|
+
/**
|
|
27
|
+
* Return whether a Koffi pointer represents NULL.
|
|
28
|
+
* @param value - pointer value returned by Koffi or a Win32 call.
|
|
29
|
+
* @returns true for null, undefined, or address zero.
|
|
30
|
+
*/
|
|
31
|
+
function isNullPtr(value) {
|
|
32
|
+
return value === null || value === void 0 || value === 0n;
|
|
33
|
+
}
|
|
34
|
+
/** Koffi STARTUPINFOW layout. */
|
|
35
|
+
const STARTUPINFOW = koffi.struct("DSH_STARTUPINFOW", {
|
|
36
|
+
cb: "uint32",
|
|
37
|
+
lpReserved: "str16",
|
|
38
|
+
lpDesktop: "str16",
|
|
39
|
+
lpTitle: "str16",
|
|
40
|
+
dwX: "uint32",
|
|
41
|
+
dwY: "uint32",
|
|
42
|
+
dwXSize: "uint32",
|
|
43
|
+
dwYSize: "uint32",
|
|
44
|
+
dwXCountChars: "uint32",
|
|
45
|
+
dwYCountChars: "uint32",
|
|
46
|
+
dwFillAttribute: "uint32",
|
|
47
|
+
dwFlags: "uint32",
|
|
48
|
+
wShowWindow: "uint16",
|
|
49
|
+
cbReserved2: "uint16",
|
|
50
|
+
lpReserved2: koffi.pointer("uint8"),
|
|
51
|
+
hStdInput: PVOID,
|
|
52
|
+
hStdOutput: PVOID,
|
|
53
|
+
hStdError: PVOID
|
|
54
|
+
});
|
|
55
|
+
/** Koffi PROCESS_INFORMATION layout. */
|
|
56
|
+
const PROCESS_INFORMATION = koffi.struct("DSH_PROCESS_INFORMATION", {
|
|
57
|
+
hProcess: PVOID,
|
|
58
|
+
hThread: PVOID,
|
|
59
|
+
dwProcessId: "uint32",
|
|
60
|
+
dwThreadId: "uint32"
|
|
61
|
+
});
|
|
62
|
+
/* v8 ignore start -- ABI guards are pinned by native header probes. */
|
|
63
|
+
if (STARTUPINFOW.size !== 104) throw new Error(`STARTUPINFOW layout mismatch: koffi computed ${STARTUPINFOW.size}, expected 104`);
|
|
64
|
+
if (PROCESS_INFORMATION.size !== 24) throw new Error(`PROCESS_INFORMATION layout mismatch: koffi computed ${PROCESS_INFORMATION.size}, expected 24`);
|
|
65
|
+
/* v8 ignore stop */
|
|
66
|
+
/**
|
|
67
|
+
* Allocate a pointer-sized out-parameter slot.
|
|
68
|
+
* @returns allocated native slot.
|
|
69
|
+
*/
|
|
70
|
+
function allocPtrSlot() {
|
|
71
|
+
return koffi.alloc(PVOID, 1);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Allocate a uint32 out-parameter slot.
|
|
75
|
+
* @returns allocated native slot.
|
|
76
|
+
*/
|
|
77
|
+
function allocUint32() {
|
|
78
|
+
return koffi.alloc("uint32", 1);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Decode a pointer out-parameter.
|
|
82
|
+
* @param slot - pointer-sized slot filled by Win32.
|
|
83
|
+
* @returns decoded pointer, or null for address zero.
|
|
84
|
+
*/
|
|
85
|
+
function decodePtr(slot) {
|
|
86
|
+
const value = koffi.decode(slot, PVOID);
|
|
87
|
+
return isNullPtr(value) ? null : value;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Decode a uint32 out-parameter.
|
|
91
|
+
* @param slot - uint32 slot filled by Win32.
|
|
92
|
+
* @returns decoded unsigned value.
|
|
93
|
+
*/
|
|
94
|
+
function decodeUint32(slot) {
|
|
95
|
+
return koffi.decode(slot, "uint32");
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Allocate a zeroed STARTUPINFOW.
|
|
99
|
+
* @returns allocated struct pointer.
|
|
100
|
+
*/
|
|
101
|
+
function allocStartupInfo() {
|
|
102
|
+
return koffi.alloc(STARTUPINFOW, 1);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Encode the stdio-bearing STARTUPINFOW fields.
|
|
106
|
+
* @param startupInfo - allocated STARTUPINFOW pointer.
|
|
107
|
+
* @param fields - fields required for inherited stdio.
|
|
108
|
+
*/
|
|
109
|
+
function encodeStartupInfo(startupInfo, fields) {
|
|
110
|
+
koffi.encode(startupInfo, STARTUPINFOW, fields);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Allocate a zeroed PROCESS_INFORMATION.
|
|
114
|
+
* @returns allocated struct pointer.
|
|
115
|
+
*/
|
|
116
|
+
function allocProcessInfo() {
|
|
117
|
+
return koffi.alloc(PROCESS_INFORMATION, 1);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Decode PROCESS_INFORMATION.
|
|
121
|
+
* @param processInfo - struct pointer filled by CreateProcess.
|
|
122
|
+
* @returns process/thread handles and ids.
|
|
123
|
+
*/
|
|
124
|
+
function decodeProcessInfo(processInfo) {
|
|
125
|
+
return koffi.decode(processInfo, PROCESS_INFORMATION);
|
|
126
|
+
}
|
|
127
|
+
let cachedContext;
|
|
128
|
+
let cached;
|
|
129
|
+
/* v8 ignore start -- exercised by native Windows ABI and sandbox jobs. */
|
|
130
|
+
function bindingContext() {
|
|
131
|
+
if (cachedContext !== void 0) return cachedContext;
|
|
132
|
+
const kernel32 = koffi.load("kernel32.dll");
|
|
133
|
+
const advapi32 = koffi.load("advapi32.dll");
|
|
134
|
+
const bind = (lib, name, result, args) => lib.func("__stdcall", name, result, args);
|
|
135
|
+
cachedContext = {
|
|
136
|
+
kernel32,
|
|
137
|
+
advapi32,
|
|
138
|
+
bind
|
|
139
|
+
};
|
|
140
|
+
return cachedContext;
|
|
141
|
+
}
|
|
142
|
+
function bindings() {
|
|
143
|
+
if (cached !== void 0) return cached;
|
|
144
|
+
const { kernel32, advapi32, bind } = bindingContext();
|
|
145
|
+
cached = {
|
|
146
|
+
closeHandle: bind(kernel32, "CloseHandle", "int", [PVOID]),
|
|
147
|
+
getLastError: bind(kernel32, "GetLastError", "uint32", []),
|
|
148
|
+
formatMessageW: bind(kernel32, "FormatMessageW", "uint32", [
|
|
149
|
+
"uint32",
|
|
150
|
+
PVOID,
|
|
151
|
+
"uint32",
|
|
152
|
+
"uint32",
|
|
153
|
+
PVOID,
|
|
154
|
+
"uint32",
|
|
155
|
+
PVOID
|
|
156
|
+
]),
|
|
157
|
+
createPipe: bind(kernel32, "CreatePipe", "int", [
|
|
158
|
+
PPVOID,
|
|
159
|
+
PPVOID,
|
|
160
|
+
PVOID,
|
|
161
|
+
"uint32"
|
|
162
|
+
]),
|
|
163
|
+
setHandleInformation: bind(kernel32, "SetHandleInformation", "int", [
|
|
164
|
+
PVOID,
|
|
165
|
+
"uint32",
|
|
166
|
+
"uint32"
|
|
167
|
+
]),
|
|
168
|
+
createProcessAsUserW: bind(advapi32, "CreateProcessAsUserW", "int", [
|
|
169
|
+
PVOID,
|
|
170
|
+
"str16",
|
|
171
|
+
"str16",
|
|
172
|
+
PVOID,
|
|
173
|
+
PVOID,
|
|
174
|
+
"int",
|
|
175
|
+
"uint32",
|
|
176
|
+
PVOID,
|
|
177
|
+
"str16",
|
|
178
|
+
koffi.pointer(STARTUPINFOW),
|
|
179
|
+
koffi.pointer(PROCESS_INFORMATION)
|
|
180
|
+
]),
|
|
181
|
+
readFile: bind(kernel32, "ReadFile", "int", [
|
|
182
|
+
PVOID,
|
|
183
|
+
PVOID,
|
|
184
|
+
"uint32",
|
|
185
|
+
koffi.pointer("uint32"),
|
|
186
|
+
PVOID
|
|
187
|
+
]),
|
|
188
|
+
peekNamedPipe: bind(kernel32, "PeekNamedPipe", "int", [
|
|
189
|
+
PVOID,
|
|
190
|
+
PVOID,
|
|
191
|
+
"uint32",
|
|
192
|
+
koffi.pointer("uint32"),
|
|
193
|
+
koffi.pointer("uint32"),
|
|
194
|
+
koffi.pointer("uint32")
|
|
195
|
+
]),
|
|
196
|
+
waitForSingleObject: bind(kernel32, "WaitForSingleObject", "uint32", [PVOID, "uint32"]),
|
|
197
|
+
getExitCodeProcess: bind(kernel32, "GetExitCodeProcess", "int", [PVOID, koffi.pointer("uint32")]),
|
|
198
|
+
createJobObjectW: bind(kernel32, "CreateJobObjectW", PVOID, [PVOID, "str16"]),
|
|
199
|
+
setInformationJobObject: bind(kernel32, "SetInformationJobObject", "int", [
|
|
200
|
+
PVOID,
|
|
201
|
+
"int",
|
|
202
|
+
PVOID,
|
|
203
|
+
"uint32"
|
|
204
|
+
]),
|
|
205
|
+
assignProcessToJobObject: bind(kernel32, "AssignProcessToJobObject", "int", [PVOID, PVOID]),
|
|
206
|
+
resumeThread: bind(kernel32, "ResumeThread", "uint32", [PVOID]),
|
|
207
|
+
terminateProcess: bind(kernel32, "TerminateProcess", "int", [PVOID, "uint32"]),
|
|
208
|
+
getStdHandle: bind(kernel32, "GetStdHandle", PVOID, ["int"])
|
|
209
|
+
};
|
|
210
|
+
return cached;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Extend the shared process table with caller-owned Win32 API families.
|
|
214
|
+
* @param create - binds only the caller-specific operations from the shared libraries.
|
|
215
|
+
* @returns generic process bindings combined with the caller-specific operations.
|
|
216
|
+
*/
|
|
217
|
+
function extendWin32ProcessBindings(create) {
|
|
218
|
+
return {
|
|
219
|
+
...bindings(),
|
|
220
|
+
...create(bindingContext())
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/* v8 ignore stop */
|
|
224
|
+
/**
|
|
225
|
+
* Format a Win32 error code through FormatMessageW.
|
|
226
|
+
* @param api - active binding table.
|
|
227
|
+
* @param win32Code - captured GetLastError value.
|
|
228
|
+
* @returns trimmed system message, or an empty string when unavailable.
|
|
229
|
+
*/
|
|
230
|
+
function errorText(api, win32Code) {
|
|
231
|
+
const buffer = Buffer.alloc(1024);
|
|
232
|
+
const length = api.formatMessageW(4608, null, win32Code, 0, buffer, buffer.length / 2, null);
|
|
233
|
+
return length === 0 ? "" : buffer.subarray(0, length * 2).toString("utf16le").trim();
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Throw the current GetLastError value.
|
|
237
|
+
* @param api - active binding table.
|
|
238
|
+
* @param name - failing Win32 operation.
|
|
239
|
+
* @param detail - optional operation context.
|
|
240
|
+
* @returns never; always throws Win32Error.
|
|
241
|
+
*/
|
|
242
|
+
function throwLastError(api, name, detail) {
|
|
243
|
+
const win32Code = api.getLastError();
|
|
244
|
+
throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code));
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Throw an explicitly captured Win32 error code.
|
|
248
|
+
* @param api - active binding table.
|
|
249
|
+
* @param name - failing Win32 operation.
|
|
250
|
+
* @param win32Code - error captured before cleanup.
|
|
251
|
+
* @param detail - optional operation context.
|
|
252
|
+
* @returns never; always throws Win32Error.
|
|
253
|
+
*/
|
|
254
|
+
function throwWin32(api, name, win32Code, detail) {
|
|
255
|
+
throw new Win32Error(name, win32Code, detail ?? errorText(api, win32Code));
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region lib/types/process.js
|
|
259
|
+
/** Typed Win32 process operations over the shared binding table. */
|
|
260
|
+
/**
|
|
261
|
+
* Quote one argument according to CommandLineToArgvW parsing.
|
|
262
|
+
* @param argument - one argv entry.
|
|
263
|
+
* @returns bare or quoted command-line segment.
|
|
264
|
+
*/
|
|
265
|
+
function quoteArg(argument) {
|
|
266
|
+
if (argument === "") return "\"\"";
|
|
267
|
+
if (!/[\s"]/u.test(argument)) return argument;
|
|
268
|
+
let quoted = "\"";
|
|
269
|
+
for (let index = 0; index < argument.length; index++) {
|
|
270
|
+
let backslashes = 0;
|
|
271
|
+
while (index < argument.length && argument.charAt(index) === "\\") {
|
|
272
|
+
backslashes += 1;
|
|
273
|
+
index += 1;
|
|
274
|
+
}
|
|
275
|
+
if (index === argument.length) quoted += "\\".repeat(backslashes * 2);
|
|
276
|
+
else if (argument.charAt(index) === "\"") quoted += "\\".repeat(backslashes * 2 + 1) + "\"";
|
|
277
|
+
else quoted += "\\".repeat(backslashes) + argument.charAt(index);
|
|
278
|
+
}
|
|
279
|
+
return quoted + "\"";
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Build the mutable command line accepted by CreateProcessAsUserW.
|
|
283
|
+
* @param program - executable argv entry.
|
|
284
|
+
* @param args - remaining argv entries.
|
|
285
|
+
* @returns joined Win32 command line.
|
|
286
|
+
*/
|
|
287
|
+
function buildCommandLine(program, args) {
|
|
288
|
+
return [program, ...args].map(quoteArg).join(" ");
|
|
289
|
+
}
|
|
290
|
+
function freeNative(pointer) {
|
|
291
|
+
if (pointer !== void 0) koffi.free(pointer);
|
|
292
|
+
}
|
|
293
|
+
function closeBestEffort(api, handle) {
|
|
294
|
+
if (!isNullPtr(handle)) api.closeHandle(handle);
|
|
295
|
+
}
|
|
296
|
+
function createPipe(api, owned) {
|
|
297
|
+
const readSlot = allocPtrSlot();
|
|
298
|
+
let writeSlot;
|
|
299
|
+
try {
|
|
300
|
+
writeSlot = allocPtrSlot();
|
|
301
|
+
if (api.createPipe(readSlot, writeSlot, null, 0) === 0) throwLastError(api, "CreatePipe");
|
|
302
|
+
const read = decodePtr(readSlot);
|
|
303
|
+
const write = decodePtr(writeSlot);
|
|
304
|
+
if (read === null || write === null) {
|
|
305
|
+
closeBestEffort(api, read);
|
|
306
|
+
closeBestEffort(api, write);
|
|
307
|
+
throwLastError(api, "CreatePipe", "null pipe handle");
|
|
308
|
+
}
|
|
309
|
+
owned.add(read);
|
|
310
|
+
owned.add(write);
|
|
311
|
+
return {
|
|
312
|
+
read,
|
|
313
|
+
write
|
|
314
|
+
};
|
|
315
|
+
} finally {
|
|
316
|
+
freeNative(writeSlot);
|
|
317
|
+
koffi.free(readSlot);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function closeOwned(api, owned, handle) {
|
|
321
|
+
/* v8 ignore next -- each successfully decoded pipe end is uniquely owned. */
|
|
322
|
+
if (!owned.delete(handle)) return;
|
|
323
|
+
api.closeHandle(handle);
|
|
324
|
+
}
|
|
325
|
+
function closeAllOwned(api, owned) {
|
|
326
|
+
for (const handle of owned) api.closeHandle(handle);
|
|
327
|
+
owned.clear();
|
|
328
|
+
}
|
|
329
|
+
function createRestrictedProcess(api, options, commandLine, creationFlags, startupInfo, processInfo) {
|
|
330
|
+
return api.createProcessAsUserW(options.token, null, commandLine, null, null, 1, creationFlags, null, options.cwd, startupInfo, processInfo);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Spawn a process with anonymous-pipe stdout/stderr and immediate stdin EOF.
|
|
334
|
+
* @param api - active binding table.
|
|
335
|
+
* @param options - command, cwd, args, and restricted primary token.
|
|
336
|
+
* @returns caller-owned process and pipe read handles.
|
|
337
|
+
*/
|
|
338
|
+
function spawnPipedProcess(api, options) {
|
|
339
|
+
const owned = /* @__PURE__ */ new Set();
|
|
340
|
+
let startupInfo;
|
|
341
|
+
let processInfo;
|
|
342
|
+
try {
|
|
343
|
+
const stdIn = createPipe(api, owned);
|
|
344
|
+
const stdOut = createPipe(api, owned);
|
|
345
|
+
const stdErr = createPipe(api, owned);
|
|
346
|
+
for (const [handle, label] of [
|
|
347
|
+
[stdIn.read, "stdin read end"],
|
|
348
|
+
[stdOut.write, "stdout write end"],
|
|
349
|
+
[stdErr.write, "stderr write end"]
|
|
350
|
+
]) if (api.setHandleInformation(handle, 1, 1) === 0) throwLastError(api, "SetHandleInformation", label);
|
|
351
|
+
startupInfo = allocStartupInfo();
|
|
352
|
+
encodeStartupInfo(startupInfo, {
|
|
353
|
+
cb: 104,
|
|
354
|
+
dwFlags: 256,
|
|
355
|
+
hStdInput: stdIn.read,
|
|
356
|
+
hStdOutput: stdOut.write,
|
|
357
|
+
hStdError: stdErr.write
|
|
358
|
+
});
|
|
359
|
+
processInfo = allocProcessInfo();
|
|
360
|
+
if (createRestrictedProcess(api, options, buildCommandLine(options.command, options.args), 0, startupInfo, processInfo) === 0) throwWin32(api, "CreateProcessAsUserW", api.getLastError(), `command: ${options.command}, cwd: ${options.cwd}`);
|
|
361
|
+
const info = decodeProcessInfo(processInfo);
|
|
362
|
+
if (info.hProcess === null || info.hThread === null) {
|
|
363
|
+
if (info.hProcess !== null) api.terminateProcess(info.hProcess, 1);
|
|
364
|
+
closeBestEffort(api, info.hThread);
|
|
365
|
+
closeBestEffort(api, info.hProcess);
|
|
366
|
+
throw new Error(`CreateProcessAsUserW succeeded but returned null process/thread handles (pid ${info.dwProcessId})`);
|
|
367
|
+
}
|
|
368
|
+
closeOwned(api, owned, stdIn.read);
|
|
369
|
+
closeOwned(api, owned, stdIn.write);
|
|
370
|
+
closeOwned(api, owned, stdOut.write);
|
|
371
|
+
closeOwned(api, owned, stdErr.write);
|
|
372
|
+
closeBestEffort(api, info.hThread);
|
|
373
|
+
owned.delete(stdOut.read);
|
|
374
|
+
owned.delete(stdErr.read);
|
|
375
|
+
return {
|
|
376
|
+
pid: info.dwProcessId,
|
|
377
|
+
process: info.hProcess,
|
|
378
|
+
stdoutRead: stdOut.read,
|
|
379
|
+
stderrRead: stdErr.read
|
|
380
|
+
};
|
|
381
|
+
} catch (error) {
|
|
382
|
+
closeAllOwned(api, owned);
|
|
383
|
+
throw error;
|
|
384
|
+
} finally {
|
|
385
|
+
freeNative(processInfo);
|
|
386
|
+
freeNative(startupInfo);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Drain one anonymous pipe until the writer closes it.
|
|
391
|
+
* @param api - active binding table.
|
|
392
|
+
* @param handle - caller-owned pipe read end.
|
|
393
|
+
* @returns complete bytes read before EOF; the handle is always closed.
|
|
394
|
+
* @throws when a Win32 pipe operation fails.
|
|
395
|
+
*/
|
|
396
|
+
async function drainPipe(api, handle) {
|
|
397
|
+
const chunks = [];
|
|
398
|
+
let countSlot;
|
|
399
|
+
try {
|
|
400
|
+
countSlot = allocUint32();
|
|
401
|
+
for (;;) {
|
|
402
|
+
if (api.peekNamedPipe(handle, null, 0, null, countSlot, null) === 0) {
|
|
403
|
+
const win32Code = api.getLastError();
|
|
404
|
+
if (win32Code === 109 || win32Code === 232) break;
|
|
405
|
+
throwLastError(api, "PeekNamedPipe", `drain failure after ${chunks.length} chunk(s)`);
|
|
406
|
+
}
|
|
407
|
+
const available = decodeUint32(countSlot);
|
|
408
|
+
if (available > 0) {
|
|
409
|
+
const chunk = Buffer.alloc(available);
|
|
410
|
+
if (api.readFile(handle, chunk, chunk.length, countSlot, null) === 0) throwLastError(api, "ReadFile", `drain failure after ${chunks.length} chunk(s)`);
|
|
411
|
+
chunks.push(chunk.subarray(0, decodeUint32(countSlot)));
|
|
412
|
+
}
|
|
413
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
414
|
+
}
|
|
415
|
+
return Buffer.concat(chunks);
|
|
416
|
+
} finally {
|
|
417
|
+
freeNative(countSlot);
|
|
418
|
+
api.closeHandle(handle);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Wait for a process and always close its handle.
|
|
423
|
+
* @param api - active binding table.
|
|
424
|
+
* @param process - caller-owned process handle.
|
|
425
|
+
* @returns direct process exit code.
|
|
426
|
+
*/
|
|
427
|
+
function waitForProcessExit(api, process) {
|
|
428
|
+
let exitCodeSlot;
|
|
429
|
+
try {
|
|
430
|
+
if (api.waitForSingleObject(process, 4294967295) === 4294967295) throwLastError(api, "WaitForSingleObject");
|
|
431
|
+
exitCodeSlot = allocUint32();
|
|
432
|
+
if (api.getExitCodeProcess(process, exitCodeSlot) === 0) throwLastError(api, "GetExitCodeProcess");
|
|
433
|
+
return decodeUint32(exitCodeSlot);
|
|
434
|
+
} finally {
|
|
435
|
+
freeNative(exitCodeSlot);
|
|
436
|
+
api.closeHandle(process);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function createKillOnCloseJob(api) {
|
|
440
|
+
const job = api.createJobObjectW(null, null);
|
|
441
|
+
if (isNullPtr(job)) throwLastError(api, "CreateJobObjectW");
|
|
442
|
+
const information = Buffer.alloc(144);
|
|
443
|
+
information.writeUInt32LE(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, 16);
|
|
444
|
+
if (api.setInformationJobObject(job, 9, information, information.length) === 0) {
|
|
445
|
+
const win32Code = api.getLastError();
|
|
446
|
+
api.closeHandle(job);
|
|
447
|
+
throwWin32(api, "SetInformationJobObject", win32Code);
|
|
448
|
+
}
|
|
449
|
+
return job;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Spawn suspended, assign the child to a kill-on-close Job, then resume it.
|
|
453
|
+
* @param api - active binding table.
|
|
454
|
+
* @param options - command, cwd, args, and restricted primary token.
|
|
455
|
+
* @returns caller-owned process and Job handles after successful resume.
|
|
456
|
+
* @remarks Node clears stdio handle inheritability at startup through
|
|
457
|
+
* uv_disable_stdio_inheritance. This operation temporarily restores the bits
|
|
458
|
+
* required by STARTF_USESTDHANDLES. Restoring them afterward is best-effort:
|
|
459
|
+
* failure must not replace the already-created child's outcome.
|
|
460
|
+
*/
|
|
461
|
+
function spawnInheritedJobProcess(api, options) {
|
|
462
|
+
const job = createKillOnCloseJob(api);
|
|
463
|
+
const getStdHandle = (selector, label) => {
|
|
464
|
+
const handle = api.getStdHandle(selector);
|
|
465
|
+
if (!isNullPtr(handle)) return handle;
|
|
466
|
+
const win32Code = api.getLastError();
|
|
467
|
+
api.closeHandle(job);
|
|
468
|
+
throwWin32(api, "GetStdHandle", win32Code, `null ${label} handle`);
|
|
469
|
+
};
|
|
470
|
+
const stdIn = getStdHandle(-10, "stdin");
|
|
471
|
+
const stdOut = getStdHandle(-11, "stdout");
|
|
472
|
+
const stdErr = getStdHandle(-12, "stderr");
|
|
473
|
+
const enabled = [];
|
|
474
|
+
let startupInfo;
|
|
475
|
+
let processInfo;
|
|
476
|
+
let created = 0;
|
|
477
|
+
let createFailureCode = 0;
|
|
478
|
+
try {
|
|
479
|
+
for (const [handle, label] of [
|
|
480
|
+
[stdIn, "stdin"],
|
|
481
|
+
[stdOut, "stdout"],
|
|
482
|
+
[stdErr, "stderr"]
|
|
483
|
+
]) {
|
|
484
|
+
if (api.setHandleInformation(handle, 1, 1) === 0) throwLastError(api, "SetHandleInformation", `${label} (enable inherit)`);
|
|
485
|
+
enabled.push(handle);
|
|
486
|
+
}
|
|
487
|
+
startupInfo = allocStartupInfo();
|
|
488
|
+
encodeStartupInfo(startupInfo, {
|
|
489
|
+
cb: 104,
|
|
490
|
+
dwFlags: 256,
|
|
491
|
+
hStdInput: stdIn,
|
|
492
|
+
hStdOutput: stdOut,
|
|
493
|
+
hStdError: stdErr
|
|
494
|
+
});
|
|
495
|
+
processInfo = allocProcessInfo();
|
|
496
|
+
created = createRestrictedProcess(api, options, buildCommandLine(options.command, options.args), 4, startupInfo, processInfo);
|
|
497
|
+
if (created === 0) createFailureCode = api.getLastError();
|
|
498
|
+
} catch (error) {
|
|
499
|
+
freeNative(processInfo);
|
|
500
|
+
api.closeHandle(job);
|
|
501
|
+
throw error;
|
|
502
|
+
} finally {
|
|
503
|
+
freeNative(startupInfo);
|
|
504
|
+
for (const handle of enabled) api.setHandleInformation(handle, 1, 0);
|
|
505
|
+
}
|
|
506
|
+
if (created === 0) {
|
|
507
|
+
freeNative(processInfo);
|
|
508
|
+
api.closeHandle(job);
|
|
509
|
+
throwWin32(api, "CreateProcessAsUserW", createFailureCode, `command: ${options.command}, cwd: ${options.cwd}`);
|
|
510
|
+
}
|
|
511
|
+
let info;
|
|
512
|
+
try {
|
|
513
|
+
info = decodeProcessInfo(processInfo);
|
|
514
|
+
} finally {
|
|
515
|
+
freeNative(processInfo);
|
|
516
|
+
}
|
|
517
|
+
if (info.hProcess === null || info.hThread === null) {
|
|
518
|
+
if (info.hProcess !== null) api.terminateProcess(info.hProcess, 1);
|
|
519
|
+
api.closeHandle(job);
|
|
520
|
+
closeBestEffort(api, info.hThread);
|
|
521
|
+
closeBestEffort(api, info.hProcess);
|
|
522
|
+
throw new Error(`CreateProcessAsUserW succeeded but returned null process/thread handles (pid ${info.dwProcessId})`);
|
|
523
|
+
}
|
|
524
|
+
if (api.assignProcessToJobObject(job, info.hProcess) === 0) {
|
|
525
|
+
const win32Code = api.getLastError();
|
|
526
|
+
api.terminateProcess(info.hProcess, 1);
|
|
527
|
+
closeBestEffort(api, info.hThread);
|
|
528
|
+
closeBestEffort(api, info.hProcess);
|
|
529
|
+
api.closeHandle(job);
|
|
530
|
+
throwWin32(api, "AssignProcessToJobObject", win32Code, `pid ${info.dwProcessId}`);
|
|
531
|
+
}
|
|
532
|
+
if (api.resumeThread(info.hThread) === 4294967295) {
|
|
533
|
+
const win32Code = api.getLastError();
|
|
534
|
+
closeBestEffort(api, info.hThread);
|
|
535
|
+
closeBestEffort(api, info.hProcess);
|
|
536
|
+
api.closeHandle(job);
|
|
537
|
+
throwWin32(api, "ResumeThread", win32Code, `pid ${info.dwProcessId}`);
|
|
538
|
+
}
|
|
539
|
+
closeBestEffort(api, info.hThread);
|
|
540
|
+
return {
|
|
541
|
+
pid: info.dwProcessId,
|
|
542
|
+
process: info.hProcess,
|
|
543
|
+
job
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
export { ERROR_INSUFFICIENT_BUFFER, Win32Error, allocPtrSlot, allocUint32, decodePtr, decodeUint32, drainPipe, extendWin32ProcessBindings, isNullPtr, spawnInheritedJobProcess, spawnPipedProcess, throwLastError, throwWin32, waitForProcessExit };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned invariant companion for `@prettier-ai/dsh-win32-process`. */
|
|
3
|
+
const PACKAGE_NAME = "@prettier-ai/dsh-win32-process";
|
|
4
|
+
const name = "win32-process-invariant";
|
|
5
|
+
const inject = ["invariants"];
|
|
6
|
+
/** No runtime invariant: operations own only call-local native handles. */
|
|
7
|
+
const install = () => {};
|
|
8
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
9
|
+
//#endregion
|
|
10
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Generic Win32 process, stdio, and Job Object constants verified on x64. */
|
|
2
|
+
/** STARTUPINFOW uses the standard input, output, and error handles. */
|
|
3
|
+
export declare const STARTF_USESTDHANDLES = 256;
|
|
4
|
+
/** HandleInformation flag that permits child inheritance. */
|
|
5
|
+
export declare const HANDLE_FLAG_INHERIT = 1;
|
|
6
|
+
/** Infinite WaitForSingleObject timeout. */
|
|
7
|
+
export declare const INFINITE = 4294967295;
|
|
8
|
+
/** CreateProcess flag that prevents user code from running before resume. */
|
|
9
|
+
export declare const CREATE_SUSPENDED = 4;
|
|
10
|
+
/** GetStdHandle selector for standard input. */
|
|
11
|
+
export declare const STD_INPUT_HANDLE = -10;
|
|
12
|
+
/** GetStdHandle selector for standard output. */
|
|
13
|
+
export declare const STD_OUTPUT_HANDLE = -11;
|
|
14
|
+
/** GetStdHandle selector for standard error. */
|
|
15
|
+
export declare const STD_ERROR_HANDLE = -12;
|
|
16
|
+
/** FormatMessage reads the operating system message table. */
|
|
17
|
+
export declare const FORMAT_MESSAGE_FROM_SYSTEM = 4096;
|
|
18
|
+
/** FormatMessage leaves insertion placeholders uninterpreted. */
|
|
19
|
+
export declare const FORMAT_MESSAGE_IGNORE_INSERTS = 512;
|
|
20
|
+
/** Win32 code reporting a caller-provided buffer is too small. */
|
|
21
|
+
export declare const ERROR_INSUFFICIENT_BUFFER = 122;
|
|
22
|
+
/** Win32 code reporting that the other pipe end closed. */
|
|
23
|
+
export declare const ERROR_BROKEN_PIPE = 109;
|
|
24
|
+
/** Win32 code reporting that a pipe has no remaining data. */
|
|
25
|
+
export declare const ERROR_NO_DATA = 232;
|
|
26
|
+
/** Job limit that terminates every member when the final Job handle closes. */
|
|
27
|
+
export declare const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 8192;
|
|
28
|
+
/** SetInformationJobObject class for JOBOBJECT_EXTENDED_LIMIT_INFORMATION. */
|
|
29
|
+
export declare const JobObjectExtendedLimitInformation = 9;
|
|
30
|
+
/** x64 JOBOBJECT_EXTENDED_LIMIT_INFORMATION byte size. */
|
|
31
|
+
export declare const JOBOBJECT_EXTENDED_LIMIT_SIZE = 144;
|
|
32
|
+
/** Byte offset of BasicLimitInformation.LimitFlags in the extended Job record. */
|
|
33
|
+
export declare const JOBOBJECT_EXTENDED_LIMIT_FLAGS_OFFSET = 16;
|
|
34
|
+
/** x64 STARTUPINFOW byte size verified by the native probe. */
|
|
35
|
+
export declare const STARTUPINFOW_SIZE = 104;
|
|
36
|
+
/** x64 PROCESS_INFORMATION byte size verified by the native probe. */
|
|
37
|
+
export declare const PROCESS_INFORMATION_SIZE = 24;
|
|
38
|
+
//# sourceMappingURL=abi.d.ts.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Win32 call failure with the exact API name and error code. */
|
|
2
|
+
export declare class Win32Error extends Error {
|
|
3
|
+
/** Win32 function whose checked result failed. */
|
|
4
|
+
readonly api: string;
|
|
5
|
+
/** Exact GetLastError value or direct Win32 API error code. */
|
|
6
|
+
readonly win32Code: number;
|
|
7
|
+
constructor(api: string, win32Code: number, detail?: string);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Lazy Koffi bindings for generic Win32 process, stdio, and Job operations. */
|
|
2
|
+
import koffi from 'koffi';
|
|
3
|
+
declare const nativePtr: unique symbol;
|
|
4
|
+
/** Koffi native pointer branded against accidental numeric use. */
|
|
5
|
+
export type NativePtr = bigint & {
|
|
6
|
+
readonly [nativePtr]: true;
|
|
7
|
+
};
|
|
8
|
+
type Ptr = ReturnType<typeof koffi.pointer>;
|
|
9
|
+
/** Loaded Win32 libraries and the shared stdcall binder used by process extensions. */
|
|
10
|
+
export interface Win32BindingContext {
|
|
11
|
+
/** Kernel process, handle, pipe, and Job APIs. */
|
|
12
|
+
readonly kernel32: ReturnType<typeof koffi.load>;
|
|
13
|
+
/** Token and security APIs. */
|
|
14
|
+
readonly advapi32: ReturnType<typeof koffi.load>;
|
|
15
|
+
/** Bind one stdcall function from a loaded Win32 library. */
|
|
16
|
+
readonly bind: (library: ReturnType<typeof koffi.load>, name: string, result: Ptr | string, args: Array<Ptr | string>) => unknown;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Return whether a Koffi pointer represents NULL.
|
|
20
|
+
* @param value - pointer value returned by Koffi or a Win32 call.
|
|
21
|
+
* @returns true for null, undefined, or address zero.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isNullPtr(value: NativePtr | null | undefined): value is null | undefined;
|
|
24
|
+
/** STARTUPINFOW fields used by inherited or piped stdio launches. */
|
|
25
|
+
export interface StartupInfoInput {
|
|
26
|
+
cb: number;
|
|
27
|
+
dwFlags: number;
|
|
28
|
+
hStdInput: NativePtr;
|
|
29
|
+
hStdOutput: NativePtr;
|
|
30
|
+
hStdError: NativePtr;
|
|
31
|
+
}
|
|
32
|
+
/** Decoded PROCESS_INFORMATION result. */
|
|
33
|
+
export interface ProcessInfoOutput {
|
|
34
|
+
hProcess: NativePtr | null;
|
|
35
|
+
hThread: NativePtr | null;
|
|
36
|
+
dwProcessId: number;
|
|
37
|
+
dwThreadId: number;
|
|
38
|
+
}
|
|
39
|
+
/** Generic Win32 calls consumed by restricted-token sandbox process operations. */
|
|
40
|
+
export interface Win32ProcessBindings {
|
|
41
|
+
closeHandle(handle: NativePtr): number;
|
|
42
|
+
getLastError(): number;
|
|
43
|
+
formatMessageW(flags: number, source: null, messageId: number, languageId: number, buffer: Buffer, size: number, args: null): number;
|
|
44
|
+
createPipe(readHandle: NativePtr, writeHandle: NativePtr, attributes: null, size: number): number;
|
|
45
|
+
setHandleInformation(handle: NativePtr, mask: number, flags: number): number;
|
|
46
|
+
createProcessAsUserW(token: NativePtr, applicationName: null, commandLine: string, processAttributes: null, threadAttributes: null, inheritHandles: number, creationFlags: number, environment: null, currentDirectory: string | null, startupInfo: NativePtr, processInfo: NativePtr): number;
|
|
47
|
+
readFile(file: NativePtr, buffer: Buffer, count: number, bytesRead: NativePtr, overlapped: null): number;
|
|
48
|
+
peekNamedPipe(pipe: NativePtr, buffer: null, size: number, bytesRead: NativePtr | null, totalAvail: NativePtr, leftThisMessage: NativePtr | null): number;
|
|
49
|
+
waitForSingleObject(handle: NativePtr, milliseconds: number): number;
|
|
50
|
+
getExitCodeProcess(process: NativePtr, exitCode: NativePtr): number;
|
|
51
|
+
createJobObjectW(attributes: null, name: null): NativePtr;
|
|
52
|
+
setInformationJobObject(job: NativePtr, cls: number, information: Buffer, length: number): number;
|
|
53
|
+
assignProcessToJobObject(job: NativePtr, process: NativePtr): number;
|
|
54
|
+
resumeThread(thread: NativePtr): number;
|
|
55
|
+
terminateProcess(process: NativePtr, exitCode: number): number;
|
|
56
|
+
getStdHandle(stdHandle: number): NativePtr;
|
|
57
|
+
}
|
|
58
|
+
/** Koffi STARTUPINFOW layout. */
|
|
59
|
+
export declare const STARTUPINFOW: import("koffi").TypeObject;
|
|
60
|
+
/** Koffi PROCESS_INFORMATION layout. */
|
|
61
|
+
export declare const PROCESS_INFORMATION: import("koffi").TypeObject;
|
|
62
|
+
/**
|
|
63
|
+
* Allocate a pointer-sized out-parameter slot.
|
|
64
|
+
* @returns allocated native slot.
|
|
65
|
+
*/
|
|
66
|
+
export declare function allocPtrSlot(): NativePtr;
|
|
67
|
+
/**
|
|
68
|
+
* Allocate a uint32 out-parameter slot.
|
|
69
|
+
* @returns allocated native slot.
|
|
70
|
+
*/
|
|
71
|
+
export declare function allocUint32(): NativePtr;
|
|
72
|
+
/**
|
|
73
|
+
* Decode a pointer out-parameter.
|
|
74
|
+
* @param slot - pointer-sized slot filled by Win32.
|
|
75
|
+
* @returns decoded pointer, or null for address zero.
|
|
76
|
+
*/
|
|
77
|
+
export declare function decodePtr(slot: NativePtr): NativePtr | null;
|
|
78
|
+
/**
|
|
79
|
+
* Decode a uint32 out-parameter.
|
|
80
|
+
* @param slot - uint32 slot filled by Win32.
|
|
81
|
+
* @returns decoded unsigned value.
|
|
82
|
+
*/
|
|
83
|
+
export declare function decodeUint32(slot: NativePtr): number;
|
|
84
|
+
/**
|
|
85
|
+
* Allocate a zeroed STARTUPINFOW.
|
|
86
|
+
* @returns allocated struct pointer.
|
|
87
|
+
*/
|
|
88
|
+
export declare function allocStartupInfo(): NativePtr;
|
|
89
|
+
/**
|
|
90
|
+
* Encode the stdio-bearing STARTUPINFOW fields.
|
|
91
|
+
* @param startupInfo - allocated STARTUPINFOW pointer.
|
|
92
|
+
* @param fields - fields required for inherited stdio.
|
|
93
|
+
*/
|
|
94
|
+
export declare function encodeStartupInfo(startupInfo: NativePtr, fields: StartupInfoInput): void;
|
|
95
|
+
/**
|
|
96
|
+
* Allocate a zeroed PROCESS_INFORMATION.
|
|
97
|
+
* @returns allocated struct pointer.
|
|
98
|
+
*/
|
|
99
|
+
export declare function allocProcessInfo(): NativePtr;
|
|
100
|
+
/**
|
|
101
|
+
* Decode PROCESS_INFORMATION.
|
|
102
|
+
* @param processInfo - struct pointer filled by CreateProcess.
|
|
103
|
+
* @returns process/thread handles and ids.
|
|
104
|
+
*/
|
|
105
|
+
export declare function decodeProcessInfo(processInfo: NativePtr): ProcessInfoOutput;
|
|
106
|
+
/**
|
|
107
|
+
* Extend the shared process table with caller-owned Win32 API families.
|
|
108
|
+
* @param create - binds only the caller-specific operations from the shared libraries.
|
|
109
|
+
* @returns generic process bindings combined with the caller-specific operations.
|
|
110
|
+
*/
|
|
111
|
+
export declare function extendWin32ProcessBindings<Extension extends object>(create: (context: Win32BindingContext) => Extension): Win32ProcessBindings & Extension;
|
|
112
|
+
/**
|
|
113
|
+
* Format a Win32 error code through FormatMessageW.
|
|
114
|
+
* @param api - active binding table.
|
|
115
|
+
* @param win32Code - captured GetLastError value.
|
|
116
|
+
* @returns trimmed system message, or an empty string when unavailable.
|
|
117
|
+
*/
|
|
118
|
+
export declare function errorText(api: Win32ProcessBindings, win32Code: number): string;
|
|
119
|
+
/**
|
|
120
|
+
* Throw the current GetLastError value.
|
|
121
|
+
* @param api - active binding table.
|
|
122
|
+
* @param name - failing Win32 operation.
|
|
123
|
+
* @param detail - optional operation context.
|
|
124
|
+
* @returns never; always throws Win32Error.
|
|
125
|
+
*/
|
|
126
|
+
export declare function throwLastError(api: Win32ProcessBindings, name: string, detail?: string): never;
|
|
127
|
+
/**
|
|
128
|
+
* Throw an explicitly captured Win32 error code.
|
|
129
|
+
* @param api - active binding table.
|
|
130
|
+
* @param name - failing Win32 operation.
|
|
131
|
+
* @param win32Code - error captured before cleanup.
|
|
132
|
+
* @param detail - optional operation context.
|
|
133
|
+
* @returns never; always throws Win32Error.
|
|
134
|
+
*/
|
|
135
|
+
export declare function throwWin32(api: Win32ProcessBindings, name: string, win32Code: number, detail?: string): never;
|
|
136
|
+
export {};
|
|
137
|
+
//# sourceMappingURL=ffi.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Low-level Win32 process, stdio, and Job Object primitives used by the Windows ACL sandbox. */
|
|
2
|
+
export { ERROR_INSUFFICIENT_BUFFER } from './abi.ts';
|
|
3
|
+
export * from './errors.ts';
|
|
4
|
+
export { allocPtrSlot, allocUint32, decodePtr, decodeUint32, extendWin32ProcessBindings, isNullPtr, throwLastError, throwWin32, } from './ffi.ts';
|
|
5
|
+
export type { NativePtr, Win32ProcessBindings, } from './ffi.ts';
|
|
6
|
+
export { drainPipe, spawnInheritedJobProcess, spawnPipedProcess, waitForProcessExit, } from './process.ts';
|
|
7
|
+
export type { SpawnedJobProcess, SpawnedPipedProcess, } from './process.ts';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Package-owned invariant companion for `@prettier-ai/dsh-win32-process`. */
|
|
2
|
+
import type { Context } from '@prettier-ai/cordis';
|
|
3
|
+
export declare const name = "win32-process-invariant";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
6
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Typed Win32 process operations over the shared binding table. */
|
|
2
|
+
import type { NativePtr, Win32ProcessBindings } from './ffi.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Quote one argument according to CommandLineToArgvW parsing.
|
|
5
|
+
* @param argument - one argv entry.
|
|
6
|
+
* @returns bare or quoted command-line segment.
|
|
7
|
+
*/
|
|
8
|
+
export declare function quoteArg(argument: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Build the mutable command line accepted by CreateProcessAsUserW.
|
|
11
|
+
* @param program - executable argv entry.
|
|
12
|
+
* @param args - remaining argv entries.
|
|
13
|
+
* @returns joined Win32 command line.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildCommandLine(program: string, args: readonly string[]): string;
|
|
16
|
+
/** Restricted-token process creation inputs owned by the Windows ACL sandbox. */
|
|
17
|
+
export interface RestrictedProcessSpawnOptions {
|
|
18
|
+
/** Executable argv entry passed through CreateProcessAsUserW. */
|
|
19
|
+
command: string;
|
|
20
|
+
/** Arguments excluding the executable. */
|
|
21
|
+
args: readonly string[];
|
|
22
|
+
/** Existing child working directory. */
|
|
23
|
+
cwd: string;
|
|
24
|
+
/** Restricted primary token supplied by sandbox policy. */
|
|
25
|
+
token: NativePtr;
|
|
26
|
+
}
|
|
27
|
+
/** Piped child resources whose process and read handles remain caller-owned. */
|
|
28
|
+
export interface SpawnedPipedProcess {
|
|
29
|
+
/** Direct child process id. */
|
|
30
|
+
pid: number;
|
|
31
|
+
/** Process handle closed by waitForProcessExit. */
|
|
32
|
+
process: NativePtr;
|
|
33
|
+
/** Stdout pipe read end closed by drainPipe. */
|
|
34
|
+
stdoutRead: NativePtr;
|
|
35
|
+
/** Stderr pipe read end closed by drainPipe. */
|
|
36
|
+
stderrRead: NativePtr;
|
|
37
|
+
}
|
|
38
|
+
/** Suspended child assigned to one caller-owned kill-on-close Job before resume. */
|
|
39
|
+
export interface SpawnedJobProcess {
|
|
40
|
+
/** Direct child process id. */
|
|
41
|
+
pid: number;
|
|
42
|
+
/** Process handle closed by waitForProcessExit. */
|
|
43
|
+
process: NativePtr;
|
|
44
|
+
/** Job handle closed by the lifecycle owner. */
|
|
45
|
+
job: NativePtr;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Spawn a process with anonymous-pipe stdout/stderr and immediate stdin EOF.
|
|
49
|
+
* @param api - active binding table.
|
|
50
|
+
* @param options - command, cwd, args, and restricted primary token.
|
|
51
|
+
* @returns caller-owned process and pipe read handles.
|
|
52
|
+
*/
|
|
53
|
+
export declare function spawnPipedProcess(api: Win32ProcessBindings, options: RestrictedProcessSpawnOptions): SpawnedPipedProcess;
|
|
54
|
+
/**
|
|
55
|
+
* Drain one anonymous pipe until the writer closes it.
|
|
56
|
+
* @param api - active binding table.
|
|
57
|
+
* @param handle - caller-owned pipe read end.
|
|
58
|
+
* @returns complete bytes read before EOF; the handle is always closed.
|
|
59
|
+
* @throws when a Win32 pipe operation fails.
|
|
60
|
+
*/
|
|
61
|
+
export declare function drainPipe(api: Win32ProcessBindings, handle: NativePtr): Promise<Buffer>;
|
|
62
|
+
/**
|
|
63
|
+
* Wait for a process and always close its handle.
|
|
64
|
+
* @param api - active binding table.
|
|
65
|
+
* @param process - caller-owned process handle.
|
|
66
|
+
* @returns direct process exit code.
|
|
67
|
+
*/
|
|
68
|
+
export declare function waitForProcessExit(api: Win32ProcessBindings, process: NativePtr): number;
|
|
69
|
+
/**
|
|
70
|
+
* Spawn suspended, assign the child to a kill-on-close Job, then resume it.
|
|
71
|
+
* @param api - active binding table.
|
|
72
|
+
* @param options - command, cwd, args, and restricted primary token.
|
|
73
|
+
* @returns caller-owned process and Job handles after successful resume.
|
|
74
|
+
* @remarks Node clears stdio handle inheritability at startup through
|
|
75
|
+
* uv_disable_stdio_inheritance. This operation temporarily restores the bits
|
|
76
|
+
* required by STARTF_USESTDHANDLES. Restoring them afterward is best-effort:
|
|
77
|
+
* failure must not replace the already-created child's outcome.
|
|
78
|
+
*/
|
|
79
|
+
export declare function spawnInheritedJobProcess(api: Win32ProcessBindings, options: RestrictedProcessSpawnOptions): SpawnedJobProcess;
|
|
80
|
+
//# sourceMappingURL=process.d.ts.map
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prettier-ai/dsh-win32-process",
|
|
3
|
+
"description": "Low-level Win32 process, stdio, and Job Object primitives for the DeepSeek Harness Windows sandbox",
|
|
4
|
+
"version": "0.1.2-alpha.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/subprocess/win32-process"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./src/*": "./src/*",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib/index.js",
|
|
30
|
+
"lib/invariant.js",
|
|
31
|
+
"lib/types/**/*.d.ts"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@prettier-ai/dsh-invariants": "^0.1.2-alpha.1",
|
|
36
|
+
"@prettier-ai/cordis": "^4.0.1"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"koffi": "^3.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@prettier-ai/cordis": "^4.0.1",
|
|
43
|
+
"@prettier-ai/dsh-invariants": "^0.1.2-alpha.1"
|
|
44
|
+
}
|
|
45
|
+
}
|