@jigging/jig 0.1.0-alpha.12 → 0.1.0-alpha.13
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 +12 -8
- package/libexec/installed-cli.js +3460 -2337
- package/libexec/linux-rootless-supervisor.js +121 -10
- package/package.json +1 -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,35 @@ async function enterMain(arguments_) {
|
|
|
211
246
|
writeSync(3, `${process.pid}
|
|
212
247
|
`);
|
|
213
248
|
closeSync(3);
|
|
214
|
-
const
|
|
249
|
+
const output = bubblewrapArguments_.includes("--sync-fd");
|
|
250
|
+
const inputDescriptors = bubblewrapArguments_.flatMap((arg, index) => arg === "--ro-bind-data" ? [Number(bubblewrapArguments_[index + 1])] : []);
|
|
251
|
+
if (inputDescriptors.some((fd, index) => fd !== 6 + index))
|
|
252
|
+
throw new Error("invalid captured input descriptor order");
|
|
253
|
+
const child = spawn(bubblewrap, bubblewrapArguments_, {
|
|
254
|
+
cwd: "/",
|
|
255
|
+
env: {},
|
|
256
|
+
stdio: [
|
|
257
|
+
"inherit",
|
|
258
|
+
"inherit",
|
|
259
|
+
"inherit",
|
|
260
|
+
"ignore",
|
|
261
|
+
output ? 4 : "ignore",
|
|
262
|
+
output ? 5 : "ignore",
|
|
263
|
+
...inputDescriptors
|
|
264
|
+
]
|
|
265
|
+
});
|
|
266
|
+
for (const fd of inputDescriptors)
|
|
267
|
+
closeSync(fd);
|
|
268
|
+
if (output) {
|
|
269
|
+
closeSync(4);
|
|
270
|
+
closeSync(5);
|
|
271
|
+
}
|
|
215
272
|
const exit = await childClose(child);
|
|
216
273
|
if (exit.signal !== null)
|
|
217
274
|
process.kill(process.pid, exit.signal);
|
|
218
275
|
process.exitCode = exit.code ?? 1;
|
|
219
276
|
}
|
|
220
|
-
async function innerMain(command) {
|
|
277
|
+
async function innerMain(command, output = false) {
|
|
221
278
|
if (command.length === 0 || !absolute(command[0]))
|
|
222
279
|
throw new Error("invalid rootless inner command");
|
|
223
280
|
const nullDevice = statSync("/dev/null");
|
|
@@ -225,6 +282,14 @@ async function innerMain(command) {
|
|
|
225
282
|
if (!nullDevice.isCharacterDevice() || !entropyDevice.isCharacterDevice() || (nullDevice.mode & 511) !== 438 || (entropyDevice.mode & 511) !== 438) {
|
|
226
283
|
throw new Error("rootless private devices do not satisfy the required projection");
|
|
227
284
|
}
|
|
285
|
+
if (output) {
|
|
286
|
+
writeSync(4, Buffer.from([1]));
|
|
287
|
+
const gate = Buffer.alloc(1);
|
|
288
|
+
if (readSync(4, gate, 0, 1, null) !== 1 || gate[0] !== 1) {
|
|
289
|
+
throw new Error("output ownership was not acquired");
|
|
290
|
+
}
|
|
291
|
+
closeSync(4);
|
|
292
|
+
}
|
|
228
293
|
const child = spawn(command[0], command.slice(1), {
|
|
229
294
|
cwd: "/work",
|
|
230
295
|
env: process.env,
|
|
@@ -271,11 +336,21 @@ function bubblewrapArguments(configuration) {
|
|
|
271
336
|
];
|
|
272
337
|
for (const mount of configuration.readOnlyMounts)
|
|
273
338
|
result.push("--ro-bind", mount.source, mount.destination);
|
|
339
|
+
if (configuration.inputDirectories.length > 0)
|
|
340
|
+
result.push("--size", "1048576", "--tmpfs", "/jig-input");
|
|
341
|
+
for (const path of configuration.inputDirectories)
|
|
342
|
+
result.push("--dir", path);
|
|
343
|
+
for (const file of configuration.capturedInputs)
|
|
344
|
+
result.push("--ro-bind-data", String(file.fd), file.destination);
|
|
345
|
+
if (configuration.inputDirectories.length > 0)
|
|
346
|
+
result.push("--remount-ro", "/jig-input");
|
|
347
|
+
if (configuration.output)
|
|
348
|
+
result.push("--size", String(16 * 1024 * 1024), "--tmpfs", "/jig-output", "--sync-fd", "4", "--json-status-fd", "5");
|
|
274
349
|
result.push("--ro-bind", configuration.supervisorPath, MODULE_DESTINATION);
|
|
275
350
|
for (const [name, value] of Object.entries(configuration.environment).sort(([a], [b]) => a.localeCompare(b))) {
|
|
276
351
|
result.push("--setenv", name, value);
|
|
277
352
|
}
|
|
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);
|
|
353
|
+
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
354
|
return result;
|
|
280
355
|
}
|
|
281
356
|
async function claimActive(configuration) {
|
|
@@ -360,12 +435,15 @@ function requireStart(value) {
|
|
|
360
435
|
"bunHostLibraryPath",
|
|
361
436
|
"bunPath",
|
|
362
437
|
"command",
|
|
438
|
+
"capturedInputs",
|
|
363
439
|
"delegatedCgroup",
|
|
364
440
|
"environment",
|
|
365
441
|
"limits",
|
|
442
|
+
"inputDirectories",
|
|
366
443
|
"mechanismDigest",
|
|
367
444
|
"nestedUserNamespaces",
|
|
368
445
|
"network",
|
|
446
|
+
"output",
|
|
369
447
|
"ownerDigest",
|
|
370
448
|
"ownerStateAllocationDigest",
|
|
371
449
|
"ownerStateDirectory",
|
|
@@ -377,7 +455,7 @@ function requireStart(value) {
|
|
|
377
455
|
"sealedPlanDigest",
|
|
378
456
|
"supervisorPath"
|
|
379
457
|
];
|
|
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"))) {
|
|
458
|
+
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
459
|
throw new Error("invalid rootless supervisor configuration");
|
|
382
460
|
}
|
|
383
461
|
return configuration;
|
|
@@ -460,6 +538,39 @@ async function readReady(child) {
|
|
|
460
538
|
throw new Error("invalid rootless entry readiness receipt");
|
|
461
539
|
return pid;
|
|
462
540
|
}
|
|
541
|
+
async function readSandboxPid(child) {
|
|
542
|
+
const stream = child.stdio[5];
|
|
543
|
+
if (stream === undefined)
|
|
544
|
+
throw new Error("missing Bubblewrap status pipe");
|
|
545
|
+
let text = "";
|
|
546
|
+
for await (const chunk of stream.iterator({ destroyOnReturn: false })) {
|
|
547
|
+
text += String(chunk);
|
|
548
|
+
if (text.length > 4096)
|
|
549
|
+
throw new Error("oversized Bubblewrap status");
|
|
550
|
+
const end = text.indexOf("}");
|
|
551
|
+
if (end < 0)
|
|
552
|
+
continue;
|
|
553
|
+
const record = JSON.parse(text.slice(0, end + 1));
|
|
554
|
+
if (!positiveInteger(record["child-pid"]))
|
|
555
|
+
throw new Error("invalid Bubblewrap child identity");
|
|
556
|
+
stream.resume();
|
|
557
|
+
return record["child-pid"];
|
|
558
|
+
}
|
|
559
|
+
throw new Error("Bubblewrap exited before output handoff");
|
|
560
|
+
}
|
|
561
|
+
function outputReady(child) {
|
|
562
|
+
const stream = child.stdio[4];
|
|
563
|
+
return new Promise((resolve, reject) => {
|
|
564
|
+
stream.once("error", reject);
|
|
565
|
+
stream.once("end", () => reject(new Error("output launcher ended before handoff")));
|
|
566
|
+
stream.once("data", (bytes) => {
|
|
567
|
+
if (bytes.length !== 1 || bytes[0] !== 1)
|
|
568
|
+
reject(new Error("invalid output readiness"));
|
|
569
|
+
else
|
|
570
|
+
resolve();
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
}
|
|
463
574
|
async function waitUntilEmpty(cgroup, timeoutMs) {
|
|
464
575
|
const deadline = Date.now() + timeoutMs;
|
|
465
576
|
while (Date.now() <= deadline) {
|
|
@@ -609,10 +720,10 @@ async function main() {
|
|
|
609
720
|
}
|
|
610
721
|
if (mode === "--enter")
|
|
611
722
|
return await enterMain(arguments_);
|
|
612
|
-
if (mode === "--inner") {
|
|
723
|
+
if (mode === "--inner" || mode === "--inner-output") {
|
|
613
724
|
if (arguments_[0] !== "--")
|
|
614
725
|
throw new Error("invalid rootless inner separator");
|
|
615
|
-
return await innerMain(arguments_.slice(1));
|
|
726
|
+
return await innerMain(arguments_.slice(1), mode === "--inner-output");
|
|
616
727
|
}
|
|
617
728
|
throw new Error("invalid rootless supervisor mode");
|
|
618
729
|
}
|