@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.
package/README.md CHANGED
@@ -659,6 +659,13 @@ A character device (`0666`, major 10, minor 251), there in every `users` mode, t
659
659
  - **Clients:** a client's (`kernel.connect`) file calls, mounts and unmounts see the paths read-only, as its processes do.
660
660
  - **Not affected:** the page's own writes to OPFS.
661
661
  - **Writers:** `protect: { paths, writers: ['bios-installer'] }` names process tags whose processes may change the paths, such as the embedder's own installs (`kernel.run(['pnpm', 'install'], { tag: 'bios-installer' })`). A tag matches exactly, with no prefix or case folding. A process gets its tag only from the embedder, through `kernel.run`, `kernel.openTerminal` or `kernel.connect({ tag })`, and passes it to its children, forks and `exec`s; a program or a client can't choose one. Writers work on their own view of the root file system, so a file a writer changes is seen by other processes as between two tabs: close-to-open, at their next open after the writer closes it.
662
+ - **Integrity:** `protect: { paths, integrity: { lookup, unknown } }` also checks what a program is made of before it runs. That covers a wasm module, an Emscripten program's glue, a WASI program's imports module, a JS program's module, the `package.json` of each package under a path (the kernel builds its commands, `binfmt` suffixes and drivers only from manifests that pass, so a package whose manifest changed has no commands, 127, and no drivers, `ENODEV`), a script started through its `#!` line or a `binfmt` suffix, a package driver's module and the TLS engine, when the file lies under a path.
663
+ - **The check:** the kernel hashes the exact bytes it is about to compile or hand to a worker (SHA-256, hex), and asks `lookup(path)` (the real path; on the page, it may be async) for the hash it expects. On a mismatch the program doesn't start: `kernel.run` rejects and a spawn fails with `EACCES` ("integrity check failed"; 126 in a shell, not `ENOEXEC`, which would make a shell run the file as a script), a driver fails to mount, TLS fails, and `onDenial` gets `{ op: 'exec-integrity', path, errno: 'EACCES', pid, uid, tag?, expected, actual }`. A `lookup` that throws counts as a mismatch.
664
+ - **No hash known:** when `lookup` has none (`undefined`), `unknown: 'allow'` (the default) runs the file and reports `{ op: 'exec-unverified', errno: 'ENODATA', … }` once per path, while `unknown: 'deny'` refuses it as a mismatch.
665
+ - **The cache:** a result is kept per real path and the version of the file OPFS itself reports (its `lastModified` and `size`), not the kernel's metadata, so a change made around the kernel is checked again at the next start. The compiled module is cached by the same key, so a program that hasn't changed costs nothing more to start.
666
+ - **Where `lookup` gets its hashes:** from a source independent of the installed files, such as the per-file hashes the embedder derives from the certified package tarball when it builds its catalog. Hashes read back from the installed files prove nothing.
667
+ - **Scripts** are checked when they're executed; the interpreter reads the file again afterwards, so a change in between isn't caught ([#454](https://github.com/ai-ecoverse/slicc-kernel/issues/454)).
668
+ - **Not covered:** what a running program reads itself, such as a JS runtime's modules, python's `.py` files and `dlopen`'s side modules ([#454](https://github.com/ai-ecoverse/slicc-kernel/issues/454)). Protected paths still keep those unwritable.
662
669
  - **Where it is a boundary:** it's one in a browser, where programs' own code can't reach OPFS around the kernel (see [JS programs](#js-programs)). Under the Node entry it isn't, until [#407](https://github.com/ai-ecoverse/slicc-kernel/issues/407), and there `kernel.writeFile` under a path fails too, so write those files into `root` before creating the kernel.
663
670
 
664
671
  ### `/proc`
package/dist/index.js CHANGED
@@ -332,7 +332,12 @@ var DEFAULT_USERS = "enforce";
332
332
  // src/fs/protect.ts
333
333
  function protectInit(protection) {
334
334
  const writers = [...writerTags(protection)];
335
- return { paths: [...protection.paths], ...writers.length > 0 ? { writers } : {} };
335
+ const { integrity } = protection;
336
+ return {
337
+ paths: [...protection.paths],
338
+ ...writers.length > 0 ? { writers } : {},
339
+ ...integrity ? { integrity: { unknown: integrityUnknown(integrity) } } : {}
340
+ };
336
341
  }
337
342
  function writerTags(protection) {
338
343
  const writers = protection?.writers ?? [];
@@ -342,6 +347,40 @@ function writerTags(protection) {
342
347
  return new Set(writers);
343
348
  }
344
349
 
350
+ // src/fs/integrity.ts
351
+ function integrityOptions(options) {
352
+ if (options && typeof options.lookup !== "function") {
353
+ throw new TypeError("protect.integrity.lookup must be a function");
354
+ }
355
+ integrityUnknown(options);
356
+ }
357
+ function integrityUnknown(options) {
358
+ const unknown = options?.unknown ?? "allow";
359
+ if (unknown !== "allow" && unknown !== "deny") {
360
+ throw new TypeError("protect.integrity.unknown must be 'allow' or 'deny'");
361
+ }
362
+ return unknown;
363
+ }
364
+
365
+ // src/integrity-relay.ts
366
+ function integrityAnswerer(port, lookup) {
367
+ const ask = async ({ id, path }) => {
368
+ try {
369
+ const hash = lookup ? await lookup(path) : void 0;
370
+ port.postMessage({ integrityReply: { id, ...typeof hash === "string" ? { hash } : {} } });
371
+ } catch (err) {
372
+ const error = err instanceof Error ? err.message : String(err);
373
+ port.postMessage({ integrityReply: { id, error } });
374
+ }
375
+ };
376
+ return (data) => {
377
+ const asked = data?.integrityAsk;
378
+ if (!asked) return false;
379
+ void ask(asked);
380
+ return true;
381
+ };
382
+ }
383
+
345
384
  // src/kernel/net/loopback-names.ts
346
385
  var HOST_LOOPBACK = "host.slicc.internal";
347
386
  var HOST_LOOPBACK_ADDRESS = [10, 0, 2, 2];
@@ -2099,12 +2138,15 @@ function checkOptions(options) {
2099
2138
  if (options.hostname !== void 0 && !isHostname(options.hostname)) {
2100
2139
  throw new Error(`not a host name: ${String(options.hostname)}`);
2101
2140
  }
2141
+ if (options.protect) protectInit(options.protect);
2142
+ integrityOptions(options.protect?.integrity);
2102
2143
  }
2103
2144
  function hookRelays(worker, options) {
2104
2145
  const sealed = sealedAnswerer(worker, options.sealed);
2105
2146
  const clipboard = clipboardAnswerer(worker, options.clipboard);
2147
+ const integrity = integrityAnswerer(worker, options.protect?.integrity?.lookup);
2106
2148
  return {
2107
- handle: (data) => sealed.handle(data) || clipboard.handle(data),
2149
+ handle: (data) => sealed.handle(data) || clipboard.handle(data) || integrity(data),
2108
2150
  close: () => sealed.close()
2109
2151
  };
2110
2152
  }
@@ -2133,7 +2175,6 @@ function traitsUpdater(transport, call) {
2133
2175
  }
2134
2176
  async function createKernel(options = {}) {
2135
2177
  checkOptions(options);
2136
- writerTags(options.protect);
2137
2178
  const url = options.worker ?? new URL("./kernel-worker.js", import.meta.url);
2138
2179
  const worker = new Worker(url, { type: "module", name: "slicc-kernel" });
2139
2180
  const pending = /* @__PURE__ */ new Map();