@ai-ecoverse/slicc-kernel 1.80.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(
@@ -2150,6 +2269,7 @@ function id(value) {
2150
2269
  return value;
2151
2270
  }
2152
2271
  function changeCred(cur, change) {
2272
+ if (typeof change !== "object" || change === null) throw fsError("EINVAL", "not a change");
2153
2273
  const privileged = cur.euid === 0;
2154
2274
  const next2 = copyCred(cur);
2155
2275
  for (const keys of [UIDS, GIDS]) {
@@ -4466,6 +4586,27 @@ function clipboardAsker(port) {
4466
4586
  return { hook, answer };
4467
4587
  }
4468
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
+
4469
4610
  // src/kernel/net/loopback-names.ts
4470
4611
  var HOST_LOOPBACK = "host.slicc.internal";
4471
4612
  var HOST_LOOPBACK_ADDRESS = [10, 0, 2, 2];
@@ -7092,7 +7233,7 @@ function owned(path, st) {
7092
7233
  function unchanged(a, b, bytes) {
7093
7234
  return a.ino === b.ino && a.mtime.getTime() === b.mtime.getTime() && a.size === b.size && bytes.length === b.size;
7094
7235
  }
7095
- async function sha256(bytes) {
7236
+ async function sha2562(bytes) {
7096
7237
  const digest = new Uint8Array(
7097
7238
  await crypto.subtle.digest("SHA-256", bytes)
7098
7239
  );
@@ -7148,7 +7289,7 @@ async function attest(fs, asked, o) {
7148
7289
  const bytes = (await fs.readFileBuffer(path)).slice();
7149
7290
  const after = await fs.lstat(path);
7150
7291
  if (unchanged(before, after, bytes)) {
7151
- return attested(path, after, bytes, await sha256(bytes), o, parents);
7292
+ return attested(path, after, bytes, await sha2562(bytes), o, parents);
7152
7293
  }
7153
7294
  if (tries >= 1) refuse("EAGAIN", path, "changed while it was read");
7154
7295
  }
@@ -16452,6 +16593,13 @@ var Launcher = class {
16452
16593
  unwatchScopes = () => void 0;
16453
16594
  warned = /* @__PURE__ */ new Set();
16454
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
+ }
16455
16603
  processes = /* @__PURE__ */ new Map();
16456
16604
  zombies = /* @__PURE__ */ new Set();
16457
16605
  orphans = /* @__PURE__ */ new Set();
@@ -16555,7 +16703,7 @@ var Launcher = class {
16555
16703
  if (options.uplink) this.net.useUplink({ uplink: options.uplink, routes: this.routes });
16556
16704
  enableNetwork(this.net, {
16557
16705
  transport: this.transport,
16558
- engine: kernelTlsEngine(packageTlsEngine(options.fs, this.modulesDir)),
16706
+ engine: kernelTlsEngine(packageTlsEngine(this.verifiedFs(options.fs), this.modulesDir)),
16559
16707
  ca: this.ca
16560
16708
  });
16561
16709
  enableCdp(this.net, this.cdp);
@@ -16590,6 +16738,16 @@ var Launcher = class {
16590
16738
  this.prefixes = protectedPrefixes(options.protect?.paths ?? []);
16591
16739
  this.processFs = this.prefixes.length > 0 ? new ProtectedFs(this.prefixes).view(this.fs) : this.fs;
16592
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
+ }
16593
16751
  this.users = new UserDb(this.fs, this.owners);
16594
16752
  this.sudoer = new Sudo({
16595
16753
  fs: this.fs,
@@ -16635,7 +16793,7 @@ var Launcher = class {
16635
16793
  commands(scope = "") {
16636
16794
  let found = this.catalogs.get(scope);
16637
16795
  if (!found) {
16638
- 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));
16639
16797
  this.catalogs.set(scope, found);
16640
16798
  }
16641
16799
  return found;
@@ -16643,7 +16801,7 @@ var Launcher = class {
16643
16801
  ownCommands(scope) {
16644
16802
  let found = this.ownCatalogs.get(scope);
16645
16803
  if (!found) {
16646
- 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()));
16647
16805
  this.ownCatalogs.set(scope, found);
16648
16806
  }
16649
16807
  return found;
@@ -16652,7 +16810,7 @@ var Launcher = class {
16652
16810
  let found = this.binfmts.get(scope);
16653
16811
  if (!found) {
16654
16812
  found = this.roots(scope).then(
16655
- (roots) => scanBinfmts(this.base, roots, (message3) => {
16813
+ (roots) => scanBinfmts(this.scanned, roots, (message3) => {
16656
16814
  if (this.warned.has(message3)) return;
16657
16815
  this.warned.add(message3);
16658
16816
  console.warn(message3);
@@ -16771,10 +16929,16 @@ var Launcher = class {
16771
16929
  }
16772
16930
  const inner = await this.interpreted(interp, [interp, ...passed], cwd, scope, depth + 1);
16773
16931
  const via = [interp, ...inner?.via ?? []];
16774
- 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 };
16775
16934
  }
16776
16935
  const target = withScriptEnv(found, command);
16777
- return { target, args: [...target.prefix ?? [], ...passed], via: [interp] };
16936
+ return {
16937
+ target,
16938
+ args: [...target.prefix ?? [], ...passed],
16939
+ via: [interp],
16940
+ sources: [script]
16941
+ };
16778
16942
  }
16779
16943
  async plan(file, argv, cwd, scope, argv0) {
16780
16944
  const found = await this.resolve(file, argv[0] ?? file, cwd, scope);
@@ -16806,7 +16970,7 @@ var Launcher = class {
16806
16970
  }
16807
16971
  async binfmtManifests(scripts) {
16808
16972
  const sources = /* @__PURE__ */ new Map();
16809
- 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);
16810
16974
  const manifests = [];
16811
16975
  for (const script of scripts) {
16812
16976
  const head = await this.head(script);
@@ -16841,25 +17005,72 @@ var Launcher = class {
16841
17005
  if (!head || head[0] === 35 && head[1] === 33) return "ENOENT";
16842
17006
  return binfmtOf(await this.interpreters(scope), path) ? "ENOENT" : "ENOEXEC";
16843
17007
  }
16844
- module(path) {
16845
- return this.base.stat(path).then((st) => {
16846
- const key = `${path}:${st.size}:${st.mtime.getTime()}`;
16847
- let module = this.compiled.get(key);
16848
- if (!module) {
16849
- module = this.base.readFileBuffer(path).then(async (bytes) => {
16850
- const memory = importedMemory(bytes);
16851
- const foreign = foreignImports(bytes);
16852
- const compiled = await WebAssembly.compile(bytes);
16853
- return {
16854
- module: compiled,
16855
- ...memory ? { memory } : {},
16856
- foreign
16857
- };
16858
- });
16859
- this.compiled.set(key, module);
16860
- module.catch(() => this.compiled.delete(key));
16861
- }
16862
- 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 };
16863
17074
  });
16864
17075
  }
16865
17076
  async names(path, module) {
@@ -16871,23 +17082,23 @@ var Launcher = class {
16871
17082
  const optional = `${root}-names${beside.slice(root.length)}`;
16872
17083
  return await this.base.exists(optional) ? optional : void 0;
16873
17084
  }
16874
- async program(target, env) {
17085
+ async program(target, env, caller) {
16875
17086
  if (target.abi === "js" && target.wasm === SUDO_PATH) {
16876
17087
  return { abi: "js", glue: SUDO_SOURCE, path: SUDO_PATH };
16877
17088
  }
16878
17089
  if (target.abi === "js") {
16879
- 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 };
16880
17091
  }
16881
17092
  if (target.abi !== "wasi") {
16882
17093
  const [glue, { module: module2 }] = await Promise.all([
16883
- this.base.readFile(target.glue),
16884
- this.module(target.wasm)
17094
+ this.text(target.glue, caller),
17095
+ this.module(target.wasm, caller)
16885
17096
  ]);
16886
17097
  return { glue, module: module2 };
16887
17098
  }
16888
- const { module, memory, foreign } = await this.module(target.wasm);
17099
+ const { module, memory, foreign } = await this.module(target.wasm, caller);
16889
17100
  const names = env.SLICC_WASM_BACKTRACE === "1" ? await this.names(target.wasm, module) : void 0;
16890
- 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;
16891
17102
  return {
16892
17103
  abi: "wasi",
16893
17104
  glue: "",
@@ -16909,7 +17120,9 @@ var Launcher = class {
16909
17120
  env.HOSTNAME = this.hostname;
16910
17121
  let program;
16911
17122
  try {
16912
- 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);
16913
17126
  } catch (err) {
16914
17127
  await req.fds.closeAll();
16915
17128
  throw err;
@@ -17401,11 +17614,14 @@ var Launcher = class {
17401
17614
  }
17402
17615
  };
17403
17616
  }
17404
- const module = (await scanFilesystems(this.base, await this.roots())).get(type);
17617
+ const module = (await scanFilesystems(this.scanned, await this.roots())).get(type);
17405
17618
  if (!module) throw fsError("ENODEV", `unknown file system type ${type}`);
17406
17619
  if (!this.createDriverWorker)
17407
17620
  throw fsError("ENODEV", `this kernel cannot start ${type} drivers`);
17408
- 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));
17409
17625
  const worker = this.createDriverWorker();
17410
17626
  const driver = new MessageChannel();
17411
17627
  const client = new MessageChannel();
@@ -17976,16 +18192,20 @@ function usersOp(l, req) {
17976
18192
  if (req.op === "groups-remove-member") return l.users.removeMember(req.group, req.user);
17977
18193
  return l.users.list();
17978
18194
  }
17979
- function hostOptions(req, port, sealed, clipboard) {
18195
+ function hostOptions(req, port, sealed, clipboard, lookup) {
17980
18196
  return {
17981
18197
  ...req.hostname !== void 0 ? { hostname: req.hostname } : {},
17982
18198
  ...req.users !== void 0 ? { users: req.users } : {},
17983
18199
  ...req.denials ? { onDenial: (denial) => port.postMessage({ denial }) } : {},
17984
- ...req.protect ? { protect: protectInit(req.protect) } : {},
18200
+ ...req.protect ? { protect: hostProtect(req.protect, lookup) } : {},
17985
18201
  ...req.sealed ? { sealed } : {},
17986
18202
  ...req.clipboard ? { clipboard } : {}
17987
18203
  };
17988
18204
  }
18205
+ function hostProtect(init, lookup) {
18206
+ const { integrity, ...rest2 } = protectInit(init);
18207
+ return { ...rest2, ...integrity ? { integrity: { ...integrity, lookup } } : {} };
18208
+ }
17989
18209
  function policyAsker(port) {
17990
18210
  const policies = /* @__PURE__ */ new Map();
17991
18211
  let nextPolicy = 0;
@@ -18014,6 +18234,7 @@ function serveKernel(port, deps) {
18014
18234
  const clients = /* @__PURE__ */ new Set();
18015
18235
  const sealed = sealedAsker(port);
18016
18236
  const clipboard = clipboardAsker(port);
18237
+ const integrity = integrityAsker(port);
18017
18238
  const approvals = sudoApprovals(port);
18018
18239
  const locks = deps.locks === null ? void 0 : deps.locks ?? locksOf();
18019
18240
  let holding;
@@ -18080,7 +18301,7 @@ function serveKernel(port, deps) {
18080
18301
  ...deps.createDriverWorker ? { createDriverWorker: deps.createDriverWorker } : {},
18081
18302
  ...req.modules ? { modules: req.modules } : {},
18082
18303
  ...req.env ? { env: req.env } : {},
18083
- ...hostOptions(req, port, sealed.hook, clipboard.hook),
18304
+ ...hostOptions(req, port, sealed.hook, clipboard.hook, integrity.lookup),
18084
18305
  ...req.approve ? { approve: approvals.ask } : {},
18085
18306
  ...net,
18086
18307
  ...media ? { media } : {},
@@ -18150,6 +18371,8 @@ function serveKernel(port, deps) {
18150
18371
  if (sealedReply) return sealed.answer(sealedReply);
18151
18372
  const clipboardReply = event.data.clipboardReply;
18152
18373
  if (clipboardReply) return clipboard.answer(clipboardReply);
18374
+ const integrityReply = event.data.integrityReply;
18375
+ if (integrityReply) return integrity.answer(integrityReply);
18153
18376
  const grant = event.data.hostfsGrant;
18154
18377
  if (grant) return granted2(grant);
18155
18378
  if (allowed(event.data)) return;