@ai-ecoverse/slicc-kernel 1.98.4 → 1.99.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
@@ -278,6 +278,36 @@ On top of preview1 a WASI program gets:
278
278
  - **Toolchain problems:**
279
279
  - **wasix-sysroot 2025.9.30-28's `epoll_ctl`:** it passes Linux's operation numbers (ADD 1, DEL 2, MOD 3) to WASIX untranslated, where WASIX numbers them ADD 0, MOD 1, DEL 2. The kernel takes both: 0 adds, 2 deletes, 3 modifies, and 1 adds a descriptor that isn't registered and modifies one that is. So a C program's second ADD of a descriptor modifies it rather than failing with `EEXIST`, and a WASIX MOD of an unregistered one adds it. That libc also returns the error number instead of -1, so its callers can't see a failure. Fixing the sysroot is [homescoop#443](https://github.com/ai-ecoverse/homescoop/issues/443); dropping the dual numbering once the fixed sysroot ships is [#684](https://github.com/ai-ecoverse/slicc-kernel/issues/684).
280
280
  - **The WASIX mio fork (1.0.3+wasix.1):** it opens its sockets blocking, unlike upstream mio's `SOCK_NONBLOCK`. On a socket whose send buffer is full, a tokio program blocks its runtime's thread, as it would on Wasmer.
