@ai-ecoverse/slicc-kernel 1.108.2 → 1.108.4

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
@@ -313,7 +313,7 @@ On top of preview1 a WASI program gets:
313
313
  - **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. An Emscripten program stops at its next kernel call as well, and at its next write, read or sleep that its glue answers without the kernel (a write to its own `/dev/null`, as in `yes > /dev/null`), so a stopped one keeps no core busy unless it computes without any of these calls. A stopped program doesn't move on while stopped: a `sleep` (or a blocked read) that would end meanwhile ends after `SIGCONT`. A call that the stop interrupted runs again after `SIGCONT`, as on Linux. One with a timeout (`select`, `poll`, `ppoll`, `pselect`, `nanosleep`, a timed semaphore wait) waits only for the time that's left, counted from when the kernel first got the call, so it still ends at its deadline. `epoll_wait` and `epoll_pwait` aren't run again: they fail with `EINTR`, even without a handler (signal(7)). A signal for such a program is also delivered at those glue-answered calls, so a handler runs in a loop that only writes to `/dev/null`. 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 pending signal is delivered at the program's next WASI or WASIX call, including the calls its worker answers without the kernel (`random_get`, `args_get`, …), except the exempt ones: `clock_time_get` (a tight loop of it measured up to 30% slower), `proc_exit`, `proc_exit2`, `thread_exit`, `thread_spawn_v2` and `wasi.thread-spawn` (wasi-threads), the calls that install handlers (`callback_signal`, `slicc.sigaction_set`), and calls made while Asyncify unwinds or rewinds the stack for `fork` or `longjmp`. A signal that arrives while a thread computes is thus delivered at its next call; when that call is a wait (`poll_oneoff` with a timeout or a descriptor, so `nanosleep` and `poll`/`select` too, `sigmask_poll`, or a futex wait), the handler has run during the call, so the wait ends at once: `EINTR` for a libc of -22 or later (an older one sees its clocks fire, as for any interruption), and a futex wait returns woken. 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. A sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
