@ai-ecoverse/slicc-kernel 1.72.0 → 1.73.1

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.
@@ -361,7 +361,17 @@ async function chownAs(fs, cred, path, change) {
361
361
  }
362
362
 
363
363
  // src/fs/protect.ts
364
- var PROTECT_WRITER = "slicc-kernel: protect.writer needs the process tag (#405), which this kernel does not have yet";
364
+ function protectInit(protection) {
365
+ const writers = [...writerTags(protection)];
366
+ return { paths: [...protection.paths], ...writers.length > 0 ? { writers } : {} };
367
+ }
368
+ function writerTags(protection) {
369
+ const writers = protection?.writers ?? [];
370
+ if (!Array.isArray(writers) || writers.some((tag) => typeof tag !== "string" || tag === "")) {
371
+ throw new TypeError("protect.writers must be a list of process tags");
372
+ }
373
+ return new Set(writers);
374
+ }
365
375
  function protectedPrefixes(paths) {
366
376
  return [...new Set(paths.map((p) => normalizePath(p)))];
367
377
  }
@@ -873,6 +883,9 @@ var OpfsFs = class {
873
883
  protect(prefixes2) {
874
884
  this.guarded = prefixes2;
875
885
  }
886
+ unguarded() {
887
+ return Object.create(this, { guarded: { value: [] } });
888
+ }
876
889
  refuse(path, holding = false) {
877
890
  if (isProtected(this.guarded, path) || holding && holdsProtected(this.guarded, path)) {
878
891
  throw fsError("EROFS", path);
@@ -1042,15 +1055,15 @@ var OpfsFs = class {
1042
1055
  }
1043
1056
  dirTimes = /* @__PURE__ */ new Map();
1044
1057
  dirDirty = /* @__PURE__ */ new Set();
1045
- dirFlush;
1058
+ flushing = {};
1046
1059
  touch(...dirs) {
1047
1060
  const now = Date.now();
1048
1061
  for (const dir of dirs) {
1049
1062
  this.dirTimes.set(dir, now);
1050
1063
  this.dirDirty.add(dir);
1051
1064
  }
1052
- this.dirFlush ??= Promise.resolve().then(() => this.flushDirTimes()).catch(() => void 0).finally(() => {
1053
- this.dirFlush = void 0;
1065
+ this.flushing.dir ??= Promise.resolve().then(() => this.flushDirTimes()).catch(() => void 0).finally(() => {
1066
+ this.flushing.dir = void 0;
1054
1067
  });
1055
1068
  }
1056
1069
  async flushDirTimes() {
@@ -1294,7 +1307,7 @@ var OpfsFs = class {
1294
1307
  const above = owner && {
1295
1308
  uid: owner.uid,
1296
1309
  gid: owner.gid,
1297
- ...mode ? { mode, parents: mode } : {}
1310
+ ...mode !== void 0 ? { mode, parents: mode } : {}
1298
1311
  };
1299
1312
  await this.mkdir(parentOf3(path), { recursive: true, ...above ? { owner: above } : {} });
1300
1313
  }
@@ -3569,6 +3582,9 @@ var UserDb = class {
3569
3582
  const pnpm = `${home}/.local/share/pnpm`;
3570
3583
  if (await this.realDir(pnpm)) await this.fs.chmod(pnpm, scoop ? 488 : 448);
3571
3584
  }
3585
+ umaskFor(options) {
3586
+ return (options.umask ?? this.umasks.get(options.name) ?? 18) & 511;
3587
+ }
3572
3588
  async realDir(path) {
3573
3589
  const parts2 = path.split("/").filter(Boolean);
3574
3590
  for (let n = 1; n <= parts2.length; n++) {
@@ -3592,9 +3608,11 @@ var UserDb = class {
3592
3608
  group: parseGroup(await this.read(GROUP, DEFAULT_GROUP))
3593
3609
  };
3594
3610
  }
3595
- async provision(row) {
3596
- const owner = this.owners ? { uid: row.uid, gid: row.gid } : void 0;
3597
- const made2 = owner ? { recursive: true, owner } : { recursive: true };
3611
+ async provision(row, umask, shared) {
3612
+ const dirs = 511 & ~umask | (shared ? 1024 : 0);
3613
+ const gid = shared?.gid ?? row.gid;
3614
+ const owner = this.owners ? { uid: row.uid, gid } : void 0;
3615
+ const made2 = owner ? { recursive: true, owner: { ...owner, mode: dirs, parents: dirs } } : { recursive: true };
3598
3616
  const pnpm = `${row.home}/.local/share/pnpm`;
3599
3617
  if (!await this.fs.exists(pnpm)) {
3600
3618
  await this.fs.mkdir(pnpm, made2);
@@ -3603,7 +3621,7 @@ var UserDb = class {
3603
3621
  const config = `${row.home}/.config/pnpm/config.yaml`;
3604
3622
  if (!await this.fs.exists(config)) {
3605
3623
  await this.fs.mkdir(`${row.home}/.config/pnpm`, made2);
3606
- await this.fs.writeFile(config, PNPM_CONFIG, owner);
3624
+ await this.fs.writeFile(config, PNPM_CONFIG, owner && { ...owner, mode: 438 & ~umask });
3607
3625
  }
3608
3626
  }
3609
3627
  async commit(passwd, group, then) {
@@ -3694,7 +3712,7 @@ var UserDb = class {
3694
3712
  throw fsError("EINVAL", `a user is no member of its own group ${name2}`);
3695
3713
  }
3696
3714
  if (found.uid >= FIRST_UID && found.uid <= LAST_UID) {
3697
- await this.provision(found);
3715
+ await this.provision(found, this.umaskFor(options), shared);
3698
3716
  await this.stampHome(found, shared, homeMode);
3699
3717
  }
3700
3718
  if (joined(name2, more)) await this.fs.writeFile(GROUP, formatGroup(group));
@@ -3712,7 +3730,7 @@ var UserDb = class {
3712
3730
  const fresh2 = !await this.fs.exists(home);
3713
3731
  await this.fs.mkdir(home, { recursive: true });
3714
3732
  try {
3715
- await this.provision(row);
3733
+ await this.provision(row, this.umaskFor(options), shared);
3716
3734
  await this.stampHome(row, shared, homeMode);
3717
3735
  await this.fs.mkdir("/etc", { recursive: true });
3718
3736
  await this.commit(formatPasswd(passwd), formatGroup(group));
@@ -3821,6 +3839,12 @@ var CLIENT_CHECKS = {
3821
3839
  readlink: (g, at) => searchable(g, "readlink", at(0), false),
3822
3840
  exists: (g, at) => searchable(g, "exists", at(0), true)
3823
3841
  };
3842
+ function clientFs(l, tag, self, req) {
3843
+ if (!FS_METHODS.includes(req.method)) {
3844
+ throw new Error(`unknown file system call ${req.method}`);
3845
+ }
3846
+ return fsCall(l.fsFor(tag), req.method, req.args, ownerFor(l, self), guardFor(l, self, tag));
3847
+ }
3824
3848
  async function fsCall(fs, method, args, owner, g) {
3825
3849
  const path = (i) => fs.resolvePath("/", String(args[i]));
3826
3850
  if (g) {
@@ -4108,10 +4132,7 @@ function serveClient(port, host) {
4108
4132
  case "ps":
4109
4133
  return { result: processes(l, self) };
4110
4134
  case "fs":
4111
- if (!FS_METHODS.includes(req.method)) {
4112
- throw new Error(`unknown file system call ${req.method}`);
4113
- }
4114
- return fsCall(l.processFs, req.method, req.args, ownerFor(l, self), guard2(l));
4135
+ return clientFs(l, host.tag, self, req);
4115
4136
  case "watch":
4116
4137
  case "unwatch":
4117
4138
  return watchOp(l, req, watches, send, guard2(l));
@@ -7816,6 +7837,11 @@ var HeldPaths = class extends Map {
7816
7837
  keptFor(handle) {
7817
7838
  return this.keptHandles.get(handle);
7818
7839
  }
7840
+ keeps(hidden) {
7841
+ for (const file of this.uses.keys()) if (file.hidden === hidden) return true;
7842
+ for (const file of this.unlinked.values()) if (file.hidden === hidden) return true;
7843
+ return false;
7844
+ }
7819
7845
  keep(path, file) {
7820
7846
  const fresh2 = this.handlesAt(path).filter((id2) => !this.keptHandles.has(id2));
7821
7847
  for (const id2 of fresh2) this.keptHandles.set(id2, file);
@@ -8917,6 +8943,7 @@ function guardOf(entry) {
8917
8943
  return { fs: entry.fs, cred, pid: entry.pid ?? 0, dac: entry.dac };
8918
8944
  }
8919
8945
  var FOLLOWING = /* @__PURE__ */ new Set(["exists", "stat", "chown", "statfs"]);
8946
+ var LOOKS = /* @__PURE__ */ new Set(["exists", "stat", "lstat"]);
8920
8947
  var NOT_FOLLOWING = /* @__PURE__ */ new Set(["lstat", "readlink", "lchown"]);
8921
8948
  async function guard(g, path, req, cwd) {
8922
8949
  const { op } = req;
@@ -8986,6 +9013,7 @@ async function accessOp(entry, path, req) {
8986
9013
  async function guarded(entry, path, req) {
8987
9014
  const g = guardOf(entry);
8988
9015
  if (!g) return void 0;
9016
+ if (LOOKS.has(req.op) && await entry.holdsKept?.(path)) return void 0;
8989
9017
  try {
8990
9018
  await guard(g, path, req, entry.cwd);
8991
9019
  return void 0;
@@ -12529,6 +12557,13 @@ var WasmProcess = class {
12529
12557
  }
12530
12558
  }
12531
12559
  keptPath;
12560
+ async holds(path) {
12561
+ for (const fd of this.fds.numbers()) {
12562
+ const { file } = this.fds.get(fd);
12563
+ if ((await file.stat?.().catch(() => void 0))?.path === path) return true;
12564
+ }
12565
+ return false;
12566
+ }
12532
12567
  keptHandle;
12533
12568
  async keptOr(path, handle) {
12534
12569
  const held = handle === void 0 ? void 0 : this.keptHandle?.(handle);
@@ -13172,7 +13207,8 @@ function spawnWasmProcess(opts) {
13172
13207
  const token = holdingToken(opts, holds, {
13173
13208
  cred: () => process.cred,
13174
13209
  umask: () => process.umask,
13175
- created: created3
13210
+ created: created3,
13211
+ holdsKept: async (path) => path.includes(`/${UNLINKED_DIR}/`) && (holds.keeps(path) || await process.holds(path))
13176
13212
  });
13177
13213
  process.keptPath = (path) => holds.keptAt(path)?.hidden;
13178
13214
  process.keptHandle = (handle2) => holds.keptFor(handle2)?.hidden;
@@ -15534,9 +15570,14 @@ var Sudo = class {
15534
15570
  if (walked.denied !== void 0 || walked.missing) return false;
15535
15571
  return (await this.o.fs.stat(cwd).catch(() => void 0))?.isDirectory === true;
15536
15572
  }
15537
- async untrusted(path, cwd) {
15538
- const files = await this.o.backing?.(path, cwd) ?? [];
15539
- for (const file of [path, ...files]) if (!await this.trusted(file)) return file;
15573
+ async untrusted(path, cwd, word) {
15574
+ const files = this.o.backing ? await this.o.backing(path, cwd) : [];
15575
+ if (!files) return failed2("ENOENT", `sudo: ${word}: command not found`);
15576
+ for (const file of [path, ...files]) {
15577
+ if (!await this.trusted(file)) {
15578
+ return failed2("EPERM", `sudo: ${file} must be root's, and writable by root only`);
15579
+ }
15580
+ }
15540
15581
  return void 0;
15541
15582
  }
15542
15583
  async locate(word, cwd) {
@@ -15577,10 +15618,8 @@ var Sudo = class {
15577
15618
  }
15578
15619
  const path = await this.locate(word, cwd);
15579
15620
  if (!path) return failed2("ENOENT", `sudo: ${word}: command not found`);
15580
- const untrusted = await this.untrusted(path, cwd);
15581
- if (untrusted) {
15582
- return failed2("EPERM", `sudo: ${untrusted} must be root's, and writable by root only`);
15583
- }
15621
+ const untrusted = await this.untrusted(path, cwd, word);
15622
+ if (untrusted) return untrusted;
15584
15623
  const argv = [path, ...args];
15585
15624
  const matching = rules.filter(
15586
15625
  (rule) => applies(rule) && (rule.target === "ALL" || rule.target === target.name) && (rule.cmnd === "*" || rule.cmnd.length === argv.length && rule.cmnd.every((w, i) => w === argv[i]))
@@ -15603,10 +15642,8 @@ var Sudo = class {
15603
15642
  if (await this.canonical(call.cwd) !== cwd || !await this.reachable(cred, cwd)) {
15604
15643
  return failed2("EACCES", `sudo: ${call.cwd} changed while the request waited`);
15605
15644
  }
15606
- const changed = await this.untrusted(path, cwd);
15607
- if (changed) {
15608
- return failed2("EPERM", `sudo: ${changed} must be root's, and writable by root only`);
15609
- }
15645
+ const changed = await this.untrusted(path, cwd, word);
15646
+ if (changed) return changed;
15610
15647
  if (answer === "always" && !await this.remember(self, target, argv)) {
15611
15648
  return failed2("EIO", "sudo: the rule could not be recorded");
15612
15649
  }
@@ -15880,6 +15917,10 @@ var Launcher = class {
15880
15917
  fs;
15881
15918
  processFs;
15882
15919
  prefixes;
15920
+ writerTags;
15921
+ unguardedRoot;
15922
+ writers;
15923
+ unlinkOwnerOption;
15883
15924
  base;
15884
15925
  createWorker;
15885
15926
  modulesDir;
@@ -15997,7 +16038,7 @@ var Launcher = class {
15997
16038
  this.nodes = new VfsNodes(fs, (path) => this.released(path));
15998
16039
  this.nodes.keepsOnDisk = true;
15999
16040
  this.openFiles.add(this.nodes);
16000
- const holders = [this.held, [this.nodes]];
16041
+ const holders = [this.held, this.openFiles];
16001
16042
  this.unlinked = new UnlinkedKeeper(
16002
16043
  fs,
16003
16044
  { [Symbol.iterator]: () => holders.flatMap((set2) => [...set2]).values() },
@@ -16010,7 +16051,12 @@ var Launcher = class {
16010
16051
  }
16011
16052
  );
16012
16053
  this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
16013
- if (options.protect?.writer) throw new Error(PROTECT_WRITER);
16054
+ this.writerTags = writerTags(options.protect);
16055
+ this.unlinkOwnerOption = options.unlinkOwner;
16056
+ if (this.writerTags.size > 0 && options.fs.protect && !options.fs.unguarded) {
16057
+ throw new TypeError("protect.writers needs a root file system with an unguarded view");
16058
+ }
16059
+ this.unguardedRoot = options.fs.unguarded?.() ?? options.fs;
16014
16060
  this.prefixes = protectedPrefixes(options.protect?.paths ?? []);
16015
16061
  this.processFs = this.prefixes.length > 0 ? new ProtectedFs(this.prefixes).view(this.fs) : this.fs;
16016
16062
  options.fs.protect?.(this.prefixes);
@@ -16227,7 +16273,7 @@ var Launcher = class {
16227
16273
  }
16228
16274
  async backing(file, cwd) {
16229
16275
  const planned = await this.plan(file, [file], cwd, "");
16230
- if (!planned) return [];
16276
+ if (!planned) return void 0;
16231
16277
  const { glue, wasm, imports } = planned.target;
16232
16278
  const files = [glue, wasm, ...imports ? [imports] : []];
16233
16279
  const scripts = [];
@@ -16334,6 +16380,7 @@ var Launcher = class {
16334
16380
  throw fsError("ESRCH", `no process group ${req.pgid}`);
16335
16381
  }
16336
16382
  const pid = this.nextPid++;
16383
+ const stack = this.stackFor(req.tag);
16337
16384
  const terminal = req.fds.stdioTerminal();
16338
16385
  const decided = req.decided ?? Promise.resolve(true);
16339
16386
  const count = (byParent) => {
@@ -16364,8 +16411,8 @@ var Launcher = class {
16364
16411
  env: req.env,
16365
16412
  cwd: req.cwd,
16366
16413
  fds: req.fds,
16367
- fs: this.processFs,
16368
- ...this.prefixes.length > 0 ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
16414
+ fs: stack.fs,
16415
+ ...this.prefixes.length > 0 && !stack.writer ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
16369
16416
  createWorker: this.createWorker,
16370
16417
  onError: req.report,
16371
16418
  spawner: this.spawner(pid, req.report),
@@ -16380,9 +16427,9 @@ var Launcher = class {
16380
16427
  }),
16381
16428
  sudo: (call, signal) => this.sudo(pid, call, signal),
16382
16429
  openFiles: this.openFiles,
16383
- nodes: this.nodes,
16430
+ nodes: stack.nodes,
16384
16431
  held: this.held,
16385
- unlinkGate: this.unlinked.gate,
16432
+ unlinkGate: stack.unlinked.gate,
16386
16433
  statfs: (path) => this.mounts.statfs(path),
16387
16434
  mounts: () => this.mounts.list(),
16388
16435
  mount: (call, signal) => this.processMount(pid, call, signal),
@@ -16683,6 +16730,7 @@ var Launcher = class {
16683
16730
  released(path) {
16684
16731
  const at = normalizePath(path);
16685
16732
  this.unlinked.closed(at);
16733
+ this.writers?.unlinked.closed(at);
16686
16734
  const open = [...this.openFiles].some((nodes) => nodes.writes(at)) || heldUnder(this.held, at);
16687
16735
  if (!open) void this.mounts.commit(at).catch(() => void 0);
16688
16736
  }
@@ -16833,6 +16881,27 @@ var Launcher = class {
16833
16881
  await this.provisionPnpmHome();
16834
16882
  return entry;
16835
16883
  }
16884
+ async ownTmp() {
16885
+ const real = await this.realLayoutPath("/tmp");
16886
+ const st = real === void 0 ? void 0 : await this.base.lstat(real).catch(() => void 0);
16887
+ if (real !== void 0 && st?.isDirectory && this.base.hasOwner && !await this.base.hasOwner(real)) {
16888
+ let removed = 0;
16889
+ for (const name2 of await this.base.readdir(real).catch(() => [])) {
16890
+ const path = joinName(real, name2);
16891
+ if (await this.base.hasOwner(path) || this.inUse(path)) continue;
16892
+ const failed3 = await this.base.rm(path, { recursive: true, force: true }).catch(String);
16893
+ if (failed3 === void 0) removed++;
16894
+ }
16895
+ if (removed > 0) {
16896
+ console.warn(`slicc-kernel users: removed ${removed} entries from /tmp that had no owner`);
16897
+ }
16898
+ await this.base.chown?.(real, { uid: 0, gid: 0, mode: 1023, follow: false });
16899
+ }
16900
+ if (st?.isDirectory) await this.base.chmod(real, 1023);
16901
+ }
16902
+ inUse(path) {
16903
+ return [...this.openFiles].some((nodes) => nodes.holds(path)) || heldUnder(this.held, path);
16904
+ }
16836
16905
  async stampLayout() {
16837
16906
  const layout = [
16838
16907
  ...LAYOUT,
@@ -16952,7 +17021,7 @@ var Launcher = class {
16952
17021
  async prepare() {
16953
17022
  await this.resolvePrefixes();
16954
17023
  for (const dir of SHARED_DIRS) await this.base.mkdir(dir, { recursive: true });
16955
- if (this.owners) await this.base.chmod("/tmp", 1023);
17024
+ if (this.owners) await this.ownTmp();
16956
17025
  await this.provisionPnpmHome();
16957
17026
  await this.users.ensure();
16958
17027
  if (this.owners) await this.stampLayout();
@@ -17001,13 +17070,49 @@ var Launcher = class {
17001
17070
  ...account.umask !== void 0 ? { umask: account.umask } : {}
17002
17071
  };
17003
17072
  }
17073
+ fsFor(tag) {
17074
+ return this.stackFor(tag).fs;
17075
+ }
17076
+ isWriter(tag) {
17077
+ return tag !== void 0 && this.prefixes.length > 0 && this.writerTags.has(tag);
17078
+ }
17079
+ stackFor(tag) {
17080
+ if (!this.isWriter(tag)) {
17081
+ return { fs: this.processFs, nodes: this.nodes, unlinked: this.unlinked, writer: false };
17082
+ }
17083
+ this.writers ??= this.writerStack();
17084
+ return this.writers;
17085
+ }
17086
+ writerStack() {
17087
+ const fs = withCommandDirs(
17088
+ this.watchers.wrap(this.mounts.wrap(this.unguardedRoot)),
17089
+ async () => new Set((await this.commands()).keys()),
17090
+ (dir) => this.userCommandDir(dir),
17091
+ this.owners
17092
+ );
17093
+ const nodes = new VfsNodes(fs, (path) => this.released(path));
17094
+ nodes.keepsOnDisk = true;
17095
+ this.openFiles.add(nodes);
17096
+ const unlinked = new UnlinkedKeeper(
17097
+ fs,
17098
+ { [Symbol.iterator]: () => [this.held, this.openFiles].flatMap((set2) => [...set2]).values() },
17099
+ (path) => this.mountRoot(path),
17100
+ {
17101
+ locks: webLocks(),
17102
+ busy: (path) => [...this.openFiles].some((open) => open.holds(path)),
17103
+ owner: this.unlinkOwnerOption,
17104
+ ...this.owners ? { dirOwner: { uid: 0, gid: 0, mode: 448 } } : {}
17105
+ }
17106
+ );
17107
+ return { fs: unlinked.wrap(keepingOpen(fs, nodes)), nodes, unlinked, writer: true };
17108
+ }
17004
17109
  async starting(argv, dir, account, tag) {
17005
17110
  this.resetCatalogs();
17006
17111
  const scope = account.uid === 0 ? "" : pnpmHomeOf(account, this.pnpmHome);
17007
17112
  const start2 = dir ?? "/";
17008
17113
  const cwd = await physicalPathAsync(this.base, start2.startsWith("/") ? start2 : `/${start2}`);
17009
17114
  const [file = ""] = argv;
17010
- if (!await this.base.exists(cwd)) await this.processFs.mkdir(cwd, { recursive: true });
17115
+ if (!await this.base.exists(cwd)) await this.stackFor(tag).fs.mkdir(cwd, { recursive: true });
17011
17116
  const planned = await this.plan(file, argv, cwd, scope);
17012
17117
  await this.mayExec(credOf(account), { file, cwd, pid: 0, tag }, planned);
17013
17118
  return { cwd, file, planned };
@@ -17270,7 +17375,7 @@ function hostOptions(req, port, sealed) {
17270
17375
  ...req.hostname !== void 0 ? { hostname: req.hostname } : {},
17271
17376
  ...req.users !== void 0 ? { users: req.users } : {},
17272
17377
  ...req.denials ? { onDenial: (denial) => port.postMessage({ denial }) } : {},
17273
- ...req.protect ? { protect: { paths: [...req.protect.paths] } } : {},
17378
+ ...req.protect ? { protect: protectInit(req.protect) } : {},
17274
17379
  ...req.sealed ? { sealed } : {}
17275
17380
  };
17276
17381
  }