@ai-ecoverse/slicc-kernel 1.48.3 → 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 +3 -2
- package/dist/kernel-worker.js +29 -0
- package/dist/kernel-worker.js.map +2 -2
- package/dist/node-process-worker.js +417 -68
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +29 -0
- package/dist/node.js.map +2 -2
- package/dist/process-worker.js +417 -67
- package/dist/process-worker.js.map +4 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -186,7 +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`.
|
|
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.
|
|
190
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.
|
|
191
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.
|
|
192
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.
|
|
@@ -235,7 +236,7 @@ A program that throws, or leaves an error or a rejected promise unhandled with n
|
|
|
235
236
|
|
|
236
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.
|
|
237
238
|
|
|
238
|
-
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`, `
|
|
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.
|
|
239
240
|
|
|
240
241
|
## Network
|
|
241
242
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -9410,6 +9410,9 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
|
|
|
9410
9410
|
"proc-cred",
|
|
9411
9411
|
"proc-setcred",
|
|
9412
9412
|
"dl-log",
|
|
9413
|
+
"dl-closure",
|
|
9414
|
+
"dl-closure-free",
|
|
9415
|
+
"dl-reserve",
|
|
9413
9416
|
"fd-renumber",
|
|
9414
9417
|
"fd-promote",
|
|
9415
9418
|
"fd-open-tty",
|
|
@@ -10026,6 +10029,13 @@ var WasmProcess = class {
|
|
|
10026
10029
|
case "dl-log":
|
|
10027
10030
|
if (req.append) this.dlLog.push(req.append);
|
|
10028
10031
|
return { ok: true, kind: "json", json: this.dlLog.slice(req.from) };
|
|
10032
|
+
case "dl-closure":
|
|
10033
|
+
return { ok: true, kind: "json", json: this.allocateClosure(req.floor) };
|
|
10034
|
+
case "dl-closure-free":
|
|
10035
|
+
this.freeClosure(req.index);
|
|
10036
|
+
return { ok: true, kind: "void" };
|
|
10037
|
+
case "dl-reserve":
|
|
10038
|
+
return { ok: true, kind: "json", json: this.reserveSlots(req.floor, req.size) };
|
|
10029
10039
|
case "sig-mask":
|
|
10030
10040
|
this.caught = req.caught;
|
|
10031
10041
|
this.ignored = req.ignored;
|
|
@@ -10062,6 +10072,24 @@ var WasmProcess = class {
|
|
|
10062
10072
|
}
|
|
10063
10073
|
alarm;
|
|
10064
10074
|
dlLog = [];
|
|
10075
|
+
closures = { next: 0, free: [], live: /* @__PURE__ */ new Set() };
|
|
10076
|
+
allocateClosure(floor) {
|
|
10077
|
+
const index = this.closures.free.pop() ?? Math.max(floor, this.closures.next);
|
|
10078
|
+
this.closures.next = Math.max(this.closures.next, index + 1);
|
|
10079
|
+
this.closures.live.add(index);
|
|
10080
|
+
this.dlLog.push({ kind: "closure", index });
|
|
10081
|
+
return index;
|
|
10082
|
+
}
|
|
10083
|
+
freeClosure(index) {
|
|
10084
|
+
if (!this.closures.live.delete(index)) throw new KernelError("EINVAL");
|
|
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;
|
|
10092
|
+
}
|
|
10065
10093
|
asyncOps = new AsyncOps(
|
|
10066
10094
|
(req, cancelled) => this.syscall(req, cancelled),
|
|
10067
10095
|
() => this.options.onAsync?.()
|
|
@@ -12228,6 +12256,7 @@ var Launcher = class {
|
|
|
12228
12256
|
module,
|
|
12229
12257
|
...memory ? { memory } : {},
|
|
12230
12258
|
foreign,
|
|
12259
|
+
path: target.wasm,
|
|
12231
12260
|
...names ? { names } : {},
|
|
12232
12261
|
...imports !== void 0 ? { imports } : {},
|
|
12233
12262
|
...target.preopenRoot ? { preopenRoot: true } : {}
|