314
314
  - **Blocked signals** (`sigprocmask`, `pthread_sigmask`, `sigpending`): a program whose libc imports `slicc.sigmask_set(how, set_lo, set_hi, old_ptr)` and `slicc.sigpending_get(ptr)` (wasix-sysroot 2025.9.30-28 and later) has a blocked mask per thread. `how` is musl's `SIG_BLOCK` 0, `SIG_UNBLOCK` 1 or `SIG_SETMASK` 2, the set is the 64-bit `sigset_t` (bit `sig - 1`), and the old mask is written as a u64 at `old_ptr`; any other `how` is `EINVAL`. `SIGKILL` and `SIGSTOP` cannot be blocked. A blocked signal is not delivered, does not interrupt a sleep or a blocked call, and its default action (terminate or stop) waits: it stays pending, `sigpending` shows it (the thread's pending signals and the process's), and it is delivered when it is unblocked, before `sigmask_set` returns. A signal ignored with `SIG_IGN` is discarded even while blocked. A forked child (with the mask of the thread that forked), the program an `exec` starts and a spawned child keep the mask, all 64 bits of it, and so does a new thread; `/proc/<pid>/status` shows the main thread's as `SigBlk`. The mask is kept in a word of the worker's shared header, so changing it costs no kernel call. A program without the import (an older libc) has no mask and gets every signal at once, as before, and one it starts by `exec` with a mask set starts with none. A signal for the whole process goes to the main thread, unless the main thread blocks it and another thread does not. Then, as on Linux, it goes to that thread: its handler runs there, or with no handler its default action ends (or stops) the whole process at once. So the "signal thread" pattern works: block in main, unblock in a worker (#520). It waits on the main thread only when every thread blocks it. A signal sent to one thread (`pthread_kill`) that it blocks waits for that thread, and if it has no handler, the thread's unblock ends the whole process by the signal, whatever the other threads block. `slicc.sigmask_poll(set_lo, set_hi, in_ptr, out_ptr, nsubscriptions, nevents_ptr)` is `pselect`, `ppoll` and `sigsuspend`: it swaps the thread's mask to the given set for the call and puts the old one back before it returns. It delivers a pending signal the new mask unblocks, without polling, or polls as `poll_oneoff` does, under the new mask, so a signal it unblocks interrupts the wait with no window between the swap and the wait; with no subscriptions it waits until a signal is delivered. An interrupted call returns `EINTR` with no events, after the handler has run. `slicc.sigmask_epoll_wait(set_lo, set_hi, epfd, events_ptr, maxevents, timeout_ns, nevents_ptr)` is `epoll_pwait` (#771). It swaps the thread's mask to the given set in the same way. A pending signal the new mask unblocks runs its handler and makes the call return `EINTR` with no events, without waiting. Otherwise the call waits exactly as `wasix_32v1.epoll_wait` with the same arguments does (the same event layout and errors; a `timeout_ns` of all bits set is forever, 0 doesn't wait), under the new mask, so a signal it unblocks ends the wait with `EINTR` after the handler. The old mask is back before the call returns, on every path, and `*nevents_ptr` is always written. A kernel without it answers `ENOSYS`, and the libc keeps its non-atomic swap around `epoll_wait`. For both masked waits, a signal that came while the program computed and was delivered as the call began (its handler ran under the old mask, as on Linux, where it would have run before the call) ends the call with `EINTR` only if the new mask leaves it unblocked; when the new mask blocks every signal delivered then, the call waits as if none had come (#799). `slicc.sigwait(set_lo, set_hi, timeout_ns, sig_ptr)` is `sigwait`, `sigwaitinfo` and `sigtimedwait` (#520). If a signal of the set is pending for the thread, or else for the process, it takes it: the lowest-numbered first, with no handler run. It writes the number as a `u32` at `sig_ptr` and returns 0. Otherwise it waits until one is pending. `timeout_ns` of all bits set is forever and 0 doesn't wait; when the time runs out it answers `EAGAIN` (`*sig_ptr = 0`). A signal outside the set that the thread has unblocked and handles runs its handler and ends the call with `EINTR` (also one that came while the program computed and was delivered as the call began, as for the other waits), and so does a job-control stop, once continued. While a thread waits, the kernel counts it as able to take the signals of its set: a process-directed signal that every thread blocks goes to it (the main thread first, if it is the one waiting), so the signal-thread pattern works with `sigwait` too. Only kernel signals 1 to 31 can be waited for. A kernel without it answers `ENOSYS`. `slicc.sigaction_inherited(ign_ptr)` (#820) writes, as a little-endian `u64` at `ign_ptr`, the signals the program arrived with set to `SIG_IGN`, inherited across `exec` or spawn (bit `sig - 1`; never `SIGKILL` or `SIGSTOP`; kernel signals are 1 to 31, so the high word is 0), and returns 0. A libc calls it once before constructors and `main` and seeds its `sigaction` table with `SIG_IGN` for each bit, so `sigaction(sig, NULL, &old)` reads an inherited ignore back (wasix-sysroot 2025.9.30-32 and later); a kernel without it answers `ENOSYS`, and the libc keeps its defaults. Handlers are never inherited. **SIGCHLD:** with `SIGCHLD` set to `SIG_IGN` (by the program, or inherited), or with `SA_NOCLDWAIT` in the flags `slicc.sigaction_set` gets for `SIGCHLD`, an exited child is reaped at once and never stays a zombie: `waitpid` for it is `ECHILD`, and `wait` blocks until every child has exited, then fails with `ECHILD`, as on Linux. `SIGCHLD` is still sent. An explicit `SIG_DFL` turns it back off. A child that a process waits on for `exec`, or that `kernel.run` and the run calls collect, is never reaped that way.
315
315
  - **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.
316
- - **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. A write to a pipe, FIFO or terminal whose reader is gone follows the program's `SIGPIPE` disposition, as the kernel knows it (#810): at the default it ends the program with status 141; ignored (also an ignore inherited across `exec` and spawn, as bash's `trap '' PIPE` passes on), the write fails with `EPIPE`; with a handler, the handler runs and then the write fails with `EPIPE`; blocked, `SIGPIPE` stays pending and the write fails with `EPIPE`. An Emscripten program's write to a pipe, and its `send` without `MSG_NOSIGNAL`, follow the same rules, except that the kernel doesn't see an Emscripten program's signal mask, so a blocked `SIGPIPE` there still ends it. A WASI program asking `sigaction` about an inherited ignore still sees the default, since its libc keeps its own table. 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. `sock_open(SOCK_DGRAM)` on `AF_INET`/`AF_INET6` gives a [datagram socket](#datagram-sockets-udp); `sock_send_to` and `sock_recv_from` carry its addresses, and `RECV_DATA_TRUNCATED` is set in `ro_flags` when a datagram did not fit.
316
+ - **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. A write to a pipe, FIFO or terminal whose reader is gone follows the program's `SIGPIPE` disposition, as the kernel knows it (#810): at the default it ends the program with status 141; ignored (also an ignore inherited across `exec` and spawn, as bash's `trap '' PIPE` passes on), the write fails with `EPIPE`; with a handler, the handler runs on the thread that wrote and then the write fails with `EPIPE` (#820); blocked, `SIGPIPE` stays pending and the write fails with `EPIPE`. An Emscripten program's write to a pipe, and its `send` without `MSG_NOSIGNAL`, follow the same rules, except that the kernel doesn't see an Emscripten program's signal mask, so a blocked `SIGPIPE` there still ends it. A WASI program asking `sigaction` about an inherited ignore still sees the default, since its libc keeps its own table. 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. `sock_open(SOCK_DGRAM)` on `AF_INET`/`AF_INET6` gives a [datagram socket](#datagram-sockets-udp); `sock_send_to` and `sock_recv_from` carry its addresses, and `RECV_DATA_TRUNCATED` is set in `ro_flags` when a datagram did not fit.
317
317
  - **Diagnostics**: a trap ends the program with 134 and its message on stderr; with `SLICC_WASM_BACKTRACE=1` the wasm frames follow, named from the module's name section, or from a sidecar for a module shipped without one (`<module>.names` beside it, or the same path in a `<package>-names` package). `SLICC_WASI_STATS=1` makes a program print its calls, counted and timed, as it ends.
318
318
 
319
319
  ### Program imports
@@ -333,7 +333,7 @@ A WASI command can name an ES module of its package that provides imports of its
333
333
 
334
334
  ### JS programs
335
335
 
336
- A command can be an ES module instead of a wasm program: `"abi": "js"` and `module` name the module in the package, which must be one self-contained file (no relative or bare imports), and `args`, `argv0` and `env` work as for other commands. An entry with `"abi": "js"` but no `module` (or one outside the package) is not a command, like a wasm entry without its module, so running it fails with 127.
336
+ A command can be an ES module instead of a wasm program: `"abi": "js"` and `module` name the module in the package, which must be one self-contained file (no relative or bare imports), and `args`, `argv0` and `env` work as for other commands. An entry with `"abi": "js"` but no `module` (or one outside the package) is not a command, like a wasm entry without its module, so running it fails with 127. Every run gets its own process worker, so each process evaluates the module afresh: module-level state never carries over from one run to the next. The kernel keeps an importable URL per module source (a `blob:` URL in the browser, a base64 `data:` URL under Node) for the 16 sources it ran last, so a command run in a loop doesn't encode its module again each time ([#846](https://github.com/ai-ecoverse/slicc-kernel/issues/846)). A changed file is a new source and gets a new URL. An evicted URL is revoked once every run it was handed to has exited, so a run that is still loading its module never loses it.
337
337
 
338
338
  ```json
339
339
  { "slicc": { "commands": { "jstool": { "abi": "js", "module": "dist/jstool.mjs" } } } }
@@ -996,15 +996,15 @@ A WASIX C program for `test/integration/mask-entry.test.mjs` (#799). It calls `s
996
996
 
997
997
  ### `pipetest`
998
998
 
999
- A WASIX C program for `test/integration/sigpipe.test.mjs` (#810). `pipetest MODE` writes one byte to a pipe whose read end it has closed and prints the result, `errno`, how often its handler ran, and whether `SIGPIPE` is pending. `MODE` is `ignore` (`SIG_IGN`, and the `sigaction` read-back), `handler`, `blocked` (`sigprocmask`), `dfl` (an explicit `SIG_DFL`), or anything else to leave the disposition as inherited.
999
+ A WASIX C program for `test/integration/sigpipe.test.mjs` (#810). `pipetest MODE` writes one byte to a pipe whose read end it has closed and prints the result, `errno`, how often its handler ran, and whether `SIGPIPE` is pending. `MODE` is `ignore` (`SIG_IGN`, and the `sigaction` read-back), `handler`, `thread` (a handler, and the write from a second thread, which reports whether the handler ran there), `blocked` (`sigprocmask`), `ignblocked` (`SIG_IGN` and blocked), `dfl` (an explicit `SIG_DFL`), or anything else to leave the disposition as inherited.
1000
1000
 
1001
- **Build:** `WASIX_SYSROOT_29=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 on `PATH`. `bin/pipetest.wasm` is 54,301 bytes, sha256 `c97961442fd1b2551942fc026b8d316d0419d1e6d71c533772e6b7f8b2c65b24`.
1001
+ **Build:** `WASIX_SYSROOT_29=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 on `PATH`. `bin/pipetest.wasm` is 57,441 bytes, sha256 `c1c8701cd4599b592d2775b23bd0b1f8e352f1fb189e753067aaa74bb4332a0b`.
1002
1002
 
1003
1003
  ### `reaptest`
1004
1004
 
1005
- A WASIX C program for `test/integration/sigchld-reap.test.mjs` (#820). `reaptest MODE` forks a child that exits with 3 at once and checks after 300 ms whether `waitpid(WNOHANG)` still finds it, then forks two that exit after 100 and 250 ms and reports what `wait` returns and when. `MODE` is `ign` (`SIGCHLD` `SIG_IGN`), `nocldwait` (`SIG_DFL` with `SA_NOCLDWAIT`), `reset` (`SIG_IGN`, then `SIG_DFL`), `dfl`, or `inherited`, which first prints what `slicc.sigaction_inherited` reports for `SIGPIPE`, `SIGCHLD` and `SIGTERM`.
1005
+ A WASIX C program for `test/integration/sigchld-reap.test.mjs` (#820). `reaptest MODE` forks a child that exits with 3 at once and polls `waitpid(WNOHANG)` until the child is no longer running, then reports whether it was waitable or already reaped, then forks two that exit after 100 and 250 ms and reports what `wait` returns and when. `MODE` is `ign` (`SIGCHLD` `SIG_IGN`), `nocldwait` (`SIG_DFL` with `SA_NOCLDWAIT`), `reset` (`SIG_IGN`, then `SIG_DFL`), `dfl`, or `inherited`, which first prints what `slicc.sigaction_inherited` reports for `SIGPIPE`, `SIGCHLD` and `SIGTERM`.
1006
1006
 
1007
- **Build:** `WASIX_SYSROOT_29=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions) and binaryen's `wasm-opt` (`--asyncify` for `proc_fork`) on `PATH`. `bin/reaptest.wasm` is 46837 bytes, sha256 `8e2718273edef94cb3882ee63ce6a9e53f03ba83ef2b2dd9759422d47fae89c7`.
1007
+ **Build:** `WASIX_SYSROOT_29=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions) and binaryen's `wasm-opt` (`--asyncify` for `proc_fork`) on `PATH`. `bin/reaptest.wasm` is 46,919 bytes, sha256 `b351d53b1a3b317a37642baceb731aef62a1bee387aa8972cba5d9949e1a9a3f`.
1008
1008
 
1009
1009
  ### `epolltest`
1010
1010