@ai-ecoverse/slicc-kernel 1.102.1 → 1.102.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -261,7 +261,7 @@ An empty path is `ENOENT` in every path call (`path_open`, `path_filestat_get`,
261
261
  On top of preview1 a WASI program gets:
262
262
 
263
263
  - **Threads**: `wasi.thread-spawn` (wasm32-wasip1-threads) and WASIX `thread_spawn_v2` start a worker per thread on the process's shared memory, sized from the module's own import (a 2 GiB maximum when the engine cannot reserve more). The threads share one descriptor table, and a file written, created, renamed or removed in one thread shows its new size and metadata to `stat` in every other thread at once, under any of its names (a symlink, a hard link, a new name after a rename in the same process), also when another thread renames a file over that name meanwhile, since the kernel reports which file an open or a change reached; this holds among the kernel's own processes, not against other tabs, `hostfs` writers or programs that aren't WASI; another process's changes show at close and after a `spawn` or `wait`, as with close-to-open caching, so a file another process renames while this one holds it open may show its old size under the new name until then; `exit` or a trap in any thread ends the process. At most 64 threads per process; `SLICC_WASM_THREADS` sets another cap, up to 256.
264
- - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors again, since their data is already in the copied memory. A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver.
264
+ - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), with the unwound frames kept in the unused part of the program's stack, below its stack pointer (for a position-independent program, the stack the kernel's linker gives it), so a fork from deep in the stack leaves the program's data alone, and a forked child's thread-locals (`errno` included) where the parent left them: the kernel reruns the module's `__wasm_init_tls` at the parent's TLS base (and puts back the block it overwrites), which also sets the internal globals that hold thread-local addresses (a position-independent program's, a side module's, and those of a variable in the `global-dynamic` TLS model), exec, `posix_spawn`, `waitpid` (with `WNOHANG`, and `WUNTRACED` for a child that stops; WASIX has no `WCONTINUED`), pipes, `dup2`, signal handlers and interval timers, terminal modes, and dynamic linking: a position-independent main module loads side modules with `dlopen` from `LD_LIBRARY_PATH`, its runtime path, `/lib`, `/usr/lib` and `/usr/local/lib`. Each thread has its own copy of the function table, so the kernel hands out its slots (a side module's range, a `dlsym`'d function, a closure): threads loading or looking up at the same time never get the same slot. As in glibc, one `dlopen` at a time runs in a process, constructors included: a `dlopen` from a library's constructor goes ahead in its own thread, and another thread's `dlopen` waits, then finds the library loaded and gets the same handle. A thread waiting there still ends or stops with its process, but a signal handler it has runs once that `dlopen` returns. The lock is let go when a `dlopen` fails (an exception, a trap or `pthread_exit` out of a constructor included) and when its thread ends; a forked child starts with it free. A forked child also gets the side modules its parent had loaded, at the same addresses and table slots, with the slot and closure counters and the parent's thread-local blocks: they are linked again in the child before it resumes, without running their constructors again, since their data is already in the copied memory. A side-module function on the stack when `fork` is called (a callback into the main module that forks) is not instrumented by Asyncify, so it runs again when the stack is rewound, in the parent and in the child, and libffi frames there can trap; side modules would need building with Asyncify for that, which the toolchain does not do yet. In such a program, `dlopen(NULL)`, and `dlopen` of the program's own file (also by a path that resolves to it through symlinks), give a handle for the program that is already running (no second copy is loaded); a program that isn't position-independent gets an error from `dlopen(NULL)`, as from any `dlopen`; `dlsym` on it searches the program, then the loaded libraries, and `dlclose` of it does nothing. A function already in the table (the program's own, or a library's) keeps its slot, so `dlsym(handle, "strlen") == strlen`. Each WASIX thread has its own Asyncify driver too, so a thread can `longjmp` through Asyncify and `fork` (#710). The unwound frames go at the bottom of the thread's own stack (the `stack` and `stack_size` its libc passes to `wasi_thread_start`), never on the main thread's. A thread whose start args don't look like wasix-libc's (a `stack_size` under 4096 or over the memory, a stack top that isn't 16-aligned or not right above the args) gets no driver, so `fork` there is `ENOSYS` as before, rather than an unwind buffer at a bad address. A child forked from a thread runs as that thread: it's rewound through `wasi_thread_start`, can fork again, and ends when the thread's function returns. A program built with exceptions (every position-independent one) does `setjmp` with exceptions rather than Asyncify, so there only `fork` uses the driver. A kernel timer can be longer than `setTimeout`'s 2^31-1 ms (about 24.8 days): an `alarm`, an interval timer, or a `select`, `poll_oneoff` or `epoll_wait` timeout waits its whole time, re-armed in steps of at most 2^31-1 ms, instead of firing at once or spinning (#767).
265
265
  - **epoll and eventfd** (WASIX `epoll_create`, `epoll_ctl`, `epoll_wait` and `fd_event`, which tokio's reactor uses through the WASIX mio fork):
266
266
  - **Where they live:** an epoll instance and an eventfd are kernel descriptors, so they survive `dup`, `fork` and `exec` (unless close-on-exec) and are shared by a process's threads.
267
267
  - **What epoll watches:** sockets (listening, connecting, connected, loopback and uplink), pipes, terminals, `kernel.run`'s stdin, eventfds and other epoll instances (a cycle of instances is `ELOOP`).
@@ -307,7 +307,7 @@ On top of preview1 a WASI program gets:
307
307
  - Nothing is undone when a process exits.
308
308
  - **Limits:**
309
309
  - Semaphores exist only within one kernel (not across tabs), and they don't appear in `/dev/shm`.
310
- - A kernel holds at most 4,096 named ones (`ENOSPC`), and a process at most 256 open handles (`EMFILE`; a refused `O_CREAT` leaves no name behind).
310
+ - A kernel holds at most 65,536 named ones (`ENOSPC`), and a process at most 65,536 open handles (`EMFILE`; a refused `O_CREAT` leaves no name behind). Those are memory bounds (a semaphore is about 100 bytes), not musl's 256 (`SEM_NSEMS_MAX`), which wasix-sysroot -31 drops: python's multiprocessing opens one per `Lock` (#770).
311
311
  - `MAP_SHARED` mappings aren't shared between processes, so unnamed semaphores (`sem_init` with `pshared`) work only between a process's threads, and `multiprocessing.shared_memory` doesn't work.
312
312
  - **Foreign calls** (libffi's WASIX backend, so Python's `ctypes`): `call_dynamic` calls a function through the program's indirect function table (a static module's exported `__indirect_function_table`, or the dynamic linker's), its arguments and results packed in memory one after another (`i32`/`f32` 4 bytes, `i64`/`f64` 8). With `strict` set, the buffers must fit the function exactly (else `EINVAL`, before the function is called); without it, missing arguments are zero (one cut short keeps the bytes given, with zeros above them) and results that do not fit are left out. A trap or exception of the callee goes through to the caller. JS cannot ask a function for its type, so the kernel reads the types from the module files (the program's and each side module's, once, when a program first calls through `call_dynamic`): a table slot that is empty, past the end, holds a function of no module the kernel knows of, or takes or returns a `v128`, gives `EINVAL`. For closures (callbacks such as `CFUNCTYPE`), a dynamically linked program gets table slots from `closure_allocate`, and `closure_free` gives them back for reuse and empties the slot in every thread; a static program gets `ENOTSUP`. `closure_prepare` puts a trampoline with the requested signature (any of `i32`, `i64`, `f32`, `f64`, `v128`) into such a slot, in every thread: each call stores its arguments below the stack pointer and calls the backing function as `backing(values, results, user_data)`, then returns what it wrote to `results`. The trampolines are small wasm modules made and compiled at run time, once per signature in each thread (an instance per closure), so a page whose Content Security Policy forbids compiling WebAssembly (no `'wasm-unsafe-eval'`) cannot run them; it cannot run the program either. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
313
313
  - **Job control:** `^Z`, `SIGTSTP`, `SIGTTIN` and `SIGTTOU` stop a WASIX program at once, unless it has a handler for them. A stopped program that is busy computing is stopped for job control (`jobs` shows it, `fg`, `bg` and `SIGCONT` work), but its worker keeps a core busy until its next system call, since a worker's running code cannot be frozen from outside. Agents should `kill` such a job rather than leave it stopped. An Emscripten program stops at its next kernel call as well, and at its next write, read or sleep that its glue answers without the kernel (a write to its own `/dev/null`, as in `yes > /dev/null`), so a stopped one keeps no core busy unless it computes without any of these calls. A stopped program doesn't move on while stopped: a `sleep` (or a blocked read) that would end meanwhile ends after `SIGCONT`. A call that the stop interrupted runs again after `SIGCONT`, as on Linux. One with a timeout (`select`, `poll`, `ppoll`, `pselect`, `nanosleep`, a timed semaphore wait) waits only for the time that's left, counted from when the kernel first got the call, so it still ends at its deadline. `epoll_wait` and `epoll_pwait` aren't run again: they fail with `EINTR`, even without a handler (signal(7)). A signal for such a program is also delivered at those glue-answered calls, so a handler runs in a loop that only writes to `/dev/null`. For the same reason, a signal with a handler or a terminating default (`^C`, `SIGTERM`) reaches a program that is busy computing only at its next system call, so a CPU-bound loop may not end on `^C`; `kill -9` always ends it. A pending signal is delivered at the program's next WASI or WASIX call, including the calls its worker answers without the kernel (`random_get`, `args_get`, …), except the exempt ones: `clock_time_get` (a tight loop of it measured up to 30% slower), `proc_exit`, `proc_exit2`, `thread_exit`, `thread_spawn_v2` and `wasi.thread-spawn` (wasi-threads), the calls that install handlers (`callback_signal`, `slicc.sigaction_set`), and calls made while Asyncify unwinds or rewinds the stack for `fork` or `longjmp`. A signal that arrives while a thread computes is thus delivered at its next call; when that call is a wait (`poll_oneoff` with a timeout or a descriptor, so `nanosleep` and `poll`/`select` too, `sigmask_poll`, or a futex wait), the handler has run during the call, so the wait ends at once: `EINTR` for a libc of -22 or later (an older one sees its clocks fire, as for any interruption), and a futex wait returns woken. A thread blocked in the kernel or waiting on a lock (`pthread_cond_wait`, a futex) is woken by a signal for it (a futex wait checks for signals every 50 ms, so each waiting thread wakes briefly that often), and `pthread_kill` runs the handler on the thread it names. A sleep, or a clock in `poll_oneoff`, never ends before the time asked (it can end later, never earlier). A signal that ends a sleep or a `poll`/`select` with a timeout makes the call fail with `EINTR` in a program whose libc says it is wasix-sysroot 2025.9.30-22 or later (its `slicc.libc` custom section). Older libcs turn that answer into `ENOTSUP` from `nanosleep` (up to -18), so for a program without the section the wait ends as if it had timed out, unless it imports `fd_fdflags_set` (then `EINTR`, as before).
@@ -1028,20 +1028,28 @@ A threaded WASIX C program for `test/integration/thread-asyncify.test.mjs` (#710
1028
1028
 
1029
1029
  ### `stopwait`
1030
1030
 
1031
- A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child 50 ms after it's ready and continues it 200 ms later. The child prints the result, the error and whether the wait ended before the deadline, at it (300–450 ms) or late.
1031
+ A WASIX C program for `test/integration/stop-restart.test.mjs` (#738). `stopwait CALL` forks a child that waits 300 ms in `CALL` (`epoll`, `poll`, `ppoll`, `select`, `pselect`, `nanosleep` or `clock_nanosleep`) on a pipe that never gets data. The parent stops the child once `/proc/<pid>/task/1/stat` shows it blocked (`S`), or 100 ms after it's ready on a kernel without that state, continues it 200 ms later, and reports whether the stop found it blocked. The child prints the result, the error and whether the wait ended before the deadline, at it (300–450 ms) or late.
1032
1032
 
1033
- **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions) and binaryen's `wasm-opt` (`--asyncify` for `proc_fork`) on `PATH`. `bin/stopwait.wasm` is 50,567 bytes, sha256 `570cb3e29cfdc083bbdc0d20707f2c07b087fe23cef13a8861a4a42b02d20f5f`.
1033
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions) and binaryen's `wasm-opt` (`--asyncify` for `proc_fork`) on `PATH`. `bin/stopwait.wasm` is 59,958 bytes, sha256 `ce49e04baed13b7ccf04d8231da4a29567fde0dac19d5cc81f36c185e0eb4555`.
1034
+
1035
+ ### `longwait`
1036
+
1037
+ A WASIX C program for `test/integration/long-timers.test.mjs` (#767).
1038
+ - `longwait alarm` sets `alarm(3000000)` (about 35 days), sleeps 0.5 s, and prints how many alarms came and how many seconds were left.
1039
+ - `longwait select|ppoll|nanosleep` waits 3,000,000 s on a pipe that never gets data, until a 300 ms interval timer interrupts the wait.
1040
+
1041
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` on `PATH`. `bin/longwait.wasm` is 44,010 bytes, sha256 `70ae2adfb46864fad3af0241fe26f401a62a5176ccd25ab82fe6588178cfb5c8`.
1034
1042
 
1035
1043
  ### `semtest`
1036
1044
 
1037
1045
  A WASIX C program for `test/integration/semaphore.test.mjs` (#717). It calls the `slicc.sem_*` imports directly (`import_module("slicc")`), so the rows don't wait for the libc side. `semtest MODE` runs one of:
1038
1046
  - `handoff`, `inherit`, `spawn` (a spawned and an exec'd image open the semaphore by name; exec drops the handles);
1039
- - `try`, `timed`, `eintr`, `stopped` (a forked child's 300 ms timed wait is stopped for 200 ms and continued, and reports whether it ended at the deadline);
1047
+ - `try`, `timed`, `eintr`, `stopped` (a forked child's 300 ms timed wait is stopped for 200 ms and continued, and reports whether it ended at the deadline; the parent stops it only once `/proc/<pid>/task/1/stat` shows it blocked, `S`, or after 100 ms on a kernel without that state, and reports which);
1040
1048
  - `threads` (FIFO, one thread blocked at a time);
1041
1049
  - `errors`, `unlink`, `owners setup|user`;
1042
- - `killed` (a waiter killed before posts loses no unit), `exited`, `cap`, `handles`.
1050
+ - `killed` (a waiter killed before posts loses no unit), `exited`, `cap [N]` (creates up to N names, 10,000 by default), `handles [N]` (opens up to N, 300 by default).
1043
1051
 
1044
- **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions, `-pthread`) and binaryen's `wasm-opt` (version 132, `--asyncify` for `proc_fork`) on `PATH`. `bin/semtest.wasm` is 68,438 bytes, sha256 `af3d38d87b63fa219bae90b1225376882ba20ace423c5ce24ef57935e80e2fde`.
1052
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (legacy exceptions, `-pthread`) and binaryen's `wasm-opt` (version 132, `--asyncify` for `proc_fork`) on `PATH`. `bin/semtest.wasm` is 69,366 bytes, sha256 `b3202708f57acad5f51e5be1b06e3b5a0ad02dd84b3791e052787117b828da4e`.
1045
1053
 
1046
1054
  ### `wlocktest`
1047
1055
 
@@ -12136,7 +12136,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12136
12136
  var EARLY_WHOLE_FILE = 64 * 1024;
12137
12137
 
12138
12138
  // src/version.ts
12139
- var KERNEL_VERSION = true ? "1.102.1" : "0.0.0-dev";
12139
+ var KERNEL_VERSION = true ? "1.102.2" : "0.0.0-dev";
12140
12140
  var OS_RELEASE = "6.0.0-slicc";
12141
12141
  function procVersion() {
12142
12142
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -12650,6 +12650,41 @@ var EventFd = class _EventFd {
12650
12650
  }
12651
12651
  };
12652
12652
 
12653
+ // src/kernel/timer.ts
12654
+ var TIMER_MAX_MS = 2147483647;
12655
+ function timerAt(deadline, fire) {
12656
+ let timer;
12657
+ const arm = () => {
12658
+ const left = deadline - performance.now();
12659
+ if (left > 0) {
12660
+ timer = setTimeout(arm, Math.min(left, TIMER_MAX_MS));
12661
+ return;
12662
+ }
12663
+ timer = void 0;
12664
+ fire();
12665
+ };
12666
+ timer = setTimeout(arm, Math.min(Math.max(0, deadline - performance.now()), TIMER_MAX_MS));
12667
+ return () => clearTimeout(timer);
12668
+ }
12669
+ function timerAfter(ms, fire) {
12670
+ return timerAt(performance.now() + ms, fire);
12671
+ }
12672
+ function repeating(first, every, fire) {
12673
+ let next2 = performance.now() + first;
12674
+ let cancel = () => {
12675
+ };
12676
+ const tick = () => {
12677
+ fire();
12678
+ if (every <= 0) return;
12679
+ next2 += every;
12680
+ const now = performance.now();
12681
+ if (next2 <= now) next2 += (Math.floor((now - next2) / every) + 1) * every;
12682
+ cancel = timerAt(next2, tick);
12683
+ };
12684
+ cancel = timerAt(next2, tick);
12685
+ return () => cancel();
12686
+ }
12687
+
12653
12688
  // src/kernel/epoll.ts
12654
12689
  var EPOLL = {
12655
12690
  IN: 1,
@@ -12701,11 +12736,11 @@ function lookup(fds, fd) {
12701
12736
  }
12702
12737
  function pause(ms, signal) {
12703
12738
  return new Promise((resolve2) => {
12704
- const timer = setTimeout(resolve2, ms);
12739
+ const cancel = timerAfter(ms, resolve2);
12705
12740
  signal.addEventListener(
12706
12741
  "abort",
12707
12742
  () => {
12708
- clearTimeout(timer);
12743
+ cancel();
12709
12744
  resolve2();
12710
12745
  },
12711
12746
  { once: true }
@@ -12771,17 +12806,14 @@ var EpollFile = class _EpollFile {
12771
12806
  const round = new AbortController();
12772
12807
  const woken = this.changes.wait(AbortSignal.any([round.signal, signal]));
12773
12808
  woken.catch(() => void 0);
12774
- let timer;
12775
- const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [
12776
- new Promise(
12777
- (resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
12778
- )
12779
- ];
12809
+ let cancel = () => {
12810
+ };
12811
+ const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => cancel = timerAt(deadline, resolve2))];
12780
12812
  try {
12781
12813
  await Promise.race([woken, ...expiry]);
12782
12814
  } finally {
12783
12815
  round.abort();
12784
- clearTimeout(timer);
12816
+ cancel();
12785
12817
  }
12786
12818
  }
12787
12819
  }
@@ -13126,9 +13158,10 @@ var AsyncOps = class {
13126
13158
  }
13127
13159
  sleep(timeoutMs, signal) {
13128
13160
  return new Promise((resolve2, reject) => {
13129
- let timer;
13161
+ let cancel = () => {
13162
+ };
13130
13163
  const cleanup = () => {
13131
- clearTimeout(timer);
13164
+ cancel();
13132
13165
  this.waiters.delete(wake);
13133
13166
  signal.removeEventListener("abort", abort);
13134
13167
  };
@@ -13143,10 +13176,10 @@ var AsyncOps = class {
13143
13176
  this.waiters.add(wake);
13144
13177
  signal.addEventListener("abort", abort, { once: true });
13145
13178
  if (timeoutMs !== void 0) {
13146
- timer = setTimeout(() => {
13179
+ cancel = timerAfter(timeoutMs, () => {
13147
13180
  cleanup();
13148
13181
  resolve2(false);
13149
- }, timeoutMs);
13182
+ });
13150
13183
  }
13151
13184
  });
13152
13185
  }
@@ -14533,14 +14566,14 @@ async function runChild(req, deps) {
14533
14566
  return frame2(outcome2, new Uint8Array(0), new Uint8Array(0));
14534
14567
  }
14535
14568
  if (failure2 !== void 0) kill();
14536
- const timer = req.timeoutMs !== void 0 && req.timeoutMs > 0 ? setTimeout(() => stop("ETIMEDOUT"), req.timeoutMs) : void 0;
14569
+ const cancel = req.timeoutMs !== void 0 && req.timeoutMs > 0 ? timerAfter(req.timeoutMs, () => stop("ETIMEDOUT")) : void 0;
14537
14570
  let status;
14538
14571
  try {
14539
14572
  status = await reaped(deps, pid);
14540
14573
  const open = controls.filter((c) => c.opened);
14541
14574
  await Promise.race([Promise.all(open.map((c) => c.eof)), decided.promise]);
14542
14575
  } finally {
14543
- clearTimeout(timer);
14576
+ cancel?.();
14544
14577
  for (const control of controls) control.shut = true;
14545
14578
  }
14546
14579
  const out = deps.children.captured(pid, 1);
@@ -14596,27 +14629,23 @@ async function waitReady(fds, read, write, deadline, interrupted3) {
14596
14629
  const file = fds.get(fd).file;
14597
14630
  return file.changed === void 0 ? [] : [file.changed(round.signal).catch(() => void 0)];
14598
14631
  });
14599
- let timer;
14600
- const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [
14601
- new Promise(
14602
- (resolve2) => timer = setTimeout(resolve2, deadline - performance.now())
14603
- )
14604
- ];
14632
+ let cancel = () => {
14633
+ };
14634
+ const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => cancel = timerAt(deadline, resolve2))];
14605
14635
  try {
14606
14636
  await Promise.race([...changes, ...expiry, interrupted3]);
14607
14637
  } finally {
14608
14638
  round.abort();
14609
- clearTimeout(timer);
14639
+ cancel();
14610
14640
  }
14611
14641
  }
14612
14642
  }
14613
14643
 
14614
14644
  // src/kernel/sem.ts
14615
14645
  var SEM_VALUE_MAX = 2147483647;
14616
- var SEM_NAMES_MAX = 4096;
14617
- var SEM_HANDLES_MAX = 256;
14646
+ var SEM_NAMES_MAX = 65536;
14647
+ var SEM_HANDLES_MAX = 65536;
14618
14648
  var NAME_MAX = 251;
14619
- var TIMER_MAX_MS = 2147483647;
14620
14649
  var SEM_OPS = [
14621
14650
  "sem-open",
14622
14651
  "sem-close",
@@ -14653,11 +14682,12 @@ var Semaphore = class {
14653
14682
  if (signal.aborted) return Promise.reject(new KernelError("EINTR"));
14654
14683
  return new Promise((resolve2, reject) => {
14655
14684
  const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : performance.now() + timeoutMs;
14656
- let timer;
14685
+ let cancel = () => {
14686
+ };
14657
14687
  const leave = () => {
14658
14688
  const at = this.waiters.indexOf(waiter);
14659
14689
  if (at >= 0) this.waiters.splice(at, 1);
14660
- clearTimeout(timer);
14690
+ cancel();
14661
14691
  signal.removeEventListener("abort", abort);
14662
14692
  };
14663
14693
  const waiter = {
@@ -14671,18 +14701,13 @@ var Semaphore = class {
14671
14701
  reject(new KernelError("EINTR"));
14672
14702
  };
14673
14703
  const expire = () => {
14674
- const left = deadline - performance.now();
14675
- if (left > 0) {
14676
- timer = setTimeout(expire, Math.min(left, TIMER_MAX_MS));
14677
- return;
14678
- }
14679
14704
  leave();
14680
14705
  resolve2(false);
14681
14706
  };
14682
14707
  this.waiters.push(waiter);
14683
14708
  signal.addEventListener("abort", abort, { once: true });
14684
14709
  if (deadline !== Number.POSITIVE_INFINITY) {
14685
- timer = setTimeout(expire, Math.min(timeoutMs, TIMER_MAX_MS));
14710
+ cancel = timerAt(deadline, expire);
14686
14711
  }
14687
14712
  });
14688
14713
  }
@@ -14717,6 +14742,10 @@ var SemTable = class {
14717
14742
  var SemHandles = class _SemHandles {
14718
14743
  byHandle = /* @__PURE__ */ new Map();
14719
14744
  next = 1;
14745
+ limit;
14746
+ constructor(limit = SEM_HANDLES_MAX) {
14747
+ this.limit = limit;
14748
+ }
14720
14749
  add(sem) {
14721
14750
  for (const [handle2, entry] of this.byHandle) {
14722
14751
  if (entry.sem === sem) {
@@ -14724,7 +14753,7 @@ var SemHandles = class _SemHandles {
14724
14753
  return handle2;
14725
14754
  }
14726
14755
  }
14727
- if (this.byHandle.size >= SEM_HANDLES_MAX) throw new KernelError("EMFILE");
14756
+ if (this.byHandle.size >= this.limit) throw new KernelError("EMFILE");
14728
14757
  const handle = this.next++;
14729
14758
  this.byHandle.set(handle, { sem, opens: 1 });
14730
14759
  return handle;
@@ -14740,7 +14769,7 @@ var SemHandles = class _SemHandles {
14740
14769
  if (--entry.opens === 0) this.byHandle.delete(handle);
14741
14770
  }
14742
14771
  fork() {
14743
- const child = new _SemHandles();
14772
+ const child = new _SemHandles(this.limit);
14744
14773
  child.next = this.next;
14745
14774
  for (const [handle, entry] of this.byHandle) child.byHandle.set(handle, { ...entry });
14746
14775
  return child;
@@ -17027,17 +17056,12 @@ var WasmProcess = class {
17027
17056
  (req, cancelled) => this.syscall(req, cancelled),
17028
17057
  () => this.options.onAsync?.()
17029
17058
  );
17030
- alarmEvery;
17031
17059
  setAlarm(sig, first, every, timer) {
17032
17060
  if (!isSignal(sig)) throw new KernelError("EINVAL");
17033
17061
  this.clearAlarm();
17034
17062
  if (first <= 0) return;
17035
17063
  const fire = timer === void 0 ? () => this.options.raise?.(sig) : () => this.timerExpired(timer);
17036
- this.alarm = setTimeout(() => {
17037
- this.alarm = void 0;
17038
- fire();
17039
- if (every > 0) this.alarmEvery = setInterval(fire, every);
17040
- }, first);
17064
+ this.alarm = repeating(first, every, fire);
17041
17065
  }
17042
17066
  timerExpired(which) {
17043
17067
  this.options.onTimer?.(which);
@@ -17046,10 +17070,8 @@ var WasmProcess = class {
17046
17070
  blocked2.abort();
17047
17071
  }
17048
17072
  clearAlarm() {
17049
- clearTimeout(this.alarm);
17050
- clearInterval(this.alarmEvery);
17073
+ this.alarm?.();
17051
17074
  this.alarm = void 0;
17052
- this.alarmEvery = void 0;
17053
17075
  }
17054
17076
  async exit() {
17055
17077
  if (this.exited) return;