@jigging/jig 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +30 -182
- package/dist/index.js +41 -2
- package/dist/project/author.d.ts +3 -0
- package/dist/project/commands.d.ts +10 -0
- package/libexec/evaluator/project-authoring-1.schema.json +40 -0
- package/libexec/evaluator/project-evaluator-sdk.bundle.js +41 -2
- package/libexec/installed-cli.js +4390 -2533
- package/libexec/linux-rootless-supervisor.js +125 -10
- package/package.json +2 -1
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
// src/internal/linux-rootless-supervisor.ts
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { randomBytes } from "crypto";
|
|
5
|
-
import { closeSync, readFileSync, statSync, writeSync } from "fs";
|
|
6
|
-
import {
|
|
5
|
+
import { closeSync, readFileSync, readSync, statSync, writeSync } from "fs";
|
|
6
|
+
import {
|
|
7
|
+
link,
|
|
8
|
+
mkdir,
|
|
9
|
+
open,
|
|
10
|
+
readFile,
|
|
11
|
+
rmdir,
|
|
12
|
+
stat,
|
|
13
|
+
unlink,
|
|
14
|
+
writeFile
|
|
15
|
+
} from "fs/promises";
|
|
7
16
|
import { connect } from "net";
|
|
8
17
|
import { posix } from "path";
|
|
9
18
|
var POLICY = Object.freeze(["--no-env-file", "--no-install", "--config=/dev/null"]);
|
|
@@ -47,12 +56,17 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
|
|
|
47
56
|
let childExit;
|
|
48
57
|
let stopReason;
|
|
49
58
|
let admitted = false;
|
|
59
|
+
let continued = false;
|
|
60
|
+
let outputDirectory;
|
|
50
61
|
let resolveAdmission;
|
|
51
62
|
let rejectAdmission;
|
|
52
63
|
const admission = new Promise((resolve, reject) => {
|
|
53
64
|
resolveAdmission = resolve;
|
|
54
65
|
rejectAdmission = reject;
|
|
55
66
|
});
|
|
67
|
+
admission.catch(() => {
|
|
68
|
+
return;
|
|
69
|
+
});
|
|
56
70
|
const killRun = async () => {
|
|
57
71
|
if (!cgroupCreated)
|
|
58
72
|
return;
|
|
@@ -76,6 +90,12 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
|
|
|
76
90
|
if (type === "admit" && !admitted) {
|
|
77
91
|
admitted = true;
|
|
78
92
|
resolveAdmission();
|
|
93
|
+
} else if (type === "continue" && configuration.output && outputDirectory !== undefined && !continued) {
|
|
94
|
+
continued = true;
|
|
95
|
+
const gate = child?.stdio[4];
|
|
96
|
+
if (gate === undefined)
|
|
97
|
+
return stop("setup_failed");
|
|
98
|
+
gate.end(Buffer.from([1]));
|
|
79
99
|
} else if (type === "cancel") {
|
|
80
100
|
stop("cancelled");
|
|
81
101
|
} else {
|
|
@@ -127,16 +147,30 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
|
|
|
127
147
|
env: {
|
|
128
148
|
LD_LIBRARY_PATH: configuration.bunHostLibraryPath
|
|
129
149
|
},
|
|
130
|
-
stdio: [
|
|
150
|
+
stdio: [
|
|
151
|
+
"inherit",
|
|
152
|
+
"inherit",
|
|
153
|
+
"inherit",
|
|
154
|
+
"pipe",
|
|
155
|
+
configuration.output ? "pipe" : "ignore",
|
|
156
|
+
configuration.output ? "pipe" : "ignore",
|
|
157
|
+
...configuration.capturedInputs.map((file) => file.fd)
|
|
158
|
+
]
|
|
131
159
|
});
|
|
132
160
|
childExit = childClose(launched);
|
|
133
161
|
child = launched;
|
|
134
162
|
const ready = await readReady(launched);
|
|
163
|
+
if (configuration.output) {
|
|
164
|
+
const sandboxPid = await readSandboxPid(launched);
|
|
165
|
+
await outputReady(launched);
|
|
166
|
+
outputDirectory = await open(`/proc/${sandboxPid}/root/jig-output`, 65536);
|
|
167
|
+
}
|
|
135
168
|
safeSend(control, {
|
|
136
169
|
type: "ready",
|
|
137
170
|
runCgroup,
|
|
138
171
|
payloadPid: ready,
|
|
139
|
-
supervisorPid: process.pid
|
|
172
|
+
supervisorPid: process.pid,
|
|
173
|
+
...outputDirectory === undefined ? {} : { outputFd: outputDirectory.fd }
|
|
140
174
|
});
|
|
141
175
|
const exit = await childExit;
|
|
142
176
|
exitCode = exit.code;
|
|
@@ -149,6 +183,7 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
|
|
|
149
183
|
`);
|
|
150
184
|
} finally {
|
|
151
185
|
clearTimeout(deadline);
|
|
186
|
+
await outputDirectory?.close();
|
|
152
187
|
let cleanupError;
|
|
153
188
|
try {
|
|
154
189
|
child?.kill("SIGKILL");
|
|
@@ -211,13 +246,39 @@ async function enterMain(arguments_) {
|
|
|
211
246
|
writeSync(3, `${process.pid}
|
|
212
247
|
`);
|
|
213
248
|
closeSync(3);
|
|
214
|
-
const
|
|
249
|
+
const separator = bubblewrapArguments_.indexOf("--");
|
|
250
|
+
if (separator < 0)
|
|
251
|
+
throw new Error("missing rootless command boundary");
|
|
252
|
+
const envelopeArguments = bubblewrapArguments_.slice(0, separator);
|
|
253
|
+
const output = envelopeArguments.includes("--sync-fd");
|
|
254
|
+
const inputDescriptors = envelopeArguments.flatMap((arg, index) => arg === "--file" ? [Number(envelopeArguments[index + 1])] : []);
|
|
255
|
+
if (inputDescriptors.some((fd, index) => fd !== 6 + index))
|
|
256
|
+
throw new Error("invalid captured input descriptor order");
|
|
257
|
+
const child = spawn(bubblewrap, bubblewrapArguments_, {
|
|
258
|
+
cwd: "/",
|
|
259
|
+
env: {},
|
|
260
|
+
stdio: [
|
|
261
|
+
"inherit",
|
|
262
|
+
"inherit",
|
|
263
|
+
"inherit",
|
|
264
|
+
"ignore",
|
|
265
|
+
output ? 4 : "ignore",
|
|
266
|
+
output ? 5 : "ignore",
|
|
267
|
+
...inputDescriptors
|
|
268
|
+
]
|
|
269
|
+
});
|
|
270
|
+
for (const fd of inputDescriptors)
|
|
271
|
+
closeSync(fd);
|
|
272
|
+
if (output) {
|
|
273
|
+
closeSync(4);
|
|
274
|
+
closeSync(5);
|
|
275
|
+
}
|
|
215
276
|
const exit = await childClose(child);
|
|
216
277
|
if (exit.signal !== null)
|
|
217
278
|
process.kill(process.pid, exit.signal);
|
|
218
279
|
process.exitCode = exit.code ?? 1;
|
|
219
280
|
}
|
|
220
|
-
async function innerMain(command) {
|
|
281
|
+
async function innerMain(command, output = false) {
|
|
221
282
|
if (command.length === 0 || !absolute(command[0]))
|
|
222
283
|
throw new Error("invalid rootless inner command");
|
|
223
284
|
const nullDevice = statSync("/dev/null");
|
|
@@ -225,6 +286,14 @@ async function innerMain(command) {
|
|
|
225
286
|
if (!nullDevice.isCharacterDevice() || !entropyDevice.isCharacterDevice() || (nullDevice.mode & 511) !== 438 || (entropyDevice.mode & 511) !== 438) {
|
|
226
287
|
throw new Error("rootless private devices do not satisfy the required projection");
|
|
227
288
|
}
|
|
289
|
+
if (output) {
|
|
290
|
+
writeSync(4, Buffer.from([1]));
|
|
291
|
+
const gate = Buffer.alloc(1);
|
|
292
|
+
if (readSync(4, gate, 0, 1, null) !== 1 || gate[0] !== 1) {
|
|
293
|
+
throw new Error("output ownership was not acquired");
|
|
294
|
+
}
|
|
295
|
+
closeSync(4);
|
|
296
|
+
}
|
|
228
297
|
const child = spawn(command[0], command.slice(1), {
|
|
229
298
|
cwd: "/work",
|
|
230
299
|
env: process.env,
|
|
@@ -271,11 +340,21 @@ function bubblewrapArguments(configuration) {
|
|
|
271
340
|
];
|
|
272
341
|
for (const mount of configuration.readOnlyMounts)
|
|
273
342
|
result.push("--ro-bind", mount.source, mount.destination);
|
|
343
|
+
if (configuration.inputDirectories.length > 0)
|
|
344
|
+
result.push("--size", String(16 * 1024 * 1024), "--tmpfs", "/jig-input");
|
|
345
|
+
for (const path of configuration.inputDirectories)
|
|
346
|
+
result.push("--dir", path);
|
|
347
|
+
for (const file of configuration.capturedInputs)
|
|
348
|
+
result.push("--perms", "0444", "--file", String(file.fd), file.destination);
|
|
349
|
+
if (configuration.inputDirectories.length > 0)
|
|
350
|
+
result.push("--remount-ro", "/jig-input");
|
|
351
|
+
if (configuration.output)
|
|
352
|
+
result.push("--size", String(16 * 1024 * 1024), "--tmpfs", "/jig-output", "--sync-fd", "4", "--json-status-fd", "5");
|
|
274
353
|
result.push("--ro-bind", configuration.supervisorPath, MODULE_DESTINATION);
|
|
275
354
|
for (const [name, value] of Object.entries(configuration.environment).sort(([a], [b]) => a.localeCompare(b))) {
|
|
276
355
|
result.push("--setenv", name, value);
|
|
277
356
|
}
|
|
278
|
-
result.push("--setenv", "LD_LIBRARY_PATH", SANDBOX_LIBRARY_PATH, "--uid", String(configuration.payloadUid), "--gid", String(configuration.payloadGid), "--cap-drop", "ALL", "--", SANDBOX_BUN, ...POLICY, MODULE_DESTINATION, "--inner", "--", ...configuration.command);
|
|
357
|
+
result.push("--setenv", "LD_LIBRARY_PATH", SANDBOX_LIBRARY_PATH, "--uid", String(configuration.payloadUid), "--gid", String(configuration.payloadGid), "--cap-drop", "ALL", "--", SANDBOX_BUN, ...POLICY, MODULE_DESTINATION, configuration.output ? "--inner-output" : "--inner", "--", ...configuration.command);
|
|
279
358
|
return result;
|
|
280
359
|
}
|
|
281
360
|
async function claimActive(configuration) {
|
|
@@ -360,12 +439,15 @@ function requireStart(value) {
|
|
|
360
439
|
"bunHostLibraryPath",
|
|
361
440
|
"bunPath",
|
|
362
441
|
"command",
|
|
442
|
+
"capturedInputs",
|
|
363
443
|
"delegatedCgroup",
|
|
364
444
|
"environment",
|
|
365
445
|
"limits",
|
|
446
|
+
"inputDirectories",
|
|
366
447
|
"mechanismDigest",
|
|
367
448
|
"nestedUserNamespaces",
|
|
368
449
|
"network",
|
|
450
|
+
"output",
|
|
369
451
|
"ownerDigest",
|
|
370
452
|
"ownerStateAllocationDigest",
|
|
371
453
|
"ownerStateDirectory",
|
|
@@ -377,7 +459,7 @@ function requireStart(value) {
|
|
|
377
459
|
"sealedPlanDigest",
|
|
378
460
|
"supervisorPath"
|
|
379
461
|
];
|
|
380
|
-
if (configuration === null || typeof configuration !== "object" || Array.isArray(configuration) || Object.keys(configuration).sort().join("\x00") !== keys.sort().join("\x00") || !absolute(configuration.delegatedCgroup) || !absolute(configuration.bunHostLibraryPath) || !absolute(configuration.runCgroup) || posix.dirname(configuration.runCgroup) !== configuration.delegatedCgroup || !/^jig-run-[a-z0-9][a-z0-9-]{0,47}-[0-9a-f]{24}$/.test(posix.basename(configuration.runCgroup)) || !configuration.runCgroup.startsWith("/sys/fs/cgroup/") || !absolute(configuration.ownerStateDirectory) || !absolute(configuration.bunPath) || !absolute(configuration.bubblewrapPath) || !absolute(configuration.supervisorPath) || typeof configuration.mechanismDigest !== "string" || !DIGEST.test(configuration.mechanismDigest) || typeof configuration.ownerDigest !== "string" || !DIGEST.test(configuration.ownerDigest) || typeof configuration.sealedPlanDigest !== "string" || !DIGEST.test(configuration.sealedPlanDigest) || typeof configuration.ownerStateAllocationDigest !== "string" || !DIGEST.test(configuration.ownerStateAllocationDigest) || typeof configuration.ownerToken !== "string" || !OWNER_TOKEN.test(configuration.ownerToken) || configuration.network !== "isolated" && configuration.network !== "inherited" || typeof configuration.nestedUserNamespaces !== "boolean" || !positiveInteger(configuration.payloadUid) || !positiveInteger(configuration.payloadGid) || !validLimits(configuration.limits) || !validMounts(configuration.readOnlyMounts) || !validEnvironment(configuration.environment) || !Array.isArray(configuration.command) || configuration.command.length === 0 || !absolute(configuration.command[0]) || configuration.command.some((part) => typeof part !== "string" || part.includes("\x00"))) {
|
|
462
|
+
if (configuration === null || typeof configuration !== "object" || Array.isArray(configuration) || Object.keys(configuration).sort().join("\x00") !== keys.sort().join("\x00") || !absolute(configuration.delegatedCgroup) || !absolute(configuration.bunHostLibraryPath) || !absolute(configuration.runCgroup) || posix.dirname(configuration.runCgroup) !== configuration.delegatedCgroup || !/^jig-run-[a-z0-9][a-z0-9-]{0,47}-[0-9a-f]{24}$/.test(posix.basename(configuration.runCgroup)) || !configuration.runCgroup.startsWith("/sys/fs/cgroup/") || !absolute(configuration.ownerStateDirectory) || !absolute(configuration.bunPath) || !absolute(configuration.bubblewrapPath) || !absolute(configuration.supervisorPath) || typeof configuration.mechanismDigest !== "string" || !DIGEST.test(configuration.mechanismDigest) || typeof configuration.ownerDigest !== "string" || !DIGEST.test(configuration.ownerDigest) || typeof configuration.sealedPlanDigest !== "string" || !DIGEST.test(configuration.sealedPlanDigest) || typeof configuration.ownerStateAllocationDigest !== "string" || !DIGEST.test(configuration.ownerStateAllocationDigest) || typeof configuration.ownerToken !== "string" || !OWNER_TOKEN.test(configuration.ownerToken) || configuration.network !== "isolated" && configuration.network !== "inherited" || typeof configuration.nestedUserNamespaces !== "boolean" || typeof configuration.output !== "boolean" || !Array.isArray(configuration.capturedInputs) || configuration.capturedInputs.length > 64 || !Array.isArray(configuration.inputDirectories) || configuration.inputDirectories.length > 8 || configuration.inputDirectories.some((path) => typeof path !== "string" || path.length > 75 || !/^\/jig-input\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(path)) || configuration.capturedInputs.some((file, index) => file === null || typeof file !== "object" || file.fd !== 6 + index || !absolute(file.destination) || !configuration.inputDirectories.some((path) => file.destination.startsWith(`${path}/`))) || !positiveInteger(configuration.payloadUid) || !positiveInteger(configuration.payloadGid) || !validLimits(configuration.limits) || !validMounts(configuration.readOnlyMounts) || !validEnvironment(configuration.environment) || !Array.isArray(configuration.command) || configuration.command.length === 0 || !absolute(configuration.command[0]) || configuration.command.some((part) => typeof part !== "string" || part.includes("\x00"))) {
|
|
381
463
|
throw new Error("invalid rootless supervisor configuration");
|
|
382
464
|
}
|
|
383
465
|
return configuration;
|
|
@@ -460,6 +542,39 @@ async function readReady(child) {
|
|
|
460
542
|
throw new Error("invalid rootless entry readiness receipt");
|
|
461
543
|
return pid;
|
|
462
544
|
}
|
|
545
|
+
async function readSandboxPid(child) {
|
|
546
|
+
const stream = child.stdio[5];
|
|
547
|
+
if (stream === undefined)
|
|
548
|
+
throw new Error("missing Bubblewrap status pipe");
|
|
549
|
+
let text = "";
|
|
550
|
+
for await (const chunk of stream.iterator({ destroyOnReturn: false })) {
|
|
551
|
+
text += String(chunk);
|
|
552
|
+
if (text.length > 4096)
|
|
553
|
+
throw new Error("oversized Bubblewrap status");
|
|
554
|
+
const end = text.indexOf("}");
|
|
555
|
+
if (end < 0)
|
|
556
|
+
continue;
|
|
557
|
+
const record = JSON.parse(text.slice(0, end + 1));
|
|
558
|
+
if (!positiveInteger(record["child-pid"]))
|
|
559
|
+
throw new Error("invalid Bubblewrap child identity");
|
|
560
|
+
stream.resume();
|
|
561
|
+
return record["child-pid"];
|
|
562
|
+
}
|
|
563
|
+
throw new Error("Bubblewrap exited before output handoff");
|
|
564
|
+
}
|
|
565
|
+
function outputReady(child) {
|
|
566
|
+
const stream = child.stdio[4];
|
|
567
|
+
return new Promise((resolve, reject) => {
|
|
568
|
+
stream.once("error", reject);
|
|
569
|
+
stream.once("end", () => reject(new Error("output launcher ended before handoff")));
|
|
570
|
+
stream.once("data", (bytes) => {
|
|
571
|
+
if (bytes.length !== 1 || bytes[0] !== 1)
|
|
572
|
+
reject(new Error("invalid output readiness"));
|
|
573
|
+
else
|
|
574
|
+
resolve();
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
463
578
|
async function waitUntilEmpty(cgroup, timeoutMs) {
|
|
464
579
|
const deadline = Date.now() + timeoutMs;
|
|
465
580
|
while (Date.now() <= deadline) {
|
|
@@ -609,10 +724,10 @@ async function main() {
|
|
|
609
724
|
}
|
|
610
725
|
if (mode === "--enter")
|
|
611
726
|
return await enterMain(arguments_);
|
|
612
|
-
if (mode === "--inner") {
|
|
727
|
+
if (mode === "--inner" || mode === "--inner-output") {
|
|
613
728
|
if (arguments_[0] !== "--")
|
|
614
729
|
throw new Error("invalid rootless inner separator");
|
|
615
|
-
return await innerMain(arguments_.slice(1));
|
|
730
|
+
return await innerMain(arguments_.slice(1), mode === "--inner-output");
|
|
616
731
|
}
|
|
617
732
|
throw new Error("invalid rootless supervisor mode");
|
|
618
733
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jigging/jig",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.14",
|
|
4
4
|
"description": "A local, secure host for admitted FLOW packages",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"dist/index.js",
|
|
43
43
|
"dist/index.d.ts",
|
|
44
44
|
"dist/project/author.d.ts",
|
|
45
|
+
"dist/project/commands.d.ts",
|
|
45
46
|
"dist/json.d.ts",
|
|
46
47
|
"libexec/installed-cli.js",
|
|
47
48
|
"libexec/linux-rootless-supervisor.js",
|