@ai-ecoverse/slicc-kernel 1.99.2 → 1.100.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
@@ -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, 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`.
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`.
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`).
@@ -568,7 +568,13 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
568
568
  - **Caching:** the kernel caches attributes and listings for those TTLs, and a driver can push `invalidate` for changes made outside.
569
569
  - **File contents:** on a mount with `ranges`, as `tmpfs` is, and on the root file system when it is in OPFS (in a browser with sync access handles) or an in-memory root, a program reads and writes a file in 64 KiB pages: reads come from the driver 1 MiB at a time, and changed pages go back in 1 MiB runs once 8 MiB have changed, at `fsync`, and when the file is closed (from the kernel's own descriptors also 250 ms after a write). A file of any size the mount can hold works, with memory to spare for about one more copy of it. In OPFS a write opens a sync access handle for just that write. An `fsa` mount (a picked folder) has no sync access handles, and Chrome copies the whole file into a swap file for every writable stream, so it writes through one stream per open file: the first write back opens it, later runs go into it at their offsets, and it is committed, one copy of the file, when the last process writing the file closes it (or exits), before another process reads what was written, and before a rename, unlink or unmount. Until then other processes see the new size but read the old bytes outside what was written. Elsewhere (`hostfs` with an older proxy, and a root outside OPFS), file contents are read whole when a program opens a file and written back when it closes it. Before a program's next call into the kernel (any wait, read of a pipe or terminal, sleep or spawn), its changed pages also go back, so other processes and the embedder see a write as soon as the writer does anything but compute: a daemon's pidfile, a log another process follows. On a whole-file mount that holds only for files of up to 64 KiB; a larger one goes back at close, fsync and exit, as before. Beyond that, both give close-to-open consistency, as NFS has. A process that ends without closing its files keeps what it wrote: on exit, on a trap, and when a signal kills it. For a killed WASI program, the kernel lets its worker write its changed pages back before `waitpid` reports the death. The only exception is a worker that makes no call into the kernel for 2 seconds after the signal; it is stopped without that write-back.
570
570
  - **One file, several processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): a process that keeps a file open sees what another process writes to it. Every change the kernel applies to a file (a write back, a truncate, an append, a rename or a removal) bumps a change ring the kernel shares with all its processes. Before each read, write or seek to the end, a process's open file checks the ring (one atomic load) and, if its file changed, drops its clean pages and reads the size again, keeping its own unwritten changes. A write by one process is visible to another one's next read once it has gone back to the kernel: at the latest at the writer's next `fsync`, close, exit or call into the kernel (see above), so sqlite, which `fsync`s every commit, sees another connection's commit at its next transaction. A write through a file the kernel holds (a JS program's `ctx.open`, a WASI program's file after a fork or spawn) is in the kernel already, so other processes see it at their next read, before it is written back. A file the kernel itself holds open (a JS program's file, a WASI program's file after a fork or spawn) is the one copy: reads, writes, truncates and appends by path from other processes go to it, and their writes go through to storage at once, so a commit is in storage (and in `kernel.readFile`) when the committing process returns from `fsync`. `O_APPEND` writes go to the kernel at once, and the kernel picks the offset, so appenders in several processes keep every line. A file system without ranged I/O (`hostfs` with an older proxy, a root outside OPFS) rereads a file another process changed, but writes it back whole, so two writers there still need locks. The same file under two names is one file too ([#669](https://github.com/ai-ecoverse/slicc-kernel/issues/669)): a write by path through a symlink reaches the file the kernel holds under its real path (resolved without following links in a `sudo` process's no-links view, so an untrusted link is still refused), a WASI program's open file also watches its real path (resolved once, the first time another file changes), and an open file with more than one hard link checks its size, version and mtime when anything changed, so a write through another name is seen. Limits for now: an alias on a mount that has no `realpath` is still only the name it was opened by; another tab's writes reach this tab's processes only as far as the root's change notifications (OPFS's `BroadcastChannel` watch) carry them.
571
- - **Locks between processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): an Emscripten program's `fcntl` byte-range locks (`F_SETLK`, `F_SETLKW`, `F_GETLK`) are the kernel's, so sqlite3 connections in different processes exclude each other: a second writer gets `database is locked` while the first holds its transaction, or waits with a busy timeout. The kernel keeps one table keyed by file identity (device and inode, so every name of a hard link is the same file), with POSIX semantics: locks belong to the process (an `exec`'d program keeps them under its pid; a forked child doesn't get them), a conflicting `F_SETLK` fails with `EAGAIN`, `F_GETLK` reports the holder's pid and range, `F_SETLKW` waits in the kernel without holding up anything else and a signal ends the wait with `EINTR`, a wait that would close a cycle between waiting processes fails with `EDEADLK`, a write lock on a descriptor not open for writing (or a read lock on one not open for reading) is `EBADF`, closing any descriptor of the file drops the process's locks on it, and exit releases them all. Before an unlock, the process writes back what it changed, so the next holder reads it. An open file's directory entry is checked against the change ring too: a name another process removed or replaced is looked up again, so sqlite's check for a hot journal sees the other connection's commit. A WASI imports module's `tryLock`/`unlock` keeps its behaviour (whole file, per process, keyed by path). Limits: locks are per kernel, not across tabs ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)); and a WASIX program's `fcntl` locks (python's `sqlite3`) are no-ops in its libc that never reach the kernel, until wasix-libc gets a kernel import for them ([homescoop#407](https://github.com/ai-ecoverse/homescoop/issues/407)), so python's sqlite doesn't exclude other processes yet. An Emscripten program's `flock()` is a stub compiled into its libc that never reaches the kernel either, until a homescoop shim routes it to a kernel import ([#680](https://github.com/ai-ecoverse/slicc-kernel/issues/680)). A lock belongs to the file its descriptor was opened on: a file renamed over the name later, or the name's removal, doesn't move it.
571
+ - **Locks between processes** ([#665](https://github.com/ai-ecoverse/slicc-kernel/issues/665)): an Emscripten program's `fcntl` byte-range locks (`F_SETLK`, `F_SETLKW`, `F_GETLK`) are the kernel's, and so are a WASIX program's through the `slicc.fcntl_lock` import (below), so sqlite3 connections in different processes exclude each other: a second writer gets `database is locked` while the first holds its transaction, or waits with a busy timeout. The kernel keeps one table keyed by file identity (device and inode, so every name of a hard link is the same file), with POSIX semantics: locks belong to the process (an `exec`'d program keeps them under its pid; a forked child doesn't get them), a conflicting `F_SETLK` fails with `EAGAIN`, `F_GETLK` reports the holder's pid and range, `F_SETLKW` waits in the kernel without holding up anything else and a signal ends the wait with `EINTR`, a wait that would close a cycle between waiting processes fails with `EDEADLK`, a write lock on a descriptor not open for writing (or a read lock on one not open for reading) is `EBADF`, closing any descriptor of the file drops the process's locks on it, and exit releases them all. Before an unlock, the process writes back what it changed, so the next holder reads it. An open file's directory entry is checked against the change ring too: a name another process removed or replaced is looked up again, so sqlite's check for a hot journal sees the other connection's commit. A WASI imports module's `tryLock`/`unlock` keeps its behaviour (whole file, per process, keyed by path). Limits: locks are per kernel, not across tabs ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)); and a WASIX program built against a wasix-sysroot without the import keeps its libc's local answer (every lock granted), so python's sqlite excludes other processes only once wasix-python is rebuilt on the sysroot that has it ([homescoop#407](https://github.com/ai-ecoverse/homescoop/issues/407)). An Emscripten program's `flock()` is a stub compiled into its libc that never reaches the kernel either, until a homescoop shim routes it to a kernel import ([#680](https://github.com/ai-ecoverse/slicc-kernel/issues/680)). A lock belongs to the file its descriptor was opened on: a file renamed over the name later, or the name's removal, doesn't move it.
572
+ - **`slicc.fcntl_lock(fd, cmd, flock_ptr) -> errno`** ([#722](https://github.com/ai-ecoverse/slicc-kernel/issues/722)): a WASIX program's `fcntl` record locks reach the same table as an Emscripten program's, so the two see each other's locks.
573
+ - **cmd:** `12` `F_GETLK`, `13` `F_SETLK` or `14` `F_SETLKW`. Any other cmd is `EINVAL`.
574
+ - **The struct:** musl's wasm32 `struct flock`, 32 bytes: `l_type` i16 at 0 (`0` read, `1` write, `2` unlock), `l_whence` i16 at 2 (`0` SET, `1` CUR, `2` END, resolved against the descriptor's offset or size), `l_start` i64 at 8, `l_len` i64 at 16 (`0` to EOF and beyond, negative before `l_start`), `l_pid` i32 at 24. The kernel reads and writes only those five fields and never the padding.
575
+ - **F_GETLK:** with a conflicting lock, it writes that lock back (`l_whence` 0, its absolute start, its length, the holder's pid). Without one, it sets only `l_type` to `2`.
576
+ - **Errors (WASI numbers):** `EAGAIN` for a conflict on `F_SETLK` (never `EACCES`), `EINTR` when a signal with a handler ends an `F_SETLKW` wait, `EDEADLK` for a wait that would close a cycle, `EBADF` for a bad descriptor or a lock type the descriptor's access mode doesn't allow, and `EINVAL` for a bad `l_type` or `l_whence` or a start before 0.
577
+ - **Semantics:** the same as Emscripten's, above. A libc that finds no import on an older kernel gets `ENOSYS` and keeps its local answer.
572
578
  - **Rules on a mount:**
573
579
  - a rename across mounts is `EXDEV`, so `mv` copies;
574
580
  - a symlink on the root file system that leads onto a mount, such as `~/proj` → `/mnt/drive/proj`, or a chain from a mount to the root and back, is followed onto the mount by every operation, in every runtime: reads, writes, `stat`, listings, `mkdir`, removals, renames, `chmod`, `chown` and times. Removing the link itself removes the link. Before [#547](https://github.com/ai-ecoverse/slicc-kernel/issues/547) was fixed, the kernel resolved such a link against the directory under the mount point for JS and WASI programs, the embedder's `fs` and attached clients (Emscripten programs weren't affected): reads failed with `ENOENT`, and writes went into that hidden directory, which shows again once the mount is gone;
@@ -818,7 +824,7 @@ A character device (`0666`), there in every `users` mode, through which programs
818
824
 
819
825
  ### `/proc`
820
826
 
821
- 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). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
827
+ 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.
822
828
 
823
829
  The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#users)). While the root has none of them, or no `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
824
830
 
@@ -994,6 +1000,12 @@ Each side prints `pattern` (a static array's damage), `low` (bytes of low memory
994
1000
 
995
1001
  **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` (version 132) on `PATH`. `bin/piefork.wasm` is 823,983 bytes, sha256 `0477598c71b02356145d1725548ee0f2b9e53ca0d43c7349f17acc2ff12b59f8`.
996
1002
 
1003
+ ### `forktls`
1004
+
1005
+ Two builds of one C program (`forktls.c` with `tlsvars.c`), a position-independent one (`bin/forktls-pie.wasm`, `WASIXCC_PIC=yes`) and a static one (`bin/forktls-static.wasm`), plus a side module with a thread-local (`tlsside.c`, `lib/libtlsside.so`), for `test/integration/fork-tls.test.mjs` (#728). Before and after a fork, parent and child call `close` on a descriptor that isn't open and print `errno` (read directly and through a pointer), then bump one thread-local in the `local-exec` TLS model and one in `global-dynamic` (`tls_model` attributes, since wasixcc drops `-ftls-model`), and the side module's, when the program is given its path to `dlopen`. A position-independent program reaches `errno` through an internal global the linker sets with the TLS base, as a `global-dynamic` variable does in either build; a forked child that didn't set it again read `errno` as 0 and those variables at the wrong address.
1006
+
1007
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with homescoop's pinned `wasixcc` 0.4.7 and binaryen's `wasm-opt` 133 on `PATH` (the same toolchain rebuilds `bin/piefork.wasm` byte for byte). `bin/forktls-pie.wasm` is 816,066 bytes, sha256 `c5ed6b06036f4d3c42c660fc13ca1a995d647f11021829f2169567574254fd9e`; `bin/forktls-static.wasm` is 45,564 bytes, sha256 `5df019d5f2a4432132c8b7c6f5992d18ecf9d8e4c4be21351acd59c46c3ca48d`; `lib/libtlsside.so` is 736 bytes, sha256 `ecd32e561c051576a99e87caee5e1aca5238c4055e2e551065fa10fe7476d0b5`.
1008
+
997
1009
  ### `semtest`
998
1010
 
999
1011
  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:
@@ -1003,7 +1015,17 @@ A WASIX C program for `test/integration/semaphore.test.mjs` (#717). It calls the
1003
1015
  - `errors`, `unlink`, `owners setup|user`;
1004
1016
  - `killed` (a waiter killed before posts loses no unit), `exited`, `cap`, `handles`.
1005
1017
 
1006
- **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 59,187 bytes, sha256 `c2091c18d799881721bb8e0e2fc7269732d52d6cd545f4f57266e8e8864c9ee6`.
1018
+ **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,078 bytes, sha256 `44178ed41ec8499c3c562b559b961438db135ca0914cc38c12316b8ab2d4e3f1`.
1019
+
1020
+ ### `wlocktest`
1021
+
1022
+ A WASIX C program for `test/integration/wasix-locks.test.mjs` (#722). It calls `slicc.fcntl_lock` directly (`import_module("slicc")`), so the rows don't wait for the wasix-sysroot side. Its arguments are a sequence of steps, run in order:
1023
+ - `open SLOT r|w|rw PATH`, `seek SLOT OFFSET`, `close SLOT`;
1024
+ - `set|setw|get SLOT TYPE WHENCE START LEN`, which call `F_SETLK`, `F_SETLKW` or `F_GETLK` on a struct pre-filled with `0x5a`. `get` reports the holder, or whether every byte but `l_type` stayed as it was;
1025
+ - `cmd SLOT N`, `badfd`;
1026
+ - `touch PATH`, `await PATH`, `sleep MS`, `pid`, `trap` (a `SIGUSR1` handler), `fork SLOT` (the child runs `F_GETLK` and `F_SETLK` on the slot).
1027
+
1028
+ **Build:** `INSTALL_WASIXCC=<homescoop>/scripts/install-wasixcc.sh WASIX_SYSROOT_28=<unpacked @ai-ecoverse/wasix-sysroot 2025.9.30-28> ./build.sh`, with homescoop's pinned wasixcc 0.4.7 (LLVM 21.1.206, binaryen 133) and Homebrew's binaryen `wasm-opt` 132 on `PATH` for the Asyncify pass (`proc_fork`). Two builds give the same bytes: `bin/wlocktest.wasm` is 56,958 bytes, sha256 `31308a54e13e7dcac497ad9d8a05799008c8beaa4ae3a36c76e218af7235ec23`.
1007
1029
 
1008
1030
  ## License
1009
1031
 
@@ -4821,7 +4821,7 @@ async function runTagged(l, self, requested, tag) {
4821
4821
  }
4822
4822
  function processes(l, self) {
4823
4823
  const viewer = credOf(self);
4824
- return l.list(viewer).map(({ tid: _, cred = viewer, umask: _umask, ...entry }) => ({
4824
+ return l.list(viewer).map(({ tid: _, cred = viewer, umask: _umask, threads: _threads, ...entry }) => ({
4825
4825
  ...entry,
4826
4826
  rss: entry.memory,
4827
4827
  uid: cred.euid,
@@ -11593,6 +11593,13 @@ var ProcessClocks = class {
11593
11593
  running() {
11594
11594
  return this.enabled ? this.all().filter((c) => c.running()).length : 0;
11595
11595
  }
11596
+ states() {
11597
+ const state = (c) => c.running() ? "R" : "S";
11598
+ return [
11599
+ { tid: 1, state: state(this.main) },
11600
+ ...[...this.threads].map(([tid, c]) => ({ tid, state: state(c) }))
11601
+ ];
11602
+ }
11596
11603
  timed(clock, dispatch2) {
11597
11604
  const c = clock ?? this.main;
11598
11605
  const block = () => c.block();
@@ -12107,7 +12114,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12107
12114
  var EARLY_WHOLE_FILE = 64 * 1024;
12108
12115
 
12109
12116
  // src/version.ts
12110
- var KERNEL_VERSION = true ? "1.99.2" : "0.0.0-dev";
12117
+ var KERNEL_VERSION = true ? "1.100.1" : "0.0.0-dev";
12111
12118
  var OS_RELEASE = "6.0.0-slicc";
12112
12119
  function procVersion() {
12113
12120
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -12167,11 +12174,11 @@ function pages(bytes) {
12167
12174
  function ticks(ms) {
12168
12175
  return Math.max(0, Math.floor(ms * HZ / 1e3));
12169
12176
  }
12170
- function pidStat(info, boot) {
12177
+ function pidStat(info, boot, state = info.state) {
12171
12178
  const fields = [
12172
12179
  info.pid,
12173
12180
  `(${commOf(info)})`,
12174
- info.state,
12181
+ state,
12175
12182
  info.ppid,
12176
12183
  info.pgid,
12177
12184
  info.sid,
@@ -12209,8 +12216,19 @@ function pidStat(info, boot) {
12209
12216
  `;
12210
12217
  }
12211
12218
  var sigMask = (mask = 0) => (mask >>> 1).toString(16).padStart(16, "0");
12212
- var STATES = { S: "S (sleeping)", T: "T (stopped)", Z: "Z (zombie)" };
12219
+ var STATES = {
12220
+ S: "S (sleeping)",
12221
+ T: "T (stopped)",
12222
+ Z: "Z (zombie)"
12223
+ };
12213
12224
  var tagLine = (tag) => `Tag: ${tag.replaceAll("\n", "\\n").replaceAll(" ", "\\t").replaceAll("\r", "\\r")}`;
12225
+ function taskStat(info, tid, boot) {
12226
+ const thread = (info.threads ?? [{ tid: 1, state: info.state }]).find((t) => t.tid === tid);
12227
+ return thread && pidStat({ ...info, pid: tid }, boot, thread.state);
12228
+ }
12229
+ function taskIds(info) {
12230
+ return (info.threads ?? [{ tid: 1 }]).map((t) => t.tid);
12231
+ }
12214
12232
  function pidStatus(info) {
12215
12233
  const state = STATES[info.state];
12216
12234
  const c = info.cred ?? ROOT;
@@ -12388,8 +12406,10 @@ var ProcTree = class {
12388
12406
  content: () => systemFile(top, this.listing(), Date.now(), this.memory)
12389
12407
  };
12390
12408
  }
12391
- if (!/^[1-9]\d*$/.test(top) || rest2.length > 0 || !this.alive(Number(top))) return void 0;
12392
- if (leaf === void 0) return { kind: "dir", names: () => [...PID_NAMES] };
12409
+ if (!/^[1-9]\d*$/.test(top) || !this.alive(Number(top))) return void 0;
12410
+ if (leaf === "task") return this.task(Number(top), rest2);
12411
+ if (rest2.length > 0) return void 0;
12412
+ if (leaf === void 0) return { kind: "dir", names: () => [...PID_NAMES, "task"] };
12393
12413
  if (!PID_NAMES.includes(leaf)) return void 0;
12394
12414
  return {
12395
12415
  kind: "file",
@@ -12401,6 +12421,28 @@ var ProcTree = class {
12401
12421
  }
12402
12422
  };
12403
12423
  }
12424
+ info(pid) {
12425
+ return this.listing().processes.find((p) => p.pid === pid);
12426
+ }
12427
+ task(pid, rest2) {
12428
+ const [tid, leaf, ...more] = rest2;
12429
+ const tids = () => taskIds(this.info(pid));
12430
+ if (tid === void 0) return { kind: "dir", names: () => tids().map(String) };
12431
+ if (!/^[1-9]\d*$/.test(tid) || more.length > 0 || !tids().includes(Number(tid)))
12432
+ return void 0;
12433
+ if (leaf === void 0) return { kind: "dir", names: () => ["stat"] };
12434
+ if (leaf !== "stat") return void 0;
12435
+ return {
12436
+ kind: "file",
12437
+ content: () => {
12438
+ const listing = this.listing();
12439
+ const info = listing.processes.find((p) => p.pid === pid);
12440
+ const line = info && taskStat(info, Number(tid), listing.boot);
12441
+ if (line === void 0) throw syncError("ENOENT", `/proc/${pid}/task/${tid}/stat`);
12442
+ return line;
12443
+ }
12444
+ };
12445
+ }
12404
12446
  parts(path, follow2) {
12405
12447
  const parts2 = path.split("/").filter((p) => p !== "").slice(1);
12406
12448
  if (parts2[0] === "self" && (follow2 || parts2.length > 1)) parts2[0] = String(this.pid);
@@ -13147,7 +13189,7 @@ async function recordLock2(req, ctx) {
13147
13189
  return done2;
13148
13190
  }
13149
13191
  const range = lockRange(req.start, req.len);
13150
- const key = await fileKey(target, req.kind);
13192
+ const key = await fileKey(target, req.kind, req.op === "lk-set" && req.fd !== void 0);
13151
13193
  if (req.op === "lk-get") {
13152
13194
  const held = locks.holder(key, owner, { key, kind: req.kind, ...range });
13153
13195
  if (!held) return json3(null);
@@ -15967,7 +16009,7 @@ var WasmProcess = class {
15967
16009
  return await hostSyscall(req, {
15968
16010
  pid: this.pid,
15969
16011
  owner: () => this.lockOwner(),
15970
- fileKey: (target, kind) => this.lockKey(target, kind),
16012
+ fileKey: (target, kind, setting) => this.lockKey(target, kind, setting),
15971
16013
  locks: this.options.locks,
15972
16014
  ops: this.asyncOps,
15973
16015
  blocking: () => this.blockingSignal(cancelled),
@@ -16019,8 +16061,11 @@ var WasmProcess = class {
16019
16061
  }
16020
16062
  return path;
16021
16063
  }
16022
- async lockKey(target, kind) {
16064
+ async lockKey(target, kind, setting = false) {
16023
16065
  const file = target.fd === void 0 ? void 0 : this.fds.get(target.fd);
16066
+ const access2 = file?.file;
16067
+ if (setting && (kind === "read" && !access2?.read || kind === "write" && !access2?.write))
16068
+ throw new KernelError("EBADF");
16024
16069
  const known = file ? fileKeys.get(file) : this.pathKeys.get(target.path ?? "");
16025
16070
  if (kind === "close" && !file && target.path !== void 0) this.pathKeys.delete(target.path);
16026
16071
  const want = kind === "read" ? R_OK : kind === "write" ? W_OK : 0;
@@ -17385,8 +17430,14 @@ function spawnWasmProcess(opts) {
17385
17430
  umask: () => process.umask,
17386
17431
  cred: () => copyCred(process.cred),
17387
17432
  writesBack: () => opts.program.abi === "wasi" && !done4,
17433
+ ...clockViews(clocks)
17434
+ };
17435
+ }
17436
+ function clockViews(clocks) {
17437
+ return {
17388
17438
  times: () => clocks.times(),
17389
- running: () => clocks.running()
17439
+ running: () => clocks.running(),
17440
+ threads: () => clocks.states()
17390
17441
  };
17391
17442
  }
17392
17443
 
@@ -21805,6 +21856,7 @@ var Launcher = class {
21805
21856
  ...described2,
21806
21857
  started: (this.described.get(root.pid) ?? described2).started,
21807
21858
  state: this.stateOf(member.pid),
21859
+ ...this.threadsOf(member.pid),
21808
21860
  memory: this.processes.get(member.pid)?.memory() ?? 0,
21809
21861
  ...this.timesOf(member, members),
21810
21862
  ...this.childrenOf(root.pid),
@@ -21816,6 +21868,10 @@ var Launcher = class {
21816
21868
  }
21817
21869
  return listed;
21818
21870
  }
21871
+ threadsOf(pid) {
21872
+ const threads = this.stateOf(pid) === "S" ? this.processes.get(pid)?.threads?.() : void 0;
21873
+ return threads ? { threads } : {};
21874
+ }
21819
21875
  sudo(pid, call, signal) {
21820
21876
  const caller = {
21821
21877
  cred: this.credOf(pid),