@ai-ecoverse/slicc-kernel 1.81.0 → 1.82.0

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.
@@ -360,10 +360,106 @@ async function chownAs(fs, cred, path, change) {
360
360
  });
361
361
  }
362
362
 
363
+ // src/fs/integrity.ts
364
+ var KERNEL_CALLER = { pid: 0, uid: 0 };
365
+ function integrityOptions(options) {
366
+ if (options && typeof options.lookup !== "function") {
367
+ throw new TypeError("protect.integrity.lookup must be a function");
368
+ }
369
+ integrityUnknown(options);
370
+ }
371
+ function integrityUnknown(options) {
372
+ const unknown = options?.unknown ?? "allow";
373
+ if (unknown !== "allow" && unknown !== "deny") {
374
+ throw new TypeError("protect.integrity.unknown must be 'allow' or 'deny'");
375
+ }
376
+ return unknown;
377
+ }
378
+ function hex(digest) {
379
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
380
+ }
381
+ async function sha256(bytes) {
382
+ return hex(await crypto.subtle.digest("SHA-256", bytes));
383
+ }
384
+ var Integrity = class {
385
+ prefixes;
386
+ root;
387
+ lookup;
388
+ unknown;
389
+ report;
390
+ unverified = /* @__PURE__ */ new Set();
391
+ constructor(prefixes2, root, options, report) {
392
+ this.prefixes = prefixes2;
393
+ this.root = root;
394
+ this.lookup = options.lookup;
395
+ this.unknown = integrityUnknown(options);
396
+ this.report = report;
397
+ }
398
+ async real(path) {
399
+ const at = normalizePath(path);
400
+ const real = await this.root.realpath?.(at, true).catch(() => void 0) ?? at;
401
+ return isProtected(this.prefixes, real) ? real : void 0;
402
+ }
403
+ async key(path) {
404
+ const real = await this.real(path);
405
+ if (real === void 0 || !this.root.version) return void 0;
406
+ return `${real}\0${await this.root.version(real)}`;
407
+ }
408
+ async read(path, caller, plain) {
409
+ return (await this.readKeyed(path, caller, plain)).bytes;
410
+ }
411
+ async readKeyed(path, caller, plain) {
412
+ const real = await this.real(path);
413
+ if (real === void 0) return { bytes: await plain() };
414
+ const read = this.root.readVersioned ? await this.root.readVersioned(real) : { bytes: await this.root.readFileBuffer(real), version: void 0 };
415
+ const { bytes } = read;
416
+ const keyed = read.version === void 0 ? { bytes } : { bytes, key: `${real}\0${read.version}` };
417
+ const actual = await sha256(bytes);
418
+ const denial = { pid: caller.pid, uid: caller.uid, path: real, actual };
419
+ const tagged = caller.tag !== void 0 ? { tag: caller.tag } : {};
420
+ let expected;
421
+ try {
422
+ expected = await this.expected(real);
423
+ } catch {
424
+ this.say({ ...denial, ...tagged, op: "exec-integrity", errno: "EACCES" });
425
+ throw fsError("EACCES", `integrity lookup failed: ${real}`);
426
+ }
427
+ if (expected === void 0) {
428
+ if (this.unknown === "allow") {
429
+ if (!this.unverified.has(real)) {
430
+ this.unverified.add(real);
431
+ this.say({ ...denial, ...tagged, op: "exec-unverified", errno: "ENODATA" });
432
+ }
433
+ return keyed;
434
+ }
435
+ } else if (expected === actual) {
436
+ return keyed;
437
+ }
438
+ const known = expected !== void 0 ? { expected } : {};
439
+ this.say({ ...denial, ...tagged, ...known, op: "exec-integrity", errno: "EACCES" });
440
+ throw fsError("EACCES", `integrity check failed: ${real}`);
441
+ }
442
+ async expected(path) {
443
+ const answer = await this.lookup(path);
444
+ return typeof answer === "string" ? answer.toLowerCase() : void 0;
445
+ }
446
+ say(event) {
447
+ try {
448
+ this.report(event);
449
+ } catch {
450
+ }
451
+ }
452
+ };
453
+
363
454
  // src/fs/protect.ts
