@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/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(['schema', 'id', 'host', 'executor', 'target', 'guest', 'resources', 'launches']),
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
- hostProcess: 'ambient-windows-process',
530
- verified: false,
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: BOUNDARIES.filter(
583
- (layer) => !(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration')
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
- ...(adapter === TCG_ADAPTER ? ['-cpu', 'max'] : []),
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: snapshotRoot,
625
- TMP: snapshotRoot,
626
- PATH: runtimeDirectory,
891
+ TEMP: temporaryDirectory,
892
+ TMP: temporaryDirectory,
627
893
  LANG: 'C',
628
894
  LC_ALL: 'C',
629
895
  };
630
- if (typeof process.env.SystemRoot === 'string') env.SystemRoot = process.env.SystemRoot;
631
- if (typeof process.env.WINDIR === 'string') env.WINDIR = process.env.WINDIR;
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
- receipt.issues.push(...executor.issues, ...kernel.issues, ...initrd.issues);
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 command = join(snapshotRuntime, EXECUTOR_BINARY);
772
- const args = qemuArguments(plan, snapshotKernel, snapshotInitrd, adapter);
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(snapshotRoot, snapshotRuntime),
776
- maxBuffer: MAX_CONSOLE_BYTES,
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 (!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd)) {
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, command)
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 (!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd)) {
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({ exitCode, signal, stdout, stderr })
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
+ }