@holoscript/holosystem 0.2.2 → 0.2.3
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 +547 -0
- package/bin/holosystem.mjs +427 -7
- package/docs/native-build-threat-model.md +150 -0
- package/docs/vm-launch-threat-model.md +91 -0
- package/package.json +3 -2
- package/src/index.mjs +37 -0
- package/src/native-build.mjs +970 -0
- package/src/substrate-debian-release.mjs +852 -0
- package/src/substrate-import-debian.mjs +979 -0
- package/src/substrate-import.mjs +578 -0
- package/src/substrate.mjs +789 -0
- package/src/vm-launch.mjs +927 -0
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import {
|
|
4
|
+
copyFileSync,
|
|
5
|
+
lstatSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
mkdtempSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
rmSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
14
|
+
|
|
15
|
+
export const HOLOSYSTEM_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.vm-launch-plan.v1';
|
|
16
|
+
export const HOLOSYSTEM_VM_EXECUTOR_SCHEMA = 'holoscript.holosystem.vm-executor.v1';
|
|
17
|
+
export const HOLOSYSTEM_VM_ASSET_SCHEMA = 'holoscript.holosystem.vm-asset.v1';
|
|
18
|
+
export const HOLOSYSTEM_VM_LAUNCH_RECEIPT_SCHEMA = 'holoscript.holosystem.vm-launch-receipt.v1';
|
|
19
|
+
export const HOLOSYSTEM_WHPX_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.whpx-vm-launch-plan.v1';
|
|
20
|
+
export const HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
21
|
+
'holoscript.holosystem.whpx-vm-launch-receipt.v1';
|
|
22
|
+
|
|
23
|
+
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
24
|
+
const EXECUTOR_BINARY = 'qemu-system-x86_64.exe';
|
|
25
|
+
const MAX_RUNTIME_FILES = 1024;
|
|
26
|
+
const MAX_RUNTIME_FILE_BYTES = 128 * 1024 * 1024;
|
|
27
|
+
const MAX_RUNTIME_BYTES = 512 * 1024 * 1024;
|
|
28
|
+
const MAX_KERNEL_BYTES = 128 * 1024 * 1024;
|
|
29
|
+
const MAX_INITRD_BYTES = 512 * 1024 * 1024;
|
|
30
|
+
const MAX_CONSOLE_BYTES = 1024 * 1024;
|
|
31
|
+
const EXPECTED_EXIT_CODE = 33;
|
|
32
|
+
const TCG_POLICY = Object.freeze({
|
|
33
|
+
accelerator: 'tcg',
|
|
34
|
+
userConfiguration: 'disabled',
|
|
35
|
+
defaultDevices: 'disabled',
|
|
36
|
+
network: 'none',
|
|
37
|
+
display: 'none',
|
|
38
|
+
monitor: 'none',
|
|
39
|
+
usb: 'disabled',
|
|
40
|
+
reboot: 'disabled',
|
|
41
|
+
processEnvironment: 'minimal',
|
|
42
|
+
guestSignal: 'serial-digest-and-debug-exit',
|
|
43
|
+
diagnostics: 'none',
|
|
44
|
+
});
|
|
45
|
+
const WHPX_POLICY = Object.freeze({
|
|
46
|
+
accelerator: 'whpx',
|
|
47
|
+
userConfiguration: 'disabled',
|
|
48
|
+
defaultDevices: 'disabled',
|
|
49
|
+
network: 'none',
|
|
50
|
+
display: 'none',
|
|
51
|
+
monitor: 'none',
|
|
52
|
+
usb: 'disabled',
|
|
53
|
+
reboot: 'disabled',
|
|
54
|
+
processEnvironment: 'minimal',
|
|
55
|
+
guestSignal: 'serial-digest-and-debug-exit',
|
|
56
|
+
diagnostics: 'pinned-digest',
|
|
57
|
+
});
|
|
58
|
+
const BOUNDARIES = Object.freeze([
|
|
59
|
+
'guest-artifact-provenance',
|
|
60
|
+
'hardware-hypervisor-acceleration',
|
|
61
|
+
'host-crash-dump-custody',
|
|
62
|
+
'host-os-and-emulator-correctness',
|
|
63
|
+
'host-process-isolation',
|
|
64
|
+
'measured-boot-and-firmware',
|
|
65
|
+
'qemu-runtime-supply-chain',
|
|
66
|
+
'side-channel-resistance',
|
|
67
|
+
]);
|
|
68
|
+
const TCG_ADAPTER = Object.freeze({
|
|
69
|
+
accelerator: 'tcg',
|
|
70
|
+
accelerationName: 'qemu-tcg',
|
|
71
|
+
hardwareBacked: false,
|
|
72
|
+
planSchema: HOLOSYSTEM_VM_LAUNCH_PLAN_SCHEMA,
|
|
73
|
+
receiptSchema: HOLOSYSTEM_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
74
|
+
policy: TCG_POLICY,
|
|
75
|
+
requiresDiagnosticsDigest: false,
|
|
76
|
+
});
|
|
77
|
+
const WHPX_ADAPTER = Object.freeze({
|
|
78
|
+
accelerator: 'whpx',
|
|
79
|
+
accelerationName: 'qemu-whpx',
|
|
80
|
+
hardwareBacked: true,
|
|
81
|
+
planSchema: HOLOSYSTEM_WHPX_VM_LAUNCH_PLAN_SCHEMA,
|
|
82
|
+
receiptSchema: HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
83
|
+
policy: WHPX_POLICY,
|
|
84
|
+
requiresDiagnosticsDigest: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function isRecord(value) {
|
|
88
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function issue(issues, code, path, message) {
|
|
92
|
+
issues.push({ code, path, message });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hashBytes(value) {
|
|
96
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function hashJson(value) {
|
|
100
|
+
return hashBytes(JSON.stringify(value));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function lexical(left, right) {
|
|
104
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function validId(value) {
|
|
108
|
+
return typeof value === 'string' && /^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(value);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function portableRuntimePath(value) {
|
|
112
|
+
return (
|
|
113
|
+
typeof value === 'string' &&
|
|
114
|
+
value.length > 0 &&
|
|
115
|
+
value.length <= 512 &&
|
|
116
|
+
!value.includes('\\') &&
|
|
117
|
+
!value.includes('\0') &&
|
|
118
|
+
!value.startsWith('/') &&
|
|
119
|
+
!/^[A-Za-z]:/u.test(value) &&
|
|
120
|
+
!/(?:^|\/)\.\.?(?:\/|$)/u.test(value) &&
|
|
121
|
+
!/[\r\n]/u.test(value)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function knownKeys(value, allowed, path, issues) {
|
|
126
|
+
if (!isRecord(value)) return;
|
|
127
|
+
for (const key of Object.keys(value)) {
|
|
128
|
+
if (!allowed.has(key)) {
|
|
129
|
+
issue(
|
|
130
|
+
issues,
|
|
131
|
+
'vm-launch-field-unknown',
|
|
132
|
+
path ? `${path}.${key}` : key,
|
|
133
|
+
'Field is not part of the declarative machine-VM launch vocabulary.'
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function inspectDigest(value, path, issues) {
|
|
140
|
+
if (!DIGEST_PATTERN.test(value || '')) {
|
|
141
|
+
issue(issues, 'vm-launch-digest-invalid', path, 'Value must be a lowercase SHA-256 digest.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizedPlan(plan) {
|
|
146
|
+
return {
|
|
147
|
+
schema: plan.schema,
|
|
148
|
+
id: plan.id,
|
|
149
|
+
host: { os: plan.host.os, architecture: plan.host.architecture },
|
|
150
|
+
executor: {
|
|
151
|
+
kind: plan.executor.kind,
|
|
152
|
+
binary: plan.executor.binary,
|
|
153
|
+
binaryDigest: plan.executor.binaryDigest,
|
|
154
|
+
runtimeDigest: plan.executor.runtimeDigest,
|
|
155
|
+
},
|
|
156
|
+
target: {
|
|
157
|
+
architecture: plan.target.architecture,
|
|
158
|
+
machine: plan.target.machine,
|
|
159
|
+
accelerator: plan.target.accelerator,
|
|
160
|
+
},
|
|
161
|
+
guest: {
|
|
162
|
+
kernelDigest: plan.guest.kernelDigest,
|
|
163
|
+
initrdDigest: plan.guest.initrdDigest,
|
|
164
|
+
expectedConsoleDigest: plan.guest.expectedConsoleDigest,
|
|
165
|
+
expectedDiagnosticsDigest: plan.guest.expectedDiagnosticsDigest || null,
|
|
166
|
+
},
|
|
167
|
+
resources: {
|
|
168
|
+
memoryMiB: plan.resources.memoryMiB,
|
|
169
|
+
cpus: plan.resources.cpus,
|
|
170
|
+
timeoutSeconds: plan.resources.timeoutSeconds,
|
|
171
|
+
},
|
|
172
|
+
launches: plan.launches,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function inspectVmLaunchPlanForAdapter(plan, adapter) {
|
|
177
|
+
const issues = [];
|
|
178
|
+
if (!isRecord(plan)) {
|
|
179
|
+
issue(issues, 'vm-launch-plan-invalid', '$', 'VM launch plan must be a JSON object.');
|
|
180
|
+
return { ready: false, issues };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
knownKeys(
|
|
184
|
+
plan,
|
|
185
|
+
new Set(['schema', 'id', 'host', 'executor', 'target', 'guest', 'resources', 'launches']),
|
|
186
|
+
'',
|
|
187
|
+
issues
|
|
188
|
+
);
|
|
189
|
+
if (plan.schema !== adapter.planSchema) {
|
|
190
|
+
issue(issues, 'vm-launch-schema-mismatch', 'schema', `Expected ${adapter.planSchema}.`);
|
|
191
|
+
}
|
|
192
|
+
if (!validId(plan.id)) {
|
|
193
|
+
issue(issues, 'vm-launch-id-invalid', 'id', 'Launch id must be a portable identifier.');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
knownKeys(plan.host, new Set(['os', 'architecture']), 'host', issues);
|
|
197
|
+
if (plan.host?.os !== 'windows' || plan.host?.architecture !== 'amd64') {
|
|
198
|
+
issue(
|
|
199
|
+
issues,
|
|
200
|
+
'vm-launch-host-unsupported',
|
|
201
|
+
'host',
|
|
202
|
+
'This tracer supports only a Windows AMD64 host.'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
knownKeys(
|
|
207
|
+
plan.executor,
|
|
208
|
+
new Set(['kind', 'binary', 'binaryDigest', 'runtimeDigest']),
|
|
209
|
+
'executor',
|
|
210
|
+
issues
|
|
211
|
+
);
|
|
212
|
+
if (plan.executor?.kind !== 'qemu-system' || plan.executor?.binary !== EXECUTOR_BINARY) {
|
|
213
|
+
issue(
|
|
214
|
+
issues,
|
|
215
|
+
'vm-launch-executor-unsupported',
|
|
216
|
+
'executor',
|
|
217
|
+
`This tracer supports only ${EXECUTOR_BINARY}.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
inspectDigest(plan.executor?.binaryDigest, 'executor.binaryDigest', issues);
|
|
221
|
+
inspectDigest(plan.executor?.runtimeDigest, 'executor.runtimeDigest', issues);
|
|
222
|
+
|
|
223
|
+
knownKeys(plan.target, new Set(['architecture', 'machine', 'accelerator']), 'target', issues);
|
|
224
|
+
if (plan.target?.architecture !== 'amd64' || plan.target?.machine !== 'q35') {
|
|
225
|
+
issue(
|
|
226
|
+
issues,
|
|
227
|
+
'vm-launch-target-unsupported',
|
|
228
|
+
'target',
|
|
229
|
+
'This tracer supports only the explicit AMD64 q35 machine target.'
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (plan.target?.accelerator !== adapter.accelerator) {
|
|
233
|
+
issue(
|
|
234
|
+
issues,
|
|
235
|
+
'vm-launch-accelerator-unsupported',
|
|
236
|
+
'target.accelerator',
|
|
237
|
+
`This adapter requires the explicit ${adapter.accelerator} accelerator and never falls back.`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
knownKeys(
|
|
242
|
+
plan.guest,
|
|
243
|
+
new Set([
|
|
244
|
+
'kernelDigest',
|
|
245
|
+
'initrdDigest',
|
|
246
|
+
'expectedConsoleDigest',
|
|
247
|
+
...(adapter.requiresDiagnosticsDigest ? ['expectedDiagnosticsDigest'] : []),
|
|
248
|
+
]),
|
|
249
|
+
'guest',
|
|
250
|
+
issues
|
|
251
|
+
);
|
|
252
|
+
inspectDigest(plan.guest?.kernelDigest, 'guest.kernelDigest', issues);
|
|
253
|
+
inspectDigest(plan.guest?.initrdDigest, 'guest.initrdDigest', issues);
|
|
254
|
+
inspectDigest(plan.guest?.expectedConsoleDigest, 'guest.expectedConsoleDigest', issues);
|
|
255
|
+
if (adapter.requiresDiagnosticsDigest) {
|
|
256
|
+
inspectDigest(plan.guest?.expectedDiagnosticsDigest, 'guest.expectedDiagnosticsDigest', issues);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
knownKeys(plan.resources, new Set(['memoryMiB', 'cpus', 'timeoutSeconds']), 'resources', issues);
|
|
260
|
+
if (
|
|
261
|
+
plan.resources?.memoryMiB !== 128 ||
|
|
262
|
+
plan.resources?.cpus !== 1 ||
|
|
263
|
+
!Number.isInteger(plan.resources?.timeoutSeconds) ||
|
|
264
|
+
plan.resources.timeoutSeconds < 5 ||
|
|
265
|
+
plan.resources.timeoutSeconds > 120
|
|
266
|
+
) {
|
|
267
|
+
issue(
|
|
268
|
+
issues,
|
|
269
|
+
'vm-launch-limit-invalid',
|
|
270
|
+
'resources',
|
|
271
|
+
'Resources require 128 MiB, one CPU, and a timeout from 5 to 120 seconds.'
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (plan.launches !== 2) {
|
|
275
|
+
issue(
|
|
276
|
+
issues,
|
|
277
|
+
'vm-launch-count-invalid',
|
|
278
|
+
'launches',
|
|
279
|
+
'Exactly two clean launches are required for a deterministic receipt.'
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { ready: issues.length === 0, issues };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function inspectVmLaunchPlan(plan) {
|
|
287
|
+
return inspectVmLaunchPlanForAdapter(plan, TCG_ADAPTER);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function inspectWhpxVmLaunchPlan(plan) {
|
|
291
|
+
return inspectVmLaunchPlanForAdapter(plan, WHPX_ADAPTER);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function scanRuntime(directory, issues) {
|
|
295
|
+
const files = [];
|
|
296
|
+
let totalBytes = 0;
|
|
297
|
+
let stopped = false;
|
|
298
|
+
|
|
299
|
+
function visit(current) {
|
|
300
|
+
if (stopped) return;
|
|
301
|
+
let entries;
|
|
302
|
+
try {
|
|
303
|
+
entries = readdirSync(current, { withFileTypes: true }).sort((left, right) =>
|
|
304
|
+
lexical(left.name, right.name)
|
|
305
|
+
);
|
|
306
|
+
} catch {
|
|
307
|
+
issue(issues, 'vm-executor-unreadable', 'executorDirectory', 'Runtime could not be read.');
|
|
308
|
+
stopped = true;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const entry of entries) {
|
|
313
|
+
if (stopped) return;
|
|
314
|
+
const absolute = join(current, entry.name);
|
|
315
|
+
const portable = relative(directory, absolute).split(sep).join('/');
|
|
316
|
+
let stats;
|
|
317
|
+
try {
|
|
318
|
+
stats = lstatSync(absolute);
|
|
319
|
+
} catch {
|
|
320
|
+
issue(
|
|
321
|
+
issues,
|
|
322
|
+
'vm-executor-unreadable',
|
|
323
|
+
`runtime.files.${portable}`,
|
|
324
|
+
'A runtime entry could not be inspected.'
|
|
325
|
+
);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (stats.isSymbolicLink()) {
|
|
329
|
+
issue(
|
|
330
|
+
issues,
|
|
331
|
+
'vm-executor-link-forbidden',
|
|
332
|
+
`runtime.files.${portable}`,
|
|
333
|
+
'Symbolic links and reparse-point indirection are not executor inputs.'
|
|
334
|
+
);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (stats.isDirectory()) {
|
|
338
|
+
visit(absolute);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (!stats.isFile()) {
|
|
342
|
+
issue(
|
|
343
|
+
issues,
|
|
344
|
+
'vm-executor-type-forbidden',
|
|
345
|
+
`runtime.files.${portable}`,
|
|
346
|
+
'Only regular runtime files are accepted.'
|
|
347
|
+
);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (!portableRuntimePath(portable)) {
|
|
351
|
+
issue(
|
|
352
|
+
issues,
|
|
353
|
+
'vm-executor-path-invalid',
|
|
354
|
+
`runtime.files.${portable}`,
|
|
355
|
+
'Runtime paths must be portable relative paths.'
|
|
356
|
+
);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (stats.size > MAX_RUNTIME_FILE_BYTES) {
|
|
360
|
+
issue(
|
|
361
|
+
issues,
|
|
362
|
+
'vm-executor-file-too-large',
|
|
363
|
+
`runtime.files.${portable}`,
|
|
364
|
+
'A runtime file exceeds the per-file limit.'
|
|
365
|
+
);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
let content;
|
|
369
|
+
try {
|
|
370
|
+
content = readFileSync(absolute);
|
|
371
|
+
} catch {
|
|
372
|
+
issue(
|
|
373
|
+
issues,
|
|
374
|
+
'vm-executor-unreadable',
|
|
375
|
+
`runtime.files.${portable}`,
|
|
376
|
+
'A runtime file could not be read.'
|
|
377
|
+
);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
totalBytes += content.length;
|
|
381
|
+
files.push({ path: portable, bytes: content.length, digest: hashBytes(content) });
|
|
382
|
+
if (files.length > MAX_RUNTIME_FILES) {
|
|
383
|
+
issue(
|
|
384
|
+
issues,
|
|
385
|
+
'vm-executor-file-limit',
|
|
386
|
+
'runtime.files',
|
|
387
|
+
'Runtime exceeds the file-count limit.'
|
|
388
|
+
);
|
|
389
|
+
stopped = true;
|
|
390
|
+
} else if (totalBytes > MAX_RUNTIME_BYTES) {
|
|
391
|
+
issue(
|
|
392
|
+
issues,
|
|
393
|
+
'vm-executor-too-large',
|
|
394
|
+
'runtime.files',
|
|
395
|
+
'Runtime exceeds the aggregate byte limit.'
|
|
396
|
+
);
|
|
397
|
+
stopped = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
visit(directory);
|
|
403
|
+
return files.sort((left, right) => lexical(left.path, right.path));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function inspectVmExecutor({ executorDirectory } = {}) {
|
|
407
|
+
const issues = [];
|
|
408
|
+
const directory = typeof executorDirectory === 'string' ? resolve(executorDirectory) : null;
|
|
409
|
+
let directoryReady = false;
|
|
410
|
+
try {
|
|
411
|
+
const stats = lstatSync(directory);
|
|
412
|
+
directoryReady = stats.isDirectory() && !stats.isSymbolicLink();
|
|
413
|
+
} catch {
|
|
414
|
+
directoryReady = false;
|
|
415
|
+
}
|
|
416
|
+
if (!directoryReady) {
|
|
417
|
+
issue(
|
|
418
|
+
issues,
|
|
419
|
+
'vm-executor-directory-invalid',
|
|
420
|
+
'executorDirectory',
|
|
421
|
+
'Executor input must be a real caller-owned directory.'
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const files = directoryReady ? scanRuntime(directory, issues) : [];
|
|
426
|
+
const binary = files.find((entry) => entry.path === EXECUTOR_BINARY);
|
|
427
|
+
if (!binary) {
|
|
428
|
+
issue(
|
|
429
|
+
issues,
|
|
430
|
+
'vm-executor-binary-missing',
|
|
431
|
+
'runtime.binary',
|
|
432
|
+
`Runtime must contain ${EXECUTOR_BINARY} at its root.`
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const envelope = { schema: HOLOSYSTEM_VM_EXECUTOR_SCHEMA, binary: EXECUTOR_BINARY, files };
|
|
436
|
+
return {
|
|
437
|
+
schema: HOLOSYSTEM_VM_EXECUTOR_SCHEMA,
|
|
438
|
+
ready: issues.length === 0,
|
|
439
|
+
binary: EXECUTOR_BINARY,
|
|
440
|
+
binaryDigest: binary?.digest || null,
|
|
441
|
+
digest: issues.length === 0 ? hashJson(envelope) : null,
|
|
442
|
+
files,
|
|
443
|
+
summary: {
|
|
444
|
+
files: files.length,
|
|
445
|
+
bytes: files.reduce((total, file) => total + file.bytes, 0),
|
|
446
|
+
},
|
|
447
|
+
issues,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function inspectVmLaunchAsset({ assetPath, kind } = {}) {
|
|
452
|
+
const issues = [];
|
|
453
|
+
if (!['kernel', 'initrd'].includes(kind)) {
|
|
454
|
+
issue(issues, 'vm-asset-kind-invalid', 'kind', 'Asset kind must be kernel or initrd.');
|
|
455
|
+
}
|
|
456
|
+
const absolute = typeof assetPath === 'string' ? resolve(assetPath) : null;
|
|
457
|
+
let content = null;
|
|
458
|
+
try {
|
|
459
|
+
const stats = lstatSync(absolute);
|
|
460
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
|
|
461
|
+
const maximum = kind === 'kernel' ? MAX_KERNEL_BYTES : MAX_INITRD_BYTES;
|
|
462
|
+
if (stats.size > maximum) {
|
|
463
|
+
issue(issues, 'vm-asset-too-large', 'assetPath', 'Guest asset exceeds its byte limit.');
|
|
464
|
+
} else {
|
|
465
|
+
content = readFileSync(absolute);
|
|
466
|
+
}
|
|
467
|
+
} catch {
|
|
468
|
+
issue(
|
|
469
|
+
issues,
|
|
470
|
+
'vm-asset-file-invalid',
|
|
471
|
+
'assetPath',
|
|
472
|
+
'Guest asset must be a readable regular caller-owned file.'
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return {
|
|
476
|
+
schema: HOLOSYSTEM_VM_ASSET_SCHEMA,
|
|
477
|
+
ready: issues.length === 0,
|
|
478
|
+
kind: ['kernel', 'initrd'].includes(kind) ? kind : null,
|
|
479
|
+
digest: content && issues.length === 0 ? hashBytes(content) : null,
|
|
480
|
+
bytes: content && issues.length === 0 ? content.length : null,
|
|
481
|
+
issues,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function logSummary(value) {
|
|
486
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
|
|
487
|
+
return { bytes: bytes.length, digest: hashBytes(bytes) };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function normalizedDiagnostics(value, command) {
|
|
491
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
|
|
492
|
+
if (bytes.length === 0) return bytes;
|
|
493
|
+
const prefix = Buffer.from(`${command}: `, 'utf8');
|
|
494
|
+
const chunks = [];
|
|
495
|
+
let offset = 0;
|
|
496
|
+
let scan = 0;
|
|
497
|
+
for (let index = bytes.indexOf(prefix, scan); index !== -1; index = bytes.indexOf(prefix, scan)) {
|
|
498
|
+
if (index !== 0 && bytes[index - 1] !== 0x0a) {
|
|
499
|
+
scan = index + 1;
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
chunks.push(bytes.subarray(offset, index));
|
|
503
|
+
offset = index + prefix.length;
|
|
504
|
+
scan = offset;
|
|
505
|
+
}
|
|
506
|
+
chunks.push(bytes.subarray(offset));
|
|
507
|
+
return Buffer.concat(chunks);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function finishReceipt(receipt) {
|
|
511
|
+
return { ...receipt, receiptHash: hashJson(receipt) };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function baseReceipt(plan, now, issues, adapter) {
|
|
515
|
+
return {
|
|
516
|
+
schema: adapter.receiptSchema,
|
|
517
|
+
generatedAt: now.toISOString(),
|
|
518
|
+
id: validId(plan?.id) ? plan.id : null,
|
|
519
|
+
status: 'blocked',
|
|
520
|
+
verified: false,
|
|
521
|
+
deterministic: false,
|
|
522
|
+
hardwareBacked: false,
|
|
523
|
+
acceleration: {
|
|
524
|
+
adapter: adapter.accelerationName,
|
|
525
|
+
evidence: adapter.hardwareBacked ? 'two-explicit-successful-launches' : 'software-emulation',
|
|
526
|
+
verified: false,
|
|
527
|
+
},
|
|
528
|
+
isolation: {
|
|
529
|
+
hostProcess: 'ambient-windows-process',
|
|
530
|
+
verified: false,
|
|
531
|
+
},
|
|
532
|
+
measurementDigest: null,
|
|
533
|
+
executor: {
|
|
534
|
+
kind: plan?.executor?.kind === 'qemu-system' ? 'qemu-system' : null,
|
|
535
|
+
binaryDigest: DIGEST_PATTERN.test(plan?.executor?.binaryDigest || '')
|
|
536
|
+
? plan.executor.binaryDigest
|
|
537
|
+
: null,
|
|
538
|
+
runtimeDigest: DIGEST_PATTERN.test(plan?.executor?.runtimeDigest || '')
|
|
539
|
+
? plan.executor.runtimeDigest
|
|
540
|
+
: null,
|
|
541
|
+
},
|
|
542
|
+
guest: {
|
|
543
|
+
kernelDigest: DIGEST_PATTERN.test(plan?.guest?.kernelDigest || '')
|
|
544
|
+
? plan.guest.kernelDigest
|
|
545
|
+
: null,
|
|
546
|
+
initrdDigest: DIGEST_PATTERN.test(plan?.guest?.initrdDigest || '')
|
|
547
|
+
? plan.guest.initrdDigest
|
|
548
|
+
: null,
|
|
549
|
+
expectedConsoleDigest: DIGEST_PATTERN.test(plan?.guest?.expectedConsoleDigest || '')
|
|
550
|
+
? plan.guest.expectedConsoleDigest
|
|
551
|
+
: null,
|
|
552
|
+
},
|
|
553
|
+
target:
|
|
554
|
+
plan?.target?.architecture === 'amd64' &&
|
|
555
|
+
plan?.target?.machine === 'q35' &&
|
|
556
|
+
plan?.target?.accelerator === adapter.accelerator
|
|
557
|
+
? { architecture: 'amd64', machine: 'q35', accelerator: adapter.accelerator }
|
|
558
|
+
: null,
|
|
559
|
+
resources:
|
|
560
|
+
isRecord(plan?.resources) &&
|
|
561
|
+
Number.isInteger(plan.resources.memoryMiB) &&
|
|
562
|
+
Number.isInteger(plan.resources.cpus) &&
|
|
563
|
+
Number.isInteger(plan.resources.timeoutSeconds)
|
|
564
|
+
? {
|
|
565
|
+
memoryMiB: plan.resources.memoryMiB,
|
|
566
|
+
cpus: plan.resources.cpus,
|
|
567
|
+
timeoutSeconds: plan.resources.timeoutSeconds,
|
|
568
|
+
}
|
|
569
|
+
: null,
|
|
570
|
+
policy: { ...adapter.policy },
|
|
571
|
+
launches: [],
|
|
572
|
+
coverage: {
|
|
573
|
+
includedLayers: [],
|
|
574
|
+
missingLayers: [
|
|
575
|
+
'guest-artifact-measurement',
|
|
576
|
+
'hardware-hypervisor-acceleration',
|
|
577
|
+
'host-process-isolation',
|
|
578
|
+
'machine-vm-launch',
|
|
579
|
+
'virtual-device-minimization',
|
|
580
|
+
],
|
|
581
|
+
},
|
|
582
|
+
boundaries: BOUNDARIES.filter(
|
|
583
|
+
(layer) => !(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration')
|
|
584
|
+
),
|
|
585
|
+
issues,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function qemuArguments(plan, kernelPath, initrdPath, adapter) {
|
|
590
|
+
return [
|
|
591
|
+
'-no-user-config',
|
|
592
|
+
'-nodefaults',
|
|
593
|
+
'-machine',
|
|
594
|
+
`q35,accel=${adapter.accelerator},usb=off`,
|
|
595
|
+
...(adapter === TCG_ADAPTER ? ['-cpu', 'max'] : []),
|
|
596
|
+
'-m',
|
|
597
|
+
`${plan.resources.memoryMiB}M`,
|
|
598
|
+
'-smp',
|
|
599
|
+
String(plan.resources.cpus),
|
|
600
|
+
'-display',
|
|
601
|
+
'none',
|
|
602
|
+
'-serial',
|
|
603
|
+
'stdio',
|
|
604
|
+
'-monitor',
|
|
605
|
+
'none',
|
|
606
|
+
'-nic',
|
|
607
|
+
'none',
|
|
608
|
+
'-no-reboot',
|
|
609
|
+
'-kernel',
|
|
610
|
+
kernelPath,
|
|
611
|
+
'-initrd',
|
|
612
|
+
initrdPath,
|
|
613
|
+
'-append',
|
|
614
|
+
'console=ttyS0,115200 quiet loglevel=0 rdinit=/init panic=-1 random.trust_cpu=off',
|
|
615
|
+
'-device',
|
|
616
|
+
'isa-debug-exit,iobase=0xf4,iosize=0x04',
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function minimalEnvironment(snapshotRoot, runtimeDirectory) {
|
|
621
|
+
const env = {
|
|
622
|
+
HOME: snapshotRoot,
|
|
623
|
+
USERPROFILE: snapshotRoot,
|
|
624
|
+
TEMP: snapshotRoot,
|
|
625
|
+
TMP: snapshotRoot,
|
|
626
|
+
PATH: runtimeDirectory,
|
|
627
|
+
LANG: 'C',
|
|
628
|
+
LC_ALL: 'C',
|
|
629
|
+
};
|
|
630
|
+
if (typeof process.env.SystemRoot === 'string') env.SystemRoot = process.env.SystemRoot;
|
|
631
|
+
if (typeof process.env.WINDIR === 'string') env.WINDIR = process.env.WINDIR;
|
|
632
|
+
return env;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function copyRuntimeSnapshot(sourceDirectory, report, destinationDirectory) {
|
|
636
|
+
mkdirSync(destinationDirectory, { recursive: true });
|
|
637
|
+
for (const file of report.files) {
|
|
638
|
+
const target = join(destinationDirectory, file.path);
|
|
639
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
640
|
+
copyFileSync(join(sourceDirectory, file.path), target);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function launchSnapshotMatches(plan, runtimeDirectory, kernelPath, initrdPath) {
|
|
645
|
+
const executor = inspectVmExecutor({ executorDirectory: runtimeDirectory });
|
|
646
|
+
const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
|
|
647
|
+
const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
|
|
648
|
+
return (
|
|
649
|
+
executor.ready &&
|
|
650
|
+
executor.digest === plan.executor.runtimeDigest &&
|
|
651
|
+
executor.binaryDigest === plan.executor.binaryDigest &&
|
|
652
|
+
kernel.ready &&
|
|
653
|
+
kernel.digest === plan.guest.kernelDigest &&
|
|
654
|
+
initrd.ready &&
|
|
655
|
+
initrd.digest === plan.guest.initrdDigest
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function runVmLaunchWithProcessRunner(
|
|
660
|
+
{ plan, executorDirectory, kernelPath, initrdPath, now = new Date() } = {},
|
|
661
|
+
processRunner,
|
|
662
|
+
adapter
|
|
663
|
+
) {
|
|
664
|
+
const inspection = inspectVmLaunchPlanForAdapter(plan, adapter);
|
|
665
|
+
const receipt = baseReceipt(plan, now, [...inspection.issues], adapter);
|
|
666
|
+
if (!inspection.ready) return finishReceipt(receipt);
|
|
667
|
+
|
|
668
|
+
const executor = inspectVmExecutor({ executorDirectory });
|
|
669
|
+
const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
|
|
670
|
+
const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
|
|
671
|
+
receipt.issues.push(...executor.issues, ...kernel.issues, ...initrd.issues);
|
|
672
|
+
if (executor.ready && executor.digest !== plan.executor.runtimeDigest) {
|
|
673
|
+
issue(
|
|
674
|
+
receipt.issues,
|
|
675
|
+
'vm-launch-runtime-mismatch',
|
|
676
|
+
'executor.runtimeDigest',
|
|
677
|
+
'QEMU runtime closure does not match the pinned plan digest.'
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (executor.ready && executor.binaryDigest !== plan.executor.binaryDigest) {
|
|
681
|
+
issue(
|
|
682
|
+
receipt.issues,
|
|
683
|
+
'vm-launch-binary-mismatch',
|
|
684
|
+
'executor.binaryDigest',
|
|
685
|
+
'QEMU binary does not match the pinned plan digest.'
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
if (kernel.ready && kernel.digest !== plan.guest.kernelDigest) {
|
|
689
|
+
issue(
|
|
690
|
+
receipt.issues,
|
|
691
|
+
'vm-launch-kernel-mismatch',
|
|
692
|
+
'guest.kernelDigest',
|
|
693
|
+
'Guest kernel does not match the pinned plan digest.'
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
if (initrd.ready && initrd.digest !== plan.guest.initrdDigest) {
|
|
697
|
+
issue(
|
|
698
|
+
receipt.issues,
|
|
699
|
+
'vm-launch-initrd-mismatch',
|
|
700
|
+
'guest.initrdDigest',
|
|
701
|
+
'Guest initrd does not match the pinned plan digest.'
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
705
|
+
|
|
706
|
+
const sourceRuntime = resolve(executorDirectory);
|
|
707
|
+
const sourceKernel = resolve(kernelPath);
|
|
708
|
+
const sourceInitrd = resolve(initrdPath);
|
|
709
|
+
const snapshotRoot = mkdtempSync(join(tmpdir(), 'holosystem-vm-launch-'));
|
|
710
|
+
const snapshotRuntime = join(snapshotRoot, 'runtime');
|
|
711
|
+
const snapshotKernel = join(snapshotRoot, 'vmlinuz');
|
|
712
|
+
const snapshotInitrd = join(snapshotRoot, 'initramfs');
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
try {
|
|
716
|
+
copyRuntimeSnapshot(sourceRuntime, executor, snapshotRuntime);
|
|
717
|
+
copyFileSync(sourceKernel, snapshotKernel);
|
|
718
|
+
copyFileSync(sourceInitrd, snapshotInitrd);
|
|
719
|
+
} catch {
|
|
720
|
+
issue(
|
|
721
|
+
receipt.issues,
|
|
722
|
+
'vm-launch-snapshot-failed',
|
|
723
|
+
'snapshot',
|
|
724
|
+
'A private verified launch snapshot could not be materialized.'
|
|
725
|
+
);
|
|
726
|
+
return finishReceipt(receipt);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const pinnedExecutor = inspectVmExecutor({ executorDirectory: snapshotRuntime });
|
|
730
|
+
const pinnedKernel = inspectVmLaunchAsset({ assetPath: snapshotKernel, kind: 'kernel' });
|
|
731
|
+
const pinnedInitrd = inspectVmLaunchAsset({ assetPath: snapshotInitrd, kind: 'initrd' });
|
|
732
|
+
if (
|
|
733
|
+
!pinnedExecutor.ready ||
|
|
734
|
+
pinnedExecutor.digest !== plan.executor.runtimeDigest ||
|
|
735
|
+
pinnedExecutor.binaryDigest !== plan.executor.binaryDigest
|
|
736
|
+
) {
|
|
737
|
+
issue(
|
|
738
|
+
receipt.issues,
|
|
739
|
+
'vm-launch-runtime-snapshot-mismatch',
|
|
740
|
+
'executor.runtimeDigest',
|
|
741
|
+
'QEMU changed while its private launch snapshot was materialized.'
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
if (!pinnedKernel.ready || pinnedKernel.digest !== plan.guest.kernelDigest) {
|
|
745
|
+
issue(
|
|
746
|
+
receipt.issues,
|
|
747
|
+
'vm-launch-kernel-snapshot-mismatch',
|
|
748
|
+
'guest.kernelDigest',
|
|
749
|
+
'Guest kernel changed while its private launch snapshot was materialized.'
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
if (!pinnedInitrd.ready || pinnedInitrd.digest !== plan.guest.initrdDigest) {
|
|
753
|
+
issue(
|
|
754
|
+
receipt.issues,
|
|
755
|
+
'vm-launch-initrd-snapshot-mismatch',
|
|
756
|
+
'guest.initrdDigest',
|
|
757
|
+
'Guest initrd changed while its private launch snapshot was materialized.'
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
761
|
+
|
|
762
|
+
receipt.measurementDigest = hashJson({
|
|
763
|
+
plan: normalizedPlan(plan),
|
|
764
|
+
executorDigest: pinnedExecutor.digest,
|
|
765
|
+
binaryDigest: pinnedExecutor.binaryDigest,
|
|
766
|
+
kernelDigest: pinnedKernel.digest,
|
|
767
|
+
initrdDigest: pinnedInitrd.digest,
|
|
768
|
+
policy: adapter.policy,
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const command = join(snapshotRuntime, EXECUTOR_BINARY);
|
|
772
|
+
const args = qemuArguments(plan, snapshotKernel, snapshotInitrd, adapter);
|
|
773
|
+
const options = {
|
|
774
|
+
encoding: null,
|
|
775
|
+
env: minimalEnvironment(snapshotRoot, snapshotRuntime),
|
|
776
|
+
maxBuffer: MAX_CONSOLE_BYTES,
|
|
777
|
+
shell: false,
|
|
778
|
+
timeout: plan.resources.timeoutSeconds * 1000,
|
|
779
|
+
windowsHide: true,
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
for (let index = 0; index < plan.launches; index += 1) {
|
|
783
|
+
if (!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd)) {
|
|
784
|
+
issue(
|
|
785
|
+
receipt.issues,
|
|
786
|
+
'vm-launch-snapshot-drift',
|
|
787
|
+
`launches[${index}].snapshot`,
|
|
788
|
+
'The private launch snapshot changed outside the measured execution boundary.'
|
|
789
|
+
);
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
let result;
|
|
793
|
+
try {
|
|
794
|
+
result = processRunner(command, args, options);
|
|
795
|
+
} catch {
|
|
796
|
+
result = { status: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
|
|
797
|
+
}
|
|
798
|
+
const stdout = logSummary(result?.stdout);
|
|
799
|
+
const stderr = logSummary(
|
|
800
|
+
adapter.requiresDiagnosticsDigest
|
|
801
|
+
? normalizedDiagnostics(result?.stderr, command)
|
|
802
|
+
: result?.stderr
|
|
803
|
+
);
|
|
804
|
+
const launch = {
|
|
805
|
+
index: index + 1,
|
|
806
|
+
exitCode: Number.isInteger(result?.status) ? result.status : null,
|
|
807
|
+
signal: typeof result?.signal === 'string' ? result.signal : null,
|
|
808
|
+
stdout,
|
|
809
|
+
stderr,
|
|
810
|
+
};
|
|
811
|
+
receipt.launches.push(launch);
|
|
812
|
+
if (result?.status === null || result?.status === undefined || result?.error) {
|
|
813
|
+
issue(
|
|
814
|
+
receipt.issues,
|
|
815
|
+
'vm-launch-execution-failed',
|
|
816
|
+
`launches[${index}]`,
|
|
817
|
+
'Measured VM execution failed or exceeded its bound.'
|
|
818
|
+
);
|
|
819
|
+
} else if (result.status !== EXPECTED_EXIT_CODE) {
|
|
820
|
+
issue(
|
|
821
|
+
receipt.issues,
|
|
822
|
+
'vm-launch-exit-mismatch',
|
|
823
|
+
`launches[${index}].exitCode`,
|
|
824
|
+
'Guest did not exit through the pinned debug-exit signal.'
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
if (stdout.digest !== plan.guest.expectedConsoleDigest) {
|
|
828
|
+
issue(
|
|
829
|
+
receipt.issues,
|
|
830
|
+
'vm-launch-console-mismatch',
|
|
831
|
+
`launches[${index}].stdout`,
|
|
832
|
+
'Guest serial output does not match the pinned success digest.'
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
if (
|
|
836
|
+
adapter.requiresDiagnosticsDigest &&
|
|
837
|
+
stderr.digest !== plan.guest.expectedDiagnosticsDigest
|
|
838
|
+
) {
|
|
839
|
+
issue(
|
|
840
|
+
receipt.issues,
|
|
841
|
+
'vm-launch-diagnostics-mismatch',
|
|
842
|
+
`launches[${index}].stderr`,
|
|
843
|
+
'Host emulator diagnostics do not match the pinned adapter digest; raw bytes are withheld.'
|
|
844
|
+
);
|
|
845
|
+
} else if (!adapter.requiresDiagnosticsDigest && stderr.bytes !== 0) {
|
|
846
|
+
issue(
|
|
847
|
+
receipt.issues,
|
|
848
|
+
'vm-launch-diagnostics-present',
|
|
849
|
+
`launches[${index}].stderr`,
|
|
850
|
+
'Host emulator diagnostics were emitted; raw bytes are withheld.'
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
if (!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd)) {
|
|
854
|
+
issue(
|
|
855
|
+
receipt.issues,
|
|
856
|
+
'vm-launch-snapshot-drift',
|
|
857
|
+
`launches[${index}].snapshot`,
|
|
858
|
+
'The private launch snapshot changed during measured execution.'
|
|
859
|
+
);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
} finally {
|
|
864
|
+
rmSync(snapshotRoot, { recursive: true, force: true });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr }) =>
|
|
868
|
+
hashJson({ exitCode, signal, stdout, stderr })
|
|
869
|
+
);
|
|
870
|
+
if (launchSignatures.length === plan.launches && new Set(launchSignatures).size !== 1) {
|
|
871
|
+
issue(
|
|
872
|
+
receipt.issues,
|
|
873
|
+
'vm-launch-nondeterministic',
|
|
874
|
+
'launches',
|
|
875
|
+
'Clean measured launches produced different observable results.'
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
receipt.deterministic =
|
|
879
|
+
receipt.issues.length === 0 &&
|
|
880
|
+
launchSignatures.length === plan.launches &&
|
|
881
|
+
new Set(launchSignatures).size === 1;
|
|
882
|
+
if (receipt.deterministic) {
|
|
883
|
+
receipt.status = 'verified';
|
|
884
|
+
receipt.verified = true;
|
|
885
|
+
receipt.hardwareBacked = adapter.hardwareBacked;
|
|
886
|
+
receipt.acceleration.verified = adapter.hardwareBacked;
|
|
887
|
+
receipt.coverage = {
|
|
888
|
+
includedLayers: [
|
|
889
|
+
'guest-artifact-measurement',
|
|
890
|
+
...(adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
891
|
+
'machine-vm-launch',
|
|
892
|
+
'virtual-device-minimization',
|
|
893
|
+
],
|
|
894
|
+
missingLayers: [
|
|
895
|
+
...(!adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
896
|
+
'host-process-isolation',
|
|
897
|
+
],
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
return finishReceipt(receipt);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
export function runVmLaunch(options = {}) {
|
|
904
|
+
return runVmLaunchWithProcessRunner(options, spawnSync, TCG_ADAPTER);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
export function runWhpxVmLaunch(options = {}) {
|
|
908
|
+
return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_ADAPTER);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Repository tests need a deterministic process boundary without publishing an
|
|
912
|
+
// injectable executor through the package root export.
|
|
913
|
+
export function runVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
914
|
+
if (typeof options.processRunner !== 'function') {
|
|
915
|
+
throw new TypeError('processRunner test adapter is required.');
|
|
916
|
+
}
|
|
917
|
+
const { processRunner, ...launchOptions } = options;
|
|
918
|
+
return runVmLaunchWithProcessRunner(launchOptions, processRunner, TCG_ADAPTER);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export function runWhpxVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
922
|
+
if (typeof options.processRunner !== 'function') {
|
|
923
|
+
throw new TypeError('processRunner test adapter is required.');
|
|
924
|
+
}
|
|
925
|
+
const { processRunner, ...launchOptions } = options;
|
|
926
|
+
return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_ADAPTER);
|
|
927
|
+
}
|