281
+ - **Named semaphores** (#717): `sem_open` and its family are kernel objects, so processes share them by name. Python's `multiprocessing` `Lock`, `Queue` and `Pool` use them; wasix-libc's `sem_*` calls these imports. Every import returns a WASI errno, and handles are u32 ≥ 1:
282
+ - **The imports:**
283
+ - `slicc.sem_open(name_ptr, name_len, oflag, mode, value, handle_out)`;
284
+ - `slicc.sem_close(handle)`;
285
+ - `slicc.sem_unlink(name_ptr, name_len)`;
286
+ - `slicc.sem_post(handle)`;
287
+ - `slicc.sem_wait(handle, how, clock, deadline_ns: i64)`;
288
+ - `slicc.sem_getvalue(handle, value_out)`.
289
+ - **Names:** checked as musl's are. Leading `/`s are skipped, so `/a` and `a` are one semaphore. Another `/`, an empty name, `.` or `..` is `EINVAL`, and more than 251 bytes is `ENAMETOOLONG`.
290
+ - **`sem_open` arguments:**
291
+ - `oflag` is the kernel's own bits, `1` for `O_CREAT` and `2` for `O_EXCL`, not libc's `O_*` values;
292
+ - `mode` has the process's umask applied;
293
+ - `value` must be 0 to `SEM_VALUE_MAX` (2^31-1), else `EINVAL`.
294
+ - **`sem_open` results:**
295
+ - an existing name with `O_CREAT|O_EXCL` is `EEXIST`, and a missing one without `O_CREAT` is `ENOENT`;
296
+ - with users on, opening needs read and write permission by the mode and owner, and only the owner or root may `sem_unlink`. A denial goes to `onDenial` as `sem_open` or `sem_unlink` of `/dev/shm/sem.<name>`: `enforce` refuses it with `EACCES`, `report` logs it and goes ahead;
297
+ - opening a semaphore the process already has open returns the same handle (one `sem_close` per open).
298
+ - **Waiting:**
299
+ - `how` 0 blocks; 1 tries (`EAGAIN`); 2 waits until `deadline_ns` on `clock` (0 `CLOCK_REALTIME`, as `sem_timedwait`; 1 `CLOCK_MONOTONIC`, as `sem_clockwait`).
300
+ - A timed wait gives `ETIMEDOUT`, never early, also when the deadline has already passed. A negative deadline is `EINVAL`.
301
+ - A wait blocks only its own thread, and a signal handler ends it with `EINTR`.
302
+ - **Posting:** `sem_post` gives the unit to the longest-waiting thread, or adds it to the value; at the maximum it's `EOVERFLOW`.
303
+ - **Lifetime:**
304
+ - A forked child inherits the open handles; spawn and exec start with none, and exit closes them.
305
+ - `sem_unlink` removes the name, but the semaphore lives on while handles to it remain.
306
+ - Nothing is undone when a process exits.
307
+ - **Limits:**
308
+ - Semaphores exist only within one kernel (not across tabs), and they don't appear in `/dev/shm`.
309
+ - 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
+ - `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.
281
311
  - **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.
282
312
  - **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 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).
283
313
  - **Blocked signals** (`sigprocmask`, `pthread_sigmask`, `sigpending`): a program whose libc imports `slicc.sigmask_set(how, set_lo, set_hi, old_ptr)` and `slicc.sigpending_get(ptr)` (wasix-sysroot 2025.9.30-28 and later) has a blocked mask per thread. `how` is musl's `SIG_BLOCK` 0, `SIG_UNBLOCK` 1 or `SIG_SETMASK` 2, the set is the 64-bit `sigset_t` (bit `sig - 1`), and the old mask is written as a u64 at `old_ptr`; any other `how` is `EINVAL`. `SIGKILL` and `SIGSTOP` cannot be blocked. A blocked signal is not delivered, does not interrupt a sleep or a blocked call, and its default action (terminate or stop) waits: it stays pending, `sigpending` shows it (the thread's pending signals and the process's), and it is delivered when it is unblocked, before `sigmask_set` returns. A signal ignored with `SIG_IGN` is discarded even while blocked. A forked child (with the mask of the thread that forked), the program an `exec` starts and a spawned child keep the mask, all 64 bits of it, and so does a new thread; `/proc/<pid>/status` shows the main thread's as `SigBlk`. The mask is kept in a word of the worker's shared header, so changing it costs no kernel call. A program without the import (an older libc) has no mask and gets every signal at once, as before, and one it starts by `exec` with a mask set starts with none. A signal for the whole process goes to the main thread, even if the main thread blocks it and another thread does not. A signal sent to one thread (`pthread_kill`) that it blocks waits for that thread, and if it has no handler, the thread's unblock ends the whole process by the signal, whatever the other threads block. `slicc.sigmask_poll(set_lo, set_hi, in_ptr, out_ptr, nsubscriptions, nevents_ptr)` is `pselect`, `ppoll` and `sigsuspend`: it swaps the thread's mask to the given set for the call and puts the old one back before it returns. It delivers a pending signal the new mask unblocks, without polling, or polls as `poll_oneoff` does, under the new mask, so a signal it unblocks interrupts the wait with no window between the swap and the wait; with no subscriptions it waits until a signal is delivered. An interrupted call returns `EINTR` with no events, after the handler has run.
@@ -316,7 +346,7 @@ The module's default export (or its `main` export) is called with a context and
316
346
  - `fdStatus(fd)` (`{ type, tty, seekable }`, `type` being `file`, `pipe`, `tty`, `socket`, `device` or `directory`) and `isatty(fd)`, to tell `< file` from a pipe;
317
347
  - `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;
318
348
  - `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;
319
- - `/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` and `/dev/ptmx` aren'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;
349
+ - `/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;
320
350
  - `/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;
321
351
  - `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;
322
352
  - `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;
@@ -524,6 +554,8 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
524
554
 
525
555
  `kernel.mount({ type, source, target, options })` mounts a file system on an existing directory. Every process sees it, Emscripten and WASI alike, and so does every attached client. `kernel.umount(target)` unmounts it, and fails with `EBUSY` while a process has a file open under it. `kernel.mounts()` lists the table, as does `/proc/mounts`. The Node entry and attached clients have the same three calls (client protocol 1.2).
526
556
 
557
+ `/dev/shm` is a tmpfs the kernel mounts at boot (source `shm`), mode `1777` and owned by root, so with `users` the sticky bit keeps one user from removing or renaming another's file there (`EPERM`). It's in `/proc/mounts`, but not in `kernel.mounts()`, which lists the mounts a caller made. Every process sees the same files, Emscripten (whose glue otherwise keeps a private `/dev/shm` in memory), WASI and JS programs alike, and they last until the kernel terminates, which is what musl's `shm_open` and `sem_open` need.
558
+
527
559
  - **Built in:** `tmpfs`, which lives in memory until unmounted, in 1 MiB chunks; `fsa`, a folder the user picks (see [Removable media](#removable-media-fsa)); and `hostfs`, a folder the local proxy exports (see [Host folders](#host-folders-hostfs)).
528
560
  - **Package drivers:** a package declares a type in `"slicc": { "filesystems": { "<type>": { "module": "<file>" } } }`, and the kernel starts that module in a worker of its own for each mount. The module is a single self-contained file: it can't import bare specifiers.
529
561
  - Its default export receives `{ fetch }`, the kernel's network transport. It returns `{ handlers, capabilities }`.
@@ -933,6 +965,17 @@ Each side prints `pattern` (a static array's damage), `low` (bytes of low memory
933
965
 
934
966
  **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`.
935
967
 
968
+ ### `semtest`
969
+
970
+ 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:
971
+ - `handoff`, `inherit`, `spawn` (a spawned and an exec'd image open the semaphore by name; exec drops the handles);
972
+ - `try`, `timed`, `eintr`;
973
+ - `threads` (FIFO, one thread blocked at a time);
974
+ - `errors`, `unlink`, `owners setup|user`;
975
+ - `killed` (a waiter killed before posts loses no unit), `exited`, `cap`, `handles`.
976
+
977
+ **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`.
978
+
936
979
  ## License
937
980
 
938
981
  Apache-2.0
@@ -3052,6 +3052,7 @@ function kernelFdKind(file) {
3052
3052
  }
3053
3053
  var FdTable = class _FdTable {
3054
3054
  static MAX_FDS = 1024;
3055
+ handles;
3055
3056
  fds = /* @__PURE__ */ new Map();
3056
3057
  cloexec = /* @__PURE__ */ new Set();
3057
3058
  status = /* @__PURE__ */ new Map();
@@ -3148,6 +3149,7 @@ var FdTable = class _FdTable {
3148
3149
  for (const [fd, file] of this.fds) child.fds.set(fd, file.retain());
3149
3150
  for (const fd of this.cloexec) child.cloexec.add(fd);
3150
3151
  for (const [fd, flags2] of this.status) child.status.set(fd, flags2);
3152
+ child.handles = this.handles?.fork();
3151
3153
  return child;
3152
3154
  }
3153
3155
  async closeAll() {
@@ -3155,6 +3157,7 @@ var FdTable = class _FdTable {
3155
3157
  this.fds.clear();
3156
3158
  this.cloexec.clear();
3157
3159
  this.status.clear();
3160
+ this.handles?.clear();
3158
3161
  await Promise.allSettled(files.map((file) => Promise.resolve().then(() => file.release())));
3159
3162
  }
3160
3163
  };
@@ -5060,7 +5063,7 @@ function serveClient(port, host) {
5060
5063
  case "umount":
5061
5064
  return { result: await l.umountAs(clientCred(self), req.target).then(() => true) };
5062
5065
  case "mounts":
5063
- return { result: l.mounts.list() };
5066
+ return { result: l.mounts.listed() };
5064
5067
  case "dial":
5065
5068
  return dialFor(l, req, watches);
5066
5069
  case "serve-cdp":
@@ -8446,6 +8449,18 @@ function withCommandDirs(fs, names, userDir = async () => void 0, owners = false
8446
8449
  };
8447
8450
  }
8448
8451
 
8452
+ // src/fs/dev-removal.ts
8453
+ var SHM = "/dev/shm";
8454
+ function inDevTree(path) {
8455
+ if (path.startsWith(`${SHM}/`)) return false;
8456
+ return path === "/dev" || path.startsWith("/dev/");
8457
+ }
8458
+ function devRemoval(path, cred) {
8459
+ if (!inDevTree(path)) return void 0;
8460
+ if (cred && cred.euid !== 0) return "EACCES";
8461
+ return path === "/dev" ? "EBUSY" : "EPERM";
8462
+ }
8463
+
8449
8464
  // src/fs/fresh-modules.ts
8450
8465
  var MAGIC = [0, 97, 115, 109];
8451
8466
  var MAX_FRESH = 65536;
@@ -9623,6 +9638,7 @@ var MountTable = class {
9623
9638
  devices = 0;
9624
9639
  pending = /* @__PURE__ */ new Set();
9625
9640
  notes = /* @__PURE__ */ new Map();
9641
+ system = /* @__PURE__ */ new Set();
9626
9642
  deps;
9627
9643
  constructor(deps) {
9628
9644
  this.deps = deps;
@@ -9644,6 +9660,13 @@ var MountTable = class {
9644
9660
  const noted = [...this.notes.values()].filter((note) => !this.mounts.has(note.target));
9645
9661
  return [...mounted, ...noted.map((note) => ({ ...note, options: { ...note.options } }))];
9646
9662
  }
9663
+ listed() {
9664
+ return this.list().filter((entry) => !this.system.has(entry.target));
9665
+ }
9666
+ async mountSystem(spec, fs) {
9667
+ await this.mount(spec, fs);
9668
+ this.system.add(normalizePath(spec.target));
9669
+ }
9647
9670
  note(spec, state, error) {
9648
9671
  const target = normalizePath(spec.target);
9649
9672
  this.notes.set(target, {
@@ -9750,6 +9773,7 @@ var MountTable = class {
9750
9773
  if (!mount && this.notes.delete(at)) return;
9751
9774
  if (!mount) throw errnoError("EINVAL", `${at} is not mounted`);
9752
9775
  if (!detach && this.deps.busy(at)) throw errnoError("EBUSY", `${at} has open files`);
9776
+ this.system.delete(at);
9753
9777
  this.notes.delete(at);
9754
9778
  this.mounts.delete(at);
9755
9779
  this.deps.reshaped?.();
@@ -10658,16 +10682,6 @@ var MountFs = class _MountFs {
10658
10682
  }
10659
10683
  };
10660
10684
 
10661
- // src/fs/dev-removal.ts
10662
- function inDevTree(path) {
10663
- return path === "/dev" || path.startsWith("/dev/");
10664
- }
10665
- function devRemoval(path, cred) {
10666
- if (!inDevTree(path)) return void 0;
10667
- if (cred && cred.euid !== 0) return "EACCES";
10668
- return path === "/dev" ? "EBUSY" : "EPERM";
10669
- }
10670
-
10671
10685
  // src/realm/sync-fs-token-registry.ts
10672
10686
  var registry = /* @__PURE__ */ new Map();
10673
10687
  function mintSyncFsToken(entry) {
@@ -12082,7 +12096,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12082
12096
  var EARLY_WHOLE_FILE = 64 * 1024;
12083
12097
 
12084
12098
  // src/version.ts
12085
- var KERNEL_VERSION = true ? "1.98.4" : "0.0.0-dev";
12099
+ var KERNEL_VERSION = true ? "1.99.1" : "0.0.0-dev";
12086
12100
  var OS_RELEASE = "6.0.0-slicc";
12087
12101
  function procVersion() {
12088
12102
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -14516,6 +14530,206 @@ async function waitReady(fds, read, write, deadline, interrupted3) {
14516
14530
  }
14517
14531
  }
14518
14532
 
14533
+ // src/kernel/sem.ts
14534
+ var SEM_VALUE_MAX = 2147483647;
14535
+ var SEM_NAMES_MAX = 4096;
14536
+ var SEM_HANDLES_MAX = 256;
14537
+ var NAME_MAX = 251;
14538
+ var TIMER_MAX_MS = 2147483647;
14539
+ var SEM_OPS = [
14540
+ "sem-open",
14541
+ "sem-close",
14542
+ "sem-unlink",
14543
+ "sem-post",
14544
+ "sem-wait",
14545
+ "sem-getvalue"
14546
+ ];
14547
+ var Semaphore = class {
14548
+ value;
14549
+ uid;
14550
+ gid;
14551
+ mode;
14552
+ waiters = [];
14553
+ constructor(value, uid, gid, mode) {
14554
+ this.value = value;
14555
+ this.uid = uid;
14556
+ this.gid = gid;
14557
+ this.mode = mode;
14558
+ }
14559
+ post() {
14560
+ const next2 = this.waiters.shift();
14561
+ if (next2) next2.grant();
14562
+ else if (this.value >= SEM_VALUE_MAX) throw new KernelError("EOVERFLOW");
14563
+ else this.value++;
14564
+ }
14565
+ take() {
14566
+ if (this.value === 0) return false;
14567
+ this.value--;
14568
+ return true;
14569
+ }
14570
+ wait(signal, timeoutMs) {
14571
+ if (this.take()) return Promise.resolve(true);
14572
+ if (signal.aborted) return Promise.reject(new KernelError("EINTR"));
14573
+ return new Promise((resolve2, reject) => {
14574
+ const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : performance.now() + timeoutMs;
14575
+ let timer;
14576
+ const leave = () => {
14577
+ const at = this.waiters.indexOf(waiter);
14578
+ if (at >= 0) this.waiters.splice(at, 1);
14579
+ clearTimeout(timer);
14580
+ signal.removeEventListener("abort", abort);
14581
+ };
14582
+ const waiter = {
14583
+ grant: () => {
14584
+ leave();
14585
+ resolve2(true);
14586
+ }
14587
+ };
14588
+ const abort = () => {
14589
+ leave();
14590
+ reject(new KernelError("EINTR"));
14591
+ };
14592
+ const expire = () => {
14593
+ const left = deadline - performance.now();
14594
+ if (left > 0) {
14595
+ timer = setTimeout(expire, Math.min(left, TIMER_MAX_MS));
14596
+ return;
14597
+ }
14598
+ leave();
14599
+ resolve2(false);
14600
+ };
14601
+ this.waiters.push(waiter);
14602
+ signal.addEventListener("abort", abort, { once: true });
14603
+ if (deadline !== Number.POSITIVE_INFINITY) {
14604
+ timer = setTimeout(expire, Math.min(timeoutMs, TIMER_MAX_MS));
14605
+ }
14606
+ });
14607
+ }
14608
+ };
14609
+ var SemTable = class {
14610
+ names = /* @__PURE__ */ new Map();
14611
+ limit;
14612
+ constructor(limit = SEM_NAMES_MAX) {
14613
+ this.limit = limit;
14614
+ }
14615
+ open(name2, create, exclusive, make, check, adopt) {
14616
+ const found = this.names.get(name2);
14617
+ if (found) {
14618
+ if (create && exclusive) throw new KernelError("EEXIST");
14619
+ check(found);
14620
+ return adopt(found);
14621
+ }
14622
+ if (!create) throw new KernelError("ENOENT");
14623
+ if (this.names.size >= this.limit) throw new KernelError("ENOSPC");
14624
+ const sem = make();
14625
+ const handle = adopt(sem);
14626
+ this.names.set(name2, sem);
14627
+ return handle;
14628
+ }
14629
+ unlink(name2, check) {
14630
+ const found = this.names.get(name2);
14631
+ if (!found) throw new KernelError("ENOENT");
14632
+ check(found);
14633
+ this.names.delete(name2);
14634
+ }
14635
+ };
14636
+ var SemHandles = class _SemHandles {
14637
+ byHandle = /* @__PURE__ */ new Map();
14638
+ next = 1;
14639
+ add(sem) {
14640
+ for (const [handle2, entry] of this.byHandle) {
14641
+ if (entry.sem === sem) {
14642
+ entry.opens++;
14643
+ return handle2;
14644
+ }
14645
+ }
14646
+ if (this.byHandle.size >= SEM_HANDLES_MAX) throw new KernelError("EMFILE");
14647
+ const handle = this.next++;
14648
+ this.byHandle.set(handle, { sem, opens: 1 });
14649
+ return handle;
14650
+ }
14651
+ get(handle) {
14652
+ const entry = this.byHandle.get(handle);
14653
+ if (!entry) throw new KernelError("EINVAL");
14654
+ return entry.sem;
14655
+ }
14656
+ close(handle) {
14657
+ const entry = this.byHandle.get(handle);
14658
+ if (!entry) throw new KernelError("EINVAL");
14659
+ if (--entry.opens === 0) this.byHandle.delete(handle);
14660
+ }
14661
+ fork() {
14662
+ const child = new _SemHandles();
14663
+ child.next = this.next;
14664
+ for (const [handle, entry] of this.byHandle) child.byHandle.set(handle, { ...entry });
14665
+ return child;
14666
+ }
14667
+ clear() {
14668
+ this.byHandle.clear();
14669
+ }
14670
+ };
14671
+ function semName(raw) {
14672
+ const name2 = raw.replace(/^\/+/, "");
14673
+ if (name2 === "" || name2.includes("/") || name2 === "." || name2 === "..") {
14674
+ throw new KernelError("EINVAL");
14675
+ }
14676
+ if (new TextEncoder().encode(name2).length > NAME_MAX) throw new KernelError("ENAMETOOLONG");
14677
+ return name2;
14678
+ }
14679
+ var ok2 = (json5) => json5 === void 0 ? { ok: true, kind: "void" } : { ok: true, kind: "json", json: json5 };
14680
+ async function semSyscall(req, proc) {
14681
+ const { table, handles, cred } = proc;
14682
+ if (!table) throw new KernelError("ENOSYS");
14683
+ const { denied: denied2 } = proc;
14684
+ const shown = (name2) => `/dev/shm/sem.${name2}`;
14685
+ const opens = (name2) => (sem) => {
14686
+ const perm = { uid: sem.uid, gid: sem.gid, mode: sem.mode };
14687
+ if (denied2 && !permits(cred, perm, R_OK | W_OK, false)) denied2("sem_open", shown(name2));
14688
+ };
14689
+ const unlinks = (name2) => (sem) => {
14690
+ if (denied2 && cred.euid !== 0 && cred.euid !== sem.uid) denied2("sem_unlink", shown(name2));
14691
+ };
14692
+ switch (req.op) {
14693
+ case "sem-open": {
14694
+ if (req.value < 0 || req.value > SEM_VALUE_MAX) throw new KernelError("EINVAL");
14695
+ const name2 = semName(req.name);
14696
+ const mode = req.mode & 511 & ~proc.umask;
14697
+ return ok2(
14698
+ table.open(
14699
+ name2,
14700
+ req.create,
14701
+ req.exclusive,
14702
+ () => new Semaphore(req.value, cred.euid, cred.egid, mode),
14703
+ opens(name2),
14704
+ (sem) => handles.add(sem)
14705
+ )
14706
+ );
14707
+ }
14708
+ case "sem-close":
14709
+ handles.close(req.handle);
14710
+ return ok2();
14711
+ case "sem-unlink": {
14712
+ const name2 = semName(req.name);
14713
+ table.unlink(name2, unlinks(name2));
14714
+ return ok2();
14715
+ }
14716
+ case "sem-post":
14717
+ handles.get(req.handle).post();
14718
+ return ok2();
14719
+ case "sem-getvalue":
14720
+ return ok2(handles.get(req.handle).value);
14721
+ case "sem-wait": {
14722
+ const sem = handles.get(req.handle);
14723
+ if (sem.take()) return ok2();
14724
+ if (req.how === "try") throw new KernelError("EAGAIN");
14725
+ if (req.how === "timed" && req.timeoutMs === 0) throw new KernelError("ETIMEDOUT");
14726
+ const got = await sem.wait(proc.blocking(), req.how === "block" ? -1 : req.timeoutMs);
14727
+ if (!got) throw new KernelError("ETIMEDOUT");
14728
+ return ok2();
14729
+ }
14730
+ }
14731
+ }
14732
+
14519
14733
  // src/kernel/net/datagram-wire.ts
14520
14734
  function encodeReceived(got) {
14521
14735
  const host = new TextEncoder().encode(got.host);
@@ -14562,20 +14776,20 @@ function dgramAt(fds, fd) {
14562
14776
  if (!(file instanceof KernelDgram)) throw new KernelError("EOPNOTSUPP");
14563
14777
  return file;
14564
14778
  }
14565
- var ok2 = (json5) => json5 === void 0 ? { ok: true, kind: "void" } : { ok: true, kind: "json", json: json5 };
14779
+ var ok3 = (json5) => json5 === void 0 ? { ok: true, kind: "void" } : { ok: true, kind: "json", json: json5 };
14566
14780
  async function socketSyscall(req, proc) {
14567
14781
  const { fds, net } = proc;
14568
14782
  switch (req.op) {
14569
14783
  case "sock-open": {
14570
14784
  if (req.type === "dgram" && req.domain !== "inet") throw new KernelError("EPROTONOSUPPORT");
14571
14785
  const file = req.type === "dgram" ? new KernelDgram(net) : net.socket(req.domain);
14572
- return ok2(fds.install(new OpenFile(file), 3));
14786
+ return ok3(fds.install(new OpenFile(file), 3));
14573
14787
  }
14574
14788
  case "sock-pair": {
14575
14789
  const [a, b] = KernelSocket.pair(net, req.domain, proc.cred?.());
14576
14790
  const first = fds.install(new OpenFile(a), 3);
14577
14791
  try {
14578
- return ok2([first, fds.install(new OpenFile(b), 3)]);
14792
+ return ok3([first, fds.install(new OpenFile(b), 3)]);
14579
14793
  } catch (e) {
14580
14794
  await Promise.resolve(fds.close(first));
14581
14795
  throw e;
@@ -14583,32 +14797,32 @@ async function socketSyscall(req, proc) {
14583
14797
  }
14584
14798
  case "sock-bind":
14585
14799
  anySocketAt(fds, req.fd).bind(req.addr);
14586
- return ok2();
14800
+ return ok3();
14587
14801
  case "sock-listen":
14588
14802
  socketAt(fds, req.fd).listen(req.backlog, proc.cred?.());
14589
- return ok2();
14803
+ return ok3();
14590
14804
  case "sock-accept":
14591
- return ok2(await accept(proc, req.fd, req.nonblock));
14805
+ return ok3(await accept(proc, req.fd, req.nonblock));
14592
14806
  case "sock-connect":
14593
14807
  return connect(req, proc);
14594
14808
  case "sock-shutdown":
14595
14809
  anySocketAt(fds, req.fd).shutdown(req.how);
14596
- return ok2();
14810
+ return ok3();
14597
14811
  case "sock-name": {
14598
14812
  const socket = anySocketAt(fds, req.fd);
14599
14813
  const addr = req.peer ? socket.peer : socket.local;
14600
14814
  if (req.peer && !addr) throw new KernelError("ENOTCONN");
14601
- return ok2(addr ?? unnamed(socket instanceof KernelDgram ? "inet" : socket.domain));
14815
+ return ok3(addr ?? unnamed(socket instanceof KernelDgram ? "inet" : socket.domain));
14602
14816
  }
14603
14817
  case "sock-getopt":
14604
- return ok2(anySocketAt(fds, req.fd).getOption(req.level, req.name));
14818
+ return ok3(anySocketAt(fds, req.fd).getOption(req.level, req.name));
14605
14819
  case "sock-setopt":
14606
14820
  anySocketAt(fds, req.fd).setOption(req.level, req.name, req.value);
14607
- return ok2();
14821
+ return ok3();
14608
14822
  case "sock-resolve": {
14609
14823
  const addresses = await proc.resolve(req.name, req.family, () => proc.blocking());
14610
14824
  if (addresses.length === 0) throw new KernelError("ENOENT");
14611
- return ok2(addresses);
14825
+ return ok3(addresses);
14612
14826
  }
14613
14827
  default:
14614
14828
  return datagram(req, proc);
@@ -14618,7 +14832,7 @@ async function connect(req, proc) {
14618
14832
  const target = anySocketAt(proc.fds, req.fd);
14619
14833
  if (target instanceof KernelDgram) {
14620
14834
  target.connectTo(req.addr);
14621
- return ok2();
14835
+ return ok3();
14622
14836
  }
14623
14837
  const stamp = proc.net.facadeAt(req.addr) && await proc.stamp?.();
14624
14838
  const dialing = target.connect(req.addr, true, proc.cred?.(), stamp || void 0);
@@ -14627,13 +14841,13 @@ async function connect(req, proc) {
14627
14841
  throw new KernelError("EINPROGRESS");
14628
14842
  }
14629
14843
  if (dialing !== void 0) await interruptible2(dialing, proc.blocking());
14630
- return ok2();
14844
+ return ok3();
14631
14845
  }
14632
14846
  async function datagram(req, proc) {
14633
14847
  const dgram = dgramAt(proc.fds, req.fd);
14634
14848
  if (req.op === "sock-sendto") {
14635
14849
  if (req.nonblock && !pollFile(dgram).writable) throw new KernelError("EAGAIN");
14636
- return ok2(await dgram.send(req.body, req.addr));
14850
+ return ok3(await dgram.send(req.body, req.addr));
14637
14851
  }
14638
14852
  const ready2 = pollFile(dgram).readable;
14639
14853
  if (req.nonblock && !ready2) throw new KernelError("EAGAIN");
@@ -15399,6 +15613,9 @@ function isHttpSyscall(req) {
15399
15613
  function isSocketSyscall(req) {
15400
15614
  return req.op.startsWith("sock-");
15401
15615
  }
15616
+ function isSemSyscall(req) {
15617
+ return req.op.startsWith("sem-");
15618
+ }
15402
15619
  function isEpollSyscall(req) {
15403
15620
  return EPOLL_OPS.includes(req.op);
15404
15621
  }
@@ -15493,6 +15710,7 @@ var SYSCALL_OPS = /* @__PURE__ */ new Set([
15493
15710
  "sig-pause",
15494
15711
  ...SOCKET_OPS,
15495
15712
  ...EPOLL_OPS,
15713
+ ...SEM_OPS,
15496
15714
  ...HTTP_OPS,
15497
15715
  ...PTY_OPS,
15498
15716
  ...HOST_OPS
@@ -15725,6 +15943,7 @@ var WasmProcess = class {
15725
15943
  if (isTtySyscall(req)) return this.ttySyscall(req);
15726
15944
  if (isJobSyscall(req)) return this.jobSyscall(req);
15727
15945
  if (isSocketSyscall(req)) return await this.socketSyscall(req, cancelled);
15946
+ if (isSemSyscall(req)) return await this.semSyscall(req, cancelled);
15728
15947
  if (isEpollSyscall(req)) {
15729
15948
  return await epollSyscall(req, {
15730
15949
  fds: this.fds,
@@ -16444,6 +16663,20 @@ var WasmProcess = class {
16444
16663
  if (!http) return Promise.resolve({ ok: false, errno: "ENETUNREACH", message: NO_TRANSPORT });
16445
16664
  return http.syscall(req);
16446
16665
  }
16666
+ semSyscall(req, cancelled) {
16667
+ const g = this.guarded();
16668
+ const known = this.fds.handles;
16669
+ const handles = known instanceof SemHandles ? known : new SemHandles();
16670
+ this.fds.handles = handles;
16671
+ return semSyscall(req, {
16672
+ table: this.options.sems,
16673
+ handles,
16674
+ cred: this.cred,
16675
+ denied: g ? (op, path) => deny(g, op, path) : void 0,
16676
+ umask: this.umask,
16677
+ blocking: () => this.blockingSignal(cancelled)
16678
+ });
16679
+ }
16447
16680
  socketSyscall(req, cancelled) {
16448
16681
  this.net ??= this.options.net ?? new LoopbackNet();
16449
16682
  const resolver = this.options.resolver ?? HOSTS_ONLY;
@@ -16954,6 +17187,7 @@ function processOptions(opts, header3, hooks, created3) {
16954
17187
  net: opts.net,
16955
17188
  http: opts.http,
16956
17189
  locks: opts.locks,
17190
+ ...opts.sems ? { sems: opts.sems } : {},
16957
17191
  onReap: opts.onReap,
16958
17192
  resolver: opts.resolver,
16959
17193
  ...notifiers(header3),
@@ -20324,6 +20558,9 @@ var NOT_EXECUTABLE = 126;
20324
20558
  var DECIDE_MS = 100;
20325
20559
  var INIT_PID = 1;
20326
20560
  var SHARED_DIRS = ["/tmp", "/home"];
20561
+ function unref2(port) {
20562
+ port.unref?.();
20563
+ }
20327
20564
  var LAYOUT = [
20328
20565
  ["/", 493],
20329
20566
  ["/root", 448],
@@ -20512,6 +20749,7 @@ var Launcher = class {
20512
20749
  sudoer;
20513
20750
  jobs = new JobTable();
20514
20751
  locks = new LockTable();
20752
+ sems = new SemTable();
20515
20753
  settling = new SettlingChildren((pid) => this.jobs.pgidOf(pid));
20516
20754
  ptys = new PtyTable(
20517
20755
  (tty, sig) => this.keySignal(tty, 0, sig, () => this.jobs.signalOwnedForeground(tty, sig)),
@@ -20533,6 +20771,7 @@ var Launcher = class {
20533
20771
  pnpmMade;
20534
20772
  pnpmEpoch = 0;
20535
20773
  mounts;
20774
+ shmPorts = [];
20536
20775
  createDriverWorker;
20537
20776
  media;
20538
20777
  onMountPending;
@@ -21370,6 +21609,7 @@ var Launcher = class {
21370
21609
  net: this.net,
21371
21610
  http: new HttpHandles(this.transport, pid, this.loopbackFor(pid)),
21372
21611
  locks: this.locks,
21612
+ sems: this.sems,
21373
21613
  resolver: this.resolver,
21374
21614
  onReap: (child) => this.reaped(child, pid),
21375
21615
  ...req.fork ? { fork: req.fork } : {},
@@ -21796,6 +22036,7 @@ var Launcher = class {
21796
22036
  },
21797
22037
  { owned: true }
21798
22038
  );
22039
+ if (spec.target === SHM) this.shmPorts = [port1, port2];
21799
22040
  return {
21800
22041
  port: port1,
21801
22042
  dispose: () => {
@@ -21898,6 +22139,12 @@ var Launcher = class {
21898
22139
  this.pnpmStale();
21899
22140
  return entry;
21900
22141
  }
22142
+ async mountShm() {
22143
+ await this.base.mkdir(SHM, { recursive: true });
22144
+ await this.mounts.mountSystem({ type: "tmpfs", source: "shm", target: SHM }, this.fs);
22145
+ for (const port of this.shmPorts) unref2(port);
22146
+ await this.fs.chmod(SHM, 1023);
22147
+ }
21901
22148
  async ownTmp() {
21902
22149
  const real = await this.realLayoutPath("/tmp");
21903
22150
  const st = real === void 0 ? void 0 : await this.base.lstat(real).catch(() => void 0);
@@ -22054,6 +22301,7 @@ var Launcher = class {
22054
22301
  await this.resolvePrefixes();
22055
22302
  for (const dir of SHARED_DIRS) await this.base.mkdir(dir, { recursive: true });
22056
22303
  if (this.owners) await this.ownTmp();
22304
+ await this.mountShm();
22057
22305
  await this.pnpmReady();
22058
22306
  await this.users.ensure();
22059
22307
  if (this.owners) await this.stampLayout();
@@ -22602,7 +22850,7 @@ function serveKernel(port, deps) {
22602
22850
  if (req.op === "mount") return l.mount(req.spec);
22603
22851
  if (req.op === "umount") return l.umount(req.target);
22604
22852
  if (req.op === "insert") return l.insert(req.target, req.handle, req.source);
22605
- return l.mounts.list();
22853
+ return l.mounts.listed();
22606
22854
  }
22607
22855
  const attestOp = attestOps(port);
22608
22856
  async function handle(req) {