@ai-ecoverse/slicc-kernel 1.70.0 → 1.71.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.
@@ -162,12 +162,12 @@ var IndexedDbMeta = class {
162
162
  return [transaction, transaction.objectStore(META_STORE)];
163
163
  }
164
164
  async get(paths) {
165
- const missing3 = paths.filter((path) => !this.cache.has(path));
166
- if (missing3.length > 0) {
165
+ const missing4 = paths.filter((path) => !this.cache.has(path));
166
+ if (missing4.length > 0) {
167
167
  const generation = this.generation;
168
168
  const [transaction, store] = this.transaction("readonly");
169
169
  const found = /* @__PURE__ */ new Map();
170
- for (const path of missing3) {
170
+ for (const path of missing4) {
171
171
  const request2 = store.get(path);
172
172
  request2.onsuccess = () => void found.set(path, request2.result ?? null);
173
173
  }
@@ -353,6 +353,183 @@ async function chownAs(fs, cred, path, change) {
353
353
  });
354
354
  }
355
355
 
356
+ // src/fs/protect.ts
357
+ var PROTECT_WRITER = "slicc-kernel: protect.writer needs the process tag (#405), which this kernel does not have yet";
358
+ function protectedPrefixes(paths) {
359
+ return [...new Set(paths.map((p) => normalizePath(p)))];
360
+ }
361
+ function within(path, prefix) {
362
+ return prefix === "/" || path === prefix || path.startsWith(`${prefix}/`);
363
+ }
364
+ function isProtected(prefixes2, path) {
365
+ return prefixes2.some((prefix) => within(path, prefix));
366
+ }
367
+ function holdsProtected(prefixes2, path) {
368
+ return prefixes2.some((prefix) => within(prefix, path));
369
+ }
370
+ var MAX_LINKS = 40;
371
+ function partsOf(path) {
372
+ return path.split("/").filter((p) => p !== "" && p !== ".");
373
+ }
374
+ function missing(err) {
375
+ const code = err?.code;
376
+ return code === "ENOENT" || code === "ENOTDIR";
377
+ }
378
+ async function resolved(fs, path, follow2) {
379
+ let done4 = [];
380
+ let todo = partsOf(path);
381
+ for (let hops = 0; todo.length > 0; ) {
382
+ const part = todo.shift();
383
+ if (part === "..") {
384
+ done4.pop();
385
+ continue;
386
+ }
387
+ if (todo.length === 0 && !follow2) {
388
+ done4.push(part);
389
+ break;
390
+ }
391
+ const at = `/${[...done4, part].join("/")}`;
392
+ const st = await fs.lstat(at).catch((err) => {
393
+ if (missing(err)) return void 0;
394
+ throw err;
395
+ });
396
+ if (!st) {
397
+ return normalizePath([...done4, part, ...todo].join("/"));
398
+ }
399
+ if (!st.isSymbolicLink) {
400
+ done4.push(part);
401
+ continue;
402
+ }
403
+ if (++hops > MAX_LINKS) throw fsError("ELOOP", path);
404
+ const link2 = await fs.readlink(at);
405
+ if (link2.startsWith("/")) done4 = [];
406
+ todo = [...partsOf(link2), ...todo];
407
+ }
408
+ return `/${done4.join("/")}`;
409
+ }
410
+ var ProtectedFs = class {
411
+ prefixes;
412
+ constructor(prefixes2) {
413
+ this.prefixes = prefixes2;
414
+ }
415
+ async check(fs, path, follow2, whole = false) {
416
+ const lexical2 = normalizePath(path);
417
+ const real = await resolved(fs, lexical2, follow2);
418
+ for (const p of /* @__PURE__ */ new Set([lexical2, real])) {
419
+ if (isProtected(this.prefixes, p) || whole && holdsProtected(this.prefixes, p)) {
420
+ throw fsError("EROFS", path);
421
+ }
422
+ }
423
+ }
424
+ view(fs) {
425
+ const check = (path, follow2, whole = false) => this.check(fs, path, follow2, whole);
426
+ const marked = (path, st) => !st.readonly && isProtected(this.prefixes, normalizePath(path)) ? { ...st, readonly: true } : st;
427
+ const own = {
428
+ async writeFile(...args) {
429
+ await check(args[0], true);
430
+ return fs.writeFile(...args);
431
+ },
432
+ async mkdir(...args) {
433
+ await check(args[0], false);
434
+ return fs.mkdir(...args);
435
+ },
436
+ async rm(...args) {
437
+ await check(args[0], false, true);
438
+ return fs.rm(...args);
439
+ },
440
+ async rename(from, to) {
441
+ await check(from, false, true);
442
+ await check(to, false, true);
443
+ return fs.rename(from, to);
444
+ },
445
+ async link(from, to, follow2) {
446
+ await check(from, follow2 === true);
447
+ await check(to, false);
448
+ return fs.link(from, to, follow2);
449
+ },
450
+ async symlink(...args) {
451
+ await check(args[1], false);
452
+ return fs.symlink(...args);
453
+ },
454
+ async chmod(path, mode) {
455
+ await check(path, true);
456
+ return fs.chmod(path, mode);
457
+ },
458
+ async utimes(path, atime, mtime) {
459
+ await check(path, true);
460
+ return fs.utimes(path, atime, mtime);
461
+ },
462
+ async stat(path) {
463
+ return marked(path, await fs.stat(path));
464
+ },
465
+ async lstat(path) {
466
+ return marked(path, await fs.lstat(path));
467
+ },
468
+ async readdirStat(path) {
469
+ const dir = normalizePath(path);
470
+ return (await fs.readdirStat(path)).map(([name2, st]) => [
471
+ name2,
472
+ st && marked(`${dir}/${name2}`, st)
473
+ ]);
474
+ },
475
+ ...fs.createExclusive ? {
476
+ async createExclusive(...args) {
477
+ await check(args[0], true);
478
+ return fs.createExclusive?.(...args);
479
+ }
480
+ } : {},
481
+ ...fs.chown ? {
482
+ async chown(...args) {
483
+ await check(args[0], args[1].follow !== false);
484
+ return fs.chown?.(...args);
485
+ }
486
+ } : {},
487
+ ...fs.lutimes ? {
488
+ async lutimes(path, atime, mtime) {
489
+ await check(path, false);
490
+ return fs.lutimes?.(path, atime, mtime);
491
+ }
492
+ } : {},
493
+ ...fs.pwrite ? {
494
+ async pwrite(...args) {
495
+ await check(args[0], true);
496
+ return fs.pwrite?.(...args);
497
+ }
498
+ } : {},
499
+ ...fs.truncate ? {
500
+ async truncate(path, size) {
501
+ await check(path, true);
502
+ return fs.truncate?.(path, size);
503
+ }
504
+ } : {},
505
+ ...fs.exclusive ? {
506
+ async exclusive(path, run2, follow2) {
507
+ return fs.exclusive(
508
+ path,
509
+ (held) => run2({
510
+ ...held,
511
+ writeFile: async (...args) => {
512
+ await check(args[0], true);
513
+ return held.writeFile(...args);
514
+ }
515
+ }),
516
+ follow2
517
+ );
518
+ }
519
+ } : {}
520
+ };
521
+ const view = {};
522
+ for (let at = fs; at && at !== Object.prototype; at = Object.getPrototypeOf(at)) {
523
+ for (const name2 of Object.getOwnPropertyNames(at)) {
524
+ if (name2 === "constructor" || Object.hasOwn(view, name2)) continue;
525
+ const value = fs[name2];
526
+ view[name2] = typeof value === "function" ? value.bind(fs) : value;
527
+ }
528
+ }
529
+ return Object.assign(view, own);
530
+ }
531
+ };
532
+
356
533
  // src/fs/opfs.ts
357
534
  var S_IFDIR = 16384;
358
535
  var S_IFREG = 32768;
@@ -360,7 +537,7 @@ var S_IFLNK = 40960;
360
537
  var PERM_MASK = 4095;
361
538
  var S_ISGID2 = 1024;
362
539
  var READ_ATTEMPTS = 3;
363
- var MAX_LINKS = 40;
540
+ var MAX_LINKS2 = 40;
364
541
  var CHECK_MS = 1e3;
365
542
  var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
366
543
  var ERRNO_BY_DOM_ERROR = {
@@ -397,7 +574,7 @@ function sidecar(meta) {
397
574
  });
398
575
  return Object.fromEntries(META_OPS.map((op) => [op, guarded2(op)]));
399
576
  }
400
- function missing(err) {
577
+ function missing2(err) {
401
578
  const code = err?.code;
402
579
  return code === "ENOENT" || code === "ENOTDIR";
403
580
  }
@@ -414,7 +591,7 @@ function prefixes(path) {
414
591
  const parts2 = split(path);
415
592
  return parts2.map((_, i) => `/${parts2.slice(0, i + 1).join("/")}`);
416
593
  }
417
- function within(path, root) {
594
+ function within2(path, root) {
418
595
  return path === root || path.startsWith(`${root}/`);
419
596
  }
420
597
  var LOCKED = /* @__PURE__ */ new Set(["NotReadableError", "NoModificationAllowedError", "InvalidStateError"]);
@@ -553,14 +730,25 @@ var OpfsFs = class {
553
730
  const fields = await this.newFields(path, owner, directory);
554
731
  if (fields) await this.meta.update(path, (entry) => ({ ...entry, path, ...fields }));
555
732
  }
733
+ guarded = [];
734
+ protect(prefixes2) {
735
+ this.guarded = prefixes2;
736
+ }
737
+ refuse(path, holding = false) {
738
+ if (isProtected(this.guarded, path) || holding && holdsProtected(this.guarded, path)) {
739
+ throw fsError("EROFS", path);
740
+ }
741
+ }
556
742
  async chown(path, change) {
557
743
  if (!this.owners) return;
558
744
  if (change.kind === "new") {
559
745
  const at = resolvePath((await this.locate(parentOf3(path), true)).path, baseOf(path));
746
+ this.refuse(at);
560
747
  const { uid = 0, gid = 0, mode } = change;
561
748
  return this.stampNew(at, { uid, gid, ...mode !== void 0 ? { mode } : {} }, false);
562
749
  }
563
750
  const found = await this.locate(path, change.follow !== false);
751
+ this.refuse(found.path);
564
752
  const [stored] = found.link ? [found.link] : await this.meta.get([found.path]);
565
753
  const current = await this.ownerAt(found.path, stored);
566
754
  const next2 = { uid: change.uid ?? current.uid, gid: change.gid ?? current.gid };
@@ -673,11 +861,11 @@ var OpfsFs = class {
673
861
  }
674
862
  async locate(path, follow2) {
675
863
  let current = path;
676
- for (let hops = 0; hops <= MAX_LINKS; hops++) {
864
+ for (let hops = 0; hops <= MAX_LINKS2; hops++) {
677
865
  try {
678
866
  return { path: current, handle: await this.handle(current) };
679
867
  } catch (err) {
680
- if (!missing(err)) throw err;
868
+ if (!missing2(err)) throw err;
681
869
  const link2 = await this.linkAlong(current);
682
870
  if (!link2) throw err;
683
871
  if (link2.path === current && !follow2) return { path: current, link: link2 };
@@ -699,7 +887,7 @@ var OpfsFs = class {
699
887
  const st = await this.lstat(at);
700
888
  if (!st.isSymbolicLink && !st.isDirectory) throw fsError("ENOTDIR", at);
701
889
  if (!st.isSymbolicLink) done4.push(part);
702
- else if (++hops > MAX_LINKS) throw fsError("ELOOP", path);
890
+ else if (++hops > MAX_LINKS2) throw fsError("ELOOP", path);
703
891
  else {
704
892
  const link2 = await this.readlink(at);
705
893
  if (link2.startsWith("/")) done4 = [];
@@ -796,7 +984,7 @@ var OpfsFs = class {
796
984
  return this.locate(path, true).then(
797
985
  () => true,
798
986
  (err) => {
799
- if (missing(err)) return false;
987
+ if (missing2(err)) return false;
800
988
  throw err;
801
989
  }
802
990
  );
@@ -814,8 +1002,10 @@ var OpfsFs = class {
814
1002
  }
815
1003
  }
816
1004
  }
817
- async existing(path) {
818
- const handle = (await this.locate(path, true)).handle;
1005
+ async existing(path, writing = false) {
1006
+ const found = await this.locate(path, true);
1007
+ if (writing) this.refuse(found.path);
1008
+ const handle = found.handle;
819
1009
  if (handle.kind === "directory") throw fsError("EISDIR", path);
820
1010
  return handle;
821
1011
  }
@@ -840,7 +1030,7 @@ var OpfsFs = class {
840
1030
  await this.retrying(path, () => positioned(handle, (io) => io.write(offset, bytes)), true);
841
1031
  }
842
1032
  async truncate(path, size) {
843
- const handle = await this.existing(path);
1033
+ const handle = await this.existing(path, true);
844
1034
  await this.retrying(path, () => positioned(handle, (io) => io.truncate(size)), true);
845
1035
  }
846
1036
  async writeSession(path, truncate) {
@@ -866,10 +1056,15 @@ var OpfsFs = class {
866
1056
  return new TextDecoder().decode(await this.readFileBuffer(path));
867
1057
  }
868
1058
  async writeTarget(path) {
1059
+ const [target, existed] = await this.locateWrite(path);
1060
+ this.refuse(target);
1061
+ return [target, existed];
1062
+ }
1063
+ async locateWrite(path) {
869
1064
  try {
870
1065
  return [(await this.locate(path, true)).path, true];
871
1066
  } catch (err) {
872
- if (!missing(err)) throw err;
1067
+ if (!missing2(err)) throw err;
873
1068
  }
874
1069
  const [entry] = await this.meta.get([path]);
875
1070
  const wanted = entry?.link === void 0 ? path : resolvePath(parentOf3(path), entry.link);
@@ -938,7 +1133,7 @@ var OpfsFs = class {
938
1133
  try {
939
1134
  found = await this.locate(path, true);
940
1135
  } catch (err) {
941
- if (!missing(err)) throw err;
1136
+ if (!missing2(err)) throw err;
942
1137
  }
943
1138
  if (found) {
944
1139
  const directory = found.handle?.kind === "directory";
@@ -959,6 +1154,7 @@ var OpfsFs = class {
959
1154
  }
960
1155
  const parent = await this.locate(parentOf3(path), true);
961
1156
  if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
1157
+ this.refuse(resolvePath(parent.path, baseOf(path)));
962
1158
  await parent.handle.getDirectoryHandle(baseOf(path), { create: true });
963
1159
  const made2 = `${parent.path === "/" ? "" : parent.path}/${baseOf(path)}`;
964
1160
  await this.stampNew(made2, owner, true);
@@ -973,6 +1169,7 @@ var OpfsFs = class {
973
1169
  if (options.force && error.code === "ENOENT") return;
974
1170
  throw error;
975
1171
  }
1172
+ this.refuse(found.path, true);
976
1173
  if (found.handle) {
977
1174
  const [dir, name2] = await this.parent(found.path);
978
1175
  try {
@@ -991,19 +1188,28 @@ var OpfsFs = class {
991
1188
  links() {
992
1189
  return false;
993
1190
  }
994
- async link(from) {
1191
+ async link(from, to, follow2) {
1192
+ await (follow2 ? this.stat(from) : this.lstat(from));
1193
+ if (!(await this.stat(parentOf3(to))).isDirectory) throw fsError("ENOTDIR", to);
1194
+ if (await this.lstat(to).then(
1195
+ () => true,
1196
+ () => false
1197
+ ))
1198
+ throw fsError("EEXIST", to);
995
1199
  throw fsError("EPERM", `${from}: this file system has no hard links`);
996
1200
  }
997
1201
  async rename(from, to) {
998
1202
  if (from === to) return;
999
1203
  const source = await this.locate(from, false);
1204
+ this.refuse(source.path, true);
1000
1205
  this.forget(source.path);
1001
1206
  const parent = await this.locate(parentOf3(to), true);
1002
1207
  const destination = resolvePath(parent.path, baseOf(to));
1208
+ this.refuse(destination, true);
1003
1209
  const sourceStat = await this.describe(source);
1004
- if (sourceStat.isDirectory && within(destination, source.path)) throw fsError("EINVAL", to);
1210
+ if (sourceStat.isDirectory && within2(destination, source.path)) throw fsError("EINVAL", to);
1005
1211
  const target = await this.locate(destination, false).catch((err) => {
1006
- if (missing(err)) return null;
1212
+ if (missing2(err)) return null;
1007
1213
  throw err;
1008
1214
  });
1009
1215
  const targetStat = target && await this.describe(target);
@@ -1097,9 +1303,10 @@ var OpfsFs = class {
1097
1303
  const parent = await this.locate(parentOf3(path), true);
1098
1304
  if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
1099
1305
  const at = resolvePath(parent.path, baseOf(path));
1306
+ this.refuse(at);
1100
1307
  if (await this.locate(at, false).then(
1101
1308
  () => true,
1102
- (err) => !missing(err)
1309
+ (err) => !missing2(err)
1103
1310
  )) {
1104
1311
  throw fsError("EEXIST", path);
1105
1312
  }
@@ -1126,6 +1333,7 @@ var OpfsFs = class {
1126
1333
  }
1127
1334
  async chmod(path, mode) {
1128
1335
  const found = await this.locate(path, true);
1336
+ this.refuse(found.path);
1129
1337
  const ctimeMs = Date.now();
1130
1338
  await this.meta.update(found.path, (entry) => ({
1131
1339
  ...entry,
@@ -1141,6 +1349,7 @@ var OpfsFs = class {
1141
1349
  await this.setTimes(await this.locate(path, false), atime, mtime);
1142
1350
  }
1143
1351
  async setTimes(found, atime, mtime) {
1352
+ this.refuse(found.path);
1144
1353
  const { link: link2 } = found;
1145
1354
  if (link2) {
1146
1355
  const ctimeMs2 = Date.now();
@@ -1345,7 +1554,7 @@ function fresh(bytes, start2 = 0, end = bytes.length) {
1345
1554
  }
1346
1555
 
1347
1556
  // src/fs/walk.ts
1348
- var MAX_LINKS2 = 40;
1557
+ var MAX_LINKS3 = 40;
1349
1558
  function parentOf4(path) {
1350
1559
  return path.slice(0, path.lastIndexOf("/")) || "/";
1351
1560
  }
@@ -1374,7 +1583,7 @@ async function realDir(fs, dir, budget = { hops: 0 }) {
1374
1583
  at = next2;
1375
1584
  continue;
1376
1585
  }
1377
- if (budget.hops >= MAX_LINKS2) throw fsError("ELOOP", dir);
1586
+ if (budget.hops >= MAX_LINKS3) throw fsError("ELOOP", dir);
1378
1587
  budget.hops++;
1379
1588
  queue.unshift(...link2.split("/").filter(Boolean));
1380
1589
  if (link2.startsWith("/")) at = "/";
@@ -1391,7 +1600,7 @@ async function realTarget(fs, path) {
1391
1600
  for (; ; ) {
1392
1601
  const link2 = await linkOf(fs, joinName(dir, name2));
1393
1602
  if (link2 === void 0) break;
1394
- if (budget.hops >= MAX_LINKS2) throw fsError("ELOOP", path);
1603
+ if (budget.hops >= MAX_LINKS3) throw fsError("ELOOP", path);
1395
1604
  budget.hops++;
1396
1605
  const target = link2.startsWith("/") ? link2 : joinName(dir, link2);
1397
1606
  dir = await realDir(fs, parentOf4(target), budget);
@@ -1404,7 +1613,7 @@ async function realTarget(fs, path) {
1404
1613
  var R_OK = 4;
1405
1614
  var W_OK = 2;
1406
1615
  var X_OK = 1;
1407
- var MAX_LINKS3 = 40;
1616
+ var MAX_LINKS4 = 40;
1408
1617
  var S_ISVTX = 512;
1409
1618
  function permOf(st, link2) {
1410
1619
  return {
@@ -1475,7 +1684,7 @@ async function walk(fs, cred, path, follow2) {
1475
1684
  const perm = await perms.at(next2);
1476
1685
  const last = queue.length === 0;
1477
1686
  if (perm?.link !== void 0 && (!last || follow2)) {
1478
- if (++hops > MAX_LINKS3) return { path: next2, perm: void 0 };
1687
+ if (++hops > MAX_LINKS4) return { path: next2, perm: void 0 };
1479
1688
  queue.unshift(...parts(perm.link));
1480
1689
  if (perm.link.startsWith("/")) at = "/";
1481
1690
  continue;
@@ -3422,7 +3631,7 @@ var UserDb = class {
3422
3631
  };
3423
3632
 
3424
3633
  // src/client/serve-client.ts
3425
- var MAX_LINKS4 = 40;
3634
+ var MAX_LINKS5 = 40;
3426
3635
  function act(session, req, signal) {
3427
3636
  if (req.action === "write") session.write(req.bytes);
3428
3637
  else if (req.action === "resize") session.resize(req.cols, req.rows);
@@ -3431,22 +3640,22 @@ function act(session, req, signal) {
3431
3640
  }
3432
3641
  async function realpath(fs, path) {
3433
3642
  let parts2 = fs.resolvePath("/", path).split("/").filter(Boolean);
3434
- let resolved = "";
3643
+ let resolved2 = "";
3435
3644
  for (let hops = 0; parts2.length > 0; ) {
3436
3645
  const [name2, ...rest2] = parts2;
3437
- const next2 = `${resolved}/${name2}`;
3646
+ const next2 = `${resolved2}/${name2}`;
3438
3647
  if (!(await fs.lstat(next2)).isSymbolicLink) {
3439
- resolved = next2;
3648
+ resolved2 = next2;
3440
3649
  parts2 = rest2;
3441
3650
  continue;
3442
3651
  }
3443
- if (++hops > MAX_LINKS4) throw fsError("ELOOP", path);
3652
+ if (++hops > MAX_LINKS5) throw fsError("ELOOP", path);
3444
3653
  const target = await fs.readlink(next2);
3445
- const base = target.startsWith("/") ? "/" : resolved || "/";
3654
+ const base = target.startsWith("/") ? "/" : resolved2 || "/";
3446
3655
  parts2 = [...fs.resolvePath(base, target).split("/").filter(Boolean), ...rest2];
3447
- resolved = "";
3656
+ resolved2 = "";
3448
3657
  }
3449
- return resolved || "/";
3658
+ return resolved2 || "/";
3450
3659
  }
3451
3660
  var CLIENT_CHECKS = {
3452
3661
  readFile: (g, at) => readable(g, "read", at(0)),
@@ -3751,7 +3960,7 @@ function serveClient(port, host) {
3751
3960
  if (!FS_METHODS.includes(req.method)) {
3752
3961
  throw new Error(`unknown file system call ${req.method}`);
3753
3962
  }
3754
- return fsCall(l.fs, req.method, req.args, ownerFor(l, self), guardFor(l, self));
3963
+ return fsCall(l.processFs, req.method, req.args, ownerFor(l, self), guardFor(l, self));
3755
3964
  case "watch":
3756
3965
  case "unwatch":
3757
3966
  return watchOp(l, req, watches, send, guardFor(l, self));
@@ -4970,11 +5179,11 @@ function ipv6(host) {
4970
5179
  const back = halves.length === 2 ? groups(halves[1]) : [];
4971
5180
  if (![...head, ...back].every((g) => /^[0-9A-Fa-f]{1,4}$/.test(g))) return void 0;
4972
5181
  const words2 = [...head, ...back].map((g) => Number.parseInt(g, 16));
4973
- const missing3 = 8 - words2.length - tail.length;
4974
- if (halves.length === 2 ? missing3 < 1 : missing3 !== 0) return void 0;
5182
+ const missing4 = 8 - words2.length - tail.length;
5183
+ if (halves.length === 2 ? missing4 < 1 : missing4 !== 0) return void 0;
4975
5184
  const all = [
4976
5185
  ...head.map((g) => Number.parseInt(g, 16)),
4977
- ...new Array(halves.length === 2 ? missing3 : 0).fill(0),
5186
+ ...new Array(halves.length === 2 ? missing4 : 0).fill(0),
4978
5187
  ...back.map((g) => Number.parseInt(g, 16)),
4979
5188
  ...tail
4980
5189
  ];
@@ -5763,7 +5972,7 @@ function parsePrefix(text) {
5763
5972
  if (length2 < 0 || length2 > max) return void 0;
5764
5973
  return { ...parsed2, length: length2 };
5765
5974
  }
5766
- function within2(prefix, addr) {
5975
+ function within3(prefix, addr) {
5767
5976
  if (prefix.v6 !== addr.v6) return false;
5768
5977
  const shift = BigInt((prefix.v6 ? 128 : 32) - prefix.length);
5769
5978
  return addr.bits >> shift === prefix.bits >> shift;
@@ -5803,9 +6012,9 @@ var Routes = class {
5803
6012
  if (!addr) return "unreachable";
5804
6013
  if (addr.v6 && addr.bits === 1n) return "loopback";
5805
6014
  if (!addr.v6 && addr.bits >> 24n === 127n) return "loopback";
5806
- if (RESERVED.some((p) => within2(p, addr))) return "reserved";
6015
+ if (RESERVED.some((p) => within3(p, addr))) return "reserved";
5807
6016
  if (addr.v6 && !this.ipv6) return "unreachable";
5808
- if (this.exit || this.table.some((p) => within2(p, addr))) return "uplink";
6017
+ if (this.exit || this.table.some((p) => within3(p, addr))) return "uplink";
5809
6018
  return "unreachable";
5810
6019
  }
5811
6020
  };
@@ -6419,8 +6628,8 @@ function binfmtOf(binfmts, path) {
6419
6628
  var S_IFDIR2 = 16384;
6420
6629
  var S_IFREG2 = 32768;
6421
6630
  var COMMAND = /^\/(?:usr\/)?bin\/([^/]+)$/;
6422
- var MAX_LINKS5 = 40;
6423
- function missing2(err) {
6631
+ var MAX_LINKS6 = 40;
6632
+ function missing3(err) {
6424
6633
  return err?.code === "ENOENT";
6425
6634
  }
6426
6635
  function synthetic(directory, path) {
@@ -6439,7 +6648,7 @@ function synthetic(directory, path) {
6439
6648
  }
6440
6649
  async function followLinks(fs, path) {
6441
6650
  let current = path;
6442
- for (let hops = 0; hops < MAX_LINKS5; hops++) {
6651
+ for (let hops = 0; hops < MAX_LINKS6; hops++) {
6443
6652
  const st = await fs.lstat(current).catch(() => void 0);
6444
6653
  if (!st?.isSymbolicLink) return current;
6445
6654
  const parent = current.slice(0, current.lastIndexOf("/")) || "/";
@@ -6486,7 +6695,7 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
6486
6695
  try {
6487
6696
  return await real(path);
6488
6697
  } catch (err) {
6489
- const found = missing2(err) ? await linked(path) : void 0;
6698
+ const found = missing3(err) ? await linked(path) : void 0;
6490
6699
  if (found) return found;
6491
6700
  throw err;
6492
6701
  }
@@ -6498,7 +6707,7 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
6498
6707
  try {
6499
6708
  return await fs.readFileBuffer(path);
6500
6709
  } catch (err) {
6501
- if (missing2(err) && (await linked(path))?.isFile) return new Uint8Array(0);
6710
+ if (missing3(err) && (await linked(path))?.isFile) return new Uint8Array(0);
6502
6711
  throw err;
6503
6712
  }
6504
6713
  },
@@ -6514,7 +6723,7 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
6514
6723
  const extra = await children2(path);
6515
6724
  if (!extra) return fs.readdir(path);
6516
6725
  const real = await fs.readdir(path).catch((err) => {
6517
- if (missing2(err)) return [];
6726
+ if (missing3(err)) return [];
6518
6727
  throw err;
6519
6728
  });
6520
6729
  return [.../* @__PURE__ */ new Set([...real, ...extra])].sort();
@@ -6523,7 +6732,7 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
6523
6732
  const extra = await children2(path);
6524
6733
  if (!extra) return fs.readdirStat(path);
6525
6734
  const real = await fs.readdirStat(path).catch((err) => {
6526
- if (missing2(err)) return [];
6735
+ if (missing3(err)) return [];
6527
6736
  throw err;
6528
6737
  });
6529
6738
  const seen = new Set(real.map(([name2]) => name2));
@@ -6607,7 +6816,7 @@ var UNLINKED_DIR = ".slicc-unlinked";
6607
6816
  function webLocks() {
6608
6817
  return globalThis.navigator?.locks;
6609
6818
  }
6610
- var within3 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
6819
+ var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
6611
6820
  var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
6612
6821
  var lockName = (instance) => `slicc-unlinked:${instance}`;
6613
6822
  var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
@@ -6707,7 +6916,7 @@ var UnlinkedKeeper = class {
6707
6916
  for (const holder of this.holders) {
6708
6917
  if (holder.owns(target)) continue;
6709
6918
  for (const path of holder.held()) {
6710
- if (!within3(path, target) || holder.isKept(path)) continue;
6919
+ if (!within4(path, target) || holder.isKept(path)) continue;
6711
6920
  held.set(path, [...held.get(path) ?? [], holder]);
6712
6921
  }
6713
6922
  }
@@ -7261,7 +7470,7 @@ function checkIds(options) {
7261
7470
  if (!ID.test(value) || Number(value) > MAX_ID2) throw errnoError("EINVAL", `${key}=${value}`);
7262
7471
  }
7263
7472
  }
7264
- var MAX_LINKS6 = 40;
7473
+ var MAX_LINKS7 = 40;
7265
7474
  var STALE_RETRIES = 3;
7266
7475
  var SETTLE_ROUNDS = 4;
7267
7476
  var DEFAULT_IO = 8 * 1024 * 1024;
@@ -7271,7 +7480,7 @@ function parseSize(text) {
7271
7480
  if (!match) throw errnoError("EINVAL", `not a size: ${text}`);
7272
7481
  return Math.floor(Number(match[1]) * UNITS[match[2].toLowerCase()]);
7273
7482
  }
7274
- var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
7483
+ var within5 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
7275
7484
  var HeldSet = class extends Set {
7276
7485
  idle;
7277
7486
  constructor(idle) {
@@ -7290,7 +7499,7 @@ var HeldSet = class extends Set {
7290
7499
  }
7291
7500
  };
7292
7501
  function heldUnder(held, target) {
7293
- for (const paths of held) for (const path of paths.keys()) if (within4(path, target)) return true;
7502
+ for (const paths of held) for (const path of paths.keys()) if (within5(path, target)) return true;
7294
7503
  return false;
7295
7504
  }
7296
7505
  var HeldPaths = class extends Map {
@@ -7401,24 +7610,24 @@ var HeldPaths = class extends Map {
7401
7610
  return this.owning === path;
7402
7611
  }
7403
7612
  revoke(prefix) {
7404
- for (const path of this.keys()) if (within4(path, prefix)) this.revoked.add(path);
7613
+ for (const path of this.keys()) if (within5(path, prefix)) this.revoked.add(path);
7405
7614
  }
7406
7615
  renamed(from, to) {
7407
7616
  for (const [path, count] of [...this]) {
7408
- if (!within4(path, from)) continue;
7617
+ if (!within5(path, from)) continue;
7409
7618
  const moved2 = to + path.slice(from.length);
7410
7619
  this.delete(path);
7411
7620
  this.set(moved2, (this.get(moved2) ?? 0) + count);
7412
7621
  if (this.revoked.delete(path)) this.revoked.add(moved2);
7413
7622
  }
7414
7623
  for (const [path, count] of [...this.opens]) {
7415
- if (!within4(path, from)) continue;
7624
+ if (!within5(path, from)) continue;
7416
7625
  const moved2 = to + path.slice(from.length);
7417
7626
  this.opens.delete(path);
7418
7627
  this.opens.set(moved2, (this.opens.get(moved2) ?? 0) + count);
7419
7628
  }
7420
7629
  for (const [id2, key] of this.handles) {
7421
- if (within4(key, from)) this.handles.set(id2, to + key.slice(from.length));
7630
+ if (within5(key, from)) this.handles.set(id2, to + key.slice(from.length));
7422
7631
  }
7423
7632
  }
7424
7633
  isRevoked(path) {
@@ -7525,7 +7734,7 @@ var MountTable = class {
7525
7734
  locate(path) {
7526
7735
  let best;
7527
7736
  for (const mount of this.mounts.values()) {
7528
- if (within4(path, mount.target) && (!best || mount.target.length > best.target.length))
7737
+ if (within5(path, mount.target) && (!best || mount.target.length > best.target.length))
7529
7738
  best = mount;
7530
7739
  }
7531
7740
  if (!best) return void 0;
@@ -7668,7 +7877,7 @@ var MountTable = class {
7668
7877
  async commitUnder(prefix) {
7669
7878
  const at = normalizePath(prefix);
7670
7879
  await Promise.all(
7671
- [...this.sessions.keys()].filter((p) => within4(p, at)).map((p) => this.commit(p))
7880
+ [...this.sessions.keys()].filter((p) => within5(p, at)).map((p) => this.commit(p))
7672
7881
  );
7673
7882
  }
7674
7883
  async commitOverlapping(real, offset, length2) {
@@ -7703,10 +7912,10 @@ var MountTable = class {
7703
7912
  }
7704
7913
  forget(mount, path) {
7705
7914
  mount.epoch++;
7706
- for (const key of [...mount.attrs.keys()]) if (within4(key, path)) mount.attrs.delete(key);
7915
+ for (const key of [...mount.attrs.keys()]) if (within5(key, path)) mount.attrs.delete(key);
7707
7916
  mount.lists.delete(path);
7708
7917
  mount.lists.delete(parentOf7(path));
7709
- for (const key of [...mount.lists.keys()]) if (within4(key, path)) mount.lists.delete(key);
7918
+ for (const key of [...mount.lists.keys()]) if (within5(key, path)) mount.lists.delete(key);
7710
7919
  }
7711
7920
  async getattr(mount, path, rel2, settled = false) {
7712
7921
  for (let round = 1; ; round++) {
@@ -7779,7 +7988,7 @@ var MountFs = class {
7779
7988
  throw err;
7780
7989
  }
7781
7990
  if (attr2.kind !== "symlink") return real;
7782
- if (hops >= MAX_LINKS6) throw errnoError("ELOOP", path);
7991
+ if (hops >= MAX_LINKS7) throw errnoError("ELOOP", path);
7783
7992
  const target = await found.mount.conn.call({ op: "readlink", path: found.rel });
7784
7993
  return this.follow(this.base.resolvePath(parentOf7(real), target), hops + 1, create);
7785
7994
  }
@@ -10813,7 +11022,7 @@ function isMissing(err) {
10813
11022
  if (err?.code === "ENOENT") return true;
10814
11023
  return err instanceof Error && err.message.startsWith("ENOENT");
10815
11024
  }
10816
- function within5(path, root) {
11025
+ function within6(path, root) {
10817
11026
  return path === root || path.startsWith(root.endsWith("/") ? root : `${root}/`);
10818
11027
  }
10819
11028
  var VfsNode = class {
@@ -11134,12 +11343,12 @@ var VfsNodes = class {
11134
11343
  return node;
11135
11344
  }
11136
11345
  holds(prefix) {
11137
- for (const path of this.byPath.keys()) if (within5(path, prefix)) return true;
11346
+ for (const path of this.byPath.keys()) if (within6(path, prefix)) return true;
11138
11347
  return false;
11139
11348
  }
11140
11349
  writes(prefix) {
11141
11350
  for (const [path, node] of this.byPath)
11142
- if (node.writers > 0 && within5(path, prefix)) return true;
11351
+ if (node.writers > 0 && within6(path, prefix)) return true;
11143
11352
  return false;
11144
11353
  }
11145
11354
  closed(node, writable2 = false) {
@@ -11183,11 +11392,11 @@ var VfsNodes = class {
11183
11392
  this.kept.delete(node);
11184
11393
  }
11185
11394
  async flush(path) {
11186
- for (const [p, node] of this.byPath) if (within5(p, path)) await node.serial(() => node.flush());
11395
+ for (const [p, node] of this.byPath) if (within6(p, path)) await node.serial(() => node.flush());
11187
11396
  }
11188
11397
  async unlinking(path) {
11189
11398
  for (const [held, node] of [...this.byPath]) {
11190
- if (!within5(held, path)) continue;
11399
+ if (!within6(held, path)) continue;
11191
11400
  await node.serial(async () => {
11192
11401
  await Promise.all([this.keepsOnDisk || node.keep(), node.limit()]);
11193
11402
  });
@@ -11195,14 +11404,14 @@ var VfsNodes = class {
11195
11404
  }
11196
11405
  revoke(prefix) {
11197
11406
  for (const [path, node] of [...this.byPath]) {
11198
- if (!within5(path, prefix)) continue;
11407
+ if (!within6(path, prefix)) continue;
11199
11408
  node.revoke();
11200
11409
  this.forget(node);
11201
11410
  }
11202
11411
  }
11203
11412
  unlinked(path) {
11204
11413
  for (const [held, node] of [...this.byPath]) {
11205
- if (!within5(held, path) || this.byPath.get(held) !== node) continue;
11414
+ if (!within6(held, path) || this.byPath.get(held) !== node) continue;
11206
11415
  if (!this.dropName(held, node)) this.orphan(node);
11207
11416
  }
11208
11417
  }
@@ -11210,8 +11419,8 @@ var VfsNodes = class {
11210
11419
  if (from === to) return;
11211
11420
  const moved2 = [];
11212
11421
  for (const [p, node] of [...this.byPath]) {
11213
- if (within5(p, from)) moved2.push([p, node]);
11214
- else if (within5(p, to) && this.byPath.get(p) === node && !this.dropName(p, node)) {
11422
+ if (within6(p, from)) moved2.push([p, node]);
11423
+ else if (within6(p, to) && this.byPath.get(p) === node && !this.dropName(p, node)) {
11215
11424
  this.orphan(node);
11216
11425
  }
11217
11426
  }
@@ -11757,6 +11966,7 @@ var WasmProcess = class {
11757
11966
  this.fds.installAt(fd, await this.procFile(req, path));
11758
11967
  return fd;
11759
11968
  }
11969
+ if (this.writesAt(req) && this.options.readonlyAt?.(path)) throw new KernelError("EROFS");
11760
11970
  const open = { ...req, path };
11761
11971
  if (req.exclusive) {
11762
11972
  await nodes.createExclusive(path, this.newOwner());
@@ -11802,6 +12012,9 @@ var WasmProcess = class {
11802
12012
  };
11803
12013
  return fs.exclusive ? fs.exclusive(first.path, look) : look();
11804
12014
  }
12015
+ writesAt(req) {
12016
+ return (req.flags & WRITE_ACCESS) !== 0 || req.truncate === true || req.create === true || req.exclusive === true;
12017
+ }
11805
12018
  async mayOpen(req) {
11806
12019
  const { guard: guard2 } = this.options;
11807
12020
  const inherited2 = req.orphan || req.contents !== void 0 || req.handle !== void 0;
@@ -12583,6 +12796,7 @@ function processOptions(opts, header3, hooks, created3) {
12583
12796
  ...inherited(opts),
12584
12797
  ...opts.owners ? { owners: true } : {},
12585
12798
  ...opts.sealed ? { sealed: opts.sealed } : {},
12799
+ ...opts.readonlyAt ? { readonlyAt: opts.readonlyAt } : {},
12586
12800
  ...opts.dac ? { guard: { dac: opts.dac, fs: opts.fs, created: created3 } } : {},
12587
12801
  ...opts.identity ? { identity: opts.identity } : {},
12588
12802
  ...opts.onSyscall ? { onSyscall: opts.onSyscall } : {},
@@ -15309,13 +15523,13 @@ function interpreter(head) {
15309
15523
  function expandDefaults(defaults, env) {
15310
15524
  const out = {};
15311
15525
  for (const [key, value] of Object.entries(defaults ?? {})) {
15312
- let missing3 = false;
15526
+ let missing4 = false;
15313
15527
  const expanded = value.replace(ENV_REFERENCE, (_, name2) => {
15314
15528
  const set2 = Object.hasOwn(env, name2) ? env[name2] : void 0;
15315
- if (set2 === void 0) missing3 = true;
15529
+ if (set2 === void 0) missing4 = true;
15316
15530
  return set2 ?? "";
15317
15531
  });
15318
- if (!missing3) out[key] = expanded;
15532
+ if (!missing4) out[key] = expanded;
15319
15533
  }
15320
15534
  return out;
15321
15535
  }
@@ -15362,6 +15576,8 @@ function concat5(chunks) {
15362
15576
  }
15363
15577
  var Launcher = class {
15364
15578
  fs;
15579
+ processFs;
15580
+ prefixes;
15365
15581
  base;
15366
15582
  createWorker;
15367
15583
  modulesDir;
@@ -15488,6 +15704,10 @@ var Launcher = class {
15488
15704
  }
15489
15705
  );
15490
15706
  this.fs = this.unlinked.wrap(keepingOpen(fs, this.nodes));
15707
+ if (options.protect?.writer) throw new Error(PROTECT_WRITER);
15708
+ this.prefixes = protectedPrefixes(options.protect?.paths ?? []);
15709
+ this.processFs = this.prefixes.length > 0 ? new ProtectedFs(this.prefixes).view(this.fs) : this.fs;
15710
+ options.fs.protect?.(this.prefixes);
15491
15711
  this.users = new UserDb(this.fs, this.owners);
15492
15712
  this.sudoer = new Sudo({
15493
15713
  fs: this.fs,
@@ -15838,7 +16058,8 @@ var Launcher = class {
15838
16058
  env: req.env,
15839
16059
  cwd: req.cwd,
15840
16060
  fds: req.fds,
15841
- fs: this.fs,
16061
+ fs: this.processFs,
16062
+ ...this.prefixes.length > 0 ? { readonlyAt: (path) => isProtected(this.prefixes, path) } : {},
15842
16063
  createWorker: this.createWorker,
15843
16064
  onError: req.report,
15844
16065
  spawner: this.spawner(pid, req.report),
@@ -16123,6 +16344,18 @@ var Launcher = class {
16123
16344
  const keyed = () => this.jobs.fromKeyboard(send);
16124
16345
  this.settling.deliver(sig, pgid, keyed, tty.reading ? void 0 : foreground);
16125
16346
  }
16347
+ async resolvePrefixes() {
16348
+ for (const prefix of [...this.prefixes]) {
16349
+ const real = await this.base.realpath?.(prefix, true).catch(() => void 0);
16350
+ if (real !== void 0 && !this.prefixes.includes(real)) this.prefixes.push(real);
16351
+ }
16352
+ }
16353
+ guardMount(target) {
16354
+ const at = normalizePath(target);
16355
+ if (isProtected(this.prefixes, at) || holdsProtected(this.prefixes, at)) {
16356
+ throw fsError("EROFS", at);
16357
+ }
16358
+ }
16126
16359
  umaskOf(pid) {
16127
16360
  return this.processes.get(pid)?.umask?.() ?? DEFAULT_UMASK;
16128
16361
  }
@@ -16257,6 +16490,7 @@ var Launcher = class {
16257
16490
  };
16258
16491
  }
16259
16492
  async mount(spec) {
16493
+ this.guardMount(spec.target);
16260
16494
  const fsa = spec.type === "fsa" && !spec.source.startsWith("fsa:");
16261
16495
  const entry = await this.mounts.mount(
16262
16496
  fsa ? { ...spec, source: `fsa:${crypto.randomUUID()}` } : spec,
@@ -16275,6 +16509,7 @@ var Launcher = class {
16275
16509
  [`/${UNLINKED_DIR}`, 448]
16276
16510
  ];
16277
16511
  for (const [path, mode] of layout) {
16512
+ if (isProtected(this.prefixes, normalizePath(path))) continue;
16278
16513
  const real = await this.realLayoutPath(path);
16279
16514
  const fresh2 = real !== void 0 && !await this.base.hasOwner?.(real);
16280
16515
  if (fresh2) await this.base.chown?.(real, { uid: 0, gid: 0, mode, follow: false });
@@ -16337,6 +16572,7 @@ var Launcher = class {
16337
16572
  }
16338
16573
  async processMount(pid, call, signal) {
16339
16574
  const spec = mountCall(call);
16575
+ this.guardMount(spec.target);
16340
16576
  await this.permit({ op: "mount", pid, ...spec, options: { ...spec.options } });
16341
16577
  await this.mountsOwn(
16342
16578
  this.credOf(pid),
@@ -16353,6 +16589,7 @@ var Launcher = class {
16353
16589
  }
16354
16590
  async umountAs(cred, target) {
16355
16591
  const at = normalizePath(target);
16592
+ this.guardMount(at);
16356
16593
  await this.mountsOwn(cred, at, async () => Number(this.mounts.mounted(at)?.options.uid));
16357
16594
  this.umount(at);
16358
16595
  }
@@ -16366,6 +16603,7 @@ var Launcher = class {
16366
16603
  async processUmount(pid, target, flags2) {
16367
16604
  const call = umountCall(target, flags2);
16368
16605
  let mounted;
16606
+ this.guardMount(call.target);
16369
16607
  for (; ; ) {
16370
16608
  mounted = this.mounts.mounted(call.target);
16371
16609
  await this.permit({
@@ -16380,6 +16618,7 @@ var Launcher = class {
16380
16618
  this.umount(call.target, call.detach);
16381
16619
  }
16382
16620
  async prepare() {
16621
+ await this.resolvePrefixes();
16383
16622
  for (const dir of SHARED_DIRS) await this.base.mkdir(dir, { recursive: true });
16384
16623
  if (this.owners) await this.base.chmod("/tmp", 1023);
16385
16624
  await this.provisionPnpmHome();
@@ -16436,7 +16675,7 @@ var Launcher = class {
16436
16675
  const start2 = dir ?? "/";
16437
16676
  const cwd = await physicalPathAsync(this.base, start2.startsWith("/") ? start2 : `/${start2}`);
16438
16677
  const [file = ""] = argv;
16439
- await this.base.mkdir(cwd, { recursive: true });
16678
+ if (!await this.base.exists(cwd)) await this.processFs.mkdir(cwd, { recursive: true });
16440
16679
  const planned = await this.plan(file, argv, cwd, scope);
16441
16680
  await this.mayExec(credOf(account), { file, cwd, pid: 0 }, planned);
16442
16681
  return { cwd, file, planned };
@@ -16677,6 +16916,7 @@ function hostOptions(req, port, sealed) {
16677
16916
  ...req.hostname !== void 0 ? { hostname: req.hostname } : {},
16678
16917
  ...req.users !== void 0 ? { users: req.users } : {},
16679
16918
  ...req.denials ? { onDenial: (denial) => port.postMessage({ denial }) } : {},
16919
+ ...req.protect ? { protect: { paths: [...req.protect.paths] } } : {},
16680
16920
  ...req.sealed ? { sealed } : {}
16681
16921
  };
16682
16922
  }