@ai-ecoverse/slicc-kernel 1.132.1 → 1.133.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.
package/README.md CHANGED
@@ -417,6 +417,23 @@ The module's default export (or its `main` export) is called with a context and
417
417
  The two codes are outside 0–255, so no `exit(n)` gives them. The main thread's exit (any exit, or a signal that ends the process) ends every thread.
418
418
  - **The process:** a thread ending changes nothing for `wait`, `ps` or the process's status. Signals go to the main thread only, so a thread's `signals.on`, `ignore` and `reset` change nothing (as Node, where a worker gets no signal events); the main thread's disposition is the process's. Kernel calls a thread leaves in flight when it ends (a `read` it never awaited, say) are cancelled, so they can't take another thread's data. A stopped process (`^Z`, `SIGSTOP`) stops its threads as well, timers included, until `SIGCONT`.
419
419
  - **The cap:** at most twice `navigator.hardwareConcurrency` threads per process at once, and at least 16 (so piscina's default of 1.5 × the parallelism fits), or what `SLICC_JS_THREADS` in the program's environment says, up to 256. Past it, `spawn` rejects with `EAGAIN`, and an ended thread frees its slot. Each thread is a dedicated worker, like a process worker, so threads count against the browser's worker budget the same way processes do.
420
+ - **`sync.wasi.create({ args, env, preopens, stdio, returnOnExit })`, an in-process `wasi_snapshot_preview1` instance** ([#1111](https://github.com/ai-ecoverse/slicc-kernel/issues/1111)). It is the base for Node's `node:wasi`, which napi-rs packages built for `wasm32-wasi` need (unrs-resolver for jest 30, oxc, rolldown, swc, lightningcss). The guest runs in the program's own worker, on memory it shares with JavaScript, and is called synchronously.
421
+ - **What it returns:** an object shaped like Node's `WASI`:
422
+ - `imports`: `{ wasi_snapshot_preview1 }`, with `wasiImport` as the same table;
423
+ - `bind(memory)`: the guest's `WebAssembly.Memory`, shared or not, before or after instantiation;
424
+ - `start(instance)`: binds `exports.memory`, runs `_start`, and returns its `proc_exit` code (`0` if it returns);
425
+ - `initialize(instance)`: the same for `_initialize`, if there is one;
426
+ - `close()`.
427
+ - **Exit:** with `returnOnExit: false`, `proc_exit(n)` throws a `JsWasiExit` whose `code` is `n`, for the caller to handle (jsh calls `process.exit`). With the default, `true`, `start` returns `n` instead.
428
+ - **Arguments and environment:** `args` and `env` are exactly what the guest sees, with nothing added.
429
+ - **Preopens and stdio:** `preopens` maps a guest name to a path of the process (relative to its cwd), and these become guest fds 3, 4, …. Guest `0`, `1` and `2` are copies of the `stdio` descriptors, `[0, 1, 2]` by default, so the guest closing its stdout leaves the program's own open.
430
+ - **One preview1 host:** the calls are the kernel's own WASI host, the one WASI processes run on, answered over the program's sync lane. Every call has the process's credentials, umask, file permissions (`users: 'enforce'`) and lockdown, with no kernel call of its own, so the guest can do nothing the JavaScript program couldn't do itself.
431
+ - **Confinement mirrors Node's and isn't a security boundary:** a path that leaves its preopen by `..`, or an absolute path, fails with `ENOTCAPABLE`. That's a lexical check, as in Node's uvwasi; a symlink inside a preopen can still lead out. The kernel's permissions are the boundary.
432
+ - **Guest descriptors:**
433
+ - **Its own fd table:** the guest has its own numbering. Each guest fd stands for a kernel descriptor of the process, kept in a map at the import boundary: fd arguments, `path_open`'s and `sock_accept`'s results, `fd_renumber`, both directories of `path_link` and `path_rename`, and the fds inside `poll_oneoff`'s subscriptions. So a guest fd 4 is never the program's own fd 4, and a guest fd with no entry is `EBADF`.
434
+ - **Counted and closed like any other:** these kernel descriptors count against the process's descriptor limit. They are closed when the guest closes them, on `close()`, when the thread that made them ends (by returning, `exit`, `sync.exit`, `terminate()` or a crash; the kernel keeps the list for a thread that cannot clean up itself), and when the process ends.
435
+ - **Threads:** in a `thread` (above), each thread makes its own instance with the same options, as emnapi's workers do with Node's `WASI`. It runs over that thread's own lane, on the `WebAssembly.Memory` posted to it. The view of the file system is the same, while each instance has its own fd table, as in Node.
436
+ - **Scope:** preview1 only, as Node's `node:wasi`. There are no WASIX calls, and no `thread-spawn` from the guest: emnapi starts its threads as JS workers, which are `thread`s here.
420
437
  - `spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`. `parentDeathSignal` (a number or `'SIGTERM'`, as Linux's `PR_SET_PDEATHSIG`) is sent to the child when the program ends, killed or not: to the child's whole group when it leads one (`pgid: 0` or `session: true`), else to the child alone. It's sent with the program's credentials, so a child the program may no longer signal (one that gained privilege) doesn't get it. If the program has already ended by the time the spawn completes, it's sent at once. A child of its own (not a group leader) that has been waited for gets nothing, and `sync.run` has no such option. A bad signal is `EINVAL`;
421
438
  - `wait(pid, { untraced })` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; with `untraced: true` it also resolves when the child stops, with `{ status: 128 + signal, signal, stopped: true }`, as `waitpid` with `WUNTRACED` (a child's `wait({ untraced })` is the same), and options that aren't an object are `EINVAL`; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
422
439
  - `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;
@@ -7034,7 +7034,7 @@ function satisfies(version, range2) {
7034
7034
  }
7035
7035
 
7036
7036
  // src/version.ts
7037
- var KERNEL_VERSION = true ? "1.132.1" : "0.0.0-dev";
7037
+ var KERNEL_VERSION = true ? "1.133.1" : "0.0.0-dev";
7038
7038
  var OS_RELEASE = "6.0.0-slicc";
7039
7039
  function procVersion() {
7040
7040
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -22633,6 +22633,7 @@ var JS_MAX_THREADS = 256;
22633
22633
  var JS_DEFAULT_THREADS = 16;
22634
22634
  var JS_THREAD_NO_MEMORY = 256;
22635
22635
  var JS_THREAD_CRASHED = 257;
22636
+ var JS_THREAD_OWNED_FDS = 1024;
22636
22637
 
22637
22638
  // src/kernel/js-threads.ts
22638
22639
  var JS_OOM = /out of memory|Array buffer allocation failed|Cannot allocate Wasm memory|could not allocate memory|ERR_WORKER_OUT_OF_MEMORY/i;
@@ -22661,6 +22662,11 @@ function codeOf4(code) {
22661
22662
  function answer(control, message3) {
22662
22663
  control.postMessage(message3);
22663
22664
  }
22665
+ function ownedFds(owned4) {
22666
+ const view2 = new Int32Array(owned4);
22667
+ const n = Math.min(Atomics.load(view2, 0), JS_THREAD_OWNED_FDS);
22668
+ return Array.from(view2.subarray(1, 1 + Math.max(0, n)));
22669
+ }
22664
22670
  var JsThreads = class {
22665
22671
  deps;
22666
22672
  live = /* @__PURE__ */ new Map();
@@ -22688,6 +22694,7 @@ var JsThreads = class {
22688
22694
  const tid = ++this.last;
22689
22695
  const sab = new SharedArrayBuffer(SAB_HEADER_BYTES + SAB_DEFAULT_WINDOW_BYTES);
22690
22696
  const header3 = new Int32Array(sab, 0, SAB_HEADER_I32);
22697
+ const owned4 = new SharedArrayBuffer(4 * (1 + JS_THREAD_OWNED_FDS));
22691
22698
  if (this.frozen) Atomics.store(header3, SAB_I_STOP, 1);
22692
22699
  const worker = this.deps.createWorker();
22693
22700
  const clock = this.deps.clocks.thread(tid + 1, sab);
@@ -22709,6 +22716,9 @@ var JsThreads = class {
22709
22716
  lane.dispose();
22710
22717
  this.deps.clocks.threadEnded(tid + 1);
22711
22718
  worker.terminate();
22719
+ for (const fd of ownedFds(owned4)) {
22720
+ void this.deps.dispatch({ op: "fd-close", fd });
22721
+ }
22712
22722
  answer(control, { exited: code });
22713
22723
  control.close();
22714
22724
  };
@@ -22748,7 +22758,7 @@ var JsThreads = class {
22748
22758
  const init = {
22749
22759
  ...this.deps.init(sab, lane.lane),
22750
22760
  type: JS_THREAD_INIT,
22751
- thread: { tid, port, data: msg.data }
22761
+ thread: { tid, port, data: msg.data, owned: owned4 }
22752
22762
  };
22753
22763
  try {
22754
22764
  worker.postMessage(init, [lane.lane.port, port, ...msg.transfer ?? []]);