364
455
  function protectInit(protection) {
365
456
  const writers = [...writerTags(protection)];
366
- return { paths: [...protection.paths], ...writers.length > 0 ? { writers } : {} };
457
+ const { integrity } = protection;
458
+ return {
459
+ paths: [...protection.paths],
460
+ ...writers.length > 0 ? { writers } : {},
461
+ ...integrity ? { integrity: { unknown: integrityUnknown(integrity) } } : {}
462
+ };
367
463
  }
368
464
  function writerTags(protection) {
369
465
  const writers = protection?.writers ?? [];
@@ -1220,6 +1316,29 @@ var OpfsFs = class {
1220
1316
  false
1221
1317
  );
1222
1318
  }
1319
+ async readVersioned(path) {
1320
+ const handle = await this.existing(path);
1321
+ return this.retrying(
1322
+ path,
1323
+ async () => {
1324
+ const file = await handle.getFile();
1325
+ const bytes = new Uint8Array(await file.arrayBuffer());
1326
+ return { bytes, version: `${file.lastModified}:${file.size}` };
1327
+ },
1328
+ false
1329
+ );
1330
+ }
1331
+ async version(path) {
1332
+ const handle = await this.existing(path);
1333
+ return this.retrying(
1334
+ path,
1335
+ async () => {
1336
+ const file = await handle.getFile();
1337
+ return `${file.lastModified}:${file.size}`;
1338
+ },
1339
+ false
1340
+ );
1341
+ }
1223
1342
  async pread(path, offset, length2) {
1224
1343
  const handle = await this.existing(path);
1225
1344
  return this.retrying(
@@ -4467,6 +4586,27 @@ function clipboardAsker(port) {
4467
4586
  return { hook, answer };
4468
4587
  }
4469
4588
 
4589
+ // src/integrity-relay.ts
4590
+ function integrityAsker(port) {
4591
+ const asks = /* @__PURE__ */ new Map();
4592
+ let next2 = 0;
4593
+ const lookup = (path) => {
4594
+ const id2 = ++next2;
4595
+ const waiting = Promise.withResolvers();
4596
+ asks.set(id2, waiting);
4597
+ port.postMessage({ integrityAsk: { id: id2, path } });
4598
+ return waiting.promise;
4599
+ };
4600
+ const answer = ({ id: id2, hash, error }) => {
4601
+ const waiting = asks.get(id2);
4602
+ if (!waiting) return;
4603
+ asks.delete(id2);
4604
+ if (error !== void 0) waiting.reject(new Error(error));
4605
+ else waiting.resolve(hash);
4606
+ };
4607
+ return { lookup, answer };
4608
+ }
4609
+
4470
4610
  // src/kernel/net/loopback-names.ts
4471
4611
  var HOST_LOOPBACK = "host.slicc.internal";
4472
4612
  var HOST_LOOPBACK_ADDRESS = [10, 0, 2, 2];
@@ -7093,7 +7233,7 @@ function owned(path, st) {
7093
7233
  function unchanged(a, b, bytes) {
7094
7234
  return a.ino === b.ino && a.mtime.getTime() === b.mtime.getTime() && a.size === b.size && bytes.length === b.size;
7095
7235
  }
7096
- async function sha256(bytes) {
7236
+ async function sha2562(bytes) {
7097
7237
  const digest = new Uint8Array(
7098
7238
  await crypto.subtle.digest("SHA-256", bytes)
7099
7239
  );
@@ -7149,7 +7289,7 @@ async function attest(fs, asked, o) {
7149
7289
  const bytes = (await fs.readFileBuffer(path)).slice();
7150
7290
  const after = await fs.lstat(path);
7151
7291
  if (unchanged(before, after, bytes)) {
7152
- return attested(path, after, bytes, await sha256(bytes), o, parents);
7292
+ return attested(path, after, bytes, await sha2562(bytes), o, parents);
7153
7293
  }
7154
7294
  if (tries >= 1) refuse("EAGAIN", path, "changed while it was read");
7155
7295
  }
@@ -16453,6 +16593,13 @@ var Launcher = class {
16453
16593
  unwatchScopes = () => void 0;
16454
16594
  warned = /* @__PURE__ */ new Set();
16455
16595
  compiled = /* @__PURE__ */ new Map();
16596
+ texts = /* @__PURE__ */ new Map();
16597
+ integrity;
16598
+ scannedFs;
16599
+ get scanned() {
16600
+ this.scannedFs ??= this.verifiedFs(this.base);
16601
+ return this.scannedFs;
16602
+ }
16456
16603
  processes = /* @__PURE__ */ new Map();
16457
16604
  zombies = /* @__PURE__ */ new Set();
16458
16605
  orphans = /* @__PURE__ */ new Set();
@@ -16556,7 +16703,7 @@ var Launcher = class {
16556
16703
  if (options.uplink) this.net.useUplink({ uplink: options.uplink, routes: this.routes });
16557
16704
  enableNetwork(this.net, {
16558
16705
  transport: this.transport,
16559
- engine: kernelTlsEngine(packageTlsEngine(options.fs, this.modulesDir)),
16706
+ engine: kernelTlsEngine(packageTlsEngine(this.verifiedFs(options.fs), this.modulesDir)),
16560
16707
  ca: this.ca
16561
16708
  });
16562
16709
  enableCdp(this.net, this.cdp);
@@ -16591,6 +16738,16 @@ var Launcher = class {
16591
16738
  this.prefixes = protectedPrefixes(options.protect?.paths ?? []);
16592
16739
  this.processFs = this.prefixes.length > 0 ? new ProtectedFs(this.prefixes).view(this.fs) : this.fs;
16593
16740
  options.fs.protect?.(this.prefixes);
16741
+ const integrity = options.protect?.integrity;
16742
+ integrityOptions(integrity);
16743
+ if (integrity && this.prefixes.length > 0) {
16744
+ this.integrity = new Integrity(
16745
+ this.prefixes,
16746
+ options.fs,
16747
+ integrity,
16748
+ (event) => options.onDenial?.(event)
16749
+ );
16750
+ }
16594
16751
  this.users = new UserDb(this.fs, this.owners);
16595
16752
  this.sudoer = new Sudo({
16596
16753
  fs: this.fs,
@@ -16636,7 +16793,7 @@ var Launcher = class {
16636
16793
  commands(scope = "") {
16637
16794
  let found = this.catalogs.get(scope);
16638
16795
  if (!found) {
16639
- found = this.roots(scope).then((roots) => scanCommands(this.base, roots)).then((commands) => commands.set(SUDO_COMMAND.name, SUDO_COMMAND));
16796
+ found = this.roots(scope).then((roots) => scanCommands(this.scanned, roots)).then((commands) => commands.set(SUDO_COMMAND.name, SUDO_COMMAND));
16640
16797
  this.catalogs.set(scope, found);
16641
16798
  }
16642
16799
  return found;
@@ -16644,7 +16801,7 @@ var Launcher = class {
16644
16801
  ownCommands(scope) {
16645
16802
  let found = this.ownCatalogs.get(scope);
16646
16803
  if (!found) {
16647
- found = pnpmGlobalRoots(this.base, scope).then((roots) => scanCommands(this.base, roots)).then((commands) => new Set(commands.keys()));
16804
+ found = pnpmGlobalRoots(this.base, scope).then((roots) => scanCommands(this.scanned, roots)).then((commands) => new Set(commands.keys()));
16648
16805
  this.ownCatalogs.set(scope, found);
16649
16806
  }
16650
16807
  return found;
@@ -16653,7 +16810,7 @@ var Launcher = class {
16653
16810
  let found = this.binfmts.get(scope);
16654
16811
  if (!found) {
16655
16812
  found = this.roots(scope).then(
16656
- (roots) => scanBinfmts(this.base, roots, (message3) => {
16813
+ (roots) => scanBinfmts(this.scanned, roots, (message3) => {
16657
16814
  if (this.warned.has(message3)) return;
16658
16815
  this.warned.add(message3);
16659
16816
  console.warn(message3);
@@ -16772,10 +16929,16 @@ var Launcher = class {
16772
16929
  }
16773
16930
  const inner = await this.interpreted(interp, [interp, ...passed], cwd, scope, depth + 1);
16774
16931
  const via = [interp, ...inner?.via ?? []];
16775
- return inner && { ...inner, target: withScriptEnv(inner.target, command), via };
16932
+ const sources = [script, ...inner?.sources ?? []];
16933
+ return inner && { ...inner, target: withScriptEnv(inner.target, command), via, sources };
16776
16934
  }
16777
16935
  const target = withScriptEnv(found, command);
16778
- return { target, args: [...target.prefix ?? [], ...passed], via: [interp] };
16936
+ return {
16937
+ target,
16938
+ args: [...target.prefix ?? [], ...passed],
16939
+ via: [interp],
16940
+ sources: [script]
16941
+ };
16779
16942
  }
16780
16943
  async plan(file, argv, cwd, scope, argv0) {
16781
16944
  const found = await this.resolve(file, argv[0] ?? file, cwd, scope);
@@ -16807,7 +16970,7 @@ var Launcher = class {
16807
16970
  }
16808
16971
  async binfmtManifests(scripts) {
16809
16972
  const sources = /* @__PURE__ */ new Map();
16810
- const binfmts = await scanBinfmts(this.base, await this.roots(), void 0, sources);
16973
+ const binfmts = await scanBinfmts(this.scanned, await this.roots(), void 0, sources);
16811
16974
  const manifests = [];
16812
16975
  for (const script of scripts) {
16813
16976
  const head = await this.head(script);
@@ -16842,25 +17005,72 @@ var Launcher = class {
16842
17005
  if (!head || head[0] === 35 && head[1] === 33) return "ENOENT";
16843
17006
  return binfmtOf(await this.interpreters(scope), path) ? "ENOENT" : "ENOEXEC";
16844
17007
  }
16845
- module(path) {
16846
- return this.base.stat(path).then((st) => {
16847
- const key = `${path}:${st.size}:${st.mtime.getTime()}`;
16848
- let module = this.compiled.get(key);
16849
- if (!module) {
16850
- module = this.base.readFileBuffer(path).then(async (bytes) => {
16851
- const memory = importedMemory(bytes);
16852
- const foreign = foreignImports(bytes);
16853
- const compiled = await WebAssembly.compile(bytes);
16854
- return {
16855
- module: compiled,
16856
- ...memory ? { memory } : {},
16857
- foreign
16858
- };
16859
- });
16860
- this.compiled.set(key, module);
16861
- module.catch(() => this.compiled.delete(key));
16862
- }
16863
- return module;
17008
+ verifiedFs(fs) {
17009
+ const read = (path) => this.verified(path, KERNEL_CALLER, fs);
17010
+ return Object.assign(Object.create(fs), {
17011
+ readFileBuffer: read,
17012
+ readFile: (path) => this.integrity ? this.text(path, KERNEL_CALLER) : fs.readFile(path)
17013
+ });
17014
+ }
17015
+ async cacheKey(path) {
17016
+ const verified = await this.integrity?.key(path);
17017
+ if (verified !== void 0) return verified;
17018
+ const st = await this.base.stat(path);
17019
+ return `${path}:${st.size}:${st.mtime.getTime()}`;
17020
+ }
17021
+ verified(path, caller, fs = this.base) {
17022
+ const plain = () => fs.readFileBuffer(path);
17023
+ return this.integrity ? this.integrity.read(path, caller, plain) : plain();
17024
+ }
17025
+ keyed(path, caller) {
17026
+ const plain = async () => this.base.readFileBuffer(path);
17027
+ return this.integrity ? this.integrity.readKeyed(path, caller, plain) : plain().then((bytes) => ({ bytes }));
17028
+ }
17029
+ cached(cache, key, load) {
17030
+ const hit = cache.get(key);
17031
+ if (hit !== void 0) return hit;
17032
+ const loading2 = load();
17033
+ const found = loading2.then(({ value }) => value);
17034
+ cache.set(key, found);
17035
+ loading2.then(
17036
+ (done4) => {
17037
+ if (done4.key === key || done4.key === void 0 && !key.includes("\0")) return;
17038
+ if (cache.get(key) === found) cache.delete(key);
17039
+ if (done4.key !== void 0) cache.set(done4.key, found);
17040
+ },
17041
+ () => cache.delete(key)
17042
+ );
17043
+ return found;
17044
+ }
17045
+ async module(path, caller) {
17046
+ const key = await this.cacheKey(path);
17047
+ return this.cached(this.compiled, key, async () => {
17048
+ const { bytes, key: read } = await this.keyed(path, caller);
17049
+ const memory = importedMemory(bytes);
17050
+ const foreign = foreignImports(bytes);
17051
+ const compiled = await WebAssembly.compile(bytes);
17052
+ return { value: { module: compiled, ...memory ? { memory } : {}, foreign }, key: read };
17053
+ });
17054
+ }
17055
+ async checkedSources(planned, caller) {
17056
+ const { target } = planned;
17057
+ const files = [target.wasm, target.glue, target.imports, ...planned.sources ?? []];
17058
+ const manifests = /* @__PURE__ */ new Set();
17059
+ for (const file of files) {
17060
+ const root = file === void 0 ? void 0 : PACKAGE_ROOT.exec(file)?.[1];
17061
+ if (root !== void 0) manifests.add(`${root}/package.json`);
17062
+ }
17063
+ for (const source of planned.sources ?? []) await this.text(source, caller);
17064
+ for (const manifest of manifests) {
17065
+ if (await this.base.exists(manifest)) await this.text(manifest, caller);
17066
+ }
17067
+ }
17068
+ async text(path, caller) {
17069
+ if (!this.integrity) return this.base.readFile(path);
17070
+ const key = await this.cacheKey(path);
17071
+ return this.cached(this.texts, key, async () => {
17072
+ const { bytes, key: read } = await this.keyed(path, caller);
17073
+ return { value: new TextDecoder().decode(bytes), key: read };
16864
17074
  });
16865
17075
  }
16866
17076
  async names(path, module) {
@@ -16872,23 +17082,23 @@ var Launcher = class {
16872
17082
  const optional = `${root}-names${beside.slice(root.length)}`;
16873
17083
  return await this.base.exists(optional) ? optional : void 0;
16874
17084
  }
16875
- async program(target, env) {
17085
+ async program(target, env, caller) {
16876
17086
  if (target.abi === "js" && target.wasm === SUDO_PATH) {
16877
17087
  return { abi: "js", glue: SUDO_SOURCE, path: SUDO_PATH };
16878
17088
  }
16879
17089
  if (target.abi === "js") {
16880
- return { abi: "js", glue: await this.base.readFile(target.wasm), path: target.wasm };
17090
+ return { abi: "js", glue: await this.text(target.wasm, caller), path: target.wasm };
16881
17091
  }
16882
17092
  if (target.abi !== "wasi") {
16883
17093
  const [glue, { module: module2 }] = await Promise.all([
16884
- this.base.readFile(target.glue),
16885
- this.module(target.wasm)
17094
+ this.text(target.glue, caller),
17095
+ this.module(target.wasm, caller)
16886
17096
  ]);
16887
17097
  return { glue, module: module2 };
16888
17098
  }
16889
- const { module, memory, foreign } = await this.module(target.wasm);
17099
+ const { module, memory, foreign } = await this.module(target.wasm, caller);
16890
17100
  const names = env.SLICC_WASM_BACKTRACE === "1" ? await this.names(target.wasm, module) : void 0;
16891
- const imports = target.imports ? await this.base.readFile(target.imports) : void 0;
17101
+ const imports = target.imports ? await this.text(target.imports, caller) : void 0;
16892
17102
  return {
16893
17103
  abi: "wasi",
16894
17104
  glue: "",
@@ -16910,7 +17120,9 @@ var Launcher = class {
16910
17120
  env.HOSTNAME = this.hostname;
16911
17121
  let program;
16912
17122
  try {
16913
- program = await this.program(target, env);
17123
+ const caller = { pid: req.ppid ?? 0, uid: req.cred.euid, tag: req.tag };
17124
+ if (this.integrity) await this.checkedSources(planned, caller);
17125
+ program = await this.program(target, env, caller);
16914
17126
  } catch (err) {
16915
17127
  await req.fds.closeAll();
16916
17128
  throw err;
@@ -17402,11 +17614,14 @@ var Launcher = class {
17402
17614
  }
17403
17615
  };
17404
17616
  }
17405
- const module = (await scanFilesystems(this.base, await this.roots())).get(type);
17617
+ const module = (await scanFilesystems(this.scanned, await this.roots())).get(type);
17406
17618
  if (!module) throw fsError("ENODEV", `unknown file system type ${type}`);
17407
17619
  if (!this.createDriverWorker)
17408
17620
  throw fsError("ENODEV", `this kernel cannot start ${type} drivers`);
17409
- const code = await this.base.readFile(module);
17621
+ const root = PACKAGE_ROOT.exec(module)?.[1];
17622
+ if (this.integrity && root !== void 0)
17623
+ await this.text(`${root}/package.json`, KERNEL_CALLER);
17624
+ const code = new TextDecoder().decode(await this.verified(module, KERNEL_CALLER));
17410
17625
  const worker = this.createDriverWorker();
17411
17626
  const driver = new MessageChannel();
17412
17627
  const client = new MessageChannel();
@@ -17977,16 +18192,20 @@ function usersOp(l, req) {
17977
18192
  if (req.op === "groups-remove-member") return l.users.removeMember(req.group, req.user);
17978
18193
  return l.users.list();
17979
18194
  }
17980
- function hostOptions(req, port, sealed, clipboard) {
18195
+ function hostOptions(req, port, sealed, clipboard, lookup) {
17981
18196
  return {
17982
18197
  ...req.hostname !== void 0 ? { hostname: req.hostname } : {},
17983
18198
  ...req.users !== void 0 ? { users: req.users } : {},
17984
18199
  ...req.denials ? { onDenial: (denial) => port.postMessage({ denial }) } : {},
17985
- ...req.protect ? { protect: protectInit(req.protect) } : {},
18200
+ ...req.protect ? { protect: hostProtect(req.protect, lookup) } : {},
17986
18201
  ...req.sealed ? { sealed } : {},
17987
18202
  ...req.clipboard ? { clipboard } : {}
17988
18203
  };
17989
18204
  }
18205
+ function hostProtect(init, lookup) {
18206
+ const { integrity, ...rest2 } = protectInit(init);
18207
+ return { ...rest2, ...integrity ? { integrity: { ...integrity, lookup } } : {} };
18208
+ }
17990
18209
  function policyAsker(port) {
17991
18210
  const policies = /* @__PURE__ */ new Map();
17992
18211
  let nextPolicy = 0;
@@ -18015,6 +18234,7 @@ function serveKernel(port, deps) {
18015
18234
  const clients = /* @__PURE__ */ new Set();
18016
18235
  const sealed = sealedAsker(port);
18017
18236
  const clipboard = clipboardAsker(port);
18237
+ const integrity = integrityAsker(port);
18018
18238
  const approvals = sudoApprovals(port);
18019
18239
  const locks = deps.locks === null ? void 0 : deps.locks ?? locksOf();
18020
18240
  let holding;
@@ -18081,7 +18301,7 @@ function serveKernel(port, deps) {
18081
18301
  ...deps.createDriverWorker ? { createDriverWorker: deps.createDriverWorker } : {},
18082
18302
  ...req.modules ? { modules: req.modules } : {},
18083
18303
  ...req.env ? { env: req.env } : {},
18084
- ...hostOptions(req, port, sealed.hook, clipboard.hook),
18304
+ ...hostOptions(req, port, sealed.hook, clipboard.hook, integrity.lookup),
18085
18305
  ...req.approve ? { approve: approvals.ask } : {},
18086
18306
  ...net,
18087
18307
  ...media ? { media } : {},
@@ -18151,6 +18371,8 @@ function serveKernel(port, deps) {
18151
18371
  if (sealedReply) return sealed.answer(sealedReply);
18152
18372
  const clipboardReply = event.data.clipboardReply;
18153
18373
  if (clipboardReply) return clipboard.answer(clipboardReply);
18374
+ const integrityReply = event.data.integrityReply;
18375
+ if (integrityReply) return integrity.answer(integrityReply);
18154
18376
  const grant = event.data.hostfsGrant;
18155
18377
  if (grant) return granted2(grant);
18156
18378
  if (allowed(event.data)) return;