@holoscript/holosystem 0.2.3 → 0.2.4
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 +44 -3
- package/bin/holosystem.mjs +7 -0
- package/docs/vm-launch-threat-model.md +53 -23
- package/native/windows-sandbox/Program.cs +667 -0
- package/native/windows-sandbox/README.md +19 -0
- package/native/windows-sandbox/build.ps1 +18 -0
- package/native/windows-x64/holosystem-sandbox-launcher.exe +0 -0
- package/package.json +2 -1
- package/src/index.mjs +8 -0
- package/src/vm-launch.mjs +388 -23
package/src/vm-launch.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from 'node:fs';
|
|
12
12
|
import { tmpdir } from 'node:os';
|
|
13
13
|
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
15
16
|
export const HOLOSYSTEM_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.vm-launch-plan.v1';
|
|
16
17
|
export const HOLOSYSTEM_VM_EXECUTOR_SCHEMA = 'holoscript.holosystem.vm-executor.v1';
|
|
@@ -19,6 +20,14 @@ export const HOLOSYSTEM_VM_LAUNCH_RECEIPT_SCHEMA = 'holoscript.holosystem.vm-lau
|
|
|
19
20
|
export const HOLOSYSTEM_WHPX_VM_LAUNCH_PLAN_SCHEMA = 'holoscript.holosystem.whpx-vm-launch-plan.v1';
|
|
20
21
|
export const HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
21
22
|
'holoscript.holosystem.whpx-vm-launch-receipt.v1';
|
|
23
|
+
export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA =
|
|
24
|
+
'holoscript.holosystem.whpx-sandboxed-vm-launch-plan.v1';
|
|
25
|
+
export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
26
|
+
'holoscript.holosystem.whpx-sandboxed-vm-launch-receipt.v1';
|
|
27
|
+
export const HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL =
|
|
28
|
+
'holoscript.holosystem.windows-sandbox-launch.v1';
|
|
29
|
+
export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA =
|
|
30
|
+
'holoscript.holosystem.windows-sandbox-launcher.v1';
|
|
22
31
|
|
|
23
32
|
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
24
33
|
const EXECUTOR_BINARY = 'qemu-system-x86_64.exe';
|
|
@@ -29,6 +38,16 @@ const MAX_KERNEL_BYTES = 128 * 1024 * 1024;
|
|
|
29
38
|
const MAX_INITRD_BYTES = 512 * 1024 * 1024;
|
|
30
39
|
const MAX_CONSOLE_BYTES = 1024 * 1024;
|
|
31
40
|
const EXPECTED_EXIT_CODE = 33;
|
|
41
|
+
const SANDBOX_LAUNCHER_BINARY = 'holosystem-sandbox-launcher.exe';
|
|
42
|
+
const SANDBOX_KIND = 'windows-low-integrity-job-v1';
|
|
43
|
+
const SANDBOX_PROCESS_MEMORY_BYTES = 512 * 1024 * 1024;
|
|
44
|
+
const SANDBOX_PROTOCOL_BYTES = 3 * 1024 * 1024;
|
|
45
|
+
const SANDBOX_LAUNCHER_PATH = fileURLToPath(
|
|
46
|
+
new URL(`../native/windows-x64/${SANDBOX_LAUNCHER_BINARY}`, import.meta.url)
|
|
47
|
+
);
|
|
48
|
+
export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST = hashBytes(
|
|
49
|
+
readFileSync(SANDBOX_LAUNCHER_PATH)
|
|
50
|
+
);
|
|
32
51
|
const TCG_POLICY = Object.freeze({
|
|
33
52
|
accelerator: 'tcg',
|
|
34
53
|
userConfiguration: 'disabled',
|
|
@@ -55,6 +74,13 @@ const WHPX_POLICY = Object.freeze({
|
|
|
55
74
|
guestSignal: 'serial-digest-and-debug-exit',
|
|
56
75
|
diagnostics: 'pinned-digest',
|
|
57
76
|
});
|
|
77
|
+
const WHPX_SANDBOXED_POLICY = Object.freeze({
|
|
78
|
+
...WHPX_POLICY,
|
|
79
|
+
hostProcess: SANDBOX_KIND,
|
|
80
|
+
token: 'disable-max-privilege-low-integrity',
|
|
81
|
+
job: 'pre-resume-kill-on-close-active-process-memory-ui',
|
|
82
|
+
writableTemp: 'low-integrity-private-snapshot',
|
|
83
|
+
});
|
|
58
84
|
const BOUNDARIES = Object.freeze([
|
|
59
85
|
'guest-artifact-provenance',
|
|
60
86
|
'hardware-hypervisor-acceleration',
|
|
@@ -82,6 +108,17 @@ const WHPX_ADAPTER = Object.freeze({
|
|
|
82
108
|
receiptSchema: HOLOSYSTEM_WHPX_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
83
109
|
policy: WHPX_POLICY,
|
|
84
110
|
requiresDiagnosticsDigest: true,
|
|
111
|
+
hostSandboxed: false,
|
|
112
|
+
});
|
|
113
|
+
const WHPX_SANDBOXED_ADAPTER = Object.freeze({
|
|
114
|
+
accelerator: 'whpx',
|
|
115
|
+
accelerationName: 'qemu-whpx',
|
|
116
|
+
hardwareBacked: true,
|
|
117
|
+
planSchema: HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA,
|
|
118
|
+
receiptSchema: HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA,
|
|
119
|
+
policy: WHPX_SANDBOXED_POLICY,
|
|
120
|
+
requiresDiagnosticsDigest: true,
|
|
121
|
+
hostSandboxed: true,
|
|
85
122
|
});
|
|
86
123
|
|
|
87
124
|
function isRecord(value) {
|
|
@@ -169,6 +206,9 @@ function normalizedPlan(plan) {
|
|
|
169
206
|
cpus: plan.resources.cpus,
|
|
170
207
|
timeoutSeconds: plan.resources.timeoutSeconds,
|
|
171
208
|
},
|
|
209
|
+
sandbox: plan.sandbox
|
|
210
|
+
? { kind: plan.sandbox.kind, launcherDigest: plan.sandbox.launcherDigest }
|
|
211
|
+
: null,
|
|
172
212
|
launches: plan.launches,
|
|
173
213
|
};
|
|
174
214
|
}
|
|
@@ -182,7 +222,17 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
|
|
|
182
222
|
|
|
183
223
|
knownKeys(
|
|
184
224
|
plan,
|
|
185
|
-
new Set([
|
|
225
|
+
new Set([
|
|
226
|
+
'schema',
|
|
227
|
+
'id',
|
|
228
|
+
'host',
|
|
229
|
+
'executor',
|
|
230
|
+
'target',
|
|
231
|
+
'guest',
|
|
232
|
+
'resources',
|
|
233
|
+
...(adapter.hostSandboxed ? ['sandbox'] : []),
|
|
234
|
+
'launches',
|
|
235
|
+
]),
|
|
186
236
|
'',
|
|
187
237
|
issues
|
|
188
238
|
);
|
|
@@ -280,6 +330,19 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
|
|
|
280
330
|
);
|
|
281
331
|
}
|
|
282
332
|
|
|
333
|
+
if (adapter.hostSandboxed) {
|
|
334
|
+
knownKeys(plan.sandbox, new Set(['kind', 'launcherDigest']), 'sandbox', issues);
|
|
335
|
+
if (plan.sandbox?.kind !== SANDBOX_KIND) {
|
|
336
|
+
issue(
|
|
337
|
+
issues,
|
|
338
|
+
'vm-launch-sandbox-unsupported',
|
|
339
|
+
'sandbox.kind',
|
|
340
|
+
`This adapter requires ${SANDBOX_KIND}.`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
inspectDigest(plan.sandbox?.launcherDigest, 'sandbox.launcherDigest', issues);
|
|
344
|
+
}
|
|
345
|
+
|
|
283
346
|
return { ready: issues.length === 0, issues };
|
|
284
347
|
}
|
|
285
348
|
|
|
@@ -291,6 +354,10 @@ export function inspectWhpxVmLaunchPlan(plan) {
|
|
|
291
354
|
return inspectVmLaunchPlanForAdapter(plan, WHPX_ADAPTER);
|
|
292
355
|
}
|
|
293
356
|
|
|
357
|
+
export function inspectWhpxSandboxedVmLaunchPlan(plan) {
|
|
358
|
+
return inspectVmLaunchPlanForAdapter(plan, WHPX_SANDBOXED_ADAPTER);
|
|
359
|
+
}
|
|
360
|
+
|
|
294
361
|
function scanRuntime(directory, issues) {
|
|
295
362
|
const files = [];
|
|
296
363
|
let totalBytes = 0;
|
|
@@ -482,6 +549,40 @@ export function inspectVmLaunchAsset({ assetPath, kind } = {}) {
|
|
|
482
549
|
};
|
|
483
550
|
}
|
|
484
551
|
|
|
552
|
+
export function inspectWindowsVmSandboxLauncher() {
|
|
553
|
+
const issues = [];
|
|
554
|
+
let content = null;
|
|
555
|
+
try {
|
|
556
|
+
const stats = lstatSync(SANDBOX_LAUNCHER_PATH);
|
|
557
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
|
|
558
|
+
content = readFileSync(SANDBOX_LAUNCHER_PATH);
|
|
559
|
+
} catch {
|
|
560
|
+
issue(
|
|
561
|
+
issues,
|
|
562
|
+
'vm-launch-sandbox-launcher-invalid',
|
|
563
|
+
'sandbox.launcher',
|
|
564
|
+
'The packaged Windows sandbox launcher must be a readable regular file.'
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
const digest = content ? hashBytes(content) : null;
|
|
568
|
+
if (digest && digest !== HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST) {
|
|
569
|
+
issue(
|
|
570
|
+
issues,
|
|
571
|
+
'vm-launch-sandbox-launcher-drift',
|
|
572
|
+
'sandbox.launcherDigest',
|
|
573
|
+
'The packaged Windows sandbox launcher changed after module initialization.'
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return {
|
|
577
|
+
schema: HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA,
|
|
578
|
+
ready: issues.length === 0,
|
|
579
|
+
kind: SANDBOX_KIND,
|
|
580
|
+
digest: issues.length === 0 ? digest : null,
|
|
581
|
+
bytes: issues.length === 0 ? content.length : null,
|
|
582
|
+
issues,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
485
586
|
function logSummary(value) {
|
|
486
587
|
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
|
|
487
588
|
return { bytes: bytes.length, digest: hashBytes(bytes) };
|
|
@@ -525,10 +626,33 @@ function baseReceipt(plan, now, issues, adapter) {
|
|
|
525
626
|
evidence: adapter.hardwareBacked ? 'two-explicit-successful-launches' : 'software-emulation',
|
|
526
627
|
verified: false,
|
|
527
628
|
},
|
|
528
|
-
isolation:
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
629
|
+
isolation: adapter.hostSandboxed
|
|
630
|
+
? {
|
|
631
|
+
hostProcess: SANDBOX_KIND,
|
|
632
|
+
scope: 'integrity-privilege-lifetime-resource-ui',
|
|
633
|
+
verified: false,
|
|
634
|
+
launcherDigest: DIGEST_PATTERN.test(plan?.sandbox?.launcherDigest || '')
|
|
635
|
+
? plan.sandbox.launcherDigest
|
|
636
|
+
: null,
|
|
637
|
+
controls: {
|
|
638
|
+
filteredToken: false,
|
|
639
|
+
disableMaxPrivilege: false,
|
|
640
|
+
enabledPrivilegeCount: null,
|
|
641
|
+
privilegesBounded: false,
|
|
642
|
+
lowIntegrity: false,
|
|
643
|
+
assignedBeforeResume: false,
|
|
644
|
+
handleAllowlist: false,
|
|
645
|
+
killOnClose: false,
|
|
646
|
+
activeProcessLimit: false,
|
|
647
|
+
processMemoryLimit: false,
|
|
648
|
+
uiRestrictions: false,
|
|
649
|
+
writableTempLowIntegrity: false,
|
|
650
|
+
},
|
|
651
|
+
}
|
|
652
|
+
: {
|
|
653
|
+
hostProcess: 'ambient-windows-process',
|
|
654
|
+
verified: false,
|
|
655
|
+
},
|
|
532
656
|
measurementDigest: null,
|
|
533
657
|
executor: {
|
|
534
658
|
kind: plan?.executor?.kind === 'qemu-system' ? 'qemu-system' : null,
|
|
@@ -579,9 +703,16 @@ function baseReceipt(plan, now, issues, adapter) {
|
|
|
579
703
|
'virtual-device-minimization',
|
|
580
704
|
],
|
|
581
705
|
},
|
|
582
|
-
boundaries:
|
|
583
|
-
(
|
|
584
|
-
|
|
706
|
+
boundaries: [
|
|
707
|
+
...BOUNDARIES.filter(
|
|
708
|
+
(layer) =>
|
|
709
|
+
!(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration') &&
|
|
710
|
+
!(adapter.hostSandboxed && layer === 'host-process-isolation')
|
|
711
|
+
),
|
|
712
|
+
...(adapter.hostSandboxed
|
|
713
|
+
? ['host-filesystem-confidentiality', 'host-network-isolation']
|
|
714
|
+
: []),
|
|
715
|
+
],
|
|
585
716
|
issues,
|
|
586
717
|
};
|
|
587
718
|
}
|
|
@@ -617,12 +748,12 @@ function qemuArguments(plan, kernelPath, initrdPath, adapter) {
|
|
|
617
748
|
];
|
|
618
749
|
}
|
|
619
750
|
|
|
620
|
-
function minimalEnvironment(snapshotRoot, runtimeDirectory) {
|
|
751
|
+
function minimalEnvironment(snapshotRoot, runtimeDirectory, temporaryDirectory = snapshotRoot) {
|
|
621
752
|
const env = {
|
|
622
753
|
HOME: snapshotRoot,
|
|
623
754
|
USERPROFILE: snapshotRoot,
|
|
624
|
-
TEMP:
|
|
625
|
-
TMP:
|
|
755
|
+
TEMP: temporaryDirectory,
|
|
756
|
+
TMP: temporaryDirectory,
|
|
626
757
|
PATH: runtimeDirectory,
|
|
627
758
|
LANG: 'C',
|
|
628
759
|
LC_ALL: 'C',
|
|
@@ -632,6 +763,104 @@ function minimalEnvironment(snapshotRoot, runtimeDirectory) {
|
|
|
632
763
|
return env;
|
|
633
764
|
}
|
|
634
765
|
|
|
766
|
+
const SANDBOX_CONTROL_KEYS = Object.freeze([
|
|
767
|
+
'filteredToken',
|
|
768
|
+
'disableMaxPrivilege',
|
|
769
|
+
'enabledPrivilegeCount',
|
|
770
|
+
'privilegesBounded',
|
|
771
|
+
'lowIntegrity',
|
|
772
|
+
'assignedBeforeResume',
|
|
773
|
+
'handleAllowlist',
|
|
774
|
+
'killOnClose',
|
|
775
|
+
'activeProcessLimit',
|
|
776
|
+
'processMemoryLimit',
|
|
777
|
+
'uiRestrictions',
|
|
778
|
+
'writableTempLowIntegrity',
|
|
779
|
+
]);
|
|
780
|
+
|
|
781
|
+
function canonicalBase64(value) {
|
|
782
|
+
if (
|
|
783
|
+
typeof value !== 'string' ||
|
|
784
|
+
value.length > Math.ceil(MAX_CONSOLE_BYTES / 3) * 4 ||
|
|
785
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(value)
|
|
786
|
+
) {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
const bytes = Buffer.from(value, 'base64');
|
|
790
|
+
return bytes.toString('base64') === value && bytes.length <= MAX_CONSOLE_BYTES ? bytes : null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function parseSandboxProtocol(result) {
|
|
794
|
+
if (
|
|
795
|
+
result?.status !== 0 ||
|
|
796
|
+
result?.signal ||
|
|
797
|
+
result?.error ||
|
|
798
|
+
logSummary(result?.stderr).bytes !== 0
|
|
799
|
+
) {
|
|
800
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-launcher-failed' };
|
|
801
|
+
}
|
|
802
|
+
let message;
|
|
803
|
+
try {
|
|
804
|
+
const encoded = Buffer.isBuffer(result.stdout)
|
|
805
|
+
? result.stdout.toString('utf8')
|
|
806
|
+
: String(result.stdout || '');
|
|
807
|
+
message = JSON.parse(encoded.trim());
|
|
808
|
+
} catch {
|
|
809
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
|
|
810
|
+
}
|
|
811
|
+
if (
|
|
812
|
+
!isRecord(message) ||
|
|
813
|
+
message.protocol !== HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL ||
|
|
814
|
+
!isRecord(message.isolation) ||
|
|
815
|
+
Object.keys(message).some(
|
|
816
|
+
(key) =>
|
|
817
|
+
![
|
|
818
|
+
'protocol',
|
|
819
|
+
'launched',
|
|
820
|
+
'timedOut',
|
|
821
|
+
'exitCode',
|
|
822
|
+
'isolation',
|
|
823
|
+
'stdoutBase64',
|
|
824
|
+
'stderrBase64',
|
|
825
|
+
'errorStage',
|
|
826
|
+
'errorCode',
|
|
827
|
+
].includes(key)
|
|
828
|
+
) ||
|
|
829
|
+
Object.keys(message.isolation).some((key) => !SANDBOX_CONTROL_KEYS.includes(key))
|
|
830
|
+
) {
|
|
831
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
|
|
832
|
+
}
|
|
833
|
+
const stdout = canonicalBase64(message.stdoutBase64);
|
|
834
|
+
const stderr = canonicalBase64(message.stderrBase64);
|
|
835
|
+
const controlsReady =
|
|
836
|
+
SANDBOX_CONTROL_KEYS.every(
|
|
837
|
+
(key) => key === 'enabledPrivilegeCount' || message.isolation[key] === true
|
|
838
|
+
) &&
|
|
839
|
+
Number.isInteger(message.isolation.enabledPrivilegeCount) &&
|
|
840
|
+
message.isolation.enabledPrivilegeCount >= 0 &&
|
|
841
|
+
message.isolation.enabledPrivilegeCount <= 1;
|
|
842
|
+
if (
|
|
843
|
+
message.launched !== true ||
|
|
844
|
+
message.timedOut !== false ||
|
|
845
|
+
!Number.isInteger(message.exitCode) ||
|
|
846
|
+
message.errorStage !== null ||
|
|
847
|
+
message.errorCode !== 0 ||
|
|
848
|
+
!controlsReady ||
|
|
849
|
+
!stdout ||
|
|
850
|
+
!stderr
|
|
851
|
+
) {
|
|
852
|
+
return { ready: false, issueCode: 'vm-launch-sandbox-evidence-invalid' };
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
ready: true,
|
|
856
|
+
status: message.exitCode,
|
|
857
|
+
signal: null,
|
|
858
|
+
stdout,
|
|
859
|
+
stderr,
|
|
860
|
+
isolation: Object.fromEntries(SANDBOX_CONTROL_KEYS.map((key) => [key, message.isolation[key]])),
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
|
|
635
864
|
function copyRuntimeSnapshot(sourceDirectory, report, destinationDirectory) {
|
|
636
865
|
mkdirSync(destinationDirectory, { recursive: true });
|
|
637
866
|
for (const file of report.files) {
|
|
@@ -656,6 +885,20 @@ function launchSnapshotMatches(plan, runtimeDirectory, kernelPath, initrdPath) {
|
|
|
656
885
|
);
|
|
657
886
|
}
|
|
658
887
|
|
|
888
|
+
function sandboxLauncherSnapshotMatches(plan, launcherPath, adapter) {
|
|
889
|
+
if (!adapter.hostSandboxed) return true;
|
|
890
|
+
try {
|
|
891
|
+
const stats = lstatSync(launcherPath);
|
|
892
|
+
return (
|
|
893
|
+
stats.isFile() &&
|
|
894
|
+
!stats.isSymbolicLink() &&
|
|
895
|
+
hashBytes(readFileSync(launcherPath)) === plan.sandbox.launcherDigest
|
|
896
|
+
);
|
|
897
|
+
} catch {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
659
902
|
function runVmLaunchWithProcessRunner(
|
|
660
903
|
{ plan, executorDirectory, kernelPath, initrdPath, now = new Date() } = {},
|
|
661
904
|
processRunner,
|
|
@@ -668,7 +911,13 @@ function runVmLaunchWithProcessRunner(
|
|
|
668
911
|
const executor = inspectVmExecutor({ executorDirectory });
|
|
669
912
|
const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
|
|
670
913
|
const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
|
|
671
|
-
|
|
914
|
+
const sandboxLauncher = adapter.hostSandboxed ? inspectWindowsVmSandboxLauncher() : null;
|
|
915
|
+
receipt.issues.push(
|
|
916
|
+
...executor.issues,
|
|
917
|
+
...kernel.issues,
|
|
918
|
+
...initrd.issues,
|
|
919
|
+
...(sandboxLauncher?.issues || [])
|
|
920
|
+
);
|
|
672
921
|
if (executor.ready && executor.digest !== plan.executor.runtimeDigest) {
|
|
673
922
|
issue(
|
|
674
923
|
receipt.issues,
|
|
@@ -701,6 +950,18 @@ function runVmLaunchWithProcessRunner(
|
|
|
701
950
|
'Guest initrd does not match the pinned plan digest.'
|
|
702
951
|
);
|
|
703
952
|
}
|
|
953
|
+
if (
|
|
954
|
+
adapter.hostSandboxed &&
|
|
955
|
+
sandboxLauncher?.ready &&
|
|
956
|
+
sandboxLauncher.digest !== plan.sandbox.launcherDigest
|
|
957
|
+
) {
|
|
958
|
+
issue(
|
|
959
|
+
receipt.issues,
|
|
960
|
+
'vm-launch-sandbox-launcher-mismatch',
|
|
961
|
+
'sandbox.launcherDigest',
|
|
962
|
+
'Packaged Windows sandbox launcher does not match the pinned plan digest.'
|
|
963
|
+
);
|
|
964
|
+
}
|
|
704
965
|
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
705
966
|
|
|
706
967
|
const sourceRuntime = resolve(executorDirectory);
|
|
@@ -710,12 +971,18 @@ function runVmLaunchWithProcessRunner(
|
|
|
710
971
|
const snapshotRuntime = join(snapshotRoot, 'runtime');
|
|
711
972
|
const snapshotKernel = join(snapshotRoot, 'vmlinuz');
|
|
712
973
|
const snapshotInitrd = join(snapshotRoot, 'initramfs');
|
|
974
|
+
const snapshotLauncher = join(snapshotRoot, SANDBOX_LAUNCHER_BINARY);
|
|
975
|
+
const snapshotTemporary = join(snapshotRoot, 'sandbox-temp');
|
|
713
976
|
|
|
714
977
|
try {
|
|
715
978
|
try {
|
|
716
979
|
copyRuntimeSnapshot(sourceRuntime, executor, snapshotRuntime);
|
|
717
980
|
copyFileSync(sourceKernel, snapshotKernel);
|
|
718
981
|
copyFileSync(sourceInitrd, snapshotInitrd);
|
|
982
|
+
if (adapter.hostSandboxed) {
|
|
983
|
+
copyFileSync(SANDBOX_LAUNCHER_PATH, snapshotLauncher);
|
|
984
|
+
mkdirSync(snapshotTemporary);
|
|
985
|
+
}
|
|
719
986
|
} catch {
|
|
720
987
|
issue(
|
|
721
988
|
receipt.issues,
|
|
@@ -729,6 +996,16 @@ function runVmLaunchWithProcessRunner(
|
|
|
729
996
|
const pinnedExecutor = inspectVmExecutor({ executorDirectory: snapshotRuntime });
|
|
730
997
|
const pinnedKernel = inspectVmLaunchAsset({ assetPath: snapshotKernel, kind: 'kernel' });
|
|
731
998
|
const pinnedInitrd = inspectVmLaunchAsset({ assetPath: snapshotInitrd, kind: 'initrd' });
|
|
999
|
+
let pinnedLauncherDigest = null;
|
|
1000
|
+
if (adapter.hostSandboxed) {
|
|
1001
|
+
try {
|
|
1002
|
+
const stats = lstatSync(snapshotLauncher);
|
|
1003
|
+
if (!stats.isFile() || stats.isSymbolicLink()) throw new TypeError('not a regular file');
|
|
1004
|
+
pinnedLauncherDigest = hashBytes(readFileSync(snapshotLauncher));
|
|
1005
|
+
} catch {
|
|
1006
|
+
pinnedLauncherDigest = null;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
732
1009
|
if (
|
|
733
1010
|
!pinnedExecutor.ready ||
|
|
734
1011
|
pinnedExecutor.digest !== plan.executor.runtimeDigest ||
|
|
@@ -757,6 +1034,14 @@ function runVmLaunchWithProcessRunner(
|
|
|
757
1034
|
'Guest initrd changed while its private launch snapshot was materialized.'
|
|
758
1035
|
);
|
|
759
1036
|
}
|
|
1037
|
+
if (adapter.hostSandboxed && pinnedLauncherDigest !== plan.sandbox.launcherDigest) {
|
|
1038
|
+
issue(
|
|
1039
|
+
receipt.issues,
|
|
1040
|
+
'vm-launch-sandbox-launcher-snapshot-mismatch',
|
|
1041
|
+
'sandbox.launcherDigest',
|
|
1042
|
+
'Windows sandbox launcher changed while its private snapshot was materialized.'
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
760
1045
|
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
761
1046
|
|
|
762
1047
|
receipt.measurementDigest = hashJson({
|
|
@@ -765,22 +1050,49 @@ function runVmLaunchWithProcessRunner(
|
|
|
765
1050
|
binaryDigest: pinnedExecutor.binaryDigest,
|
|
766
1051
|
kernelDigest: pinnedKernel.digest,
|
|
767
1052
|
initrdDigest: pinnedInitrd.digest,
|
|
1053
|
+
...(adapter.hostSandboxed ? { sandboxLauncherDigest: pinnedLauncherDigest } : {}),
|
|
768
1054
|
policy: adapter.policy,
|
|
769
1055
|
});
|
|
770
1056
|
|
|
771
|
-
const
|
|
772
|
-
const
|
|
1057
|
+
const qemuCommand = join(snapshotRuntime, EXECUTOR_BINARY);
|
|
1058
|
+
const qemuArgs = qemuArguments(plan, snapshotKernel, snapshotInitrd, adapter);
|
|
1059
|
+
const command = adapter.hostSandboxed ? snapshotLauncher : qemuCommand;
|
|
1060
|
+
const args = adapter.hostSandboxed
|
|
1061
|
+
? [
|
|
1062
|
+
'--executable',
|
|
1063
|
+
qemuCommand,
|
|
1064
|
+
'--working-directory',
|
|
1065
|
+
snapshotRuntime,
|
|
1066
|
+
'--sandbox-root',
|
|
1067
|
+
snapshotRoot,
|
|
1068
|
+
'--writable-temp',
|
|
1069
|
+
snapshotTemporary,
|
|
1070
|
+
'--timeout-ms',
|
|
1071
|
+
String(plan.resources.timeoutSeconds * 1000),
|
|
1072
|
+
'--process-memory-bytes',
|
|
1073
|
+
String(SANDBOX_PROCESS_MEMORY_BYTES),
|
|
1074
|
+
'--',
|
|
1075
|
+
...qemuArgs,
|
|
1076
|
+
]
|
|
1077
|
+
: qemuArgs;
|
|
773
1078
|
const options = {
|
|
774
1079
|
encoding: null,
|
|
775
|
-
env: minimalEnvironment(
|
|
776
|
-
|
|
1080
|
+
env: minimalEnvironment(
|
|
1081
|
+
snapshotRoot,
|
|
1082
|
+
snapshotRuntime,
|
|
1083
|
+
adapter.hostSandboxed ? snapshotTemporary : snapshotRoot
|
|
1084
|
+
),
|
|
1085
|
+
maxBuffer: adapter.hostSandboxed ? SANDBOX_PROTOCOL_BYTES : MAX_CONSOLE_BYTES,
|
|
777
1086
|
shell: false,
|
|
778
|
-
timeout: plan.resources.timeoutSeconds * 1000,
|
|
1087
|
+
timeout: plan.resources.timeoutSeconds * 1000 + (adapter.hostSandboxed ? 5000 : 0),
|
|
779
1088
|
windowsHide: true,
|
|
780
1089
|
};
|
|
781
1090
|
|
|
782
1091
|
for (let index = 0; index < plan.launches; index += 1) {
|
|
783
|
-
if (
|
|
1092
|
+
if (
|
|
1093
|
+
!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
|
|
1094
|
+
!sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
|
|
1095
|
+
) {
|
|
784
1096
|
issue(
|
|
785
1097
|
receipt.issues,
|
|
786
1098
|
'vm-launch-snapshot-drift',
|
|
@@ -795,10 +1107,30 @@ function runVmLaunchWithProcessRunner(
|
|
|
795
1107
|
} catch {
|
|
796
1108
|
result = { status: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
|
|
797
1109
|
}
|
|
1110
|
+
if (adapter.hostSandboxed) {
|
|
1111
|
+
const protocol = parseSandboxProtocol(result);
|
|
1112
|
+
if (!protocol.ready) {
|
|
1113
|
+
issue(
|
|
1114
|
+
receipt.issues,
|
|
1115
|
+
protocol.issueCode,
|
|
1116
|
+
`launches[${index}].isolation`,
|
|
1117
|
+
'Measured Windows sandbox launcher did not return complete closed-protocol evidence.'
|
|
1118
|
+
);
|
|
1119
|
+
result = {
|
|
1120
|
+
status: null,
|
|
1121
|
+
signal: null,
|
|
1122
|
+
stdout: Buffer.alloc(0),
|
|
1123
|
+
stderr: Buffer.alloc(0),
|
|
1124
|
+
isolation: null,
|
|
1125
|
+
};
|
|
1126
|
+
} else {
|
|
1127
|
+
result = protocol;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
798
1130
|
const stdout = logSummary(result?.stdout);
|
|
799
1131
|
const stderr = logSummary(
|
|
800
1132
|
adapter.requiresDiagnosticsDigest
|
|
801
|
-
? normalizedDiagnostics(result?.stderr,
|
|
1133
|
+
? normalizedDiagnostics(result?.stderr, qemuCommand)
|
|
802
1134
|
: result?.stderr
|
|
803
1135
|
);
|
|
804
1136
|
const launch = {
|
|
@@ -807,6 +1139,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
807
1139
|
signal: typeof result?.signal === 'string' ? result.signal : null,
|
|
808
1140
|
stdout,
|
|
809
1141
|
stderr,
|
|
1142
|
+
...(adapter.hostSandboxed ? { isolation: result?.isolation || null } : {}),
|
|
810
1143
|
};
|
|
811
1144
|
receipt.launches.push(launch);
|
|
812
1145
|
if (result?.status === null || result?.status === undefined || result?.error) {
|
|
@@ -850,7 +1183,10 @@ function runVmLaunchWithProcessRunner(
|
|
|
850
1183
|
'Host emulator diagnostics were emitted; raw bytes are withheld.'
|
|
851
1184
|
);
|
|
852
1185
|
}
|
|
853
|
-
if (
|
|
1186
|
+
if (
|
|
1187
|
+
!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
|
|
1188
|
+
!sandboxLauncherSnapshotMatches(plan, snapshotLauncher, adapter)
|
|
1189
|
+
) {
|
|
854
1190
|
issue(
|
|
855
1191
|
receipt.issues,
|
|
856
1192
|
'vm-launch-snapshot-drift',
|
|
@@ -864,8 +1200,14 @@ function runVmLaunchWithProcessRunner(
|
|
|
864
1200
|
rmSync(snapshotRoot, { recursive: true, force: true });
|
|
865
1201
|
}
|
|
866
1202
|
|
|
867
|
-
const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr }) =>
|
|
868
|
-
hashJson({
|
|
1203
|
+
const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr, isolation }) =>
|
|
1204
|
+
hashJson({
|
|
1205
|
+
exitCode,
|
|
1206
|
+
signal,
|
|
1207
|
+
stdout,
|
|
1208
|
+
stderr,
|
|
1209
|
+
...(adapter.hostSandboxed ? { isolation } : {}),
|
|
1210
|
+
})
|
|
869
1211
|
);
|
|
870
1212
|
if (launchSignatures.length === plan.launches && new Set(launchSignatures).size !== 1) {
|
|
871
1213
|
issue(
|
|
@@ -884,16 +1226,27 @@ function runVmLaunchWithProcessRunner(
|
|
|
884
1226
|
receipt.verified = true;
|
|
885
1227
|
receipt.hardwareBacked = adapter.hardwareBacked;
|
|
886
1228
|
receipt.acceleration.verified = adapter.hardwareBacked;
|
|
1229
|
+
if (adapter.hostSandboxed) {
|
|
1230
|
+
receipt.isolation = {
|
|
1231
|
+
...receipt.isolation,
|
|
1232
|
+
verified: true,
|
|
1233
|
+
controls: { ...receipt.launches[0].isolation },
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
887
1236
|
receipt.coverage = {
|
|
888
1237
|
includedLayers: [
|
|
889
1238
|
'guest-artifact-measurement',
|
|
890
1239
|
...(adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
1240
|
+
...(adapter.hostSandboxed ? ['host-process-isolation'] : []),
|
|
891
1241
|
'machine-vm-launch',
|
|
892
1242
|
'virtual-device-minimization',
|
|
893
1243
|
],
|
|
894
1244
|
missingLayers: [
|
|
895
1245
|
...(!adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
896
|
-
'host-process-isolation',
|
|
1246
|
+
...(!adapter.hostSandboxed ? ['host-process-isolation'] : []),
|
|
1247
|
+
...(adapter.hostSandboxed
|
|
1248
|
+
? ['host-filesystem-confidentiality', 'host-network-isolation']
|
|
1249
|
+
: []),
|
|
897
1250
|
],
|
|
898
1251
|
};
|
|
899
1252
|
}
|
|
@@ -908,6 +1261,10 @@ export function runWhpxVmLaunch(options = {}) {
|
|
|
908
1261
|
return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_ADAPTER);
|
|
909
1262
|
}
|
|
910
1263
|
|
|
1264
|
+
export function runWhpxSandboxedVmLaunch(options = {}) {
|
|
1265
|
+
return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_SANDBOXED_ADAPTER);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
911
1268
|
// Repository tests need a deterministic process boundary without publishing an
|
|
912
1269
|
// injectable executor through the package root export.
|
|
913
1270
|
export function runVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
@@ -925,3 +1282,11 @@ export function runWhpxVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
|
925
1282
|
const { processRunner, ...launchOptions } = options;
|
|
926
1283
|
return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_ADAPTER);
|
|
927
1284
|
}
|
|
1285
|
+
|
|
1286
|
+
export function runWhpxSandboxedVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
1287
|
+
if (typeof options.processRunner !== 'function') {
|
|
1288
|
+
throw new TypeError('processRunner test adapter is required.');
|
|
1289
|
+
}
|
|
1290
|
+
const { processRunner, ...launchOptions } = options;
|
|
1291
|
+
return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_SANDBOXED_ADAPTER);
|
|
1292
|
+
}
|