@ai-ecoverse/slicc-kernel 1.49.0 → 1.49.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
@@ -186,8 +186,8 @@ A descriptor a program inherits at one of those numbers (from bash's `3<file`, o
186
186
  On top of preview1 a WASI program gets:
187
187
 
188
188
  - **Threads**: `wasi.thread-spawn` (wasm32-wasip1-threads) and WASIX `thread_spawn_v2` start a worker per thread on the process's shared memory, sized from the module's own import (a 2 GiB maximum when the engine cannot reserve more). The threads share one descriptor table; `exit` or a trap in any thread ends the process. At most 64 threads per process; `SLICC_WASM_THREADS` sets another cap, up to 256.
189
- - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`.
190
- - **Foreign calls** (libffi's WASIX backend, so Python's `ctypes`): `call_dynamic` calls a function through the program's indirect function table (a static module's exported `__indirect_function_table`, or the dynamic linker's), its arguments and results packed in memory one after another (`i32`/`f32` 4 bytes, `i64`/`f64` 8). With `strict` set, the buffers must fit the function exactly (else `EINVAL`); without it, missing arguments are zero and results that do not fit are left out. A trap or exception of the callee goes through to the caller. JS cannot ask a function for its type, so the kernel reads the types from the module files (the program's and each side module's, once, when a program first calls through `call_dynamic`): a table slot that is empty, past the end, holds a function of no module the kernel knows of, or takes or returns a `v128`, gives `EINVAL`. For closures (callbacks such as `CFUNCTYPE`), a dynamically linked program gets table slots from `closure_allocate`, handed out by the kernel so that the program's threads, each with its own copy of the table, never pick the same one, and `closure_free` gives them back for reuse; a static program gets `ENOTSUP`. `closure_prepare`, which puts a function into such a slot, is not there yet and answers `ENOSYS`. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
189
+ - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot.
190
+ - **Foreign calls** (libffi's WASIX backend, so Python's `ctypes`): `call_dynamic` calls a function through the program's indirect function table (a static module's exported `__indirect_function_table`, or the dynamic linker's), its arguments and results packed in memory one after another (`i32`/`f32` 4 bytes, `i64`/`f64` 8). With `strict` set, the buffers must fit the function exactly (else `EINVAL`); without it, missing arguments are zero and results that do not fit are left out. A trap or exception of the callee goes through to the caller. JS cannot ask a function for its type, so the kernel reads the types from the module files (the program's and each side module's, once, when a program first calls through `call_dynamic`): a table slot that is empty, past the end, holds a function of no module the kernel knows of, or takes or returns a `v128`, gives `EINVAL`. For closures (callbacks such as `CFUNCTYPE`), a dynamically linked program gets table slots from `closure_allocate`, and `closure_free` gives them back for reuse and empties the slot in every thread; a static program gets `ENOTSUP`. `closure_prepare`, which puts a function into such a slot, is not there yet and answers `ENOSYS`. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
191
191
  - **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names.
192
192
  - **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
193
193
  - **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names.
@@ -236,7 +236,7 @@ A program that throws, or leaves an error or a rejected promise unhandled with n
236
236
 
237
237
  The module has the trust of an Emscripten program's glue or a WASI program's imports module: it comes from a package that was installed, and it runs in the worker of its own process. The kernel withholds the worker's own ways out before it loads the module: `fetch`, `XMLHttpRequest`, `WebSocket`, `WebTransport`, `EventSource`, `importScripts`, `indexedDB`, `caches`, `cookieStore`, `Worker`, `SharedWorker`, `BroadcastChannel`, `postMessage`, `close`, and `navigator.storage`, `navigator.locks` and `navigator.serviceWorker` are `undefined`, and a worker where one of them cannot be removed refuses to run the program (126). A program's files are the kernel's, through the context; it reaches the kernel only through its own process's system calls, as the context makes them, and no other process's. This is a guard against programs going around the kernel, not a sandbox: `import()` of a URL is a request the kernel cannot withhold.
238
238
 
239
- Under the Node entry, the kernel also withholds the host's Node from the program before its module loads ([#297](https://github.com/ai-ecoverse/slicc-kernel/issues/297)). `require`, `module`, `exports`, `__filename`, `__dirname`, `localStorage` and `sessionStorage` are `undefined` for good. `process`, `Buffer`, `global`, `setImmediate` and `clearImmediate` are `undefined` too, but stay writable, so a program can install its own (as slicc-jsh does). Imports of Node's builtin modules (`node:fs`, `fs`, `node:worker_threads`, …) and of anything but a `data:` URL, `file:` included, fail with `ERR_ACCESS_DENIED`, whether static, dynamic or through `import.meta.resolve`. A process worker that exits or crashes on its own ends its process with 70. This is the same guard, not a sandbox: a host that runs code it does not trust, such as an agent's, needs its own isolation around the Node process as well.
239
+ Under the Node entry, the kernel also withholds the host's Node from the program before its module loads ([#297](https://github.com/ai-ecoverse/slicc-kernel/issues/297)). `require`, `module`, `exports`, `__filename`, `__dirname`, `localStorage` and `sessionStorage` are `undefined` for good. `process`, `global`, `setImmediate` and `clearImmediate` are `undefined` too, but stay writable, so a program can install its own (as slicc-jsh does). `Buffer` stays Node's own, since Node's `Response`, `Request`, `Blob` and stream bodies read it when they run; it reaches nothing outside the program, and a program can replace it. Imports of Node's builtin modules (`node:fs`, `fs`, `node:worker_threads`, …) and of anything but a `data:` URL, `file:` included, fail with `ERR_ACCESS_DENIED`, whether static, dynamic or through `import.meta.resolve`. A process worker that exits or crashes on its own ends its process with 70. This is the same guard, not a sandbox: a host that runs code it does not trust, such as an agent's, needs its own isolation around the Node process as well.
240
240
 
241
241
  ## Network
242
242
 
@@ -9412,6 +9412,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
9412
9412
  "dl-log",
9413
9413
  "dl-closure",
9414
9414
  "dl-closure-free",
9415
+ "dl-reserve",
9415
9416
  "fd-renumber",
9416
9417
  "fd-promote",
9417
9418
  "fd-open-tty",
@@ -10033,6 +10034,8 @@ var WasmProcess = class {
10033
10034
  case "dl-closure-free":
10034
10035
  this.freeClosure(req.index);
10035
10036
  return { ok: true, kind: "void" };
10037
+ case "dl-reserve":
10038
+ return { ok: true, kind: "json", json: this.reserveSlots(req.floor, req.size) };
10036
10039
  case "sig-mask":
10037
10040
  this.caught = req.caught;
10038
10041
  this.ignored = req.ignored;
@@ -10080,6 +10083,12 @@ var WasmProcess = class {
10080
10083
  freeClosure(index) {
10081
10084
  if (!this.closures.live.delete(index)) throw new KernelError("EINVAL");
10082
10085
  this.closures.free.push(index);
10086
+ this.dlLog.push({ kind: "closure-free", index });
10087
+ }
10088
+ reserveSlots(floor, size) {
10089
+ const base = Math.max(floor, this.closures.next);
10090
+ this.closures.next = base + size;
10091
+ return base;
10083
10092
  }
10084
10093
  asyncOps = new AsyncOps(
10085
10094
  (req, cancelled) => this.syscall(req, cancelled),