@holoscript/holosystem 0.2.4 → 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 +49 -0
- package/bin/holosystem.mjs +14 -0
- package/docs/vm-launch-threat-model.md +43 -5
- package/native/windows-sandbox/Canary.c +107 -0
- package/native/windows-sandbox/Program.cs +462 -12
- package/native/windows-sandbox/README.md +14 -3
- package/native/windows-sandbox/build.ps1 +31 -1
- package/native/windows-x64/holosystem-appcontainer-canary.exe +0 -0
- package/native/windows-x64/holosystem-sandbox-launcher.exe +0 -0
- package/package.json +1 -1
- package/src/index.mjs +12 -0
- package/src/vm-launch.mjs +336 -45
package/src/vm-launch.mjs
CHANGED
|
@@ -8,6 +8,7 @@ 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';
|
|
@@ -24,10 +25,22 @@ export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_PLAN_SCHEMA =
|
|
|
24
25
|
'holoscript.holosystem.whpx-sandboxed-vm-launch-plan.v1';
|
|
25
26
|
export const HOLOSYSTEM_WHPX_SANDBOXED_VM_LAUNCH_RECEIPT_SCHEMA =
|
|
26
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';
|
|
27
36
|
export const HOLOSYSTEM_WINDOWS_SANDBOX_PROTOCOL =
|
|
28
37
|
'holoscript.holosystem.windows-sandbox-launch.v1';
|
|
38
|
+
export const HOLOSYSTEM_WINDOWS_APPCONTAINER_PROTOCOL =
|
|
39
|
+
'holoscript.holosystem.windows-appcontainer-launch.v1';
|
|
29
40
|
export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_SCHEMA =
|
|
30
41
|
'holoscript.holosystem.windows-sandbox-launcher.v1';
|
|
42
|
+
export const HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_SCHEMA =
|
|
43
|
+
'holoscript.holosystem.windows-appcontainer-canary.v1';
|
|
31
44
|
|
|
32
45
|
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
|
|
33
46
|
const EXECUTOR_BINARY = 'qemu-system-x86_64.exe';
|
|
@@ -39,15 +52,23 @@ const MAX_INITRD_BYTES = 512 * 1024 * 1024;
|
|
|
39
52
|
const MAX_CONSOLE_BYTES = 1024 * 1024;
|
|
40
53
|
const EXPECTED_EXIT_CODE = 33;
|
|
41
54
|
const SANDBOX_LAUNCHER_BINARY = 'holosystem-sandbox-launcher.exe';
|
|
55
|
+
const APPCONTAINER_CANARY_BINARY = 'holosystem-appcontainer-canary.exe';
|
|
42
56
|
const SANDBOX_KIND = 'windows-low-integrity-job-v1';
|
|
57
|
+
const APPCONTAINER_KIND = 'windows-appcontainer-deny-v1';
|
|
43
58
|
const SANDBOX_PROCESS_MEMORY_BYTES = 512 * 1024 * 1024;
|
|
44
59
|
const SANDBOX_PROTOCOL_BYTES = 3 * 1024 * 1024;
|
|
45
60
|
const SANDBOX_LAUNCHER_PATH = fileURLToPath(
|
|
46
61
|
new URL(`../native/windows-x64/${SANDBOX_LAUNCHER_BINARY}`, import.meta.url)
|
|
47
62
|
);
|
|
63
|
+
const APPCONTAINER_CANARY_PATH = fileURLToPath(
|
|
64
|
+
new URL(`../native/windows-x64/${APPCONTAINER_CANARY_BINARY}`, import.meta.url)
|
|
65
|
+
);
|
|
48
66
|
export const HOLOSYSTEM_WINDOWS_SANDBOX_LAUNCHER_DIGEST = hashBytes(
|
|
49
67
|
readFileSync(SANDBOX_LAUNCHER_PATH)
|
|
50
68
|
);
|
|
69
|
+
export const HOLOSYSTEM_WINDOWS_APPCONTAINER_CANARY_DIGEST = hashBytes(
|
|
70
|
+
readFileSync(APPCONTAINER_CANARY_PATH)
|
|
71
|
+
);
|
|
51
72
|
const TCG_POLICY = Object.freeze({
|
|
52
73
|
accelerator: 'tcg',
|
|
53
74
|
userConfiguration: 'disabled',
|
|
@@ -81,6 +102,27 @@ const WHPX_SANDBOXED_POLICY = Object.freeze({
|
|
|
81
102
|
job: 'pre-resume-kill-on-close-active-process-memory-ui',
|
|
82
103
|
writableTemp: 'low-integrity-private-snapshot',
|
|
83
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
|
+
});
|
|
84
126
|
const BOUNDARIES = Object.freeze([
|
|
85
127
|
'guest-artifact-provenance',
|
|
86
128
|
'hardware-hypervisor-acceleration',
|
|
@@ -119,6 +161,44 @@ const WHPX_SANDBOXED_ADAPTER = Object.freeze({
|
|
|
119
161
|
policy: WHPX_SANDBOXED_POLICY,
|
|
120
162
|
requiresDiagnosticsDigest: true,
|
|
121
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,
|
|
122
202
|
});
|
|
123
203
|
|
|
124
204
|
function isRecord(value) {
|
|
@@ -331,16 +411,24 @@ function inspectVmLaunchPlanForAdapter(plan, adapter) {
|
|
|
331
411
|
}
|
|
332
412
|
|
|
333
413
|
if (adapter.hostSandboxed) {
|
|
334
|
-
knownKeys(
|
|
335
|
-
|
|
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) {
|
|
336
421
|
issue(
|
|
337
422
|
issues,
|
|
338
423
|
'vm-launch-sandbox-unsupported',
|
|
339
424
|
'sandbox.kind',
|
|
340
|
-
`This adapter requires ${
|
|
425
|
+
`This adapter requires ${adapter.sandboxKind}.`
|
|
341
426
|
);
|
|
342
427
|
}
|
|
343
428
|
inspectDigest(plan.sandbox?.launcherDigest, 'sandbox.launcherDigest', issues);
|
|
429
|
+
if (adapter.appContainer) {
|
|
430
|
+
inspectDigest(plan.sandbox?.canaryDigest, 'sandbox.canaryDigest', issues);
|
|
431
|
+
}
|
|
344
432
|
}
|
|
345
433
|
|
|
346
434
|
return { ready: issues.length === 0, issues };
|
|
@@ -358,6 +446,14 @@ export function inspectWhpxSandboxedVmLaunchPlan(plan) {
|
|
|
358
446
|
return inspectVmLaunchPlanForAdapter(plan, WHPX_SANDBOXED_ADAPTER);
|
|
359
447
|
}
|
|
360
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
|
+
|
|
361
457
|
function scanRuntime(directory, issues) {
|
|
362
458
|
const files = [];
|
|
363
459
|
let totalBytes = 0;
|
|
@@ -583,6 +679,40 @@ export function inspectWindowsVmSandboxLauncher() {
|
|
|
583
679
|
};
|
|
584
680
|
}
|
|
585
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
|
+
|
|
586
716
|
function logSummary(value) {
|
|
587
717
|
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(String(value || ''), 'utf8');
|
|
588
718
|
return { bytes: bytes.length, digest: hashBytes(bytes) };
|
|
@@ -628,26 +758,20 @@ function baseReceipt(plan, now, issues, adapter) {
|
|
|
628
758
|
},
|
|
629
759
|
isolation: adapter.hostSandboxed
|
|
630
760
|
? {
|
|
631
|
-
hostProcess:
|
|
632
|
-
scope:
|
|
761
|
+
hostProcess: adapter.sandboxKind,
|
|
762
|
+
scope: adapter.isolationScope,
|
|
633
763
|
verified: false,
|
|
634
764
|
launcherDigest: DIGEST_PATTERN.test(plan?.sandbox?.launcherDigest || '')
|
|
635
765
|
? plan.sandbox.launcherDigest
|
|
636
766
|
: null,
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
killOnClose: false,
|
|
646
|
-
activeProcessLimit: false,
|
|
647
|
-
processMemoryLimit: false,
|
|
648
|
-
uiRestrictions: false,
|
|
649
|
-
writableTempLowIntegrity: false,
|
|
650
|
-
},
|
|
767
|
+
...(adapter.appContainer
|
|
768
|
+
? {
|
|
769
|
+
canaryDigest: DIGEST_PATTERN.test(plan?.sandbox?.canaryDigest || '')
|
|
770
|
+
? plan.sandbox.canaryDigest
|
|
771
|
+
: null,
|
|
772
|
+
}
|
|
773
|
+
: {}),
|
|
774
|
+
controls: emptyIsolationControls(adapter),
|
|
651
775
|
}
|
|
652
776
|
: {
|
|
653
777
|
hostProcess: 'ambient-windows-process',
|
|
@@ -709,21 +833,24 @@ function baseReceipt(plan, now, issues, adapter) {
|
|
|
709
833
|
!(adapter.hardwareBacked && layer === 'hardware-hypervisor-acceleration') &&
|
|
710
834
|
!(adapter.hostSandboxed && layer === 'host-process-isolation')
|
|
711
835
|
),
|
|
712
|
-
...(adapter.hostSandboxed
|
|
713
|
-
? ['host-filesystem-confidentiality'
|
|
836
|
+
...(adapter.hostSandboxed && !adapter.filesystemConfidentiality
|
|
837
|
+
? ['host-filesystem-confidentiality']
|
|
714
838
|
: []),
|
|
839
|
+
...(adapter.hostSandboxed && !adapter.networkIsolation ? ['host-network-isolation'] : []),
|
|
715
840
|
],
|
|
716
841
|
issues,
|
|
717
842
|
};
|
|
718
843
|
}
|
|
719
844
|
|
|
720
845
|
function qemuArguments(plan, kernelPath, initrdPath, adapter) {
|
|
846
|
+
const appContainerTcg = adapter === APPCONTAINER_TCG_ADAPTER;
|
|
721
847
|
return [
|
|
722
848
|
'-no-user-config',
|
|
723
849
|
'-nodefaults',
|
|
724
850
|
'-machine',
|
|
725
|
-
`q35,accel=${adapter.accelerator},usb=off`,
|
|
726
|
-
...(
|
|
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'] : []),
|
|
727
854
|
'-m',
|
|
728
855
|
`${plan.resources.memoryMiB}M`,
|
|
729
856
|
'-smp',
|
|
@@ -749,17 +876,29 @@ function qemuArguments(plan, kernelPath, initrdPath, adapter) {
|
|
|
749
876
|
}
|
|
750
877
|
|
|
751
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);
|
|
752
881
|
const env = {
|
|
882
|
+
APPDATA: temporaryDirectory,
|
|
753
883
|
HOME: snapshotRoot,
|
|
884
|
+
HOMEDRIVE: systemDrive,
|
|
885
|
+
HOMEPATH: '\\',
|
|
886
|
+
LOCALAPPDATA: temporaryDirectory,
|
|
887
|
+
OS: 'Windows_NT',
|
|
888
|
+
Path: runtimeDirectory,
|
|
889
|
+
PATHEXT: '.COM;.EXE;.BAT;.CMD',
|
|
754
890
|
USERPROFILE: snapshotRoot,
|
|
755
891
|
TEMP: temporaryDirectory,
|
|
756
892
|
TMP: temporaryDirectory,
|
|
757
|
-
PATH: runtimeDirectory,
|
|
758
893
|
LANG: 'C',
|
|
759
894
|
LC_ALL: 'C',
|
|
760
895
|
};
|
|
761
|
-
if (
|
|
762
|
-
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
|
+
}
|
|
763
902
|
return env;
|
|
764
903
|
}
|
|
765
904
|
|
|
@@ -777,6 +916,74 @@ const SANDBOX_CONTROL_KEYS = Object.freeze([
|
|
|
777
916
|
'uiRestrictions',
|
|
778
917
|
'writableTempLowIntegrity',
|
|
779
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
|
+
}
|
|
780
987
|
|
|
781
988
|
function canonicalBase64(value) {
|
|
782
989
|
if (
|
|
@@ -790,7 +997,7 @@ function canonicalBase64(value) {
|
|
|
790
997
|
return bytes.toString('base64') === value && bytes.length <= MAX_CONSOLE_BYTES ? bytes : null;
|
|
791
998
|
}
|
|
792
999
|
|
|
793
|
-
function parseSandboxProtocol(result) {
|
|
1000
|
+
function parseSandboxProtocol(result, adapter) {
|
|
794
1001
|
if (
|
|
795
1002
|
result?.status !== 0 ||
|
|
796
1003
|
result?.signal ||
|
|
@@ -810,7 +1017,7 @@ function parseSandboxProtocol(result) {
|
|
|
810
1017
|
}
|
|
811
1018
|
if (
|
|
812
1019
|
!isRecord(message) ||
|
|
813
|
-
message.protocol !==
|
|
1020
|
+
message.protocol !== adapter.sandboxProtocol ||
|
|
814
1021
|
!isRecord(message.isolation) ||
|
|
815
1022
|
Object.keys(message).some(
|
|
816
1023
|
(key) =>
|
|
@@ -826,19 +1033,14 @@ function parseSandboxProtocol(result) {
|
|
|
826
1033
|
'errorCode',
|
|
827
1034
|
].includes(key)
|
|
828
1035
|
) ||
|
|
829
|
-
Object.keys(message.isolation).some((key) => !
|
|
1036
|
+
Object.keys(message.isolation).some((key) => !isolationControlKeys(adapter).includes(key)) ||
|
|
1037
|
+
Object.keys(message.isolation).length !== isolationControlKeys(adapter).length
|
|
830
1038
|
) {
|
|
831
1039
|
return { ready: false, issueCode: 'vm-launch-sandbox-protocol-invalid' };
|
|
832
1040
|
}
|
|
833
1041
|
const stdout = canonicalBase64(message.stdoutBase64);
|
|
834
1042
|
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;
|
|
1043
|
+
const controlsReady = isolationControlsReady(message.isolation, adapter);
|
|
842
1044
|
if (
|
|
843
1045
|
message.launched !== true ||
|
|
844
1046
|
message.timedOut !== false ||
|
|
@@ -857,7 +1059,9 @@ function parseSandboxProtocol(result) {
|
|
|
857
1059
|
signal: null,
|
|
858
1060
|
stdout,
|
|
859
1061
|
stderr,
|
|
860
|
-
isolation: Object.fromEntries(
|
|
1062
|
+
isolation: Object.fromEntries(
|
|
1063
|
+
isolationControlKeys(adapter).map((key) => [key, message.isolation[key]])
|
|
1064
|
+
),
|
|
861
1065
|
};
|
|
862
1066
|
}
|
|
863
1067
|
|
|
@@ -885,14 +1089,19 @@ function launchSnapshotMatches(plan, runtimeDirectory, kernelPath, initrdPath) {
|
|
|
885
1089
|
);
|
|
886
1090
|
}
|
|
887
1091
|
|
|
888
|
-
function
|
|
1092
|
+
function sandboxArtifactsSnapshotMatch(plan, launcherPath, canaryPath, adapter) {
|
|
889
1093
|
if (!adapter.hostSandboxed) return true;
|
|
890
1094
|
try {
|
|
891
1095
|
const stats = lstatSync(launcherPath);
|
|
1096
|
+
const canaryStats = adapter.appContainer ? lstatSync(canaryPath) : null;
|
|
892
1097
|
return (
|
|
893
1098
|
stats.isFile() &&
|
|
894
1099
|
!stats.isSymbolicLink() &&
|
|
895
|
-
hashBytes(readFileSync(launcherPath)) === plan.sandbox.launcherDigest
|
|
1100
|
+
hashBytes(readFileSync(launcherPath)) === plan.sandbox.launcherDigest &&
|
|
1101
|
+
(!adapter.appContainer ||
|
|
1102
|
+
(canaryStats.isFile() &&
|
|
1103
|
+
!canaryStats.isSymbolicLink() &&
|
|
1104
|
+
hashBytes(readFileSync(canaryPath)) === plan.sandbox.canaryDigest))
|
|
896
1105
|
);
|
|
897
1106
|
} catch {
|
|
898
1107
|
return false;
|
|
@@ -912,11 +1121,13 @@ function runVmLaunchWithProcessRunner(
|
|
|
912
1121
|
const kernel = inspectVmLaunchAsset({ assetPath: kernelPath, kind: 'kernel' });
|
|
913
1122
|
const initrd = inspectVmLaunchAsset({ assetPath: initrdPath, kind: 'initrd' });
|
|
914
1123
|
const sandboxLauncher = adapter.hostSandboxed ? inspectWindowsVmSandboxLauncher() : null;
|
|
1124
|
+
const appContainerCanary = adapter.appContainer ? inspectWindowsVmAppContainerCanary() : null;
|
|
915
1125
|
receipt.issues.push(
|
|
916
1126
|
...executor.issues,
|
|
917
1127
|
...kernel.issues,
|
|
918
1128
|
...initrd.issues,
|
|
919
|
-
...(sandboxLauncher?.issues || [])
|
|
1129
|
+
...(sandboxLauncher?.issues || []),
|
|
1130
|
+
...(appContainerCanary?.issues || [])
|
|
920
1131
|
);
|
|
921
1132
|
if (executor.ready && executor.digest !== plan.executor.runtimeDigest) {
|
|
922
1133
|
issue(
|
|
@@ -962,6 +1173,18 @@ function runVmLaunchWithProcessRunner(
|
|
|
962
1173
|
'Packaged Windows sandbox launcher does not match the pinned plan digest.'
|
|
963
1174
|
);
|
|
964
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
|
+
}
|
|
965
1188
|
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
966
1189
|
|
|
967
1190
|
const sourceRuntime = resolve(executorDirectory);
|
|
@@ -972,7 +1195,12 @@ function runVmLaunchWithProcessRunner(
|
|
|
972
1195
|
const snapshotKernel = join(snapshotRoot, 'vmlinuz');
|
|
973
1196
|
const snapshotInitrd = join(snapshotRoot, 'initramfs');
|
|
974
1197
|
const snapshotLauncher = join(snapshotRoot, SANDBOX_LAUNCHER_BINARY);
|
|
1198
|
+
const snapshotCanary = join(snapshotRoot, APPCONTAINER_CANARY_BINARY);
|
|
975
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;
|
|
976
1204
|
|
|
977
1205
|
try {
|
|
978
1206
|
try {
|
|
@@ -983,6 +1211,10 @@ function runVmLaunchWithProcessRunner(
|
|
|
983
1211
|
copyFileSync(SANDBOX_LAUNCHER_PATH, snapshotLauncher);
|
|
984
1212
|
mkdirSync(snapshotTemporary);
|
|
985
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
|
+
}
|
|
986
1218
|
} catch {
|
|
987
1219
|
issue(
|
|
988
1220
|
receipt.issues,
|
|
@@ -997,6 +1229,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
997
1229
|
const pinnedKernel = inspectVmLaunchAsset({ assetPath: snapshotKernel, kind: 'kernel' });
|
|
998
1230
|
const pinnedInitrd = inspectVmLaunchAsset({ assetPath: snapshotInitrd, kind: 'initrd' });
|
|
999
1231
|
let pinnedLauncherDigest = null;
|
|
1232
|
+
let pinnedCanaryDigest = null;
|
|
1000
1233
|
if (adapter.hostSandboxed) {
|
|
1001
1234
|
try {
|
|
1002
1235
|
const stats = lstatSync(snapshotLauncher);
|
|
@@ -1006,6 +1239,15 @@ function runVmLaunchWithProcessRunner(
|
|
|
1006
1239
|
pinnedLauncherDigest = null;
|
|
1007
1240
|
}
|
|
1008
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
|
+
}
|
|
1009
1251
|
if (
|
|
1010
1252
|
!pinnedExecutor.ready ||
|
|
1011
1253
|
pinnedExecutor.digest !== plan.executor.runtimeDigest ||
|
|
@@ -1042,6 +1284,14 @@ function runVmLaunchWithProcessRunner(
|
|
|
1042
1284
|
'Windows sandbox launcher changed while its private snapshot was materialized.'
|
|
1043
1285
|
);
|
|
1044
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
|
+
}
|
|
1045
1295
|
if (receipt.issues.length > 0) return finishReceipt(receipt);
|
|
1046
1296
|
|
|
1047
1297
|
receipt.measurementDigest = hashJson({
|
|
@@ -1051,6 +1301,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
1051
1301
|
kernelDigest: pinnedKernel.digest,
|
|
1052
1302
|
initrdDigest: pinnedInitrd.digest,
|
|
1053
1303
|
...(adapter.hostSandboxed ? { sandboxLauncherDigest: pinnedLauncherDigest } : {}),
|
|
1304
|
+
...(adapter.appContainer ? { appContainerCanaryDigest: pinnedCanaryDigest } : {}),
|
|
1054
1305
|
policy: adapter.policy,
|
|
1055
1306
|
});
|
|
1056
1307
|
|
|
@@ -1071,6 +1322,16 @@ function runVmLaunchWithProcessRunner(
|
|
|
1071
1322
|
String(plan.resources.timeoutSeconds * 1000),
|
|
1072
1323
|
'--process-memory-bytes',
|
|
1073
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
|
+
: []),
|
|
1074
1335
|
'--',
|
|
1075
1336
|
...qemuArgs,
|
|
1076
1337
|
]
|
|
@@ -1091,7 +1352,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
1091
1352
|
for (let index = 0; index < plan.launches; index += 1) {
|
|
1092
1353
|
if (
|
|
1093
1354
|
!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
|
|
1094
|
-
!
|
|
1355
|
+
!sandboxArtifactsSnapshotMatch(plan, snapshotLauncher, snapshotCanary, adapter)
|
|
1095
1356
|
) {
|
|
1096
1357
|
issue(
|
|
1097
1358
|
receipt.issues,
|
|
@@ -1108,7 +1369,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
1108
1369
|
result = { status: null, signal: null, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0) };
|
|
1109
1370
|
}
|
|
1110
1371
|
if (adapter.hostSandboxed) {
|
|
1111
|
-
const protocol = parseSandboxProtocol(result);
|
|
1372
|
+
const protocol = parseSandboxProtocol(result, adapter);
|
|
1112
1373
|
if (!protocol.ready) {
|
|
1113
1374
|
issue(
|
|
1114
1375
|
receipt.issues,
|
|
@@ -1185,7 +1446,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
1185
1446
|
}
|
|
1186
1447
|
if (
|
|
1187
1448
|
!launchSnapshotMatches(plan, snapshotRuntime, snapshotKernel, snapshotInitrd) ||
|
|
1188
|
-
!
|
|
1449
|
+
!sandboxArtifactsSnapshotMatch(plan, snapshotLauncher, snapshotCanary, adapter)
|
|
1189
1450
|
) {
|
|
1190
1451
|
issue(
|
|
1191
1452
|
receipt.issues,
|
|
@@ -1198,6 +1459,7 @@ function runVmLaunchWithProcessRunner(
|
|
|
1198
1459
|
}
|
|
1199
1460
|
} finally {
|
|
1200
1461
|
rmSync(snapshotRoot, { recursive: true, force: true });
|
|
1462
|
+
if (protectedRoot) rmSync(protectedRoot, { recursive: true, force: true });
|
|
1201
1463
|
}
|
|
1202
1464
|
|
|
1203
1465
|
const launchSignatures = receipt.launches.map(({ exitCode, signal, stdout, stderr, isolation }) =>
|
|
@@ -1238,14 +1500,19 @@ function runVmLaunchWithProcessRunner(
|
|
|
1238
1500
|
'guest-artifact-measurement',
|
|
1239
1501
|
...(adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
1240
1502
|
...(adapter.hostSandboxed ? ['host-process-isolation'] : []),
|
|
1503
|
+
...(adapter.filesystemConfidentiality ? ['host-filesystem-confidentiality'] : []),
|
|
1504
|
+
...(adapter.networkIsolation ? ['host-network-isolation'] : []),
|
|
1241
1505
|
'machine-vm-launch',
|
|
1242
1506
|
'virtual-device-minimization',
|
|
1243
1507
|
],
|
|
1244
1508
|
missingLayers: [
|
|
1245
1509
|
...(!adapter.hardwareBacked ? ['hardware-hypervisor-acceleration'] : []),
|
|
1246
1510
|
...(!adapter.hostSandboxed ? ['host-process-isolation'] : []),
|
|
1247
|
-
...(adapter.hostSandboxed
|
|
1248
|
-
? ['host-filesystem-confidentiality'
|
|
1511
|
+
...(adapter.hostSandboxed && !adapter.filesystemConfidentiality
|
|
1512
|
+
? ['host-filesystem-confidentiality']
|
|
1513
|
+
: []),
|
|
1514
|
+
...(adapter.hostSandboxed && !adapter.networkIsolation
|
|
1515
|
+
? ['host-network-isolation']
|
|
1249
1516
|
: []),
|
|
1250
1517
|
],
|
|
1251
1518
|
};
|
|
@@ -1265,6 +1532,14 @@ export function runWhpxSandboxedVmLaunch(options = {}) {
|
|
|
1265
1532
|
return runVmLaunchWithProcessRunner(options, spawnSync, WHPX_SANDBOXED_ADAPTER);
|
|
1266
1533
|
}
|
|
1267
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
|
+
|
|
1268
1543
|
// Repository tests need a deterministic process boundary without publishing an
|
|
1269
1544
|
// injectable executor through the package root export.
|
|
1270
1545
|
export function runVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
@@ -1290,3 +1565,19 @@ export function runWhpxSandboxedVmLaunchWithProcessRunnerForTest(options = {}) {
|
|
|
1290
1565
|
const { processRunner, ...launchOptions } = options;
|
|
1291
1566
|
return runVmLaunchWithProcessRunner(launchOptions, processRunner, WHPX_SANDBOXED_ADAPTER);
|
|
1292
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
|
+
}
|