@lqc123qwe/car-runtime 1.9.0 → 1.10.0
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/README.md +17 -1
- package/dist/SHA256SUMS +1 -0
- package/dist/authz/authz.d.ts +44 -0
- package/dist/authz/authz.js +65 -0
- package/dist/car-runtime-1.10.0.tgz +0 -0
- package/dist/car-runtime-1.10.0.tgz.sig.bundle +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +825 -0
- package/dist/dx/doctor.d.ts +62 -0
- package/dist/dx/doctor.js +127 -0
- package/dist/governance/dump.d.ts +47 -0
- package/dist/governance/dump.js +17 -0
- package/dist/host/facade.d.ts +13 -0
- package/dist/host/facade.js +154 -0
- package/dist/host/hostGateway.d.ts +117 -0
- package/dist/host/hostGateway.js +65 -0
- package/dist/host/mappings.d.ts +27 -0
- package/dist/host/mappings.js +52 -0
- package/dist/host/stdio.d.ts +14 -0
- package/dist/host/stdio.js +83 -0
- package/dist/kernel/context.d.ts +76 -0
- package/dist/kernel/context.js +203 -0
- package/dist/kernel/events.d.ts +37 -0
- package/dist/kernel/events.js +82 -0
- package/dist/load/config.d.ts +79 -0
- package/dist/load/config.js +285 -0
- package/dist/load/loader.d.ts +72 -0
- package/dist/load/loader.js +162 -0
- package/dist/load/registry.d.ts +44 -0
- package/dist/load/registry.js +58 -0
- package/dist/load/report.d.ts +98 -0
- package/dist/load/report.js +301 -0
- package/dist/load/sigGate.d.ts +22 -0
- package/dist/load/sigGate.js +68 -0
- package/dist/load/sign.d.ts +25 -0
- package/dist/load/sign.js +47 -0
- package/dist/load/verifier.d.ts +35 -0
- package/dist/load/verifier.js +56 -0
- package/dist/loop/goal.d.ts +31 -0
- package/dist/loop/goal.js +34 -0
- package/dist/loop/recover.d.ts +17 -0
- package/dist/loop/recover.js +39 -0
- package/dist/loop/stop.d.ts +79 -0
- package/dist/loop/stop.js +151 -0
- package/dist/mcp/gateway.d.ts +96 -0
- package/dist/mcp/gateway.js +69 -0
- package/dist/mcp/httpClient.d.ts +27 -0
- package/dist/mcp/httpClient.js +196 -0
- package/dist/mcp/stdioClient.d.ts +8 -0
- package/dist/mcp/stdioClient.js +73 -0
- package/dist/ptc/budget.d.ts +21 -0
- package/dist/ptc/budget.js +36 -0
- package/dist/ptc/erasable.d.ts +14 -0
- package/dist/ptc/erasable.js +27 -0
- package/dist/ptc/runCode.d.ts +65 -0
- package/dist/ptc/runCode.js +140 -0
- package/dist/ptc/schemaRender.d.ts +15 -0
- package/dist/ptc/schemaRender.js +90 -0
- package/dist/ptc/sdk.d.ts +27 -0
- package/dist/ptc/sdk.js +23 -0
- package/dist/ptc/worker-entry.d.ts +1 -0
- package/dist/ptc/worker-entry.js +71 -0
- package/dist/release-audit.jsonl +4 -0
- package/dist/runtime-core/chatStep.d.ts +40 -0
- package/dist/runtime-core/chatStep.js +104 -0
- package/dist/runtime-core/credentials.d.ts +49 -0
- package/dist/runtime-core/credentials.js +118 -0
- package/dist/runtime-core/errors.d.ts +30 -0
- package/dist/runtime-core/errors.js +36 -0
- package/dist/runtime-core/llm.d.ts +76 -0
- package/dist/runtime-core/llm.js +553 -0
- package/dist/runtime-core/redaction.d.ts +39 -0
- package/dist/runtime-core/redaction.js +79 -0
- package/dist/runtime-core/telemetry.d.ts +88 -0
- package/dist/runtime-core/telemetry.js +332 -0
- package/dist/runtime-core/tools.d.ts +15 -0
- package/dist/runtime-core/tools.js +12 -0
- package/dist/runtime-core/trace.d.ts +55 -0
- package/dist/runtime-core/trace.js +137 -0
- package/dist/runtime-core/types.d.ts +84 -0
- package/dist/runtime-core/types.js +13 -0
- package/dist/sandbox/sandbox.d.ts +68 -0
- package/dist/sandbox/sandbox.js +153 -0
- package/dist/sbom.json +263 -0
- package/dist/seam.d.ts +21 -0
- package/dist/seam.js +20 -0
- package/dist/security/secrets-baseline.d.ts +43 -0
- package/dist/security/secrets-baseline.js +176 -0
- package/dist/security/secrets.d.ts +45 -0
- package/dist/security/secrets.js +138 -0
- package/dist/session/export.d.ts +55 -0
- package/dist/session/export.js +81 -0
- package/dist/session/fork.d.ts +51 -0
- package/dist/session/fork.js +63 -0
- package/dist/session/format.d.ts +21 -0
- package/dist/session/format.js +49 -0
- package/dist/session/indexStore.d.ts +44 -0
- package/dist/session/indexStore.js +265 -0
- package/dist/session/log.d.ts +79 -0
- package/dist/session/log.js +157 -0
- package/dist/session/store.d.ts +9 -0
- package/dist/session/store.js +37 -0
- package/dist/telemetry/metrics.d.ts +25 -0
- package/dist/telemetry/metrics.js +26 -0
- package/package.json +17 -1
- package/src/seam.ts +21 -0
- package/.gitattributes +0 -11
- package/.github/workflows/ci.yml +0 -293
- package/.github/workflows/release.yml +0 -54
- package/adr/ADR-001.md +0 -4
- package/adr/ADR-002.md +0 -5
- package/adr/ADR-003.md +0 -35
- package/adr/host-onboarding.md +0 -68
- package/adr/mlps-audit-checklist.md +0 -41
- package/adr/win32-spike.md +0 -45
- package/docs/plugin-authoring.md +0 -82
- package/native/landlock-run/Makefile +0 -19
- package/native/landlock-run/landlock-run.c +0 -162
- package/scripts/contract-check.ts +0 -56
- package/scripts/invariant-n1.ts +0 -96
- package/scripts/package-check.ts +0 -29
- package/scripts/ptc-baseline/run-baseline.ts +0 -118
- package/scripts/release-artifacts.ts +0 -111
- package/scripts/release-pipeline.ts +0 -245
- package/scripts/secrets-baseline.ts +0 -33
- package/scripts/secrets-scan.ts +0 -56
- package/scripts/win32-ffi-debug.ts +0 -143
- package/scripts/win32-koffi-spike.ts +0 -537
- package/test/contract/eventmap.golden.json +0 -7
- package/test/fixtures/hello-plugin.ts +0 -5
- package/test/kernel.spec.ts +0 -136
- package/test/registry-e2e/run-e2e.ts +0 -213
- package/test/s11.spec.ts +0 -129
- package/test/s12.spec.ts +0 -135
- package/test/s13.spec.ts +0 -117
- package/test/s14.spec.ts +0 -117
- package/test/s15.spec.ts +0 -80
- package/test/s16.spec.ts +0 -133
- package/test/s17.spec.ts +0 -102
- package/test/s18.spec.ts +0 -81
- package/test/s19.spec.ts +0 -138
- package/test/s2.spec.ts +0 -245
- package/test/s20.spec.ts +0 -241
- package/test/s21.spec.ts +0 -231
- package/test/s22-m7-store-format.spec.ts +0 -135
- package/test/s23-m7-export-index.spec.ts +0 -189
- package/test/s24-signature-gate.spec.ts +0 -185
- package/test/s25-m8-runtime-core.spec.ts +0 -600
- package/test/s26-signature-closeout.spec.ts +0 -349
- package/test/s27-host-load-wiring.spec.ts +0 -134
- package/test/s28-session-index-followup.spec.ts +0 -236
- package/test/s29-otel-producer.spec.ts +0 -252
- package/test/s3.spec.ts +0 -179
- package/test/s30-release-artifacts.spec.ts +0 -127
- package/test/s31-real-model-wiring.spec.ts +0 -329
- package/test/s32-telemetry-mcp-ptc.spec.ts +0 -218
- package/test/s33-anthropic-adapter.spec.ts +0 -354
- package/test/s34-mcp-remote-transport.spec.ts +0 -250
- package/test/s35-config-ts-zod.spec.ts +0 -145
- package/test/s36-host-tool-surface.spec.ts +0 -250
- package/test/s37-projection-dedupe.spec.ts +0 -161
- package/test/s38-config-jiti.spec.ts +0 -116
- package/test/s39-otlp-span-semantics.spec.ts +0 -100
- package/test/s4.spec.ts +0 -169
- package/test/s40-metrics-cumulative.spec.ts +0 -67
- package/test/s41-mcp-serve-turn-span.spec.ts +0 -148
- package/test/s42-traceparent-propagation.spec.ts +0 -415
- package/test/s43-remote-http-traceparent.spec.ts +0 -262
- package/test/s5.spec.ts +0 -95
- package/test/s6.spec.ts +0 -169
- package/test/s7.spec.ts +0 -136
- package/test/s7b-baseline.spec.ts +0 -20
- package/test/s9.spec.ts +0 -89
- package/test/sandbox-escape/j17-orchestrate.sh +0 -30
- package/test/sandbox-escape/run-matrix.sh +0 -98
- package/tsconfig.json +0 -18
|
@@ -1,537 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Windows 沙箱 spike(S8 骨架;M2系统设计增补 T-3 / adr/win32-spike.md)
|
|
3
|
-
*
|
|
4
|
-
* M5-S24 阶段 1 实装(2026-09-09):判据 1 自动断言——
|
|
5
|
-
* OpenProcessToken(TOKEN_ASSIGN_PRIMARY|TOKEN_DUPLICATE|TOKEN_QUERY)
|
|
6
|
-
* → CreateRestrictedToken(DISABLE_MAX_PRIVILEGE + Administrators SID → SidsToDisable)
|
|
7
|
-
* → CreateProcessAsUserW → 子进程 `whoami /groups /fo list` + `whoami /priv /fo list`
|
|
8
|
-
* → 与父进程基线做差分断言(deny-only + 特权剥离,双证据)。
|
|
9
|
-
*
|
|
10
|
-
* M5-S25 阶段 2 实装(2026-09-09):判据 3 自动断言——
|
|
11
|
-
* CreateJobObjectW → SetInformationJobObject(JobObjectExtendedLimitInformation:
|
|
12
|
-
* ProcessMemoryLimit=256MB + ActiveProcessLimit=32 + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)
|
|
13
|
-
* → 受限子进程(CREATE_SUSPENDED)AssignProcessToJobObject → ResumeThread → IsProcessInJob 复核
|
|
14
|
-
* → fork 炸弹探针:并发 spawn 64 个 node 子进程(> ActiveProcessLimit=32),超限创建被拒
|
|
15
|
-
* → KILL_ON_JOB_CLOSE 击杀验证:关 Job 句柄后 30s 长睡子进程在 15s 内被终止(父进程存活)
|
|
16
|
-
* (fork 炸弹口径 = POC-4 T-17 同源:超限创建在配额内被拒,adr/win32-spike.md 阶段 2)
|
|
17
|
-
*
|
|
18
|
-
* M5-S26 阶段 3 实装(2026-09-10):判据 2 自动断言(同进程写探针)——
|
|
19
|
-
* 受限子进程内:写沙箱允许区(%TEMP% 会话目录)必须成功;写工作区外管理员门控路径
|
|
20
|
-
* (C:\Windows\)必须显式 ACCESS_DENIED(EPERM/EACCES,fail-closed 非静默);
|
|
21
|
-
* 未受限 token 子进程作对照组(本机父进程非提权,对照组同拒属预期,已如实记录)。
|
|
22
|
-
* 四判据至此全部自动断言整合:VERDICT=FOUR-CRITERIA-ALL-PASS 时退出码 0,任一 FAIL 退出码 1
|
|
23
|
-
* (CI 门禁直连)。DEC-2 已裁决接入 Windows CI 矩阵:ci.yml win32-spike job(windows-latest)。
|
|
24
|
-
*
|
|
25
|
-
* 判据 4(koffi 纯 JS 依赖链)随 koffi 可加载 + prebuilt 在位即断言。
|
|
26
|
-
*
|
|
27
|
-
* koffi 为 optionalDependencies:未安装(非 Windows 或沙箱内无 npm)时输出 SKIP + 判据清单,
|
|
28
|
-
* 与 POC-4 windows-skip 后端口径一致(verdict=SKIPPED 不满足发布门禁)。
|
|
29
|
-
*
|
|
30
|
-
* ABI 备注(koffi 3.2.1 本机探针已核实):
|
|
31
|
-
* - koffi.struct 默认 C 自然对齐(SID_AND_ATTRIBUTES=16B,与 Win32 一致);
|
|
32
|
-
* - Node Buffer 可直接作 `void *` 参数传入(调用期 pin 住);null 传 `void *` 合法;
|
|
33
|
-
* - JS 对象数组不能直传 `void *`(报 ambiguous),须以 koffi.struct 声明参数类型;
|
|
34
|
-
* - HANDLE 以 uint64 + BigInt 传参(x64 ABI 下与指针寄存器等价);
|
|
35
|
-
* - JOBOBJECT_EXTENDED_LIMIT_INFORMATION x64 = 144B(BASIC_LIMIT 64B + IO_COUNTERS 48B + 4 个 SIZE_T)。
|
|
36
|
-
*/
|
|
37
|
-
import { spawnSync } from 'node:child_process'
|
|
38
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
39
|
-
import { tmpdir } from 'node:os'
|
|
40
|
-
import path from 'node:path'
|
|
41
|
-
|
|
42
|
-
// ---------------- Win32 常量 ----------------
|
|
43
|
-
const TOKEN_ASSIGN_PRIMARY = 0x0001
|
|
44
|
-
const TOKEN_DUPLICATE = 0x0002
|
|
45
|
-
const TOKEN_QUERY = 0x0008
|
|
46
|
-
const DISABLE_MAX_PRIVILEGE = 0x00000001
|
|
47
|
-
const CREATE_NO_WINDOW = 0x08000000
|
|
48
|
-
const CREATE_SUSPENDED = 0x00000004
|
|
49
|
-
const CREATE_UNICODE_ENVIRONMENT = 0x00000400
|
|
50
|
-
const DETACHED_PROCESS = 0x00000008
|
|
51
|
-
const WAIT_TIMEOUT = 0x00000102
|
|
52
|
-
const STILL_ACTIVE = 259
|
|
53
|
-
const PSEUDO_CURRENT_PROCESS = BigInt.asUintN(64, -1n) // GetCurrentProcess() 伪句柄
|
|
54
|
-
|
|
55
|
-
// Job Object(JobObjectExtendedLimitInformation = 9)
|
|
56
|
-
const JobObjectExtendedLimitInformation = 9
|
|
57
|
-
const JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008
|
|
58
|
-
const JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100
|
|
59
|
-
const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
|
60
|
-
const JOB_MEMORY_LIMIT_BYTES = 256 * 1024 * 1024 // 256MB(规划口径)
|
|
61
|
-
const JOB_ACTIVE_PROCESS_LIMIT = 32 // 规划口径
|
|
62
|
-
|
|
63
|
-
const ADMIN_SID = 'S-1-5-32-544' // BUILTIN\Administrators
|
|
64
|
-
const DENY_ONLY_RE = /Deny[- ]only|仅用于拒绝|只用于拒绝|用于拒绝/i // zh-CN / en-US whoami 属性文本(含 "Group used for deny only" 变体)
|
|
65
|
-
const PRIV_NAME_RE = /Se[A-Z][A-Za-z]+Privilege/g // 特权名不做本地化,可靠
|
|
66
|
-
const DANGEROUS_PRIVS = [
|
|
67
|
-
'SeDebugPrivilege', 'SeBackupPrivilege', 'SeRestorePrivilege',
|
|
68
|
-
'SeTakeOwnershipPrivilege', 'SeLoadDriverPrivilege', 'SeSecurityPrivilege',
|
|
69
|
-
'SeManageVolumePrivilege', 'SeTrustedCredManAccessPrivilege',
|
|
70
|
-
]
|
|
71
|
-
|
|
72
|
-
const SystemRoot = process.env.SystemRoot ?? 'C:\\Windows'
|
|
73
|
-
const WHOAMI_EXE = path.join(SystemRoot, 'System32', 'whoami.exe')
|
|
74
|
-
|
|
75
|
-
type CriterionStatus = 'PASS' | 'FAIL' | 'PENDING' | 'SKIP'
|
|
76
|
-
interface Criterion { id: number; name: string; status: CriterionStatus; evidence: string }
|
|
77
|
-
|
|
78
|
-
const criteria: Criterion[] = [
|
|
79
|
-
{ id: 1, name: '受限 token 下 whoami /groups 显示 Administrators 剥离(deny-only)+ 特权剥离', status: 'PENDING', evidence: '' },
|
|
80
|
-
{ id: 2, name: '工作区外写操作 ACCESS_DENIED(fail-closed,非静默)', status: 'PENDING', evidence: '' },
|
|
81
|
-
{ id: 3, name: 'Job Object 配额触发:fork 炸弹被拒/被杀、父进程存活', status: 'PENDING', evidence: '' },
|
|
82
|
-
{ id: 4, name: 'koffi 纯 JS 依赖链(零原生编译,prebuilt 在位)', status: 'PENDING', evidence: '' },
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
// Administrators SID(S-1-5-32-544):Revision=1, Count=2, Authority={0,0,0,0,0,5}, Sub=[32,544]
|
|
86
|
-
function buildAdministratorsSid(): Buffer {
|
|
87
|
-
const b = Buffer.alloc(16) // sizeof(SID) x64 = 16
|
|
88
|
-
b[0] = 1
|
|
89
|
-
b[1] = 2
|
|
90
|
-
b.set([0, 0, 0, 0, 0, 5], 2)
|
|
91
|
-
b.writeUInt32LE(32, 8)
|
|
92
|
-
b.writeUInt32LE(544, 12)
|
|
93
|
-
return b
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// JOBOBJECT_EXTENDED_LIMIT_INFORMATION(x64 = 144B),manual 布局写入
|
|
97
|
-
function buildExtendedLimitInfo(): Buffer {
|
|
98
|
-
const b = Buffer.alloc(144)
|
|
99
|
-
b.writeUInt32LE(
|
|
100
|
-
JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOB_OBJECT_LIMIT_PROCESS_MEMORY | JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
|
101
|
-
16, // BasicLimitInformation.LimitFlags
|
|
102
|
-
)
|
|
103
|
-
b.writeUInt32LE(JOB_ACTIVE_PROCESS_LIMIT, 40) // BasicLimitInformation.ActiveProcessLimit
|
|
104
|
-
b.writeBigUInt64LE(BigInt(JOB_MEMORY_LIMIT_BYTES), 112) // ProcessMemoryLimit
|
|
105
|
-
return b
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// 显式 Unicode 环境块:CreateProcessAsUserW 传 NULL 环境时,受限子进程可能因缺
|
|
109
|
-
// SystemRoot/SystemDrive 等关键变量而 0xC0000142(STATUS_DLL_INIT_FAILED)——
|
|
110
|
-
// windows-2025 runner(提权上下文)实测踩坑,本机非提权交互环境不可复现
|
|
111
|
-
function buildEnvBlock(): Buffer {
|
|
112
|
-
const env: Record<string, string> = {}
|
|
113
|
-
for (const [k, v] of Object.entries(process.env)) if (v !== undefined) env[k] = v
|
|
114
|
-
if (env.SystemRoot === undefined) env.SystemRoot = SystemRoot
|
|
115
|
-
if (env.windir === undefined) env.windir = SystemRoot
|
|
116
|
-
if (env.SystemDrive === undefined) env.SystemDrive = 'C:'
|
|
117
|
-
return Buffer.from(Object.entries(env).map(([k, v]) => `${k}=${v}`).join('\0') + '\0\0', 'utf16le')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 输出解码:优先 UTF-8,含替换符则回退 GBK(zh-CN whoami 重定向输出走 OEM 代码页)
|
|
121
|
-
function decodeOutput(buf: Buffer): string {
|
|
122
|
-
const utf8 = buf.toString('utf8')
|
|
123
|
-
return utf8.includes('\uFFFD') ? new TextDecoder('gbk').decode(buf) : utf8
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// /fo list 格式:SID 行之后的下一条「属性/Attributes:」行即该组属性
|
|
127
|
-
function parseGroupAttrs(listText: string, sid: string): string | null {
|
|
128
|
-
const lines = listText.split(/\r?\n/)
|
|
129
|
-
for (let i = 0; i < lines.length; i++) {
|
|
130
|
-
if (!lines[i].includes(sid)) continue
|
|
131
|
-
for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
|
|
132
|
-
const l = lines[j].trim()
|
|
133
|
-
const m = l.match(/^(?:属性|Attributes)\s*[::]\s*(.+)$/i)
|
|
134
|
-
if (m) return m[1]
|
|
135
|
-
if (!l) continue
|
|
136
|
-
if (/[::]/.test(l)) break // 已进入下一条目(组名/类型/SID 标签行)
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
return null
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function mandatoryLabel(listText: string): string | null {
|
|
143
|
-
const m = listText.match(/S-1-16-\d+/g)
|
|
144
|
-
return m ? m[m.length - 1] : null
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function privilegeNames(listText: string): string[] {
|
|
148
|
-
return [...listText.matchAll(PRIV_NAME_RE)].map((m) => m[0])
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
interface RestrictedChild {
|
|
152
|
-
hProcess: bigint
|
|
153
|
-
hThread: bigint
|
|
154
|
-
pid: number
|
|
155
|
-
resume: () => void
|
|
156
|
-
wait: (ms?: number) => number
|
|
157
|
-
exitCode: () => number
|
|
158
|
-
outText: () => string
|
|
159
|
-
cleanup: () => void
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async function main() {
|
|
163
|
-
console.log('=== CAR win32-koffi spike(M5-S24/S25/S26:四判据自动断言)===')
|
|
164
|
-
if (process.platform !== 'win32') {
|
|
165
|
-
console.log(`SKIP: spike requires win32 (current: ${process.platform})`)
|
|
166
|
-
for (const c of criteria) c.status = 'SKIP'
|
|
167
|
-
printReport()
|
|
168
|
-
return
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let koffi: any
|
|
172
|
-
try { koffi = (await import('koffi')).default ?? (await import('koffi') as any) } catch { koffi = null }
|
|
173
|
-
if (!koffi) {
|
|
174
|
-
console.log('SKIP: koffi not installed(npm install koffi 后重跑)')
|
|
175
|
-
console.log('判据清单(adr/win32-spike.md 四判据,见脚本头注释)')
|
|
176
|
-
for (const c of criteria) c.status = 'SKIP'
|
|
177
|
-
printReport()
|
|
178
|
-
return
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ---- 判据 4:koffi prebuilt 在位、零原生编译 ----
|
|
182
|
-
// koffi 3.x 的本机模块走平台包 @koromix/koffi-win32-x64(npm install 即得,无 node-gyp 编译)
|
|
183
|
-
const repoRoot = path.join(import.meta.dirname ?? '.', '..')
|
|
184
|
-
const prebuiltPkg = path.join(repoRoot, 'node_modules', '@koromix', 'koffi-win32-x64', 'win32_x64', 'koffi.node')
|
|
185
|
-
const prebuiltBuild = path.join(repoRoot, 'node_modules', 'koffi', 'build', 'koffi', 'win32_x64')
|
|
186
|
-
criteria[3].status = 'PASS'
|
|
187
|
-
criteria[3].evidence = `koffi 3.2.1 经 optionalDependencies 加载成功;prebuilt 本机模块在位=${existsSync(prebuiltPkg) || existsSync(prebuiltBuild)}(@koromix/koffi-win32-x64 平台包,无编译步骤)`
|
|
188
|
-
|
|
189
|
-
// ---- FFI 绑定 ----
|
|
190
|
-
const kernel32 = koffi.load('kernel32.dll')
|
|
191
|
-
const advapi32 = koffi.load('advapi32.dll')
|
|
192
|
-
// SID_AND_ATTRIBUTES 需显式 struct 类型:void * 参数收 JS 对象数组会被判歧义,
|
|
193
|
-
// 声明为 SID_AND_ATTRIBUTES * 后 koffi 按 C 布局(16B,x64 自然对齐,探针已核实)转换
|
|
194
|
-
const SID_AND_ATTRIBUTES = koffi.struct('SID_AND_ATTRIBUTES', { Sid: 'void *', Attributes: 'uint32' })
|
|
195
|
-
const CloseHandle = kernel32.func('int32 __stdcall CloseHandle(uint64 hObject)')
|
|
196
|
-
const OpenProcess = kernel32.func('uint64 __stdcall OpenProcess(uint32 dwDesiredAccess, int32 bInheritHandle, uint32 dwProcessId)')
|
|
197
|
-
const CreateFileW = kernel32.func('uint64 __stdcall CreateFileW(void *lpFileName, uint32 dwDesiredAccess, uint32 dwShareMode, void *lpSecurityAttributes, uint32 dwCreationDisposition, uint32 dwFlagsAndAttributes, uint64 hTemplateFile)')
|
|
198
|
-
const WaitForSingleObject = kernel32.func('uint32 __stdcall WaitForSingleObject(uint64 hHandle, uint32 dwMilliseconds)')
|
|
199
|
-
const ResumeThread = kernel32.func('uint32 __stdcall ResumeThread(uint64 hThread)')
|
|
200
|
-
const GetExitCodeProcess = kernel32.func('int32 __stdcall GetExitCodeProcess(uint64 hProcess, void *lpExitCode)')
|
|
201
|
-
const OpenProcessToken = advapi32.func('int32 __stdcall OpenProcessToken(uint64 ProcessHandle, uint32 DesiredAccess, void *TokenHandle)')
|
|
202
|
-
const GetTokenInformation = advapi32.func('int32 __stdcall GetTokenInformation(uint64 TokenHandle, int32 TokenInformationClass, void *TokenInformation, uint32 TokenInformationLength, void *ReturnLength)')
|
|
203
|
-
const CreateRestrictedToken = advapi32.func('int32 __stdcall CreateRestrictedToken(uint64 ExistingTokenHandle, uint32 Flags, uint32 DisableSidCount, SID_AND_ATTRIBUTES *SidsToDisable, uint32 DeletePrivilegeCount, void *PrivilegesToDelete, uint32 RestrictedSidCount, void *SidsToRestrict, void *NewTokenHandle)')
|
|
204
|
-
const CreateProcessAsUserW = advapi32.func('int32 __stdcall CreateProcessAsUserW(uint64 hToken, void *lpApplicationName, void *lpCommandLine, void *lpProcessAttributes, void *lpThreadAttributes, int32 bInheritHandles, uint32 dwCreationFlags, void *lpEnvironment, void *lpCurrentDirectory, void *lpStartupInfo, void *lpProcessInformation)')
|
|
205
|
-
// Job Object(S25 阶段 2)
|
|
206
|
-
const CreateJobObjectW = kernel32.func('uint64 __stdcall CreateJobObjectW(void *lpJobAttributes, void *lpName)')
|
|
207
|
-
const SetInformationJobObject = kernel32.func('int32 __stdcall SetInformationJobObject(uint64 hJob, int32 JobObjectInformationClass, void *lpJobObjectInformation, uint32 cbJobObjectInformationLength)')
|
|
208
|
-
const AssignProcessToJobObject = kernel32.func('int32 __stdcall AssignProcessToJobObject(uint64 hJob, uint64 hProcess)')
|
|
209
|
-
const IsProcessInJob = kernel32.func('int32 __stdcall IsProcessInJob(uint64 hProcess, uint64 hJob, void *pbResult)')
|
|
210
|
-
|
|
211
|
-
const closeQuiet = (h: bigint) => { try { if (h !== 0n) CloseHandle(h) } catch { /* noop */ } }
|
|
212
|
-
// koffi uint64 返回值在安全整数范围内可能给 Number(>2^53 才给 BigInt),统一归一化为 BigInt
|
|
213
|
-
const asBig = (v: bigint | number): bigint => (typeof v === 'bigint' ? v : BigInt(Math.round(v)))
|
|
214
|
-
|
|
215
|
-
// ---- 受限子进程工厂(无 cmd 中转:lpApplicationName 直指程序 + STARTF_USESTDHANDLES
|
|
216
|
-
// 文件句柄重定向,输出捕获内核级、无引号解析/无控制台依赖)----
|
|
217
|
-
const createRestrictedChild = (hToken: bigint, program: string, args: string[], opts?: { suspended?: boolean }): RestrictedChild => {
|
|
218
|
-
const dir = mkdtempSync(path.join(tmpdir(), 'car-spike-'))
|
|
219
|
-
const outFile = path.join(dir, 'out.txt')
|
|
220
|
-
// 可继承句柄安全属性:SECURITY_ATTRIBUTES { nLength=24, lpSecurityDescriptor=NULL, bInheritHandle=TRUE }
|
|
221
|
-
const sa = Buffer.alloc(24)
|
|
222
|
-
sa.writeUInt32LE(24, 0)
|
|
223
|
-
sa.writeUInt32LE(1, 16)
|
|
224
|
-
const nameBuf = Buffer.from(outFile + '\0', 'utf16le')
|
|
225
|
-
const hFile = asBig(CreateFileW(nameBuf, 0x40000000 /* GENERIC_WRITE */, 0x1 /* FILE_SHARE_READ */, sa, 2 /* CREATE_ALWAYS */, 0x80 /* FILE_ATTRIBUTE_NORMAL */, 0n))
|
|
226
|
-
if (hFile === BigInt.asUintN(64, -1n)) {
|
|
227
|
-
rmSync(dir, { recursive: true, force: true })
|
|
228
|
-
throw new Error('CreateFileW failed for probe output file')
|
|
229
|
-
}
|
|
230
|
-
const appBuf = Buffer.from(program + '\0', 'utf16le')
|
|
231
|
-
const q = (a: string) => (a.includes(' ') ? `"${a}"` : a)
|
|
232
|
-
const cmdBuf = Buffer.from(`"${program}" ${args.map(q).join(' ')}\0`, 'utf16le')
|
|
233
|
-
const si = Buffer.alloc(104) // sizeof(STARTUPINFOW) x64
|
|
234
|
-
si.writeUInt32LE(104, 0) // cb
|
|
235
|
-
si.writeUInt32LE(0x100, 60) // dwFlags = STARTF_USESTDHANDLES
|
|
236
|
-
si.writeBigUInt64LE(0n, 80) // hStdInput
|
|
237
|
-
si.writeBigUInt64LE(hFile, 88) // hStdOutput
|
|
238
|
-
si.writeBigUInt64LE(hFile, 96) // hStdError
|
|
239
|
-
const pi = Buffer.alloc(24) // sizeof(PROCESS_INFORMATION) x64
|
|
240
|
-
// DETACHED_PROCESS:不分配控制台。CREATE_NO_WINDOW 仍会 spawn conhost,
|
|
241
|
-
// windows-2025 提权会话上受限 token 控制台初始化失败 → 0xC0000142(FFI debug 矩阵实测);
|
|
242
|
-
// 输出经 STARTF_USESTDHANDLES 落盘,无控制台需求
|
|
243
|
-
const flags = DETACHED_PROCESS | CREATE_UNICODE_ENVIRONMENT | (opts?.suspended ? CREATE_SUSPENDED : 0)
|
|
244
|
-
if (!CreateProcessAsUserW(hToken, appBuf, cmdBuf, null, null, 1 /* bInheritHandles */, flags, buildEnvBlock(), null, si, pi)) {
|
|
245
|
-
closeQuiet(hFile)
|
|
246
|
-
rmSync(dir, { recursive: true, force: true })
|
|
247
|
-
throw new Error('CreateProcessAsUserW failed (rc=0)——可能缺 SeIncreaseQuotaPrivilege 或受宿主沙箱拦截')
|
|
248
|
-
}
|
|
249
|
-
const hProcess = pi.readBigUInt64LE(0)
|
|
250
|
-
const hThread = pi.readBigUInt64LE(8)
|
|
251
|
-
const codeBuf = Buffer.alloc(4)
|
|
252
|
-
return {
|
|
253
|
-
hProcess,
|
|
254
|
-
hThread,
|
|
255
|
-
pid: pi.readUInt32LE(16),
|
|
256
|
-
resume: () => { ResumeThread(hThread) },
|
|
257
|
-
wait: (ms = 60000) => WaitForSingleObject(hProcess, ms),
|
|
258
|
-
exitCode: () => { GetExitCodeProcess(hProcess, codeBuf); return codeBuf.readUInt32LE(0) },
|
|
259
|
-
outText: () => (existsSync(outFile) ? decodeOutput(readFileSync(outFile)) : ''),
|
|
260
|
-
cleanup: () => { closeQuiet(hThread); closeQuiet(hProcess); closeQuiet(hFile); rmSync(dir, { recursive: true, force: true }) },
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// ---- 父进程基线(未受限)----
|
|
265
|
-
const parentGroups = decodeOutput(spawnSync(WHOAMI_EXE, ['/groups', '/fo', 'list'], { encoding: 'buffer' }).stdout as Buffer)
|
|
266
|
-
const parentPrivsText = decodeOutput(spawnSync(WHOAMI_EXE, ['/priv', '/fo', 'list'], { encoding: 'buffer' }).stdout as Buffer)
|
|
267
|
-
const parentAdminAttrs = parseGroupAttrs(parentGroups, ADMIN_SID)
|
|
268
|
-
const parentPrivs = privilegeNames(parentPrivsText)
|
|
269
|
-
|
|
270
|
-
// ---- 阶段 1 调用链 ----
|
|
271
|
-
const hTokBuf = Buffer.alloc(8)
|
|
272
|
-
if (!OpenProcessToken(PSEUDO_CURRENT_PROCESS, TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY, hTokBuf)) {
|
|
273
|
-
throw new Error('OpenProcessToken failed (rc=0)')
|
|
274
|
-
}
|
|
275
|
-
const hToken = hTokBuf.readBigUInt64LE(0)
|
|
276
|
-
|
|
277
|
-
const sidBuf = buildAdministratorsSid()
|
|
278
|
-
const saaArray = [{ Sid: sidBuf, Attributes: 0 }] // SID_AND_ATTRIBUTES[1],Attributes 入参不参与语义
|
|
279
|
-
const hRestrBuf = Buffer.alloc(8)
|
|
280
|
-
const okRestrict = CreateRestrictedToken(hToken, DISABLE_MAX_PRIVILEGE, 1, saaArray, 0, null, 0, null, hRestrBuf)
|
|
281
|
-
if (!okRestrict) {
|
|
282
|
-
closeQuiet(hToken)
|
|
283
|
-
throw new Error('CreateRestrictedToken failed (rc=0)——检查 SID_AND_ATTRIBUTES ABI(期望 16B C 对齐)')
|
|
284
|
-
}
|
|
285
|
-
const hRestricted = hRestrBuf.readBigUInt64LE(0)
|
|
286
|
-
|
|
287
|
-
try {
|
|
288
|
-
// ========== 判据 1(S24 阶段 1)==========
|
|
289
|
-
// 文本探针(whoami)仅作辅助证据:runner 提权会话上受限 token 自查 /groups 会
|
|
290
|
-
// Access denied(预期行为),主断言由父侧代读子进程 token 完成(见判据 3 块内)
|
|
291
|
-
const c1a = createRestrictedChild(hRestricted, WHOAMI_EXE, ['/groups', '/fo', 'list'])
|
|
292
|
-
const c1p = createRestrictedChild(hRestricted, WHOAMI_EXE, ['/priv', '/fo', 'list'])
|
|
293
|
-
c1a.wait(); c1p.wait()
|
|
294
|
-
const childGroupsRes = { out: c1a.outText(), exitCode: c1a.exitCode() }
|
|
295
|
-
const childPrivsRes = { out: c1p.outText(), exitCode: c1p.exitCode() }
|
|
296
|
-
c1a.cleanup(); c1p.cleanup()
|
|
297
|
-
const childAdminAttrs = parseGroupAttrs(childGroupsRes.out, ADMIN_SID)
|
|
298
|
-
const childPrivs = privilegeNames(childPrivsRes.out)
|
|
299
|
-
const stripped = parentPrivs.filter((p) => !childPrivs.includes(p))
|
|
300
|
-
const childDangerous = childPrivs.filter((p) => DANGEROUS_PRIVS.includes(p))
|
|
301
|
-
const parentAlreadyDeny = parentAdminAttrs !== null && DENY_ONLY_RE.test(parentAdminAttrs)
|
|
302
|
-
const childDeny = childAdminAttrs !== null && DENY_ONLY_RE.test(childAdminAttrs)
|
|
303
|
-
|
|
304
|
-
// ========== 判据 3(S25 阶段 2:Job Object 配额 + fork 炸弹,POC-4 T-17 同源)==========
|
|
305
|
-
const hJob = asBig(CreateJobObjectW(null, null))
|
|
306
|
-
if (hJob === 0n) throw new Error('CreateJobObjectW failed (returned NULL)')
|
|
307
|
-
const okJobInfo = SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, buildExtendedLimitInfo(), 144)
|
|
308
|
-
if (!okJobInfo) throw new Error('SetInformationJobObject failed (rc=0)——检查 JOBOBJECT_EXTENDED_LIMIT_INFORMATION 布局(期望 144B)')
|
|
309
|
-
|
|
310
|
-
// fork 炸弹探针:并发 spawn 64 个 3s 长睡 node 子进程,统计超限被拒数
|
|
311
|
-
const forkProbeJs = [
|
|
312
|
-
'const { spawn } = require("child_process");',
|
|
313
|
-
'const N = 64;',
|
|
314
|
-
'let ok = 0, fail = 0, done = 0;',
|
|
315
|
-
'const samples = [];',
|
|
316
|
-
'function finish(extra) { console.log("FORK_RESULT " + JSON.stringify(Object.assign({ ok: ok, fail: fail, samples: samples.slice(0, 5), mem: process.memoryUsage().rss }, extra || {}))); process.exit(0); }',
|
|
317
|
-
'function check() { if (done >= N) finish(); }',
|
|
318
|
-
'for (let i = 0; i < N; i++) {',
|
|
319
|
-
' let c;',
|
|
320
|
-
' try { c = spawn(process.execPath, ["-e", "setTimeout(function(){process.exit(0)},3000)"], { stdio: "ignore", detached: true }); }',
|
|
321
|
-
' catch (e) { fail++; done++; if (samples.length < 5) samples.push({ kind: "throw", msg: String(e && e.message).slice(0, 120) }); continue; }',
|
|
322
|
-
' c.on("error", function (e) { fail++; done++; if (samples.length < 5) samples.push({ kind: "error", msg: String(e && e.code || e).slice(0, 120) }); check(); });',
|
|
323
|
-
' c.on("exit", function (code, sig) { if (code === 0) { ok++; } else { fail++; if (samples.length < 5) samples.push({ kind: "exit", code: code, sig: sig }); } done++; check(); });',
|
|
324
|
-
'}',
|
|
325
|
-
'setTimeout(function () { finish({ timeout: true }); }, 20000);',
|
|
326
|
-
].join('\n')
|
|
327
|
-
const sleepJs = 'setTimeout(function () { process.exit(0) }, 30000)\n'
|
|
328
|
-
|
|
329
|
-
const s25Dir = mkdtempSync(path.join(tmpdir(), 'car-s25-'))
|
|
330
|
-
const forkProbePath = path.join(s25Dir, 'fork-probe.js')
|
|
331
|
-
const sleepPath = path.join(s25Dir, 'sleep-30s.js')
|
|
332
|
-
writeFileSync(forkProbePath, forkProbeJs, 'utf8')
|
|
333
|
-
writeFileSync(sleepPath, sleepJs, 'utf8')
|
|
334
|
-
const nodeExe = process.execPath
|
|
335
|
-
|
|
336
|
-
try {
|
|
337
|
-
// ① 长睡哨兵子进程(挂 Job,用于 KILL_ON_JOB_CLOSE 击杀验证 + 判据 1 父侧代读取证)
|
|
338
|
-
const sentinel = createRestrictedChild(hRestricted, nodeExe, [sleepPath], { suspended: true })
|
|
339
|
-
const okAssignSentinel = AssignProcessToJobObject(hJob, sentinel.hProcess)
|
|
340
|
-
sentinel.resume()
|
|
341
|
-
|
|
342
|
-
// ①' 判据 1 主断言(父侧代读):受限 token 自查 whoami /groups 在提权会话上会
|
|
343
|
-
// Access denied,故由父进程(High IL admin)打开哨兵进程读取其 token 的
|
|
344
|
-
// TokenGroups/TokenPrivileges,机器级断言 Administrators SID = deny-only
|
|
345
|
-
const SE_GROUP_USE_FOR_DENY_ONLY = 0x10
|
|
346
|
-
const SE_GROUP_ENABLED_MASK = 0x2 | 0x4
|
|
347
|
-
const readTokenGroups = (token: bigint): Array<{ sid: string; attrs: number }> | null => {
|
|
348
|
-
const lenBuf = Buffer.alloc(4)
|
|
349
|
-
GetTokenInformation(token, 2 /* TokenGroups */, null, 0, lenBuf)
|
|
350
|
-
const len = lenBuf.readUInt32LE(0)
|
|
351
|
-
if (len <= 8) return null
|
|
352
|
-
const buf = Buffer.alloc(len)
|
|
353
|
-
if (!GetTokenInformation(token, 2, buf, len, lenBuf)) return null
|
|
354
|
-
const count = buf.readUInt32LE(0)
|
|
355
|
-
if (count === 0 || 8 + count * 16 + 8 > len) return null
|
|
356
|
-
const sidPtrs: bigint[] = []
|
|
357
|
-
for (let i = 0; i < count; i++) sidPtrs.push(buf.readBigUInt64LE(8 + i * 16))
|
|
358
|
-
// 内核将 SID blob 连续放在 entries 数组之后:buffer 基址 = min(sidPtr) - (8 + count*16)
|
|
359
|
-
const minSid = sidPtrs.reduce((a, b) => (b < a ? b : a))
|
|
360
|
-
const base = Number(minSid - BigInt(8 + count * 16))
|
|
361
|
-
const out: Array<{ sid: string; attrs: number }> = []
|
|
362
|
-
for (let i = 0; i < count; i++) {
|
|
363
|
-
const attrs = buf.readUInt32LE(8 + i * 16 + 8)
|
|
364
|
-
const off = Number(sidPtrs[i] - BigInt(base)) // 相对 buffer 起点的偏移
|
|
365
|
-
if (off < 0 || off + 8 > len) return null
|
|
366
|
-
const rev = buf[off]; const subCnt = buf[off + 1]
|
|
367
|
-
if (rev !== 1 || subCnt > 15 || off + 8 + subCnt * 4 > len) return null
|
|
368
|
-
let auth = 0n
|
|
369
|
-
for (let k = 0; k < 6; k++) auth = (auth << 8n) | BigInt(buf[off + 2 + k])
|
|
370
|
-
if (auth > 0xffffffffffffn) return null
|
|
371
|
-
const subs: number[] = []
|
|
372
|
-
for (let k = 0; k < subCnt; k++) subs.push(buf.readUInt32LE(off + 8 + k * 4))
|
|
373
|
-
out.push({ sid: `S-1-${auth}-${subs.join('-')}`, attrs })
|
|
374
|
-
}
|
|
375
|
-
return out
|
|
376
|
-
}
|
|
377
|
-
const readTokenPrivilegeCount = (token: bigint): number | null => {
|
|
378
|
-
const lenBuf = Buffer.alloc(4)
|
|
379
|
-
GetTokenInformation(token, 3 /* TokenPrivileges */, null, 0, lenBuf)
|
|
380
|
-
const len = lenBuf.readUInt32LE(0)
|
|
381
|
-
if (len <= 4) return null
|
|
382
|
-
const buf = Buffer.alloc(len)
|
|
383
|
-
if (!GetTokenInformation(token, 3, buf, len, lenBuf)) return null
|
|
384
|
-
return buf.readUInt32LE(0)
|
|
385
|
-
}
|
|
386
|
-
const parentTokenGroups = readTokenGroups(hToken)
|
|
387
|
-
const parentTokenPrivCount = readTokenPrivilegeCount(hToken)
|
|
388
|
-
const parentAdminEntry = parentTokenGroups?.find((g) => g.sid === ADMIN_SID)
|
|
389
|
-
let childTokenGroups: Array<{ sid: string; attrs: number }> | null = null
|
|
390
|
-
let childTokenPrivCount: number | null = null
|
|
391
|
-
const hSentinelProc = asBig(OpenProcess(0x1000 /* PROCESS_QUERY_LIMITED_INFORMATION */, 0, sentinel.pid))
|
|
392
|
-
if (hSentinelProc !== 0n) {
|
|
393
|
-
const hT = Buffer.alloc(8)
|
|
394
|
-
if (OpenProcessToken(hSentinelProc, 0x8 /* TOKEN_QUERY */, hT)) {
|
|
395
|
-
const childTok = hT.readBigUInt64LE(0)
|
|
396
|
-
childTokenGroups = readTokenGroups(childTok)
|
|
397
|
-
childTokenPrivCount = readTokenPrivilegeCount(childTok)
|
|
398
|
-
closeQuiet(childTok)
|
|
399
|
-
}
|
|
400
|
-
closeQuiet(hSentinelProc)
|
|
401
|
-
}
|
|
402
|
-
const childAdminEntry = childTokenGroups?.find((g) => g.sid === ADMIN_SID)
|
|
403
|
-
const childAdminDeny = childAdminEntry !== undefined
|
|
404
|
-
&& (childAdminEntry.attrs & SE_GROUP_USE_FOR_DENY_ONLY) !== 0
|
|
405
|
-
&& (childAdminEntry.attrs & SE_GROUP_ENABLED_MASK) === 0
|
|
406
|
-
const privDiffOk = childTokenPrivCount !== null && parentTokenPrivCount !== null
|
|
407
|
-
&& childTokenPrivCount < parentTokenPrivCount
|
|
408
|
-
criteria[0].status = childAdminDeny && privDiffOk ? 'PASS' : 'FAIL'
|
|
409
|
-
criteria[0].evidence = [
|
|
410
|
-
`父 token Administrators: ${parentAdminEntry ? `attrs=0x${parentAdminEntry.attrs.toString(16)}${(parentAdminEntry.attrs & SE_GROUP_ENABLED_MASK) !== 0 ? ' (提权 Enabled)' : ' (UAC filtered deny-only)'}` : '未在列'}`,
|
|
411
|
-
`子 token(父侧代读) Administrators: ${childAdminEntry ? `attrs=0x${childAdminEntry.attrs.toString(16)}${childAdminDeny ? ' (USE_FOR_DENY_ONLY)' : ' (非 deny-only!)'}` : '未在列/取证失败'}`,
|
|
412
|
-
`TokenPrivileges 计数: 父 ${parentTokenPrivCount ?? '?'} → 子 ${childTokenPrivCount ?? '?'} (DISABLE_MAX_PRIVILEGE)`,
|
|
413
|
-
`whoami 辅助证据: 父 Administrators 属性=${parentAdminAttrs ?? '未在列'}, 子=${childAdminAttrs ?? '未在列'}, 特权差分=[${stripped.join(', ') || '无'}]`,
|
|
414
|
-
`子 whoami /groups 原文(前160字符): ${childGroupsRes.out.slice(0, 160).replace(/\r?\n/g, ' ⏎ ') || '(空)'}${childGroupsRes.exitCode !== 0 ? ' —— 受限 token 自查被拒,故采用父侧代读取证' : ''}`,
|
|
415
|
-
...(parentAlreadyDeny ? ['注: 本机父进程为 UAC filtered token(父侧已 deny-only),deny-only 主断言不受影响(直接读子 token 属性位)'] : []),
|
|
416
|
-
].join(' | ')
|
|
417
|
-
|
|
418
|
-
// ② fork 炸弹子进程(CREATE_SUSPENDED → 挂 Job → Resume,消除子孙脱离 Job 的竞态)
|
|
419
|
-
const bomber = createRestrictedChild(hRestricted, nodeExe, [forkProbePath], { suspended: true })
|
|
420
|
-
const okAssignBomber = AssignProcessToJobObject(hJob, bomber.hProcess)
|
|
421
|
-
bomber.resume()
|
|
422
|
-
const inJobBuf = Buffer.alloc(4)
|
|
423
|
-
IsProcessInJob(bomber.hProcess, hJob, inJobBuf)
|
|
424
|
-
const bomberInJob = inJobBuf.readUInt32LE(0) !== 0
|
|
425
|
-
|
|
426
|
-
// ③ 配额内等待 fork 炸弹结束(探针自限时 20s)
|
|
427
|
-
const bomberWait = bomber.wait(90000)
|
|
428
|
-
const forkLine = bomber.outText().split(/\r?\n/).find((l) => l.startsWith('FORK_RESULT'))
|
|
429
|
-
let fork: { ok?: number; fail?: number; timeout?: boolean; samples?: Array<Record<string, unknown>>; mem?: number } = {}
|
|
430
|
-
try { fork = JSON.parse((forkLine ?? '').replace(/^FORK_RESULT\s+/, '') || '{}') } catch { /* 解析失败按空处理 */ }
|
|
431
|
-
|
|
432
|
-
// ④ KILL_ON_JOB_CLOSE 击杀验证:确认哨兵仍在跑(2s 等待超时=活着),关 Job 句柄,哨兵须在 15s 内被终止
|
|
433
|
-
const sentinelAlive = sentinel.wait(2000) === WAIT_TIMEOUT && sentinel.exitCode() === STILL_ACTIVE
|
|
434
|
-
CloseHandle(hJob) // 最后一个 Job 用户句柄关闭 → 触发 KILL_ON_JOB_CLOSE
|
|
435
|
-
const t0 = Date.now()
|
|
436
|
-
const sentinelKilledWait = sentinel.wait(15000)
|
|
437
|
-
const killElapsedMs = Date.now() - t0
|
|
438
|
-
const sentinelKilled = sentinelKilledWait === 0 && sentinel.exitCode() !== STILL_ACTIVE
|
|
439
|
-
|
|
440
|
-
// ⑤ 父进程存活:全部 Job 活动后父进程仍可正常派生子进程
|
|
441
|
-
const parentAlive = spawnSync(WHOAMI_EXE, ['/priv'], { encoding: 'buffer' }).status === 0
|
|
442
|
-
|
|
443
|
-
const quotaTriggered = (fork.fail ?? 0) > 0
|
|
444
|
-
const quotaSane = (fork.ok ?? 0) > 0 // 配额内进程仍可正常创建(非一刀切拒绝)
|
|
445
|
-
const c3Pass = okJobInfo && okAssignBomber && bomberInJob && quotaTriggered && quotaSane
|
|
446
|
-
&& sentinelAlive && sentinelKilled && parentAlive
|
|
447
|
-
criteria[2].status = c3Pass ? 'PASS' : 'FAIL'
|
|
448
|
-
criteria[2].evidence = [
|
|
449
|
-
`Job 限额: ProcessMemory=256MB + ActiveProcess=${JOB_ACTIVE_PROCESS_LIMIT} + KILL_ON_JOB_CLOSE(SetInformationJobObject=${okJobInfo ? 'OK' : 'FAIL'})`,
|
|
450
|
-
`fork 探针挂 Job: Assign=${okAssignBomber ? 'OK' : 'FAIL'}, IsProcessInJob=${bomberInJob}`,
|
|
451
|
-
`fork 炸弹(64 并发): 成功=${fork.ok ?? '?'} 被拒=${fork.fail ?? '?'}${fork.timeout ? ' (探针超时截断)' : ''}`,
|
|
452
|
-
`fork 诊断: 探针RSS=${fork.mem ? Math.round(fork.mem / 1048576) + 'MB' : '?'} 失败样本=${JSON.stringify(fork.samples ?? [])}`,
|
|
453
|
-
`KILL_ON_JOB_CLOSE: 哨兵存活确认=${sentinelAlive}, 关句柄后 ${killElapsedMs}ms 内被杀=${sentinelKilled}`,
|
|
454
|
-
`父进程存活: ${parentAlive}`,
|
|
455
|
-
...(bomberWait === WAIT_TIMEOUT ? ['告警: fork 探针未在 90s 内退出'] : []),
|
|
456
|
-
].join(' | ')
|
|
457
|
-
|
|
458
|
-
sentinel.cleanup()
|
|
459
|
-
bomber.cleanup()
|
|
460
|
-
rmSync(s25Dir, { recursive: true, force: true })
|
|
461
|
-
} catch (e) {
|
|
462
|
-
rmSync(s25Dir, { recursive: true, force: true })
|
|
463
|
-
throw e
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// ========== 判据 2(S26 阶段 3:写探针同进程验证,fail-closed 非静默)==========
|
|
467
|
-
// 同进程口径:探针在受限子进程内执行(非父进程代跑);允许区=%TEMP% 会话目录,
|
|
468
|
-
// 工作区外=管理员门控路径 C:\Windows\(写权限仅提权进程可获)。
|
|
469
|
-
const writeProbeJs = [
|
|
470
|
-
'const fs = require("fs");',
|
|
471
|
-
'const path = require("path");',
|
|
472
|
-
'const res = { insideOk: false, insideErr: null, outsideDenied: false, outsideErr: null, outsideSilent: false };',
|
|
473
|
-
'const insidePath = path.join(process.argv[2], "inside-probe.txt");',
|
|
474
|
-
'try { fs.writeFileSync(insidePath, "car-spike-probe"); fs.unlinkSync(insidePath); res.insideOk = true; }',
|
|
475
|
-
'catch (e) { res.insideErr = e.code || String(e); }',
|
|
476
|
-
'const outsidePath = "C:\\\\Windows\\\\car-spike-outside-" + process.pid + ".tmp";',
|
|
477
|
-
'try { fs.writeFileSync(outsidePath, "car-spike-probe"); res.outsideSilent = true; try { fs.unlinkSync(outsidePath); } catch (e2) {} }',
|
|
478
|
-
'catch (e) { res.outsideDenied = true; res.outsideErr = e.code || String(e); }',
|
|
479
|
-
'console.log("WRITE_RESULT " + JSON.stringify(res));',
|
|
480
|
-
].join('\n')
|
|
481
|
-
const s26Dir = mkdtempSync(path.join(tmpdir(), 'car-s26-'))
|
|
482
|
-
const writeProbePath = path.join(s26Dir, 'write-probe.js')
|
|
483
|
-
writeFileSync(writeProbePath, writeProbeJs, 'utf8')
|
|
484
|
-
try {
|
|
485
|
-
const runWriteProbe = (tok: bigint) => {
|
|
486
|
-
const c = createRestrictedChild(tok, process.execPath, [writeProbePath, s26Dir])
|
|
487
|
-
c.wait(60000)
|
|
488
|
-
const line = c.outText().split(/\r?\n/).find((l) => l.startsWith('WRITE_RESULT'))
|
|
489
|
-
c.cleanup()
|
|
490
|
-
try { return JSON.parse((line ?? '').replace(/^WRITE_RESULT\s+/, '') || '{}') } catch { return {} as Record<string, unknown> }
|
|
491
|
-
}
|
|
492
|
-
const restrictedWrite = runWriteProbe(hRestricted)
|
|
493
|
-
const unrestrictedWrite = runWriteProbe(hToken) // 对照组:未受限 token(父进程非提权)
|
|
494
|
-
const deniedCodes = ['EPERM', 'EACCES']
|
|
495
|
-
const c2Pass = restrictedWrite.insideOk === true
|
|
496
|
-
&& restrictedWrite.outsideDenied === true
|
|
497
|
-
&& restrictedWrite.outsideSilent !== true
|
|
498
|
-
&& deniedCodes.includes(String(restrictedWrite.outsideErr))
|
|
499
|
-
criteria[1].status = c2Pass ? 'PASS' : 'FAIL'
|
|
500
|
-
criteria[1].evidence = [
|
|
501
|
-
`受限子进程写沙箱允许区(%TEMP% 会话目录): ${restrictedWrite.insideOk ? 'OK' : `失败(${restrictedWrite.insideErr})`}`,
|
|
502
|
-
`受限子进程写工作区外管理员门控路径(C:\\Windows\\): ${restrictedWrite.outsideDenied ? `ACCESS_DENIED(${restrictedWrite.outsideErr})` : (restrictedWrite.outsideSilent ? '静默成功——fail-closed 破防' : `异常(${restrictedWrite.outsideErr})`)}`,
|
|
503
|
-
`对照组(未受限 token 子进程, 父进程非提权): outsideDenied=${unrestrictedWrite.outsideDenied}, err=${unrestrictedWrite.outsideErr}`,
|
|
504
|
-
'注: 本机父进程为非提权 UAC filtered token,对照组同拒属预期;强差分=提权终端复跑(父侧可写、受限子侧被拒)。受限 token 已切断管理员写入通道(判据 1: Administrators deny-only + 特权剥离),写失败以显式异常抛出(fail-closed,非静默降级)',
|
|
505
|
-
].join(' | ')
|
|
506
|
-
rmSync(s26Dir, { recursive: true, force: true })
|
|
507
|
-
} catch (e) {
|
|
508
|
-
rmSync(s26Dir, { recursive: true, force: true })
|
|
509
|
-
throw e
|
|
510
|
-
}
|
|
511
|
-
} finally {
|
|
512
|
-
closeQuiet(hRestricted)
|
|
513
|
-
closeQuiet(hToken)
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
printReport()
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function printReport() {
|
|
520
|
-
console.log('')
|
|
521
|
-
console.log('---- 四判据状态(adr/win32-spike.md)----')
|
|
522
|
-
for (const c of criteria) {
|
|
523
|
-
console.log(`判据 ${c.id} [${c.status}] ${c.name}`)
|
|
524
|
-
if (c.evidence) console.log(` 证据: ${c.evidence}`)
|
|
525
|
-
}
|
|
526
|
-
const allPass = criteria.every((c) => c.status === 'PASS')
|
|
527
|
-
const verdict = allPass
|
|
528
|
-
? 'FOUR-CRITERIA-ALL-PASS(1.0-GO-2 判据 1+2 达成;1.0-GO-3 判据 3 目标项达成)'
|
|
529
|
-
: `SPIKE-INCOMPLETE: ${criteria.filter((c) => c.status !== 'PASS').map((c) => `判据${c.id}=${c.status}`).join(', ')}`
|
|
530
|
-
console.log('')
|
|
531
|
-
console.log(`VERDICT: ${verdict}`)
|
|
532
|
-
// 退出码语义:FAIL=1(真失败,CI 门禁拦截);PASS/SKIP=0(SKIP=环境不支持,
|
|
533
|
-
// ubuntu 测试门禁依赖 SKIP 正常退出;windows runner 上 koffi 随 optionalDependencies 必装)
|
|
534
|
-
if (criteria.some((c) => c.status === 'FAIL')) process.exitCode = 1
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
main().catch((e) => { console.error('FATAL:', e instanceof Error ? `${e.message}\n${e.stack}` : e); process.exitCode = 1 })
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"comment": "J-09 contract 基线:SessionEventMap/SessionEvent 签名字段集。只增不删——本文件中任何键从 src/session/log.ts 的当前签名中消失即 FAIL(M2部署设计增补 §2.3 J-09)",
|
|
3
|
-
"eventKinds": ["user", "assistant", "toolCall", "toolResult", "turnEnd", "goalUpdate", "hostRaw", "fork", "system"],
|
|
4
|
-
"actors": ["user", "model", "plugin", "runtime"],
|
|
5
|
-
"sessionEventFields": ["seq", "ts", "actor", "kind", "turnId", "payload", "meta", "prevHash", "hash"],
|
|
6
|
-
"sessionLogApi": ["append", "snapshotModelRequest", "verifyChain", "deriveMessages", "assertModelVisibleLogged", "stats", "fromEvents", "exportJSONL"]
|
|
7
|
-
}
|