@ai-ecoverse/slicc-kernel 1.102.0 → 1.102.2

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
@@ -261,7 +261,7 @@ An empty path is `ENOENT` in every path call (`path_open`, `path_filestat_get`,
261
261
  On top of preview1 a WASI program gets:
262
262
 
263
263
  - **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, and a file written, created, renamed or removed in one thread shows its new size and metadata to `stat` in every other thread at once, under any of its names (a symlink, a hard link, a new name after a rename in the same process), also when another thread renames a file over that name meanwhile, since the kernel reports which file an open or a change reached; this holds among the kernel's own processes, not against other tabs, `hostfs` writers or programs that aren't WASI; another process's changes show at close and after a `spawn` or `wait`, as with close-to-open caching, so a file another process renames while this one holds it open may show its old size under the new name until then; `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.
264
- - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), 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. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors again, since their data is already in the copied memory. A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver.
264
+ - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), 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. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors again, since their data is already in the copied memory. A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver. A kernel timer can be longer than `setTimeout`'s 2^31-1 ms (about 24.8 days): an `alarm`, an interval timer, or a `select`, `poll_oneoff` or `epoll_wait` timeout waits its whole time, re-armed in steps of at most 2^31-1 ms, instead of firing at once or spinning (#767).
265
265
  - **epoll and eventfd** (WASIX `epoll_create`, `epoll_ctl`, `epoll_wait` and `fd_event`, which tokio's reactor uses through the WASIX mio fork):
266
266
  - **Where they live:** an epoll instance and an eventfd are kernel descriptors, so they survive `dup`, `fork` and `exec` (unless close-on-exec) and are shared by a process's threads.
267
267
  - **What epoll watches:** sockets (listening, connecting, connected, loopback and uplink), pipes, terminals, `kernel.run`'s stdin, eventfds and other epoll instances (a cycle of instances is `ELOOP`).
@@ -307,7 +307,7 @@ On top of preview1 a WASI program gets:
307
307
  - Nothing is undone when a process exits.
308
308
  - **Limits:**
309
309
  - Semaphores exist only within one kernel (not across tabs), and they don't appear in `/dev/shm`.
310
- - A kernel holds at most 4,096 named ones (`ENOSPC`), and a process at most 256 open handles (`EMFILE`; a refused `O_CREAT` leaves no name behind).
310
+ - A kernel holds at most 65,536 named ones (`ENOSPC`), and a process at most 65,536 open handles (`EMFILE`; a refused `O_CREAT` leaves no name behind). Those are memory bounds (a semaphore is about 100 bytes), not musl's 256 (`SEM_NSEMS_MAX`), which wasix-sysroot -31 drops: python's multiprocessing opens one per `Lock` (#770).
311
311
  - `MAP_SHARED` mappings aren't shared between processes, so unnamed semaphores (`sem_init` with `pshared`) work only between a process's threads, and `multiprocessing.shared_memory` doesn't work.
312
312
  - **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`, before the function is called); without it, missing arguments are zero (one cut short keeps the bytes given, with zeros above them) 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` puts a trampoline with the requested signature (any of `i32`, `i64`, `f32`, `f64`, `v128`) into such a slot, in every thread: each call stores its arguments below the stack pointer and calls the backing function as `backing(values, results, user_data)`, then returns what it wrote to `results`. The trampolines are small wasm modules made and compiled at run time, once per signature in each thread (an instance per closure), so a page whose Content Security Policy forbids compiling WebAssembly (no `'wasm-unsafe-eval'`) cannot run them; it cannot run the program either. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
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).
@@ -347,7 +347,7 @@ The module's default export (or its `main` export) is called with a context and
347
347
  - `fdStatus(fd)` (`{ type, tty, seekable }`, `type` being `file`, `pipe`, `tty`, `socket`, `device` or `directory`) and `isatty(fd)`, to tell `< file` from a pipe;
348
348
  - `open(path, { read, write, append, create, exclusive, truncate })`, which returns a file with `read(position, length)`, `write(position, bytes)`, `size()`, `truncate(size)`, `sync()` and `close()`. Reads and writes go through the kernel's paged files at those positions, so a large file is never read whole;
349
349
  - `fs`: `stat`, `lstat`, `exists`, `readdir`, `mkdir` (with parents, or, with `{ recursive: false }`, as Node's `fs.mkdir` without `recursive`: `EEXIST` for an existing entry and `ENOENT` for a missing parent, atomically among the processes of one kernel, so it can serve as a lock there, but not between two kernels on one origin, such as two tabs ([#519](https://github.com/ai-ecoverse/slicc-kernel/issues/519)); `sync.fs.mkdir` takes the same option), `rm` (recursive), `unlink`, `rename`, `symlink`, `readlink`, `statfs`, `chmod(path, mode)`, and `readFile`/`writeFile` for small files. `chmod` takes the permission bits (`0` to `0o7777`, `EINVAL` otherwise) and follows a symlink; as `chmod(2)`, only the file's owner or root may (`EPERM`), so a program can make a script it wrote runnable by path. `sync.fs.chmod` is the same, synchronously. `statfs(path)` answers `{ type, bsize, blocks, bfree, bavail, files, ffree, quota, usage }`: the fields of Node's `StatFs`, plus `quota` and `usage` in bytes (what jsh 0.17 reads). The values come from the same source as an Emscripten program's `statfs`/`df`: the driver of the mount the path is on, or, on the root file system, the origin's storage estimate (`navigator.storage.estimate()`). `bsize` is 4096, `bfree` and `bavail` are what's left of the quota, and `type`, `files` and `ffree` are 0, since the kernel doesn't know them. Where there's no estimate (the Node entry's root), every count is 0. A missing path is `ENOENT`. `sync.fs` has it too. `rm` removes a whole tree in one kernel call (on the OPFS root, one recursive removal and one metadata delete: 50,000 files take about 3 s in Chrome, where `rm -r` takes about 25 s) with the result of a file-by-file removal: symlinks inside the tree are removed, never followed; a tree holding a protected path is refused with `EROFS` and nothing is removed; a mount inside the tree is emptied and its mount point answers `EBUSY`, while the rest is removed; under `users`, what the user may remove is removed and the call then fails with the first `EACCES`/`EPERM`; files a process holds open stay readable to it;
350
- - `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Creating, removing or renaming names in `/dev` goes to the root file system as before;
350
+ - `/dev`, in `open`, `fs` and their `sync` forms, as a WASI or Emscripten program sees it, with the same modes and owners as under `users`: `null`, `zero`, `full`, `random`, `urandom` and `tty` (character devices, `0666`, root's), `stdin`, `stdout` and `stderr` (links to `/proc/self/fd/0`, `1`, `2`), `fd` (a link to `/proc/self/fd`, so `/dev/fd/N` is descriptor N) and the kernel's terminals `/dev/ttyN` and `/dev/pts/N` (with users on, owned by the session's user or the master's opener, `0620`). `/dev/shm` is the kernel's shared tmpfs (see [Mounts](#mounts)), read and written like any other mount; `/dev/ptmx` isn't there. `stat` through `/dev/stdin`, `/dev/stdout` or `/dev/fd/N` describes the program's own descriptor (the file it is redirected to, a pipe, a terminal), `readlink` of `/dev/fd/N` names it (`pipe:[N]` and `socket:[N]` for a pipe or a socket), and a closed descriptor is `ENOENT`. Opening a device gives a kernel descriptor on it; opening `/dev/std*` or `/dev/fd/N` opens the file again with the flags asked for when the descriptor is on a file (as Linux reopens `/proc/self/fd/N`, so `append` appends and `truncate` truncates), and otherwise duplicates the descriptor; `/dev/tty` is the program's controlling terminal (`ENXIO` without one); and `/dev/ttyN` and `/dev/pts/N` are opened by name through the kernel, so a terminal that isn't the user's is `EACCES` under `enforce`. A file on anything but a regular file reads and writes as a stream (the position is ignored) and only in the directions it was opened for (`EBADF` otherwise), its `size()` is 0 and `truncate` is `EINVAL`. `readFile` reads a device or a descriptor to its end, up to 64 MiB, and fails with `EFBIG` past that (where Node's `readFile` throws `ERR_FS_FILE_TOO_LARGE`), so `readFile('/dev/zero')` fails instead of hanging, while a bounded `read` of it works. Other paths pay only a prefix check: they make the same kernel calls as before. Removing a `/dev` entry fails (see [`/dev`](#dev-entries)), and so does a new name directly in `/dev` or `/proc`;
351
351
  - `/proc`, in `fs`, `open` and their `sync` forms, the same as a WASI program's (see [`/proc`](#proc)): `stat`, `lstat`, `exists`, `readdir`, `readlink` and `readFile` answer it, and `/proc/self` is the program's own pid. It's read-only, so every change in it is `EROFS`: `writeFile`, `mkdir`, `rm`, `unlink`, `chmod`, a `rename`, `link` or `symlink` into or out of it, and an `open` for writing, creating or truncating. A file opened in it is a snapshot read at the open, with no descriptor (`fd` is `-1`): its `read` and `size` answer from the snapshot until it's closed, and `write` and `truncate` are `EBADF`. Other paths pay only a prefix check;
352
352
  - `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
353
353
  - `sync`: `read`, `write`, `close`, `open` (whose files' methods are synchronous too) and `fs`, made synchronously, for code that cannot `await`, such as a compiled program's file system or a `readFileSync`. A synchronous call holds the worker until the kernel answers, so nothing else of the program runs meanwhile, timers and callbacks included; a signal that arrives during a blocked read or write runs its handler, and the call is then restarted. Descriptors are shared with the rest of the context, so a file opened with `sync.open` can be closed with `close`, and the other way around; `sync.run` and `sync.exit` are described next;
@@ -825,6 +825,14 @@ A character device (`0666`), there in every `users` mode, through which programs
825
825
  - **Not caught:** a file changed around the kernel between an open and a later ranged read of it is read as it is then. Checking every ranged read would cost a metadata read for each one. Nor is a same-size rewrite by a writer in another kernel on the origin (another tab), read before that tab's change notice arrives: these checks take the write count from the metadata store's cache, which the notice clears, so the window runs from the write to the notice. OPFS's own `lastModified` and size are read every time, so a change made outside every kernel is still caught.
826
826
  - **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.
827
827
 
828
+ ### `/dev` entries
829
+
830
+ `/dev` and `/proc` hold the kernel's own entries, and a program can't add or remove names there. Only `/dev/shm`, a tmpfs, takes new files.
831
+ - **Removing** an entry fails and leaves it: `EACCES` for a user, `EPERM` for root on a device, `EBUSY` for `/dev` itself.
832
+ - **Creating** a name that doesn't exist fails in every runtime, whether it's a file, a directory, a symlink, a device node, or the new name of a link or a rename: `EACCES` for a user, `EPERM` for root in `/dev`, and `EROFS` in `/proc`.
833
+ - **Why:** an Emscripten program's `/dev` and `/proc` live in its glue's own memory, so such a file used to look written and then vanish when the process exited. On the root file system no other runtime would see it either.
834
+ - **Existing entries** (`/dev/null`, the terminals, `/dev/std*`) open and write as before.
835
+
828
836
  ### `/proc`
829
837
 
830
838
  Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client. A process that exits while a program reads the `/proc` directory doesn't fail or cut short the listing: its pid is still listed, and opening its files fails with `ENOENT`, which `ps`, `pgrep` and `pidof` skip, as on Linux; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times are measured per process, its threads and the images it exec'd included: time a worker spends waiting for the kernel's answer is system time (at most 20 ms of each call; the rest of a longer one, such as a write into a full pipe, counts as blocked), time it spends blocked (a read, a `poll` or `select` that waits, a sleep, `wait`, a futex, while stopped) is neither, from the moment the call actually starts to wait (a call that could wait but answers at once, such as a `select` with a zero timeout or a ready descriptor, a `waitpid` with `WNOHANG`, or a futex wait whose value has already changed, is system time), and the rest is user time; a process's times stop when it ends, a call still in flight then adds nothing, so a loop that never calls the kernel shows as user time while it runs; they show in `/proc/<pid>/stat`, `ps` and `ps()`, and the times of the children a process has waited for (theirs and their own waited-for children's, as POSIX counts them) show as its `cutime` and `cstime` in `/proc/<pid>/stat`; a stopped process is `T` there, in `/proc/<pid>/status` (`T (stopped)`) and in `ps()`; a WASI or WASIX program's `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` (`clock_time_get`) count only the time its thread runs, its own waits and blocked kernel calls left out (a sleep is not CPU time), per thread for now, so a threaded program's process clock is its calling thread's; and `/proc/stat` counts every process of the kernel, the reaped ones included, and `/proc/loadavg` averages the processes not blocked, sampled every 5 seconds. A JS program's time isn't measured yet. `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) is a placeholder. A process that isn't root sees only its own user's processes (see [Users](#users)). WASI and WASIX programs see the same `/proc`, read-only (opening a file in it for writing, or linking one of its files elsewhere, fails with `EROFS`), from every thread, also through a symlink that points into it (where every change, `mkdir`, `rename`, `unlink`, `chmod`, a hard link from it or into it, and so on, fails too: with `EROFS` when the root file system has a physical `/proc`, which stays hidden and untouched, and otherwise with the `ENOENT` of the missing directory, since that check is only paid for when a physical `/proc` exists; with one, a symlink whose target passes `..` after a file or a missing name, such as `/f/../proc`, fails with `ENOTDIR` or `ENOENT`, as on Linux), and `/` lists it, so a WASI program gets a preopen for it; `/proc/self` is the program's own pid there too (the pid an exec'd program took over). WASI and WASIX programs also have `/proc/<pid>/task/<tid>/stat` for each of a process's threads (the main thread is tid 1), whose state is `S` while the thread is blocked in the kernel (a `sem_wait`, a blocking read, a sleep, a futex) and `R` otherwise (`T` and `Z` as for the process). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
@@ -1020,20 +1028,28 @@ A threaded WASIX C program for `test/integration/thread-asyncify.test.mjs` (#710
1020
1028
 
1021
1029
  ### `stopwait`
1022
1030
 
1023
- A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child 50 ms after it's ready and continues it 200 ms later. The child prints the result, the error and whether the wait ended before the deadline, at it (300–450 ms) or late.
1031
+ A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child once `/proc/<pid>/task/1/stat` shows it blocked (`S`), or 100 ms after it's ready on a kernel without that state, continues it 200 ms later, and reports whether the stop found it blocked. The child prints the result, the error and whether the wait ended before the deadline, at it (300–450 ms) or late.
1032
+
1033
+ **Build:** `WASIX_SYSROOT_28=/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/stopwait.wasm` is 59,958 bytes, sha256 `ce49e04baed13b7ccf04d8231da4a29567fde0dac19d5cc81f36c185e0eb4555`.
1034
+
1035
+ ### `longwait`
1036
+
1037
+ A WASIX C program for `test/integration/long-timers.test.mjs` (#767).
1038
+ - `longwait alarm` sets `alarm(3000000)` (about 35 days), sleeps 0.5 s, and prints how many alarms came and how many seconds were left.
1039
+ - `longwait select|ppoll|nanosleep` waits 3,000,000 s on a pipe that never gets data, until a 300 ms interval timer interrupts the wait.
1024
1040
 
1025
- **Build:** `WASIX_SYSROOT_28=/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/stopwait.wasm` is 50,567 bytes, sha256 `570cb3e29cfdc083bbdc0d20707f2c07b087fe23cef13a8861a4a42b02d20f5f`.
1041
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` on `PATH`. `bin/longwait.wasm` is 44,010 bytes, sha256 `70ae2adfb46864fad3af0241fe26f401a62a5176ccd25ab82fe6588178cfb5c8`.
1026
1042
 
1027
1043
  ### `semtest`
1028
1044
 
1029
1045
  A WASIX C program for `test/integration/semaphore.test.mjs` (#717). It calls the `slicc.sem_*` imports directly (`import_module("slicc")`), so the rows don't wait for the libc side. `semtest MODE` runs one of:
1030
1046
  - `handoff`, `inherit`, `spawn` (a spawned and an exec'd image open the semaphore by name; exec drops the handles);
1031
- - `try`, `timed`, `eintr`, `stopped` (a forked child's 300 ms timed wait is stopped for 200 ms and continued, and reports whether it ended at the deadline);
1047
+ - `try`, `timed`, `eintr`, `stopped` (a forked child's 300 ms timed wait is stopped for 200 ms and continued, and reports whether it ended at the deadline; the parent stops it only once `/proc/<pid>/task/1/stat` shows it blocked, `S`, or after 100 ms on a kernel without that state, and reports which);
1032
1048
  - `threads` (FIFO, one thread blocked at a time);
1033
1049
  - `errors`, `unlink`, `owners setup|user`;
1034
- - `killed` (a waiter killed before posts loses no unit), `exited`, `cap`, `handles`.
1050
+ - `killed` (a waiter killed before posts loses no unit), `exited`, `cap [N]` (creates up to N names, 10,000 by default), `handles [N]` (opens up to N, 300 by default).
1035
1051
 
1036
- **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions, `-pthread`) and binaryen's `wasm-opt` (version 132, `--asyncify` for `proc_fork`) on `PATH`. `bin/semtest.wasm` is 68,438 bytes, sha256 `af3d38d87b63fa219bae90b1225376882ba20ace423c5ce24ef57935e80e2fde`.
1052
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions, `-pthread`) and binaryen's `wasm-opt` (version 132, `--asyncify` for `proc_fork`) on `PATH`. `bin/semtest.wasm` is 69,366 bytes, sha256 `b3202708f57acad5f51e5be1b06e3b5a0ad02dd84b3791e052787117b828da4e`.
1037
1053
 
1038
1054
  ### `wlocktest`
1039
1055
 
@@ -8460,6 +8460,11 @@ function devRemoval(path, cred) {
8460
8460
  if (cred && cred.euid !== 0) return "EACCES";
8461
8461
  return path === "/dev" ? "EBUSY" : "EPERM";
8462
8462
  }
8463
+ function devCreation(path, cred) {
8464
+ if (path === "/proc" || path.startsWith("/proc/")) return "EROFS";
8465
+ if (path === "/dev" || !inDevTree(path)) return void 0;
8466
+ return cred && cred.euid !== 0 ? "EACCES" : "EPERM";
8467
+ }
8463
8468
 
8464
8469
  // src/fs/fresh-modules.ts
8465
8470
  var MAGIC = [0, 97, 115, 109];
@@ -11266,12 +11271,29 @@ async function guardAndResolve(entry, path, req) {
11266
11271
  };
11267
11272
  }
11268
11273
  var REMOVALS = /* @__PURE__ */ new Set(["unlink", "rmdir", "rm"]);
11274
+ var CREATIONS = /* @__PURE__ */ new Set(["write", "mkdir", "symlink", "create"]);
11275
+ var NAMING = /* @__PURE__ */ new Set(["link", "rename"]);
11276
+ async function newDevName(entry, path, req) {
11277
+ const named = NAMING.has(req.op);
11278
+ if (!named && !CREATIONS.has(req.op)) return void 0;
11279
+ const target = named ? entry.fs.resolvePath(entry.cwd, req.arg2 ?? "") : path;
11280
+ const refused2 = devCreation(target, entry.cred?.());
11281
+ if (!refused2) return void 0;
11282
+ const there = await entry.fs.lstat(target).then(
11283
+ () => true,
11284
+ () => false
11285
+ );
11286
+ if (there) return void 0;
11287
+ return { ok: false, errno: refused2, message: `${target}: no new names in /dev or /proc` };
11288
+ }
11269
11289
  async function dispatchOne(entry, req) {
11270
11290
  const { fs, cwd } = entry;
11271
11291
  try {
11272
11292
  const path = fs.resolvePath(cwd, req.path);
11273
11293
  const device = REMOVALS.has(req.op) ? devRemoval(path, entry.cred?.()) : void 0;
11274
11294
  if (device) return { ok: false, errno: device, message: `${path}: a device entry stays` };
11295
+ const made2 = await newDevName(entry, path, req);
11296
+ if (made2) return made2;
11275
11297
  const held = await byHandle(entry, path, req);
11276
11298
  if (held) return held;
11277
11299
  if (req.op === "access") return await accessOp(entry, path, req);
@@ -12114,7 +12136,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12114
12136
  var EARLY_WHOLE_FILE = 64 * 1024;
12115
12137
 
12116
12138
  // src/version.ts
12117
- var KERNEL_VERSION = true ? "1.102.0" : "0.0.0-dev";
12139
+ var KERNEL_VERSION = true ? "1.102.2" : "0.0.0-dev";
12118
12140
  var OS_RELEASE = "6.0.0-slicc";
12119
12141
  function procVersion() {
12120
12142
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -12628,6 +12650,41 @@ var EventFd = class _EventFd {
12628
12650
  }
12629
12651
  };
12630
12652
 
12653
+ // src/kernel/timer.ts
12654
+ var TIMER_MAX_MS = 2147483647;
12655
+ function timerAt(deadline, fire) {
12656
+ let timer;
12657
+ const arm = () => {
12658
+ const left = deadline - performance.now();
12659
+ if (left > 0) {
12660
+ timer = setTimeout(arm, Math.min(left, TIMER_MAX_MS));
12661
+ return;
12662
+ }
12663
+ timer = void 0;
12664
+ fire();
12665
+ };
12666
+ timer = setTimeout(arm, Math.min(Math.max(0, deadline - performance.now()), TIMER_MAX_MS));
12667
+ return () => clearTimeout(timer);
12668
+ }
12669
+ function timerAfter(ms, fire) {
12670
+ return timerAt(performance.now() + ms, fire);
12671
+ }
12672
+ function repeating(first, every, fire) {
12673
+ let next2 = performance.now() + first;
12674
+ let cancel = () => {
12675
+ };
12676
+ const tick = () => {
12677
+ fire();
12678
+ if (every <= 0) return;
12679
+ next2 += every;
12680
+ const now = performance.now();
12681
+ if (next2 <= now) next2 += (Math.floor((now - next2) / every) + 1) * every;
12682
+ cancel = timerAt(next2, tick);
12683
+ };
12684
+ cancel = timerAt(next2, tick);
12685
+ return () => cancel();
12686
+ }
12687
+
12631
12688
  // src/kernel/epoll.ts
12632
12689
  var EPOLL = {
12633
12690
  IN: 1,
@@ -12679,11 +12736,11 @@ function lookup(fds, fd) {
12679
12736
  }
12680
12737
  function pause(ms, signal) {
12681
12738
  return new Promise((resolve2) => {
12682
- const timer = setTimeout(resolve2, ms);
12739
+ const cancel = timerAfter(ms, resolve2);
12683
12740
  signal.addEventListener(
12684
12741
  "abort",
12685
12742
  () => {
12686
- clearTimeout(timer);
12743
+ cancel();
12687
12744
  resolve2();
12688
12745
  },
12689
12746
  { once: true }
@@ -12749,17 +12806,14 @@ var EpollFile = class _EpollFile {
12749
12806
  const round = new AbortController();
12750
12807
  const woken = this.changes.wait(AbortSignal.any([round.signal, signal]));
12751
12808
  woken.catch(() => void 0);
12752
- let timer;
12753
- const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [
12754
- new Promise(
12755
- (resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
12756
- )
12757
- ];
12809
+ let cancel = () => {
12810
+ };
12811
+ const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => cancel = timerAt(deadline, resolve2))];
12758
12812
  try {
12759
12813
  await Promise.race([woken, ...expiry]);
12760
12814
  } finally {
12761
12815
  round.abort();
12762
- clearTimeout(timer);
12816
+ cancel();
12763
12817
  }
12764
12818
  }
12765
12819
  }
@@ -13104,9 +13158,10 @@ var AsyncOps = class {
13104
13158
  }
13105
13159
  sleep(timeoutMs, signal) {
13106
13160
  return new Promise((resolve2, reject) => {
13107
- let timer;
13161
+ let cancel = () => {
13162
+ };
13108
13163
  const cleanup = () => {
13109
- clearTimeout(timer);
13164
+ cancel();
13110
13165
  this.waiters.delete(wake);
13111
13166
  signal.removeEventListener("abort", abort);
13112
13167
  };
@@ -13121,10 +13176,10 @@ var AsyncOps = class {
13121
13176
  this.waiters.add(wake);
13122
13177
  signal.addEventListener("abort", abort, { once: true });
13123
13178
  if (timeoutMs !== void 0) {
13124
- timer = setTimeout(() => {
13179
+ cancel = timerAfter(timeoutMs, () => {
13125
13180
  cleanup();
13126
13181
  resolve2(false);
13127
- }, timeoutMs);
13182
+ });
13128
13183
  }
13129
13184
  });
13130
13185
  }
@@ -14511,14 +14566,14 @@ async function runChild(req, deps) {
14511
14566
  return frame2(outcome2, new Uint8Array(0), new Uint8Array(0));
14512
14567
  }
14513
14568
  if (failure2 !== void 0) kill();
14514
- const timer = req.timeoutMs !== void 0 && req.timeoutMs > 0 ? setTimeout(() => stop("ETIMEDOUT"), req.timeoutMs) : void 0;
14569
+ const cancel = req.timeoutMs !== void 0 && req.timeoutMs > 0 ? timerAfter(req.timeoutMs, () => stop("ETIMEDOUT")) : void 0;
14515
14570
  let status;
14516
14571
  try {
14517
14572
  status = await reaped(deps, pid);
14518
14573
  const open = controls.filter((c) => c.opened);
14519
14574
  await Promise.race([Promise.all(open.map((c) => c.eof)), decided.promise]);
14520
14575
  } finally {
14521
- clearTimeout(timer);
14576
+ cancel?.();
14522
14577
  for (const control of controls) control.shut = true;
14523
14578
  }
14524
14579
  const out = deps.children.captured(pid, 1);
@@ -14574,27 +14629,23 @@ async function waitReady(fds, read, write, deadline, interrupted3) {
14574
14629
  const file = fds.get(fd).file;
14575
14630
  return file.changed === void 0 ? [] : [file.changed(round.signal).catch(() => void 0)];
14576
14631
  });
14577
- let timer;
14578
- const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [
14579
- new Promise(
14580
- (resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
14581
- )
14582
- ];
14632
+ let cancel = () => {
14633
+ };
14634
+ const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => cancel = timerAt(deadline, resolve2))];
14583
14635
  try {
14584
14636
  await Promise.race([...changes, ...expiry, interrupted3]);
14585
14637
  } finally {
14586
14638
  round.abort();
14587
- clearTimeout(timer);
14639
+ cancel();
14588
14640
  }
14589
14641
  }
14590
14642
  }
14591
14643
 
14592
14644
  // src/kernel/sem.ts
14593
14645
  var SEM_VALUE_MAX = 2147483647;
14594
- var SEM_NAMES_MAX = 4096;
14595
- var SEM_HANDLES_MAX = 256;
14646
+ var SEM_NAMES_MAX = 65536;
14647
+ var SEM_HANDLES_MAX = 65536;
14596
14648
  var NAME_MAX = 251;
14597
- var TIMER_MAX_MS = 2147483647;
14598
14649
  var SEM_OPS = [
14599
14650
  "sem-open",
14600
14651
  "sem-close",
@@ -14631,11 +14682,12 @@ var Semaphore = class {
14631
14682
  if (signal.aborted) return Promise.reject(new KernelError("EINTR"));
14632
14683
  return new Promise((resolve2, reject) => {
14633
14684
  const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : performance.now() + timeoutMs;
14634
- let timer;
14685
+ let cancel = () => {
14686
+ };
14635
14687
  const leave = () => {
14636
14688
  const at = this.waiters.indexOf(waiter);
14637
14689
  if (at >= 0) this.waiters.splice(at, 1);
14638
- clearTimeout(timer);
14690
+ cancel();
14639
14691
  signal.removeEventListener("abort", abort);
14640
14692
  };
14641
14693
  const waiter = {
@@ -14649,18 +14701,13 @@ var Semaphore = class {
14649
14701
  reject(new KernelError("EINTR"));
14650
14702
  };
14651
14703
  const expire = () => {
14652
- const left = deadline - performance.now();
14653
- if (left > 0) {
14654
- timer = setTimeout(expire, Math.min(left, TIMER_MAX_MS));
14655
- return;
14656
- }
14657
14704
  leave();
14658
14705
  resolve2(false);
14659
14706
  };
14660
14707
  this.waiters.push(waiter);
14661
14708
  signal.addEventListener("abort", abort, { once: true });
14662
14709
  if (deadline !== Number.POSITIVE_INFINITY) {
14663
- timer = setTimeout(expire, Math.min(timeoutMs, TIMER_MAX_MS));
14710
+ cancel = timerAt(deadline, expire);
14664
14711
  }
14665
14712
  });
14666
14713
  }
@@ -14695,6 +14742,10 @@ var SemTable = class {
14695
14742
  var SemHandles = class _SemHandles {
14696
14743
  byHandle = /* @__PURE__ */ new Map();
14697
14744
  next = 1;
14745
+ limit;
14746
+ constructor(limit = SEM_HANDLES_MAX) {
14747
+ this.limit = limit;
14748
+ }
14698
14749
  add(sem) {
14699
14750
  for (const [handle2, entry] of this.byHandle) {
14700
14751
  if (entry.sem === sem) {
@@ -14702,7 +14753,7 @@ var SemHandles = class _SemHandles {
14702
14753
  return handle2;
14703
14754
  }
14704
14755
  }
14705
- if (this.byHandle.size >= SEM_HANDLES_MAX) throw new KernelError("EMFILE");
14756
+ if (this.byHandle.size >= this.limit) throw new KernelError("EMFILE");
14706
14757
  const handle = this.next++;
14707
14758
  this.byHandle.set(handle, { sem, opens: 1 });
14708
14759
  return handle;
@@ -14718,7 +14769,7 @@ var SemHandles = class _SemHandles {
14718
14769
  if (--entry.opens === 0) this.byHandle.delete(handle);
14719
14770
  }
14720
14771
  fork() {
14721
- const child = new _SemHandles();
14772
+ const child = new _SemHandles(this.limit);
14722
14773
  child.next = this.next;
14723
14774
  for (const [handle, entry] of this.byHandle) child.byHandle.set(handle, { ...entry });
14724
14775
  return child;
@@ -17005,17 +17056,12 @@ var WasmProcess = class {
17005
17056
  (req, cancelled) => this.syscall(req, cancelled),
17006
17057
  () => this.options.onAsync?.()
17007
17058
  );
17008
- alarmEvery;
17009
17059
  setAlarm(sig, first, every, timer) {
17010
17060
  if (!isSignal(sig)) throw new KernelError("EINVAL");
17011
17061
  this.clearAlarm();
17012
17062
  if (first <= 0) return;
17013
17063
  const fire = timer === void 0 ? () => this.options.raise?.(sig) : () => this.timerExpired(timer);
17014
- this.alarm = setTimeout(() => {
17015
- this.alarm = void 0;
17016
- fire();
17017
- if (every > 0) this.alarmEvery = setInterval(fire, every);
17018
- }, first);
17064
+ this.alarm = repeating(first, every, fire);
17019
17065
  }
17020
17066
  timerExpired(which) {
17021
17067
  this.options.onTimer?.(which);
@@ -17024,10 +17070,8 @@ var WasmProcess = class {
17024
17070
  blocked2.abort();
17025
17071
  }
17026
17072
  clearAlarm() {
17027
- clearTimeout(this.alarm);
17028
- clearInterval(this.alarmEvery);
17073
+ this.alarm?.();
17029
17074
  this.alarm = void 0;
17030
- this.alarmEvery = void 0;
17031
17075
  }
17032
17076
  async exit() {
17033
17077
  if (this.exited) return;