@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.
@@ -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 { link, mkdir, open, readFile, rmdir, stat, unlink, writeFile } from "fs/promises";
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: ["inherit", "inherit", "inherit", "pipe"]
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 child = spawn(bubblewrap, bubblewrapArguments_, { cwd: "/", env: {}, stdio: "inherit" });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jigging/jig",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "A local, secure host for admitted FLOW packages",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {