@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 +26 -4
- package/dist/kernel-worker.js +67 -11
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +210 -40
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +67 -11
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +176 -6
- package/dist/process-worker.js.map +4 -4
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 = {
|
|
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) ||
|
|
12392
|
-
if (leaf ===
|
|
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),
|