@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.
@@ -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,39 @@ 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 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.12",
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",