@holoscript/holosystem 0.2.3 → 0.2.5
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 +93 -3
- package/bin/holosystem.mjs +21 -0
- package/docs/vm-launch-threat-model.md +91 -23
- package/native/windows-sandbox/Canary.c +107 -0
- package/native/windows-sandbox/Program.cs +1117 -0
- package/native/windows-sandbox/README.md +30 -0
- package/native/windows-sandbox/build.ps1 +48 -0
- package/native/windows-x64/holosystem-appcontainer-canary.exe +0 -0
- package/native/windows-x64/holosystem-sandbox-launcher.exe +0 -0
- package/package.json +2 -1
- package/src/index.mjs +20 -0
- package/src/vm-launch.mjs +684 -28
package/src/vm-launch.mjs
CHANGED
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
readdirSync,
|
|
9
9
|
readFileSync,
|
|
10
10
|
rmSync,
|
|
11
|
+
writeFileSync,
|
|
11
12
|
} from 'node:fs';
|
|
12
13
|
import { tmpdir } from 'node:os';
|
|
13
14
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
14
16
|
|
|
15
17
|
export const HOLOSYSTEM_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.vm-launch-plan.v1';
|
|
16
18
|
export const HOLOSYSTEM_VM_EXECUTOR_SCHEMA = 'holoscript.holosystem.vm-executor.v1';
|
|
@@ -19,6 +21,26 @@ export const HOLOSYSTEM_VM_LAUNCH_RECEIPT_SCHEMA = 'holoscript.holosystem.vm-lau
|
|
|
19
21
|
export const HOLOSYSTEM_WHPX_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.whpx-vm-launch-plan.v1';
|
|
20
22
|
export const HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
21
23
|
'holoscript.holosystem.whpx-vm-launch-receipt.v1';
|
|
24
|
+
export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA =
|
|
25
|
+
'holoscript.holosystem.whpx-sandboxed-vm-launch-plan.v1';
|
|
26
|
+
export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
27
|
+
'holoscript.holosystem.whpx-sandboxed-vm-launch-receipt.v1';
|
|
28
|
+
export const HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA =
|
|
29
|
+
'holoscript.holosystem.whpx-appcontainer-vm-launch-plan.v1';
|
|
30
|
+
export const HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
31
|
+
'holoscript.holosystem.whpx-appcontainer-vm-launch-receipt.v1';
|
|
32
|
+
export const HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA =
|
|
33
|
+
'holoscript.holosystem.appcontainer-vm-launch-plan.v1';
|
|
34
|
+
export const HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
35
|
+
'holoscript.holosystem.appcontainer-vm-launch-receipt.v1';
|
|
36
|
+
export const HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL =
|
|
37
|
+
'holoscript.holosystem.windows-sandbox-launch.v1';
|
|
38
|
+
export const HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL =
|
|
39
|
+
'holoscript.holosystem.windows-appcontainer-launch.v1';
|
|
40
|
+
export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA =
|
|
41
|
+
'holoscript.holosystem.windows-sandbox-launcher.v1';
|
|
42
|
+
export const HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_SCHEMA =
|
|
43
|
+
'holoscript.holosystem.windows-appcontainer-canary.v1';
|
|
22
44
|
|
|
23
45
|
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
24
46
|
const EXECUTOR_BINARY = 'qemu-system-x86_64.exe';
|
|
@@ -29,6 +51,24 @@ const MAX_KERNEL_BYTES = 128 * 1024 * 1024;
|
|
|
29
51
|
const MAX_INITRD_BYTES = 512 * 1024 * 1024;
|
|
30
52
|
const MAX_CONSOLE_BYTES = 1024 * 1024;
|
|
31
53
|
const EXPECTED_EXIT_CODE = 33;
|
|
54
|
+
const SANDBOX_LAUNCHER_BINARY = 'holosystem-sandbox-launcher.exe';
|
|
55
|
+
const APPCONTAINER_CANARY_BINARY = 'holosystem-appcontainer-canary.exe';
|
|
56
|
+
const SANDBOX_KIND = 'windows-low-integrity-job-v1';
|
|
57
|
+
const APPCONTAINER_KIND = 'windows-appcontainer-deny-v1';
|
|
58
|
+
const SANDBOX_PROCESS_MEMORY_BYTES = 512 * 1024 * 1024;
|
|
59
|
+
const SANDBOX_PROTOCOL_BYTES = 3 * 1024 * 1024;
|
|
60
|
+
const SANDBOX_LAUNCHER_PATH = fileURLToPath(
|
|
61
|
+
new URL(`../native/windows-x64/${SANDBOX_LAUNCHER_BINARY}`, import.meta.url)
|
|
62
|
+
);
|
|
63
|
+
const APPCONTAINER_CANARY_PATH = fileURLToPath(
|
|
64
|
+
new URL(`../native/windows-x64/${APPCONTAINER_CANARY_BINARY}`, import.meta.url)
|
|
65
|
+
);
|
|
66
|
+
export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST = hashBytes(
|
|
67
|
+
readFileSync(SANDBOX_LAUNCHER_PATH)
|
|
68
|
+
);
|
|
69
|
+
export const HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_DIGEST = hashBytes(
|
|
70
|
+
readFileSync(APPCONTAINER_CANARY_PATH)
|
|
71
|
+
);
|
|
32
72
|
const TCG_POLICY = Object.freeze({
|
|
33
73
|
accelerator: 'tcg',
|
|
34
74
|
userConfiguration: 'disabled',
|
|
@@ -55,6 +95,34 @@ const WHPX_POLICY = Object.freeze({
|
|
|
55
95
|
guestSignal: 'serial-digest-and-debug-exit',
|
|
56
96
|
diagnostics: 'pinned-digest',
|
|
57
97
|
});
|
|
98
|
+
const WHPX_SANDBOXED_POLICY = Object.freeze({
|
|
99
|
+
...WHPX_POLICY,
|
|
100
|
+
hostProcess: SANDBOX_KIND,
|
|
101
|
+
token: 'disable-max-privilege-low-integrity',
|
|
102
|
+
job: 'pre-resume-kill-on-close-active-process-memory-ui',
|
|
103
|
+
writableTemp: 'low-integrity-private-snapshot',
|
|
104
|
+
});
|
|
105
|
+
const WHPX_APPCONTAINER_POLICY = Object.freeze({
|
|
106
|
+
...WHPX_POLICY,
|
|
107
|
+
hostProcess: APPCONTAINER_KIND,
|
|
108
|
+
token: 'disable-max-privilege-appcontainer-low-integrity',
|
|
109
|
+
capabilities: 'none',
|
|
110
|
+
filesystem: 'snapshot-read-execute-writable-temp-modify',
|
|
111
|
+
network: 'appcontainer-default-deny',
|
|
112
|
+
canaries: 'caller-readable-sentinel-and-loopback-listener',
|
|
113
|
+
job: 'pre-resume-kill-on-close-active-process-memory-ui',
|
|
114
|
+
});
|
|
115
|
+
const APPCONTAINER_TCG_POLICY = Object.freeze({
|
|
116
|
+
...TCG_POLICY,
|
|
117
|
+
network: 'appcontainer-default-deny',
|
|
118
|
+
hostProcess: APPCONTAINER_KIND,
|
|
119
|
+
token: 'disable-max-privilege-appcontainer-low-integrity',
|
|
120
|
+
capabilities: 'none',
|
|
121
|
+
filesystem: 'snapshot-read-execute-writable-temp-modify',
|
|
122
|
+
canaries: 'caller-readable-sentinel-and-loopback-listener',
|
|
123
|
+
job: 'pre-resume-kill-on-close-active-process-memory-ui',
|
|
124
|
+
translationBufferMiB: 64,
|
|
125
|
+
});
|
|
58
126
|
const BOUNDARIES = Object.freeze([
|
|
59
127
|
'guest-artifact-provenance',
|
|
60
128
|
'hardware-hypervisor-acceleration',
|
|
@@ -82,6 +150,55 @@ const WHPX_ADAPTER = Object.freeze({
|
|
|
82
150
|
receiptSchema: HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
83
151
|
policy: WHPX_POLICY,
|
|
84
152
|
requiresDiagnosticsDigest: true,
|
|
153
|
+
hostSandboxed: false,
|
|
154
|
+
});
|
|
155
|
+
const WHPX_SANDBOXED_ADAPTER = Object.freeze({
|
|
156
|
+
accelerator: 'whpx',
|
|
157
|
+
accelerationName: 'qemu-whpx',
|
|
158
|
+
hardwareBacked: true,
|
|
159
|
+
planSchema: HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA,
|
|
160
|
+
receiptSchema: HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
161
|
+
policy: WHPX_SANDBOXED_POLICY,
|
|
162
|
+
requiresDiagnosticsDigest: true,
|
|
163
|
+
hostSandboxed: true,
|
|
164
|
+
sandboxKind: SANDBOX_KIND,
|
|
165
|
+
sandboxProtocol: HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL,
|
|
166
|
+
isolationScope: 'integrity-privilege-lifetime-resource-ui',
|
|
167
|
+
filesystemConfidentiality: false,
|
|
168
|
+
networkIsolation: false,
|
|
169
|
+
appContainer: false,
|
|
170
|
+
});
|
|
171
|
+
const WHPX_APPCONTAINER_ADAPTER = Object.freeze({
|
|
172
|
+
accelerator: 'whpx',
|
|
173
|
+
accelerationName: 'qemu-whpx',
|
|
174
|
+
hardwareBacked: true,
|
|
175
|
+
planSchema: HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA,
|
|
176
|
+
receiptSchema: HOLOSYSTEM_WHPX_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
177
|
+
policy: WHPX_APPCONTAINER_POLICY,
|
|
178
|
+
requiresDiagnosticsDigest: true,
|
|
179
|
+
hostSandboxed: true,
|
|
180
|
+
sandboxKind: APPCONTAINER_KIND,
|
|
181
|
+
sandboxProtocol: HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL,
|
|
182
|
+
isolationScope: 'appcontainer-zero-capability-snapshot-grant',
|
|
183
|
+
filesystemConfidentiality: true,
|
|
184
|
+
networkIsolation: true,
|
|
185
|
+
appContainer: true,
|
|
186
|
+
});
|
|
187
|
+
const APPCONTAINER_TCG_ADAPTER = Object.freeze({
|
|
188
|
+
accelerator: 'tcg',
|
|
189
|
+
accelerationName: 'qemu-tcg',
|
|
190
|
+
hardwareBacked: false,
|
|
191
|
+
planSchema: HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_PLAN_SCHEMA,
|
|
192
|
+
receiptSchema: HOLOSYSTEM_APPCONTAINER_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
193
|
+
policy: APPCONTAINER_TCG_POLICY,
|
|
194
|
+
requiresDiagnosticsDigest: false,
|
|
195
|
+
hostSandboxed: true,
|
|
196
|
+
sandboxKind: APPCONTAINER_KIND,
|
|
197
|
+
sandboxProtocol: HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL,
|
|
198
|
+
isolationScope: 'appcontainer-zero-capability-snapshot-grant',
|
|
199
|
+
filesystemConfidentiality: true,
|
|
200
|
+
networkIsolation: true,
|
|
201
|
+
appContainer: true,
|
|
85
202
|
});
|
|
86
203
|
|
|
87
204
|
function isRecord(value) {
|
|
@@ -169,6 +286,9 @@ function normalizedPlan(plan) {
|
|
|
169
286
|
cpus: plan.resources.cpus,
|
|
170
287
|
timeoutSeconds: plan.resources.timeoutSeconds,
|
|
171
288
|
},
|
|
289
|
+
sandbox: plan.sandbox
|
|
290
|
+
? { kind: plan.sandbox.kind, launcherDigest: plan.sandbox.launcherDigest }
|
|
291
|
+
: null,
|
|
172
292
|
launches: plan.launches,
|
|
173
293
|
};
|
|
174
294
|
}
|
|
@@ -182,7 +302,17 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
|
|
|
182
302
|
|
|
183
303
|
knownKeys(
|
|
184
304
|
plan,
|
|
185
|
-
new Set([
|
|
305
|
+
new Set([
|
|
306
|
+
'schema',
|
|
307
|
+
'id',
|
|
308
|
+
'host',
|
|
309
|
+
'executor',
|
|
310
|
+
'target',
|
|
311
|
+
'guest',
|
|
312
|
+
'resources',
|
|
313
|
+
...(adapter.hostSandboxed ? ['sandbox'] : []),
|
|
314
|
+
'launches',
|
|
315
|
+
]),
|
|
186
316
|
'',
|
|
187
317
|
issues
|
|
188
318
|
);
|
|
@@ -280,6 +410,27 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
|
|
|
280
410
|
);
|
|
281
411
|
}
|
|
282
412
|
|
|
413
|
+
if (adapter.hostSandboxed) {
|
|
414
|
+
knownKeys(
|
|
415
|
+
plan.sandbox,
|
|
416
|
+
new Set(['kind', 'launcherDigest', ...(adapter.appContainer ? ['canaryDigest'] : [])]),
|
|
417
|
+
'sandbox',
|
|
418
|
+
issues
|
|
419
|
+
);
|
|
420
|
+
if (plan.sandbox?.kind !== adapter.sandboxKind) {
|
|
421
|
+
issue(
|
|
422
|
+
issues,
|
|
423
|
+
'vm-launch-sandbox-unsupported',
|
|
424
|
+
'sandbox.kind',
|
|
425
|
+
`This adapter requires ${adapter.sandboxKind}.`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
inspectDigest(plan.sandbox?.launcherDigest, 'sandbox.launcherDigest', issues);
|
|
429
|
+
if (adapter.appContainer) {
|
|
430
|
+
inspectDigest(plan.sandbox?.canaryDigest, 'sandbox.canaryDigest', issues);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
283
434
|
return { ready: issues.length === 0, issues };
|
|
284
435
|
}
|
|
285
436
|
|
|
@@ -291,6 +442,18 @@ export function inspectWhpxVmLaunchPlan(plan) {
|
|
|
291
442
|
return inspectVmLaunchPlanForAdapter(plan, WHPX_ADAPTER);
|
|
292
443
|
}
|
|
293
444
|
|
|
445
|
+
export function inspectWhpxSandboxedVmLaunchPlan(plan) {
|
|
446
|
+
return inspectVmLaunchPlanForAdapter(plan, WHPX_SANDBOXED_ADAPTER);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function inspectWhpxAppContainerVmLaunchPlan(plan) {
|
|
450
|
+
return inspectVmLaunchPlanForAdapter(plan, WHPX_APPCONTAINER_ADAPTER);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function inspectAppContainerVmLaunchPlan(plan) {
|
|
454
|
+
return inspectVmLaunchPlanForAdapter(plan, APPCONTAINER_TCG_ADAPTER);
|
|
455
|
+
}
|
|
456
|
+
|
|
294
457
|
function scanRuntime(directory, issues) {
|
|
295
458
|
const files = [];
|
|
296
459
|
let totalBytes = 0;
|
|
@@ -482,6 +645,74 @@ export function inspectVmLaunchAsset({ assetPath, kind } = {}) {
|
|
|
482
645
|
};
|
|
483
646
|
}
|
|
484
647
|
|
|
648
|
+
export function inspectWindowsVmSandboxLauncher() {
|
|
649
|
+
const issues = [];
|
|
650
|
+
let content = null;
|
|
651
|
+
try {
|
|
652
|
+
const stats = lstatSync(SANDBOX_LAUNCHER_PATH);
|
|
653
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
|
|
654
|
+
content = readFileSync(SANDBOX_LAUNCHER_PATH);
|
|
655
|
+
} catch {
|
|
656
|
+
issue(
|
|
657
|
+
issues,
|
|
658
|
+
'vm-launch-sandbox-launcher-invalid',
|
|
659
|
+
'sandbox.launcher',
|
|
660
|
+
'The packaged Windows sandbox launcher must be a readable regular file.'
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
const digest = content ? hashBytes(content) : null;
|
|
664
|
+
if (digest && digest !== HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST) {
|
|
665
|
+
issue(
|
|
666
|
+
issues,
|
|
667
|
+
'vm-launch-sandbox-launcher-drift',
|
|
668
|
+
'sandbox.launcherDigest',
|
|
669
|
+
'The packaged Windows sandbox launcher changed after module initialization.'
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
schema: HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA,
|
|
674
|
+
ready: issues.length === 0,
|
|
675
|
+
kind: SANDBOX_KIND,
|
|
676
|
+
digest: issues.length === 0 ? digest : null,
|
|
677
|
+
bytes: issues.length === 0 ? content.length : null,
|
|
678
|
+
issues,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
export function inspectWindowsVmAppContainerCanary() {
|
|
683
|
+
const issues = [];
|
|
684
|
+
let content = null;
|
|
685
|
+
try {
|
|
686
|
+
const stats = lstatSync(APPCONTAINER_CANARY_PATH);
|
|
687
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
|
|
688
|
+
content = readFileSync(APPCONTAINER_CANARY_PATH);
|
|
689
|
+
} catch {
|
|
690
|
+
issue(
|
|
691
|
+
issues,
|
|
692
|
+
'vm-launch-appcontainer-canary-invalid',
|
|
693
|
+
'sandbox.canary',
|
|
694
|
+
'The packaged Windows AppContainer canary must be a readable regular file.'
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
const digest = content ? hashBytes(content) : null;
|
|
698
|
+
if (digest && digest !== HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_DIGEST) {
|
|
699
|
+
issue(
|
|
700
|
+
issues,
|
|
701
|
+
'vm-launch-appcontainer-canary-drift',
|
|
702
|
+
'sandbox.canaryDigest',
|
|
703
|
+
'The packaged Windows AppContainer canary changed after module initialization.'
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
schema: HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_SCHEMA,
|
|
708
|
+
ready: issues.length === 0,
|
|
709
|
+
kind: APPCONTAINER_KIND,
|
|
710
|
+
digest: issues.length === 0 ? digest : null,
|
|
711
|
+
bytes: issues.length === 0 ? content.length : null,
|
|
712
|
+
issues,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
485
716
|
function logSummary(value) {
|
|
486
717
|
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
|
|
487
718
|
return { bytes: bytes.length, digest: hashBytes(bytes) };
|
|
@@ -525,10 +756,27 @@ function baseReceipt(plan, now, issues, adapter) {
|
|
|
525
756
|
evidence: adapter.hardwareBacked ? 'two-explicit-successful-launches' : 'software-emulation',
|
|
526
757
|
verified: false,
|
|
527
758
|
},
|
|
528
|
-
isolation:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
759
|
+
isolation: adapter.hostSandboxed
|
|
760
|
+
? {
|
|
761
|
+
hostProcess: adapter.sandboxKind,
|
|
762
|
+
scope: adapter.isolationScope,
|
|
763
|
+
verified: false,
|
|
764
|
+
launcherDigest: DIGEST_PATTERN.test(plan?.sandbox?.launcherDigest || '')
|
|
765
|
+
? plan.sandbox.launcherDigest
|
|
766
|
+
: null,
|
|
767
|
+
...(adapter.appContainer
|
|
768
|
+
? {
|
|
769
|
+
canaryDigest: DIGEST_PATTERN.test(plan?.sandbox?.canaryDigest || '')
|
|
770
|
+
? plan.sandbox.canaryDigest
|
|
771
|
+
: null,
|
|
772
|
+
}
|
|
773
|
+
: {}),
|
|
774
|
+
controls: emptyIsolationControls(adapter),
|
|
775
|
+
}
|
|
776
|
+
: {
|
|
777
|
+
hostProcess: 'ambient-windows-process',
|
|
778
|
+
verified: false,
|
|
779
|
+
},
|
|
532
780
|
measurementDigest: null,
|
|
533
781
|
executor: {
|
|
534
782
|
kind: plan?.executor?.kind === 'qemu-system' ? 'qemu-system' : null,
|
|
@@ -579,20 +827,30 @@ function baseReceipt(plan, now, issues, adapter) {
|
|
|
579
827
|
'virtual-device-minimization',
|
|
580
828
|
],
|
|
581
829
|
},
|
|
582
|
-
boundaries:
|
|
583
|
-
(
|
|
584
|
-
|
|
830
|
+
boundaries: [
|
|
831
|
+
...BOUNDARIES.filter(
|
|
832
|
+
(layer) =>
|
|
833
|
+
!(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration') &&
|
|
834
|
+
!(adapter.hostSandboxed && layer === 'host-process-isolation')
|
|
835
|
+
),
|
|
836
|
+
...(adapter.hostSandboxed && !adapter.filesystemConfidentiality
|
|
837
|
+
? ['host-filesystem-confidentiality']
|
|
838
|
+
: []),
|
|
839
|
+
...(adapter.hostSandboxed && !adapter.networkIsolation ? ['host-network-isolation'] : []),
|
|
840
|
+
],
|
|
585
841
|
issues,
|
|
586
842
|
};
|
|
587
843
|
}
|
|
588
844
|
|
|
589
845
|
function qemuArguments(plan, kernelPath, initrdPath, adapter) {
|
|
846
|
+
const appContainerTcg = adapter === APPCONTAINER_TCG_ADAPTER;
|
|
590
847
|
return [
|
|
591
848
|
'-no-user-config',
|
|
592
849
|
'-nodefaults',
|
|
593
850
|
'-machine',
|
|
594
|
-
`q35,accel=${adapter.accelerator},usb=off`,
|
|
595
|
-
...(
|
|
851
|
+
appContainerTcg ? 'q35,usb=off' : `q35,accel=${adapter.accelerator},usb=off`,
|
|
852
|
+
...(appContainerTcg ? ['-accel', 'tcg,tb-size=64'] : []),
|
|
853
|
+
...([TCG_ADAPTER, APPCONTAINER_TCG_ADAPTER].includes(adapter) ? ['-cpu', 'max'] : []),
|
|
596
854
|
'-m',
|
|
597
855
|
`${plan.resources.memoryMiB}M`,
|
|
598
856
|
'-smp',
|
|
@@ -617,21 +875,196 @@ function qemuArguments(plan, kernelPath, initrdPath, adapter) {
|
|
|
617
875
|
];
|
|
618
876
|
}
|
|
619
877
|
|
|
620
|
-
function minimalEnvironment(snapshotRoot, runtimeDirectory) {
|
|
878
|
+
function minimalEnvironment(snapshotRoot, runtimeDirectory, temporaryDirectory = snapshotRoot) {
|
|
879
|
+
const systemRoot = process.env.SystemRoot || process.env.WINDIR;
|
|
880
|
+
const systemDrive = process.env.SystemDrive || systemRoot?.slice(0, 2);
|
|
621
881
|
const env = {
|
|
882
|
+
APPDATA: temporaryDirectory,
|
|
622
883
|
HOME: snapshotRoot,
|
|
884
|
+
HOMEDRIVE: systemDrive,
|
|
885
|
+
HOMEPATH: '\\',
|
|
886
|
+
LOCALAPPDATA: temporaryDirectory,
|
|
887
|
+
OS: 'Windows_NT',
|
|
888
|
+
Path: runtimeDirectory,
|
|
889
|
+
PATHEXT: '.COM;.EXE;.BAT;.CMD',
|
|
623
890
|
USERPROFILE: snapshotRoot,
|
|
624
|
-
TEMP:
|
|
625
|
-
TMP:
|
|
626
|
-
PATH: runtimeDirectory,
|
|
891
|
+
TEMP: temporaryDirectory,
|
|
892
|
+
TMP: temporaryDirectory,
|
|
627
893
|
LANG: 'C',
|
|
628
894
|
LC_ALL: 'C',
|
|
629
895
|
};
|
|
630
|
-
if (
|
|
631
|
-
if (
|
|
896
|
+
if (systemDrive) env.SystemDrive = systemDrive;
|
|
897
|
+
if (systemRoot) {
|
|
898
|
+
env.SystemRoot = systemRoot;
|
|
899
|
+
env.windir = systemRoot;
|
|
900
|
+
env.ComSpec = join(systemRoot, 'System32', 'cmd.exe');
|
|
901
|
+
}
|
|
632
902
|
return env;
|
|
633
903
|
}
|
|
634
904
|
|
|
905
|
+
const SANDBOX_CONTROL_KEYS = Object.freeze([
|
|
906
|
+
'filteredToken',
|
|
907
|
+
'disableMaxPrivilege',
|
|
908
|
+
'enabledPrivilegeCount',
|
|
909
|
+
'privilegesBounded',
|
|
910
|
+
'lowIntegrity',
|
|
911
|
+
'assignedBeforeResume',
|
|
912
|
+
'handleAllowlist',
|
|
913
|
+
'killOnClose',
|
|
914
|
+
'activeProcessLimit',
|
|
915
|
+
'processMemoryLimit',
|
|
916
|
+
'uiRestrictions',
|
|
917
|
+
'writableTempLowIntegrity',
|
|
918
|
+
]);
|
|
919
|
+
const APPCONTAINER_CONTROL_KEYS = Object.freeze([
|
|
920
|
+
'filteredToken',
|
|
921
|
+
'disableMaxPrivilege',
|
|
922
|
+
'enabledPrivilegeCount',
|
|
923
|
+
'privilegesBounded',
|
|
924
|
+
'lowIntegrity',
|
|
925
|
+
'assignedBeforeResume',
|
|
926
|
+
'handleAllowlist',
|
|
927
|
+
'killOnClose',
|
|
928
|
+
'activeProcessLimit',
|
|
929
|
+
'processMemoryLimit',
|
|
930
|
+
'uiRestrictions',
|
|
931
|
+
'appContainer',
|
|
932
|
+
'appContainerSidMatched',
|
|
933
|
+
'capabilityCount',
|
|
934
|
+
'snapshotReadExecuteGrant',
|
|
935
|
+
'writableTempModifyGrant',
|
|
936
|
+
'filesystemCanaryDenied',
|
|
937
|
+
'filesystemCanaryError',
|
|
938
|
+
'networkCanaryDenied',
|
|
939
|
+
'networkCanaryError',
|
|
940
|
+
'loopbackAccepted',
|
|
941
|
+
'profileDeleted',
|
|
942
|
+
]);
|
|
943
|
+
|
|
944
|
+
function isolationControlKeys(adapter) {
|
|
945
|
+
return adapter.appContainer ? APPCONTAINER_CONTROL_KEYS : SANDBOX_CONTROL_KEYS;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function emptyIsolationControls(adapter) {
|
|
949
|
+
return Object.fromEntries(
|
|
950
|
+
isolationControlKeys(adapter).map((key) => [
|
|
951
|
+
key,
|
|
952
|
+
['enabledPrivilegeCount', 'capabilityCount', 'filesystemCanaryError', 'networkCanaryError'].includes(
|
|
953
|
+
key
|
|
954
|
+
)
|
|
955
|
+
? null
|
|
956
|
+
: false,
|
|
957
|
+
])
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function isolationControlsReady(controls, adapter) {
|
|
962
|
+
if (
|
|
963
|
+
!Number.isInteger(controls.enabledPrivilegeCount) ||
|
|
964
|
+
controls.enabledPrivilegeCount < 0 ||
|
|
965
|
+
controls.enabledPrivilegeCount > 1
|
|
966
|
+
) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
if (!adapter.appContainer) {
|
|
970
|
+
return SANDBOX_CONTROL_KEYS.every(
|
|
971
|
+
(key) => key === 'enabledPrivilegeCount' || controls[key] === true
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
return (
|
|
975
|
+
APPCONTAINER_CONTROL_KEYS.every(
|
|
976
|
+
(key) =>
|
|
977
|
+
['enabledPrivilegeCount', 'capabilityCount', 'filesystemCanaryError', 'networkCanaryError'].includes(
|
|
978
|
+
key
|
|
979
|
+
) ||
|
|
980
|
+
(key === 'loopbackAccepted' ? controls[key] === false : controls[key] === true)
|
|
981
|
+
) &&
|
|
982
|
+
controls.capabilityCount === 0 &&
|
|
983
|
+
controls.filesystemCanaryError === 5 &&
|
|
984
|
+
[10013, 10060].includes(controls.networkCanaryError)
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function canonicalBase64(value) {
|
|
989
|
+
if (
|
|
990
|
+
typeof value !== 'string' ||
|
|
991
|
+
value.length > Math.ceil(MAX_CONSOLE_BYTES / 3) * 4 ||
|
|
992
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value)
|
|
993
|
+
) {
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
const bytes = Buffer.from(value, 'base64');
|
|
997
|
+
return bytes.toString('base64') === value && bytes.length <= MAX_CONSOLE_BYTES ? bytes : null;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function parseSandboxProtocol(result, adapter) {
|
|
1001
|
+
if (
|
|
1002
|
+
result?.status !== 0 ||
|
|
1003
|
+
result?.signal ||
|
|
1004
|
+
result?.error ||
|
|
1005
|
+
logSummary(result?.stderr).bytes !== 0
|
|
1006
|
+
) {
|
|
1007
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-launcher-failed' };
|
|
1008
|
+
}
|
|
1009
|
+
let message;
|
|
1010
|
+
try {
|
|
1011
|
+
const encoded = Buffer.isBuffer(result.stdout)
|
|
1012
|
+
? result.stdout.toString('utf8')
|
|
1013
|
+
: String(result.stdout || '');
|
|
1014
|
+
message = JSON.parse(encoded.trim());
|
|
1015
|
+
} catch {
|
|
1016
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
|
|
1017
|
+
}
|
|
1018
|
+
if (
|
|
1019
|
+
!isRecord(message) ||
|
|
1020
|
+
message.protocol !== adapter.sandboxProtocol ||
|
|
1021
|
+
!isRecord(message.isolation) ||
|
|
1022
|
+
Object.keys(message).some(
|
|
1023
|
+
(key) =>
|
|
1024
|
+
![
|
|
1025
|
+
'protocol',
|
|
1026
|
+
'launched',
|
|
1027
|
+
'timedOut',
|
|
1028
|
+
'exitCode',
|
|
1029
|
+
'isolation',
|
|
1030
|
+
'stdoutBase64',
|
|
1031
|
+
'stderrBase64',
|
|
1032
|
+
'errorStage',
|
|
1033
|
+
'errorCode',
|
|
1034
|
+
].includes(key)
|
|
1035
|
+
) ||
|
|
1036
|
+
Object.keys(message.isolation).some((key) => !isolationControlKeys(adapter).includes(key)) ||
|
|
1037
|
+
Object.keys(message.isolation).length !== isolationControlKeys(adapter).length
|
|
1038
|
+
) {
|
|
1039
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
|
|
1040
|
+
}
|
|
1041
|
+
const stdout = canonicalBase64(message.stdoutBase64);
|
|
1042
|
+
const stderr = canonicalBase64(message.stderrBase64);
|
|
1043
|
+
const controlsReady = isolationControlsReady(message.isolation, adapter);
|
|
1044
|
+
if (
|
|
1045
|
+
message.launched !== true ||
|
|
1046
|
+
message.timedOut !== false ||
|
|
1047
|
+
!Number.isInteger(message.exitCode) ||
|
|
1048
|
+
message.errorStage !== null ||
|
|
1049
|
+
message.errorCode !== 0 ||
|
|
1050
|
+
!controlsReady ||
|
|
1051
|
+
!stdout ||
|
|
1052
|
+
!stderr
|
|
1053
|
+
) {
|
|
1054
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-evidence-invalid' };
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
ready: true,
|
|
1058
|
+
status: message.exitCode,
|
|
1059
|
+
signal: null,
|
|
1060
|
+
stdout,
|
|
1061
|
+
stderr,
|
|
1062
|
+
isolation: Object.fromEntries(
|
|
1063
|
+
isolationControlKeys(adapter).map((key) => [key, message.isolation[key]])
|
|
1064
|
+
),
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
635
1068
|
function copyRuntimeSnapshot(sourceDirectory, report, destinationDirectory) {
|
|
636
1069
|
mkdirSync(destinationDirectory, { recursive: true });
|
|
637
1070
|
for (const file of report.files) {
|
|
@@ -656,6 +1089,25 @@ function launchSnapshotMatches(plan, runtimeDirectory, kernelPath, initrdPath) {
|
|
|
656
1089
|
);
|
|
657
1090
|
}
|
|
658
1091
|
|
|
1092
|
+
function sandboxArtifactsSnapshotMatch(plan, launcherPath, canaryPath, adapter) {
|
|
1093
|
+
if (!adapter.hostSandboxed) return true;
|
|
1094
|
+
try {
|
|
1095
|
+
const stats = lstatSync(launcherPath);
|
|
1096
|
+
const canaryStats = adapter.appContainer ? lstatSync(canaryPath) : null;
|
|
1097
|
+
return (
|
|
1098
|
+
stats.isFile() &&
|
|
1099
|
+
!stats.isSymbolicLink() &&
|
|
1100
|
+
hashBytes(readFileSync(launcherPath)) === plan.sandbox.launcherDigest &&
|
|
1101
|
+
(!adapter.appContainer ||
|
|
1102
|
+
(canaryStats.isFile() &&
|
|
1103
|
+
!canaryStats.isSymbolicLink() &&
|
|
1104
|
+
hashBytes(readFileSync(canaryPath)) === plan.sandbox.canaryDigest))
|
|
1105
|
+
);
|
|
1106
|
+
} catch {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
659
1111
|
function runVmLaunchWithProcessRunner(
|
|
660
1112
|
{ plan, executorDirectory, kernelPath, initrdPath, now = new Date() } = {},
|
|
661
1113
|
processRunner,
|
|
@@ -668,7 +1120,15 @@ function runVmLaunchWithProcessRunner(
|
|
|
668
1120
|
const executor = inspectVmExecutor({ executorDirectory });
|
|
669
1121
|
const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
|
|
670
1122
|
const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
|
|
671
|
-
|
|
1123
|
+
const sandboxLauncher = adapter.hostSandboxed ? inspectWindowsVmSandboxLauncher() : null;
|
|
1124
|
+
const appContainerCanary = adapter.appContainer ? inspectWindowsVmAppContainerCanary() : null;
|
|
1125
|
+
receipt.issues.push(
|
|
1126
|
+
...executor.issues,
|
|
1127
|
+
...kernel.issues,
|
|
1128
|
+
...initrd.issues,
|
|
1129
|
+
...(sandboxLauncher?.issues || []),
|
|
1130
|
+
...(appContainerCanary?.issues || [])
|
|
1131
|
+
);
|
|
672
1132
|
if (executor.ready && executor.digest !== plan.executor.runtimeDigest) {
|
|
673
1133
|
issue(
|
|
674
1134
|
receipt.issues,
|
|
@@ -701,6 +1161,30 @@ function runVmLaunchWithProcessRunner(
|
|
|
701
1161
|
'Guest initrd does not match the pinned plan digest.'
|
|
702
1162
|
);
|
|
703
1163
|
}
|
|
1164
|
+
if (
|
|
1165
|
+
adapter.hostSandboxed &&
|
|
1166
|
+
sandboxLauncher?.ready &&
|
|
1167
|
+
sandboxLauncher.digest !== plan.sandbox.launcherDigest
|
|
1168
|
+
) {
|
|
1169
|
+
issue(
|
|
1170
|
+
receipt.issues,
|
|
1171
|
+
'vm-launch-sandbox-launcher-mismatch',
|
|
1172
|
+
'sandbox.launcherDigest',
|
|
1173
|
+
'Packaged Windows sandbox launcher does not match the pinned plan digest.'
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
if (
|
|
1177
|
+
adapter.appContainer &&
|
|
1178
|
+
appContainerCanary?.ready &&
|
|
1179
|
+
appContainerCanary.digest !== plan.sandbox.canaryDigest
|
|
1180
|
+
) {
|
|
1181
|
+
issue(
|
|
1182
|
+
receipt.issues,
|
|
1183
|
+
'vm-launch-appcontainer-canary-mismatch',
|
|
1184
|
+
'sandbox.canaryDigest',
|
|
1185
|
+
'Packaged Windows AppContainer canary does not match the pinned plan digest.'
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
704
1188
|
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
705
1189
|
|
|
706
1190
|
const sourceRuntime = resolve(executorDirectory);
|
|
@@ -710,12 +1194,27 @@ function runVmLaunchWithProcessRunner(
|
|
|
710
1194
|
const snapshotRuntime = join(snapshotRoot, 'runtime');
|
|
711
1195
|
const snapshotKernel = join(snapshotRoot, 'vmlinuz');
|
|
712
1196
|
const snapshotInitrd = join(snapshotRoot, 'initramfs');
|
|
1197
|
+
const snapshotLauncher = join(snapshotRoot, SANDBOX_LAUNCHER_BINARY);
|
|
1198
|
+
const snapshotCanary = join(snapshotRoot, APPCONTAINER_CANARY_BINARY);
|
|
1199
|
+
const snapshotTemporary = join(snapshotRoot, 'sandbox-temp');
|
|
1200
|
+
const protectedRoot = adapter.appContainer
|
|
1201
|
+
? mkdtempSync(join(tmpdir(), 'holosystem-vm-protected-'))
|
|
1202
|
+
: null;
|
|
1203
|
+
const protectedSentinel = protectedRoot ? join(protectedRoot, 'caller-readable.bin') : null;
|
|
713
1204
|
|
|
714
1205
|
try {
|
|
715
1206
|
try {
|
|
716
1207
|
copyRuntimeSnapshot(sourceRuntime, executor, snapshotRuntime);
|
|
717
1208
|
copyFileSync(sourceKernel, snapshotKernel);
|
|
718
1209
|
copyFileSync(sourceInitrd, snapshotInitrd);
|
|
1210
|
+
if (adapter.hostSandboxed) {
|
|
1211
|
+
copyFileSync(SANDBOX_LAUNCHER_PATH, snapshotLauncher);
|
|
1212
|
+
mkdirSync(snapshotTemporary);
|
|
1213
|
+
}
|
|
1214
|
+
if (adapter.appContainer) copyFileSync(APPCONTAINER_CANARY_PATH, snapshotCanary);
|
|
1215
|
+
if (adapter.appContainer) {
|
|
1216
|
+
writeFileSync(protectedSentinel, Buffer.from('holosystem-appcontainer-read-canary-v1'));
|
|
1217
|
+
}
|
|
719
1218
|
} catch {
|
|
720
1219
|
issue(
|
|
721
1220
|
receipt.issues,
|
|
@@ -729,6 +1228,26 @@ function runVmLaunchWithProcessRunner(
|
|
|
729
1228
|
const pinnedExecutor = inspectVmExecutor({ executorDirectory: snapshotRuntime });
|
|
730
1229
|
const pinnedKernel = inspectVmLaunchAsset({ assetPath: snapshotKernel, kind: 'kernel' });
|
|
731
1230
|
const pinnedInitrd = inspectVmLaunchAsset({ assetPath: snapshotInitrd, kind: 'initrd' });
|
|
1231
|
+
let pinnedLauncherDigest = null;
|
|
1232
|
+
let pinnedCanaryDigest = null;
|
|
1233
|
+
if (adapter.hostSandboxed) {
|
|
1234
|
+
try {
|
|
1235
|
+
const stats = lstatSync(snapshotLauncher);
|
|
1236
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
|
|
1237
|
+
pinnedLauncherDigest = hashBytes(readFileSync(snapshotLauncher));
|
|
1238
|
+
} catch {
|
|
1239
|
+
pinnedLauncherDigest = null;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (adapter.appContainer) {
|
|
1243
|
+
try {
|
|
1244
|
+
const stats = lstatSync(snapshotCanary);
|
|
1245
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
|
|
1246
|
+
pinnedCanaryDigest = hashBytes(readFileSync(snapshotCanary));
|
|
1247
|
+
} catch {
|
|
1248
|
+
pinnedCanaryDigest = null;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
732
1251
|
if (
|
|
733
1252
|
!pinnedExecutor.ready ||
|
|
734
1253
|
pinnedExecutor.digest !== plan.executor.runtimeDigest ||
|
|
@@ -757,6 +1276,22 @@ function runVmLaunchWithProcessRunner(
|
|
|
757
1276
|
'Guest initrd changed while its private launch snapshot was materialized.'
|
|
758
1277
|
);
|
|
759
1278
|
}
|
|
1279
|
+
if (adapter.hostSandboxed && pinnedLauncherDigest !== plan.sandbox.launcherDigest) {
|
|
1280
|
+
issue(
|
|
1281
|
+
receipt.issues,
|
|
1282
|
+
'vm-launch-sandbox-launcher-snapshot-mismatch',
|
|
1283
|
+
'sandbox.launcherDigest',
|
|
1284
|
+
'Windows sandbox launcher changed while its private snapshot was materialized.'
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
if (adapter.appContainer && pinnedCanaryDigest !== plan.sandbox.canaryDigest) {
|
|
1288
|
+
issue(
|
|
1289
|
+
receipt.issues,
|
|
1290
|
+
'vm-launch-appcontainer-canary-snapshot-mismatch',
|
|
1291
|
+
'sandbox.canaryDigest',
|
|
1292
|
+
'Windows AppContainer canary changed while its private snapshot was materialized.'
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
760
1295
|
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
761
1296
|
|
|
762
1297
|
receipt.measurementDigest = hashJson({
|
|
@@ -765,22 +1300,60 @@ function runVmLaunchWithProcessRunner(
|
|
|
765
1300
|
binaryDigest: pinnedExecutor.binaryDigest,
|
|
766
1301
|
kernelDigest: pinnedKernel.digest,
|
|
767
1302
|
initrdDigest: pinnedInitrd.digest,
|
|
1303
|
+
...(adapter.hostSandboxed ? { sandboxLauncherDigest: pinnedLauncherDigest } : {}),
|
|
1304
|
+
...(adapter.appContainer ? { appContainerCanaryDigest: pinnedCanaryDigest } : {}),
|
|
768
1305
|
policy: adapter.policy,
|
|
769
1306
|
});
|
|
770
1307
|
|
|
771
|
-
const
|
|
772
|
-
const
|
|
1308
|
+
const qemuCommand = join(snapshotRuntime, EXECUTOR_BINARY);
|
|
1309
|
+
const qemuArgs = qemuArguments(plan, snapshotKernel, snapshotInitrd, adapter);
|
|
1310
|
+
const command = adapter.hostSandboxed ? snapshotLauncher : qemuCommand;
|
|
1311
|
+
const args = adapter.hostSandboxed
|
|
1312
|
+
? [
|
|
1313
|
+
'--executable',
|
|
1314
|
+
qemuCommand,
|
|
1315
|
+
'--working-directory',
|
|
1316
|
+
snapshotRuntime,
|
|
1317
|
+
'--sandbox-root',
|
|
1318
|
+
snapshotRoot,
|
|
1319
|
+
'--writable-temp',
|
|
1320
|
+
snapshotTemporary,
|
|
1321
|
+
'--timeout-ms',
|
|
1322
|
+
String(plan.resources.timeoutSeconds * 1000),
|
|
1323
|
+
'--process-memory-bytes',
|
|
1324
|
+
String(SANDBOX_PROCESS_MEMORY_BYTES),
|
|
1325
|
+
...(adapter.appContainer
|
|
1326
|
+
? [
|
|
1327
|
+
'--appcontainer-deny',
|
|
1328
|
+
'v1',
|
|
1329
|
+
'--protected-sentinel',
|
|
1330
|
+
protectedSentinel,
|
|
1331
|
+
'--canary-executable',
|
|
1332
|
+
snapshotCanary,
|
|
1333
|
+
]
|
|
1334
|
+
: []),
|
|
1335
|
+
'--',
|
|
1336
|
+
...qemuArgs,
|
|
1337
|
+
]
|
|
1338
|
+
: qemuArgs;
|
|
773
1339
|
const options = {
|
|
774
1340
|
encoding: null,
|
|
775
|
-
env: minimalEnvironment(
|
|
776
|
-
|
|
1341
|
+
env: minimalEnvironment(
|
|
1342
|
+
snapshotRoot,
|
|
1343
|
+
snapshotRuntime,
|
|
1344
|
+
adapter.hostSandboxed ? snapshotTemporary : snapshotRoot
|
|
1345
|
+
),
|
|
1346
|
+
maxBuffer: adapter.hostSandboxed ? SANDBOX_PROTOCOL_BYTES : MAX_CONSOLE_BYTES,
|
|
777
1347
|
shell: false,
|
|
778
|
-
timeout: plan.resources.timeoutSeconds * 1000,
|
|
1348
|
+
timeout: plan.resources.timeoutSeconds * 1000 + (adapter.hostSandboxed ? 5000 : 0),
|
|
779
1349
|
windowsHide: true,
|
|
780
1350
|
};
|
|
781
1351
|
|
|
782
1352
|
for (let index = 0; index < plan.launches; index += 1) {
|
|
783
|
-
if (
|
|
1353
|
+
if (
|
|
1354
|
+
!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
|
|
1355
|
+
!sandboxArtifactsSnapshotMatch(plan, snapshotLauncher, snapshotCanary, adapter)
|
|
1356
|
+
) {
|
|
784
1357
|
issue(
|
|
785
1358
|
receipt.issues,
|
|
786
1359
|
'vm-launch-snapshot-drift',
|
|
@@ -795,10 +1368,30 @@ function runVmLaunchWithProcessRunner(
|
|
|
795
1368
|
} catch {
|
|
796
1369
|
result = { status: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
|
|
797
1370
|
}
|
|
1371
|
+
if (adapter.hostSandboxed) {
|
|
1372
|
+
const protocol = parseSandboxProtocol(result, adapter);
|
|
1373
|
+
if (!protocol.ready) {
|
|
1374
|
+
issue(
|
|
1375
|
+
receipt.issues,
|
|
1376
|
+
protocol.issueCode,
|
|
1377
|
+
`launches[${index}].isolation`,
|
|
1378
|
+
'Measured Windows sandbox launcher did not return complete closed-protocol evidence.'
|
|
1379
|
+
);
|
|
1380
|
+
result = {
|
|
1381
|
+
status: null,
|
|
1382
|
+
signal: null,
|
|
1383
|
+
stdout: Buffer.alloc(0),
|
|
1384
|
+
stderr: Buffer.alloc(0),
|
|
1385
|
+
isolation: null,
|
|
1386
|
+
};
|
|
1387
|
+
} else {
|
|
1388
|
+
result = protocol;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
798
1391
|
const stdout = logSummary(result?.stdout);
|
|
799
1392
|
const stderr = logSummary(
|
|
800
1393
|
adapter.requiresDiagnosticsDigest
|
|
801
|
-
? normalizedDiagnostics(result?.stderr,
|
|
1394
|
+
? normalizedDiagnostics(result?.stderr, qemuCommand)
|
|
802
1395
|
: result?.stderr
|
|
803
1396
|
);
|
|
804
1397
|
const launch = {
|
|
@@ -807,6 +1400,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
807
1400
|
signal: typeof result?.signal === 'string' ? result.signal : null,
|
|
808
1401
|
stdout,
|
|
809
1402
|
stderr,
|
|
1403
|
+
...(adapter.hostSandboxed ? { isolation: result?.isolation || null } : {}),
|
|
810
1404
|
};
|
|
811
1405
|
receipt.launches.push(launch);
|
|
812
1406
|
if (result?.status === null || result?.status === undefined || result?.error) {
|
|
@@ -850,7 +1444,10 @@ function runVmLaunchWithProcessRunner(
|
|
|
850
1444
|
'Host emulator diagnostics were emitted; raw bytes are withheld.'
|
|
851
1445
|
);
|
|
852
1446
|
}
|
|
853
|
-
if (
|
|
1447
|
+
if (
|
|
1448
|
+
!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
|
|
1449
|
+
!sandboxArtifactsSnapshotMatch(plan, snapshotLauncher, snapshotCanary, adapter)
|
|
1450
|
+
) {
|
|
854
1451
|
issue(
|
|
855
1452
|
receipt.issues,
|
|
856
1453
|
'vm-launch-snapshot-drift',
|
|
@@ -862,10 +1459,17 @@ function runVmLaunchWithProcessRunner(
|
|
|
862
1459
|
}
|
|
863
1460
|
} finally {
|
|
864
1461
|
rmSync(snapshotRoot, { recursive: true, force: true });
|
|
1462
|
+
if (protectedRoot) rmSync(protectedRoot, { recursive: true, force: true });
|
|
865
1463
|
}
|
|
866
1464
|
|
|
867
|
-
const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr }) =>
|
|
868
|
-
hashJson({
|
|
1465
|
+
const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr, isolation }) =>
|
|
1466
|
+
hashJson({
|
|
1467
|
+
exitCode,
|
|
1468
|
+
signal,
|
|
1469
|
+
stdout,
|
|
1470
|
+
stderr,
|
|
1471
|
+
...(adapter.hostSandboxed ? { isolation } : {}),
|
|
1472
|
+
})
|
|
869
1473
|
);
|
|
870
1474
|
if (launchSignatures.length === plan.launches && new Set(launchSignatures).size !== 1) {
|
|
871
1475
|
issue(
|
|
@@ -884,16 +1488,32 @@ function runVmLaunchWithProcessRunner(
|
|
|
884
1488
|
receipt.verified = true;
|
|
885
1489
|
receipt.hardwareBacked = adapter.hardwareBacked;
|
|
886
1490
|
receipt.acceleration.verified = adapter.hardwareBacked;
|
|
1491
|
+
if (adapter.hostSandboxed) {
|
|
1492
|
+
receipt.isolation = {
|
|
1493
|
+
...receipt.isolation,
|
|
1494
|
+
verified: true,
|
|
1495
|
+
controls: { ...receipt.launches[0].isolation },
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
887
1498
|
receipt.coverage = {
|
|
888
1499
|
includedLayers: [
|
|
889
1500
|
'guest-artifact-measurement',
|
|
890
1501
|
...(adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
1502
|
+
...(adapter.hostSandboxed ? ['host-process-isolation'] : []),
|
|
1503
|
+
...(adapter.filesystemConfidentiality ? ['host-filesystem-confidentiality'] : []),
|
|
1504
|
+
...(adapter.networkIsolation ? ['host-network-isolation'] : []),
|
|
891
1505
|
'machine-vm-launch',
|
|
892
1506
|
'virtual-device-minimization',
|
|
893
1507
|
],
|
|
894
1508
|
missingLayers: [
|
|
895
1509
|
...(!adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
896
|
-
'host-process-isolation',
|
|
1510
|
+
...(!adapter.hostSandboxed ? ['host-process-isolation'] : []),
|
|
1511
|
+
...(adapter.hostSandboxed && !adapter.filesystemConfidentiality
|
|
1512
|
+
? ['host-filesystem-confidentiality']
|
|
1513
|
+
: []),
|
|
1514
|
+
...(adapter.hostSandboxed && !adapter.networkIsolation
|
|
1515
|
+
? ['host-network-isolation']
|
|
1516
|
+
: []),
|
|
897
1517
|
],
|
|
898
1518
|
};
|
|
899
1519
|
}
|
|
@@ -908,6 +1528,18 @@ export function runWhpxVmLaunch(options = {}) {
|
|
|
908
1528
|
return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_ADAPTER);
|
|
909
1529
|
}
|
|
910
1530
|
|
|
1531
|
+
export function runWhpxSandboxedVmLaunch(options = {}) {
|
|
1532
|
+
return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_SANDBOXED_ADAPTER);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
export function runWhpxAppContainerVmLaunch(options = {}) {
|
|
1536
|
+
return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_APPCONTAINER_ADAPTER);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
export function runAppContainerVmLaunch(options = {}) {
|
|
1540
|
+
return runVmLaunchWithProcessRunner(options, spawnSync, APPCONTAINER_TCG_ADAPTER);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
911
1543
|
// Repository tests need a deterministic process boundary without publishing an
|
|
912
1544
|
// injectable executor through the package root export.
|
|
913
1545
|
export function runVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
@@ -925,3 +1557,27 @@ export function runWhpxVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
|
925
1557
|
const { processRunner, ...launchOptions } = options;
|
|
926
1558
|
return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_ADAPTER);
|
|
927
1559
|
}
|
|
1560
|
+
|
|
1561
|
+
export function runWhpxSandboxedVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
1562
|
+
if (typeof options.processRunner !== 'function') {
|
|
1563
|
+
throw new TypeError('processRunner test adapter is required.');
|
|
1564
|
+
}
|
|
1565
|
+
const { processRunner, ...launchOptions } = options;
|
|
1566
|
+
return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_SANDBOXED_ADAPTER);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
export function runWhpxAppContainerVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
1570
|
+
if (typeof options.processRunner !== 'function') {
|
|
1571
|
+
throw new TypeError('processRunner test adapter is required.');
|
|
1572
|
+
}
|
|
1573
|
+
const { processRunner, ...launchOptions } = options;
|
|
1574
|
+
return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_APPCONTAINER_ADAPTER);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
export function runAppContainerVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
1578
|
+
if (typeof options.processRunner !== 'function') {
|
|
1579
|
+
throw new TypeError('processRunner test adapter is required.');
|
|
1580
|
+
}
|
|
1581
|
+
const { processRunner, ...launchOptions } = options;
|
|
1582
|
+
return runVmLaunchWithProcessRunner(launchOptions, processRunner, APPCONTAINER_TCG_ADAPTER);
|
|
1583
|
+
}
|