@torkbot/sandbox 0.1.1 → 0.2.0
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 +237 -133
- package/dist/artifacts.d.ts +4 -0
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +44 -0
- package/dist/artifacts.js.map +1 -1
- package/dist/control-codec.d.ts +23 -1
- package/dist/control-codec.d.ts.map +1 -1
- package/dist/control-codec.js.map +1 -1
- package/dist/control.d.ts +15 -1
- package/dist/control.d.ts.map +1 -1
- package/dist/control.js.map +1 -1
- package/dist/host-process.d.ts +2 -7
- package/dist/host-process.d.ts.map +1 -1
- package/dist/host-process.js +239 -10
- package/dist/host-process.js.map +1 -1
- package/dist/index.d.ts +104 -199
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -268
- package/dist/index.js.map +1 -1
- package/dist/launch-options.d.ts +64 -0
- package/dist/launch-options.d.ts.map +1 -0
- package/dist/launch-options.js +2 -0
- package/dist/launch-options.js.map +1 -0
- package/dist/memory-fs.d.ts +3 -0
- package/dist/memory-fs.d.ts.map +1 -0
- package/dist/memory-fs.js +308 -0
- package/dist/memory-fs.js.map +1 -0
- package/dist/spawn-options.d.ts +7 -6
- package/dist/spawn-options.d.ts.map +1 -1
- package/dist/vfs.d.ts +2 -1
- package/dist/vfs.d.ts.map +1 -1
- package/dist/vfs.js +14 -0
- package/dist/vfs.js.map +1 -1
- package/package.json +3 -3
- package/dist/host-filesystem-tools.d.ts +0 -3
- package/dist/host-filesystem-tools.d.ts.map +0 -1
- package/dist/host-filesystem-tools.js +0 -330
- package/dist/host-filesystem-tools.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,176 +1,85 @@
|
|
|
1
|
+
import { builtInRootfsIdentity, builtInRootfsPath, } from "./artifacts.js";
|
|
1
2
|
import { HostControlTransport } from "./control.js";
|
|
2
3
|
import { HostProcessSandboxVm } from "./host-process.js";
|
|
3
|
-
import {
|
|
4
|
-
import { isSandboxWritableFileSystem } from "./vfs.js";
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
readonly: options.readonly ?? true,
|
|
23
|
-
format: options.format,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export function scratchFs() {
|
|
27
|
-
return {
|
|
28
|
-
kind: "scratch-fs",
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
export function linuxOverlayFs(input) {
|
|
32
|
-
return {
|
|
33
|
-
kind: "linux-overlay-fs",
|
|
34
|
-
lower: input.lower,
|
|
35
|
-
upper: input.upper,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
export function virtualFsMount(path, fileSystem) {
|
|
4
|
+
import { createMemoryFileSystem } from "./memory-fs.js";
|
|
5
|
+
import { isSandboxPosixFileSystem, isSandboxWritableFileSystem, } from "./vfs.js";
|
|
6
|
+
const networkPolicyHandler = Symbol("networkPolicyHandler");
|
|
7
|
+
export const rootfs = {
|
|
8
|
+
builtIn(name) {
|
|
9
|
+
return {
|
|
10
|
+
kind: "built-in-rootfs",
|
|
11
|
+
name,
|
|
12
|
+
};
|
|
13
|
+
},
|
|
14
|
+
cow(options) {
|
|
15
|
+
return {
|
|
16
|
+
kind: "cow-rootfs",
|
|
17
|
+
base: options.base,
|
|
18
|
+
writable: options.writable,
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
function virtualFs(fileSystem) {
|
|
39
23
|
return {
|
|
40
24
|
kind: "virtual-fs",
|
|
41
|
-
path,
|
|
42
25
|
fileSystem,
|
|
43
26
|
};
|
|
44
27
|
}
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
ports: input.ports,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
export function acceptUdp(input) {
|
|
64
|
-
return {
|
|
65
|
-
action: "accept",
|
|
66
|
-
protocol: "udp",
|
|
67
|
-
cidr: input.cidr,
|
|
68
|
-
ports: input.ports,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
export function acceptPublicInternet(input = {}) {
|
|
72
|
-
return {
|
|
73
|
-
action: "accept",
|
|
74
|
-
scope: "public-internet",
|
|
75
|
-
ports: input.ports,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
export async function spawnSandbox(options) {
|
|
79
|
-
validateSandboxOptions(options);
|
|
80
|
-
const hostOptions = toHostSpawnOptions(options, []);
|
|
81
|
-
const hostVm = await HostProcessSandboxVm.spawn(options, hostOptions, new Map());
|
|
82
|
-
return new HostBackedSandboxVm(hostVm, options);
|
|
83
|
-
}
|
|
84
|
-
export function createSandbox(options) {
|
|
85
|
-
validateSandboxOptions(options);
|
|
86
|
-
return new ConfiguredSandboxBuilder(options);
|
|
28
|
+
export const fs = {
|
|
29
|
+
memory: createMemoryFileSystem,
|
|
30
|
+
virtual: virtualFs,
|
|
31
|
+
};
|
|
32
|
+
export const network = {
|
|
33
|
+
policy(onConnectionRequest) {
|
|
34
|
+
return {
|
|
35
|
+
kind: "network-policy",
|
|
36
|
+
[networkPolicyHandler]: onConnectionRequest,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export function defineSandbox(options) {
|
|
41
|
+
validateSandboxDefinitionOptions(options);
|
|
42
|
+
return new DefinedSandbox(options);
|
|
87
43
|
}
|
|
88
|
-
class
|
|
89
|
-
http;
|
|
44
|
+
class DefinedSandbox {
|
|
90
45
|
#options;
|
|
91
|
-
#requestHeaderHooks = new Set();
|
|
92
|
-
#nextRequestHeaderHookId = 1;
|
|
93
|
-
#runStarted = false;
|
|
94
|
-
#vm = null;
|
|
95
|
-
#closed = false;
|
|
96
46
|
constructor(options) {
|
|
97
47
|
this.#options = options;
|
|
98
|
-
this.http = {
|
|
99
|
-
onRequest: (selector, hook) => {
|
|
100
|
-
this.#assertOpen();
|
|
101
|
-
if (this.#runStarted) {
|
|
102
|
-
throw new Error("sandbox has already been run");
|
|
103
|
-
}
|
|
104
|
-
const registration = {
|
|
105
|
-
id: `http-request-headers-${this.#nextRequestHeaderHookId++}`,
|
|
106
|
-
selector,
|
|
107
|
-
hook,
|
|
108
|
-
active: true,
|
|
109
|
-
};
|
|
110
|
-
this.#requestHeaderHooks.add(registration);
|
|
111
|
-
return new ConfiguredSandboxHttpHook(this, registration);
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
async run() {
|
|
116
|
-
this.#assertOpen();
|
|
117
|
-
if (this.#runStarted) {
|
|
118
|
-
throw new Error("sandbox has already been run");
|
|
119
|
-
}
|
|
120
|
-
this.#runStarted = true;
|
|
121
|
-
const registrations = Array.from(this.#requestHeaderHooks);
|
|
122
|
-
const hostOptions = toHostSpawnOptions(this.#options, registrations);
|
|
123
|
-
const hostVm = await HostProcessSandboxVm.spawn(this.#options, hostOptions, new Map(registrations.map((registration) => [registration.id, registration])));
|
|
124
|
-
this.#vm = new HostBackedSandboxVm(hostVm, this.#options);
|
|
125
|
-
return this.#vm;
|
|
126
|
-
}
|
|
127
|
-
async [Symbol.asyncDispose]() {
|
|
128
|
-
if (this.#closed) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
this.#closed = true;
|
|
132
|
-
await this.#vm?.close();
|
|
133
|
-
this.#requestHeaderHooks.clear();
|
|
134
48
|
}
|
|
135
|
-
async
|
|
136
|
-
|
|
137
|
-
this.#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
49
|
+
async boot(options = {}) {
|
|
50
|
+
validateSandboxBootOptions(options);
|
|
51
|
+
const networkPolicy = this.#options.network === undefined
|
|
52
|
+
? undefined
|
|
53
|
+
: createNetworkPolicyHookRegistration(this.#options.network);
|
|
54
|
+
const launchOptions = await toInternalSandboxOptions(this.#options, options, networkPolicy?.network);
|
|
55
|
+
try {
|
|
56
|
+
validateInternalSandboxOptions(launchOptions);
|
|
57
|
+
const hostOptions = toHostSpawnOptions(launchOptions, networkPolicy?.hooks ?? []);
|
|
58
|
+
const hostVm = await HostProcessSandboxVm.spawn(launchOptions, hostOptions, new Map((networkPolicy?.hooks ?? []).map((hook) => [hook.id, hook])));
|
|
59
|
+
return new HostBackedSandboxVm(hostVm, launchOptions);
|
|
142
60
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
class ConfiguredSandboxHttpHook {
|
|
146
|
-
#sandbox;
|
|
147
|
-
#registration;
|
|
148
|
-
#disposed = false;
|
|
149
|
-
constructor(sandbox, registration) {
|
|
150
|
-
this.#sandbox = sandbox;
|
|
151
|
-
this.#registration = registration;
|
|
152
|
-
}
|
|
153
|
-
async [Symbol.asyncDispose]() {
|
|
154
|
-
if (this.#disposed) {
|
|
155
|
-
return;
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw error;
|
|
156
63
|
}
|
|
157
|
-
this.#disposed = true;
|
|
158
|
-
await this.#sandbox.removeHook(this.#registration);
|
|
159
64
|
}
|
|
160
65
|
}
|
|
161
66
|
class HostBackedSandboxVm {
|
|
162
|
-
mounts;
|
|
163
67
|
control;
|
|
164
68
|
diagnostics;
|
|
69
|
+
#exec;
|
|
70
|
+
#rootExec;
|
|
71
|
+
#options;
|
|
165
72
|
#hostVm;
|
|
166
73
|
#closed = false;
|
|
167
74
|
constructor(hostVm, options) {
|
|
168
75
|
this.#hostVm = hostVm;
|
|
169
|
-
this
|
|
76
|
+
this.#options = options;
|
|
170
77
|
this.control = new HostControlTransport({
|
|
171
78
|
connected: hostVm.hasControlSocket,
|
|
172
79
|
channel: hostVm,
|
|
173
80
|
});
|
|
81
|
+
this.#exec = new ControlBackedSandboxExec(this.control, options.cwd);
|
|
82
|
+
this.#rootExec = new ControlBackedSandboxExec(this.control, "/");
|
|
174
83
|
if (hostVm.terminateHostForTest !== undefined) {
|
|
175
84
|
this.diagnostics = {
|
|
176
85
|
terminateHostForTest: () => hostVm.terminateHostForTest?.() ?? Promise.resolve(),
|
|
@@ -182,65 +91,80 @@ class HostBackedSandboxVm {
|
|
|
182
91
|
return;
|
|
183
92
|
}
|
|
184
93
|
this.#closed = true;
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
constructor(mounts, bindings) {
|
|
197
|
-
for (const mount of mounts) {
|
|
198
|
-
this.#mounts.set(mount.path, mount.fileSystem);
|
|
199
|
-
this.#virtualMounts.set(mount.path, mount.fileSystem);
|
|
200
|
-
this.#hostTools.set(mount.path, createSandboxHostFileSystemTools(mount.fileSystem));
|
|
94
|
+
let syncError;
|
|
95
|
+
if (this.#options.rootfs.storage !== undefined) {
|
|
96
|
+
try {
|
|
97
|
+
const result = await this.#rootExec.exec("/bin/sync");
|
|
98
|
+
if (result.exitCode !== 0) {
|
|
99
|
+
throw new Error(`sandbox close sync failed with exit code ${result.exitCode}: ${result.stderr}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
syncError = error;
|
|
104
|
+
}
|
|
201
105
|
}
|
|
202
|
-
|
|
203
|
-
this
|
|
106
|
+
try {
|
|
107
|
+
await this.control.close();
|
|
108
|
+
await this.#hostVm.close();
|
|
204
109
|
}
|
|
205
|
-
|
|
206
|
-
get(path) {
|
|
207
|
-
const mount = this.#mounts.get(path);
|
|
208
|
-
if (mount === undefined) {
|
|
209
|
-
throw new Error(`sandbox mount not found: ${path}`);
|
|
110
|
+
finally {
|
|
210
111
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
virtualFs(path) {
|
|
214
|
-
const mount = this.#virtualMounts.get(path);
|
|
215
|
-
if (mount === undefined) {
|
|
216
|
-
throw new Error(`virtualFs mount not found: ${path}`);
|
|
112
|
+
if (syncError !== undefined) {
|
|
113
|
+
throw syncError;
|
|
217
114
|
}
|
|
218
|
-
return mount;
|
|
219
115
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
116
|
+
async [Symbol.asyncDispose]() {
|
|
117
|
+
await this.close();
|
|
118
|
+
}
|
|
119
|
+
async exec(command, args = [], options = {}) {
|
|
120
|
+
return await this.#exec.exec(command, args, options);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
class ControlBackedSandboxExec {
|
|
124
|
+
#control;
|
|
125
|
+
#cwd;
|
|
126
|
+
constructor(control, cwd) {
|
|
127
|
+
this.#control = control;
|
|
128
|
+
this.#cwd = cwd;
|
|
129
|
+
}
|
|
130
|
+
async exec(command, args = [], options = {}) {
|
|
131
|
+
const cwd = options.cwd ?? this.#cwd;
|
|
132
|
+
const env = cwd === undefined
|
|
133
|
+
? options.env
|
|
134
|
+
: {
|
|
135
|
+
...options.env,
|
|
136
|
+
SANDBOX_EXEC_CWD: cwd,
|
|
137
|
+
PWD: cwd,
|
|
138
|
+
};
|
|
139
|
+
const argv = cwd === undefined
|
|
140
|
+
? [command, ...args]
|
|
141
|
+
: ["/bin/sh", "-lc", "cd \"$SANDBOX_EXEC_CWD\" && exec \"$@\"", "sandbox-exec", command, ...args];
|
|
142
|
+
const result = await this.#control.exec({
|
|
143
|
+
argv,
|
|
144
|
+
env,
|
|
145
|
+
});
|
|
146
|
+
return {
|
|
147
|
+
exitCode: result.exitCode,
|
|
148
|
+
stdout: result.stdout,
|
|
149
|
+
stderr: result.stderr,
|
|
150
|
+
};
|
|
226
151
|
}
|
|
227
152
|
}
|
|
228
153
|
function toHostSpawnOptions(options, requestHeaderHooks) {
|
|
229
|
-
const rootfs = lowerNativeRootfs(options.rootfs);
|
|
230
154
|
if ((requestHeaderHooks.length > 0 || options.network?.http !== undefined)
|
|
231
155
|
&& options.network?.outbound === undefined) {
|
|
232
|
-
throw new Error("invalid
|
|
156
|
+
throw new Error("invalid sandbox options: network.outbound is required when HTTP interception is configured");
|
|
233
157
|
}
|
|
234
158
|
const network = options.network === undefined && requestHeaderHooks.length === 0
|
|
235
159
|
? undefined
|
|
236
160
|
: {
|
|
237
161
|
outbound: options.network?.outbound,
|
|
238
162
|
http: requestHeaderHooks.length === 0
|
|
239
|
-
&& options.network?.http?.
|
|
163
|
+
&& options.network?.http?.caCertificatePem === undefined
|
|
240
164
|
? undefined
|
|
241
165
|
: {
|
|
242
|
-
caCertificatePem: options.network?.http?.
|
|
243
|
-
caPrivateKeyPem: options.network?.http?.
|
|
166
|
+
caCertificatePem: options.network?.http?.caCertificatePem,
|
|
167
|
+
caPrivateKeyPem: options.network?.http?.caPrivateKeyPem,
|
|
244
168
|
requestHeaderHooks: requestHeaderHooks.map((hook) => ({
|
|
245
169
|
id: hook.id,
|
|
246
170
|
origin: hook.selector.origin,
|
|
@@ -248,26 +172,22 @@ function toHostSpawnOptions(options, requestHeaderHooks) {
|
|
|
248
172
|
},
|
|
249
173
|
};
|
|
250
174
|
return {
|
|
251
|
-
name: options.name,
|
|
252
|
-
cpu: options.cpu,
|
|
253
|
-
memory: options.memory,
|
|
254
175
|
kernel: {
|
|
255
|
-
format:
|
|
176
|
+
format: undefined,
|
|
256
177
|
},
|
|
257
|
-
|
|
258
|
-
|
|
178
|
+
cpu: {
|
|
179
|
+
vcpus: options.resources?.cpus,
|
|
180
|
+
},
|
|
181
|
+
memory: {
|
|
182
|
+
mib: options.resources?.memoryMiB,
|
|
259
183
|
},
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
readonly: rootfs.readonly,
|
|
263
|
-
format: rootfs.format,
|
|
184
|
+
init: {
|
|
185
|
+
crateName: "sandbox-init",
|
|
264
186
|
},
|
|
265
|
-
|
|
266
|
-
? { mode: "writable" }
|
|
267
|
-
: undefined,
|
|
187
|
+
rootfs: options.rootfs,
|
|
268
188
|
mounts: options.mounts?.map((mount) => {
|
|
269
189
|
return {
|
|
270
|
-
kind:
|
|
190
|
+
kind: "virtual-fs",
|
|
271
191
|
path: mount.path,
|
|
272
192
|
writable: isSandboxWritableFileSystem(mount.fileSystem),
|
|
273
193
|
};
|
|
@@ -275,56 +195,187 @@ function toHostSpawnOptions(options, requestHeaderHooks) {
|
|
|
275
195
|
network,
|
|
276
196
|
};
|
|
277
197
|
}
|
|
278
|
-
function
|
|
279
|
-
|
|
280
|
-
|
|
198
|
+
async function toInternalSandboxOptions(config, boot, network) {
|
|
199
|
+
const rootfs = await lowerRootfs(config.rootfs);
|
|
200
|
+
return {
|
|
201
|
+
resources: config.resources,
|
|
202
|
+
rootfs,
|
|
203
|
+
cwd: boot.cwd,
|
|
204
|
+
mounts: Object.entries(boot.mounts ?? {}).map(([path, source]) => {
|
|
205
|
+
return {
|
|
206
|
+
path,
|
|
207
|
+
fileSystem: source.fileSystem,
|
|
208
|
+
};
|
|
209
|
+
}),
|
|
210
|
+
network,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function createNetworkPolicyHookRegistration(policy) {
|
|
214
|
+
const hook = async (request) => {
|
|
215
|
+
const grants = [];
|
|
216
|
+
const connection = {
|
|
217
|
+
transport: "tcp",
|
|
218
|
+
host: request.url.hostname,
|
|
219
|
+
ip: request.destination.upstreamIp,
|
|
220
|
+
port: request.destination.upstreamPort,
|
|
221
|
+
allow() {
|
|
222
|
+
grants.push({ kind: "http" });
|
|
223
|
+
return {};
|
|
224
|
+
},
|
|
225
|
+
allowHttp(middleware) {
|
|
226
|
+
grants.push({ kind: "http", middleware });
|
|
227
|
+
return {};
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
await policy[networkPolicyHandler](connection);
|
|
231
|
+
if (grants.length === 0) {
|
|
232
|
+
throw new Error(`network connection denied: ${request.url.origin}`);
|
|
233
|
+
}
|
|
234
|
+
for (const grant of grants) {
|
|
235
|
+
if (grant.kind === "http") {
|
|
236
|
+
await grant.middleware?.(request);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
const hooks = [
|
|
241
|
+
{
|
|
242
|
+
id: "network-policy-http",
|
|
243
|
+
selector: { origin: "http://*" },
|
|
244
|
+
hook,
|
|
245
|
+
active: true,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "network-policy-https",
|
|
249
|
+
selector: { origin: "https://*" },
|
|
250
|
+
hook,
|
|
251
|
+
active: true,
|
|
252
|
+
},
|
|
253
|
+
];
|
|
254
|
+
return {
|
|
255
|
+
hooks,
|
|
256
|
+
network: {
|
|
257
|
+
outbound: {
|
|
258
|
+
policy: "deny",
|
|
259
|
+
rules: [
|
|
260
|
+
{ action: "accept", scope: "public-internet", ports: [] },
|
|
261
|
+
{ action: "accept", protocol: "udp", cidr: "10.0.2.1/32", ports: [53] },
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function lowerRootfs(rootfs) {
|
|
268
|
+
switch (rootfs.kind) {
|
|
269
|
+
case "built-in-rootfs":
|
|
270
|
+
return {
|
|
271
|
+
path: builtInRootfsPath(rootfs.name),
|
|
272
|
+
readonly: true,
|
|
273
|
+
format: "erofs",
|
|
274
|
+
};
|
|
275
|
+
case "cow-rootfs":
|
|
276
|
+
return {
|
|
277
|
+
path: builtInRootfsPath(rootfs.base.name, "ext4"),
|
|
278
|
+
readonly: false,
|
|
279
|
+
format: "ext4",
|
|
280
|
+
storage: {
|
|
281
|
+
kind: "cow-block-store",
|
|
282
|
+
blockSize: rootfs.writable.blockSize,
|
|
283
|
+
blockStore: rootfs.writable,
|
|
284
|
+
context: {
|
|
285
|
+
base: builtInRootfsIdentity(rootfs.base.name, "ext4"),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function validateRootfs(rootfs) {
|
|
292
|
+
switch (rootfs.kind) {
|
|
293
|
+
case "built-in-rootfs":
|
|
294
|
+
validateBuiltInRootfsName(rootfs.name);
|
|
295
|
+
return;
|
|
296
|
+
case "cow-rootfs":
|
|
297
|
+
if (rootfs.base.kind !== "built-in-rootfs") {
|
|
298
|
+
throw new Error("invalid sandbox definition: rootfs.cow base must be created with rootfs.builtIn(...)");
|
|
299
|
+
}
|
|
300
|
+
validateBuiltInRootfsName(rootfs.base.name);
|
|
301
|
+
validateBlockStore(rootfs.writable);
|
|
302
|
+
return;
|
|
303
|
+
default:
|
|
304
|
+
throw new Error("invalid sandbox definition: rootfs must be created with rootfs.builtIn(...) or rootfs.cow(...)");
|
|
281
305
|
}
|
|
282
|
-
|
|
283
|
-
|
|
306
|
+
}
|
|
307
|
+
function validateBlockStore(blockStore) {
|
|
308
|
+
if (!Number.isInteger(blockStore.blockSize) || blockStore.blockSize <= 0) {
|
|
309
|
+
throw new Error("invalid sandbox definition: rootfs COW block size must be a positive integer");
|
|
284
310
|
}
|
|
285
|
-
if (
|
|
286
|
-
throw new Error(
|
|
311
|
+
if (blockStore.blockSize % 512 !== 0) {
|
|
312
|
+
throw new Error("invalid sandbox definition: rootfs COW block size must be a multiple of 512 bytes");
|
|
313
|
+
}
|
|
314
|
+
if (typeof blockStore.list !== "function") {
|
|
315
|
+
throw new Error("invalid sandbox definition: rootfs COW block store must provide list()");
|
|
316
|
+
}
|
|
317
|
+
if (typeof blockStore.read !== "function") {
|
|
318
|
+
throw new Error("invalid sandbox definition: rootfs COW block store must provide read()");
|
|
319
|
+
}
|
|
320
|
+
if (typeof blockStore.write !== "function") {
|
|
321
|
+
throw new Error("invalid sandbox definition: rootfs COW block store must provide write()");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function validateSandboxDefinitionOptions(options) {
|
|
325
|
+
validateRootfs(options.rootfs);
|
|
326
|
+
if (options.resources?.cpus !== undefined && (!Number.isInteger(options.resources.cpus) || options.resources.cpus <= 0)) {
|
|
327
|
+
throw new Error("invalid sandbox definition: resources.cpus must be a positive integer");
|
|
328
|
+
}
|
|
329
|
+
if (options.resources?.cpus !== undefined && options.resources.cpus > 255) {
|
|
330
|
+
throw new Error("invalid sandbox definition: resources.cpus must be less than or equal to 255");
|
|
331
|
+
}
|
|
332
|
+
if (options.resources?.memoryMiB !== undefined
|
|
333
|
+
&& (!Number.isInteger(options.resources.memoryMiB) || options.resources.memoryMiB <= 0)) {
|
|
334
|
+
throw new Error("invalid sandbox definition: resources.memoryMiB must be a positive integer");
|
|
335
|
+
}
|
|
336
|
+
if (options.network !== undefined && options.network.kind !== "network-policy") {
|
|
337
|
+
throw new Error("invalid sandbox definition: network must be created with network.policy(...)");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function validateBuiltInRootfsName(name) {
|
|
341
|
+
if (name !== "alpine:3.20") {
|
|
342
|
+
throw new Error(`unsupported built-in rootfs: ${name}`);
|
|
287
343
|
}
|
|
288
|
-
return {
|
|
289
|
-
...rootfs.lower,
|
|
290
|
-
readonly: true,
|
|
291
|
-
};
|
|
292
344
|
}
|
|
293
|
-
function
|
|
294
|
-
|
|
295
|
-
|
|
345
|
+
function validateSandboxBootOptions(options) {
|
|
346
|
+
const mountPaths = new Set();
|
|
347
|
+
for (const [path, source] of Object.entries(options.mounts ?? {})) {
|
|
348
|
+
validateGuestPath(path, "mount.path");
|
|
349
|
+
if (mountPaths.has(path)) {
|
|
350
|
+
throw new Error(`invalid sandbox boot options: duplicate mount path: ${path}`);
|
|
351
|
+
}
|
|
352
|
+
if (isSandboxWritableFileSystem(source.fileSystem)
|
|
353
|
+
&& !isSandboxPosixFileSystem(source.fileSystem)) {
|
|
354
|
+
throw new Error(`invalid sandbox boot options: writable mount must implement the POSIX filesystem interface: ${path}`);
|
|
355
|
+
}
|
|
356
|
+
mountPaths.add(path);
|
|
296
357
|
}
|
|
297
|
-
if (options.
|
|
298
|
-
throw new Error("invalid
|
|
358
|
+
if (options.cwd !== undefined && !options.cwd.startsWith("/")) {
|
|
359
|
+
throw new Error("invalid sandbox boot options: cwd must be absolute");
|
|
299
360
|
}
|
|
300
|
-
|
|
301
|
-
|
|
361
|
+
}
|
|
362
|
+
function validateInternalSandboxOptions(options) {
|
|
363
|
+
if (options.rootfs.path.length === 0) {
|
|
364
|
+
throw new Error("invalid sandbox options: rootfs.path must not be empty");
|
|
302
365
|
}
|
|
303
|
-
if (options.
|
|
304
|
-
throw new Error(
|
|
366
|
+
if (options.rootfs.format !== "erofs" && options.rootfs.format !== "ext4") {
|
|
367
|
+
throw new Error("invalid sandbox options: rootfs.format must be erofs or ext4");
|
|
305
368
|
}
|
|
306
|
-
validateRootfsConfig(options.rootfs, "rootfs");
|
|
307
369
|
const mountPaths = new Set();
|
|
308
370
|
for (const mount of options.mounts ?? []) {
|
|
309
371
|
validateGuestPath(mount.path, "mount.path");
|
|
310
372
|
if (mountPaths.has(mount.path)) {
|
|
311
|
-
throw new Error(`invalid
|
|
373
|
+
throw new Error(`invalid sandbox options: duplicate mount path: ${mount.path}`);
|
|
312
374
|
}
|
|
313
375
|
mountPaths.add(mount.path);
|
|
314
376
|
}
|
|
315
|
-
const bindingPaths = new Set();
|
|
316
|
-
for (const binding of options.bindings ?? []) {
|
|
317
|
-
validateGuestPath(binding.path, "binding.path");
|
|
318
|
-
if (mountPaths.has(binding.path)) {
|
|
319
|
-
throw new Error(`invalid spawnSandbox options: binding path conflicts with mount path: ${binding.path}`);
|
|
320
|
-
}
|
|
321
|
-
if (bindingPaths.has(binding.path)) {
|
|
322
|
-
throw new Error(`invalid spawnSandbox options: duplicate binding path: ${binding.path}`);
|
|
323
|
-
}
|
|
324
|
-
bindingPaths.add(binding.path);
|
|
325
|
-
}
|
|
326
377
|
if (options.network?.outbound?.policy !== undefined && options.network.outbound.policy !== "deny") {
|
|
327
|
-
throw new Error("invalid
|
|
378
|
+
throw new Error("invalid sandbox options: network.outbound.policy must be deny");
|
|
328
379
|
}
|
|
329
380
|
for (const rule of options.network?.outbound?.rules ?? []) {
|
|
330
381
|
if ("cidr" in rule) {
|
|
@@ -333,71 +384,50 @@ function validateSandboxOptions(options) {
|
|
|
333
384
|
validateOutboundPorts(rule.ports);
|
|
334
385
|
}
|
|
335
386
|
}
|
|
336
|
-
function validateRootfsConfig(rootfs, field) {
|
|
337
|
-
if (rootfs.kind === "prebuilt-rootfs") {
|
|
338
|
-
if (rootfs.path.length === 0) {
|
|
339
|
-
throw new Error(`invalid spawnSandbox options: ${field}.path must not be empty`);
|
|
340
|
-
}
|
|
341
|
-
if (rootfs.format === "directory") {
|
|
342
|
-
const prefix = field === "rootfs" ? "" : `${field} `;
|
|
343
|
-
throw new Error(`invalid spawnSandbox options: ${prefix}directory rootfs is not supported for sandboxed VM launch; use an EROFS rootfs`);
|
|
344
|
-
}
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (rootfs.kind === "linux-overlay-fs") {
|
|
348
|
-
validateRootfsConfig(rootfs.lower, `${field}.lower`);
|
|
349
|
-
validateRootfsConfig(rootfs.upper, `${field}.upper`);
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (rootfs.kind === "scratch-fs") {
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
throw new Error(`invalid spawnSandbox options: unsupported ${field} kind`);
|
|
356
|
-
}
|
|
357
387
|
function validateGuestPath(path, field) {
|
|
358
388
|
if (!path.startsWith("/")) {
|
|
359
|
-
throw new Error(`invalid
|
|
389
|
+
throw new Error(`invalid sandbox options: ${field} must be absolute`);
|
|
360
390
|
}
|
|
361
391
|
if (path === "/") {
|
|
362
|
-
throw new Error(`invalid
|
|
392
|
+
throw new Error(`invalid sandbox options: ${field} must not be root`);
|
|
363
393
|
}
|
|
364
394
|
if (path.includes("\0")) {
|
|
365
|
-
throw new Error(`invalid
|
|
395
|
+
throw new Error(`invalid sandbox options: ${field} must not contain NUL bytes`);
|
|
366
396
|
}
|
|
367
397
|
if (path.split("/").some((component) => component === "." || component === "..")) {
|
|
368
|
-
throw new Error(`invalid
|
|
398
|
+
throw new Error(`invalid sandbox options: ${field} must not contain '.' or '..' components`);
|
|
369
399
|
}
|
|
370
400
|
}
|
|
371
401
|
function validateOutboundPorts(ports) {
|
|
372
402
|
for (const port of ports ?? []) {
|
|
373
403
|
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
374
|
-
throw new Error(`invalid
|
|
404
|
+
throw new Error(`invalid sandbox options: invalid outbound network port: ${port}`);
|
|
375
405
|
}
|
|
376
406
|
}
|
|
377
407
|
}
|
|
378
408
|
function validateCidr(range) {
|
|
379
409
|
const [address, prefixText, extra] = range.split("/");
|
|
380
410
|
if (address === undefined || prefixText === undefined || extra !== undefined) {
|
|
381
|
-
throw new Error(`invalid
|
|
411
|
+
throw new Error(`invalid sandbox options: invalid CIDR range: ${range}`);
|
|
382
412
|
}
|
|
383
413
|
const prefix = Number(prefixText);
|
|
384
414
|
if (!Number.isInteger(prefix)) {
|
|
385
|
-
throw new Error(`invalid
|
|
415
|
+
throw new Error(`invalid sandbox options: invalid CIDR prefix: ${range}`);
|
|
386
416
|
}
|
|
387
417
|
if (address.includes(":")) {
|
|
388
418
|
if (prefix < 0 || prefix > 128) {
|
|
389
|
-
throw new Error(`invalid
|
|
419
|
+
throw new Error(`invalid sandbox options: invalid CIDR prefix: ${range}`);
|
|
390
420
|
}
|
|
391
421
|
if (parseIpv6Address(address) === null) {
|
|
392
|
-
throw new Error(`invalid
|
|
422
|
+
throw new Error(`invalid sandbox options: invalid CIDR address: ${range}`);
|
|
393
423
|
}
|
|
394
|
-
throw new Error(`invalid
|
|
424
|
+
throw new Error(`invalid sandbox options: IPv6 outbound CIDR ranges are not supported yet: ${range}`);
|
|
395
425
|
}
|
|
396
426
|
if (prefix < 0 || prefix > 32) {
|
|
397
|
-
throw new Error(`invalid
|
|
427
|
+
throw new Error(`invalid sandbox options: invalid CIDR prefix: ${range}`);
|
|
398
428
|
}
|
|
399
429
|
if (parseIpv4Address(address) === null) {
|
|
400
|
-
throw new Error(`invalid
|
|
430
|
+
throw new Error(`invalid sandbox options: invalid CIDR address: ${range}`);
|
|
401
431
|
}
|
|
402
432
|
}
|
|
403
433
|
function parseIpv4Address(address) {
|