@ai-ecoverse/slicc-kernel 1.125.9 → 1.125.11

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
@@ -321,7 +321,7 @@ On top of preview1 a WASI program gets:
321
321
  - `pthread_kill` to another thread gives `SI_TKILL` (-6) from the process. wasix-sysroot's `pthread_kill` of the calling thread itself goes through `kill(getpid())` and so arrives as `SI_USER`, until its libc takes the thread route for itself too;
322
322
  - `SIGCHLD` gives `CLD_EXITED` (1) with the exit status, `CLD_KILLED` (2) with the signal, `CLD_STOPPED` (5) or `CLD_CONTINUED` (6), with the child's pid and real uid;
323
323
  - a terminal's ^C/^Z/^\, `alarm` and the kernel's own signals give `SI_KERNEL` (0x80) with pid and uid 0, and so does a `sigwait_info` take with nothing recorded;
324
- - a standard signal doesn't queue, so the first pending instance's sender is kept until the signal is taken.
324
+ - a standard signal doesn't queue, so the first pending instance's sender is kept until the signal is taken. A caught signal sent again while it's still pending coalesces with it: the handler runs once and the program goes on, as on Linux ([#1088](https://github.com/ai-ecoverse/slicc-kernel/issues/1088)). One deliberate deviation: a second `SIGINT`, `SIGTERM`, `SIGQUIT` or `SIGHUP` while the first is still pending **and the program has made no kernel call since** ends the program with the signal's default action. A WASI program that computes without making calls can't run a handler, so this is what lets a second ^C end it (a Python `while True: pass`). Timer and user signals (`SIGALRM`, `SIGUSR1` …) always coalesce, and so does a signal the program has blocked, or is still handling, while it makes calls.
325
325
  A kernel without the imports answers `ENOSYS`, and the libc reports `SI_USER` with pid and uid 0, as before. **SIGCHLD:** with `SIGCHLD` set to `SIG_IGN` (by the program, or inherited), or with `SA_NOCLDWAIT` in the flags `slicc.sigaction_set` gets for `SIGCHLD`, an exited child is reaped at once and never stays a zombie: `waitpid` for it is `ECHILD`, and `wait` blocks until every child has exited, then fails with `ECHILD`, as on Linux. `SIGCHLD` is still sent. An explicit `SIG_DFL` turns it back off. A child that a process waits on for `exec`, or that `kernel.run` and the run calls collect, is never reaped that way.
326
326
  - **Pipes** have no file type for WASI and WASIX programs' `fstat` (`st_mode` type 0), because WASI has no FIFO file type. A socket type, as kernels 1.47.3 to 1.48.1 gave them, made Ruby treat a `popen` pipe as a socket (`close_write` then never ends the child's input). The kernel's `slicc_fs.fd_mode` answers `S_IFIFO | 0600` (Linux numbering) for a pipe, and wasix-sysroot 2025.9.30-21 and later take the type from it, so programs built on it see a FIFO (`S_ISFIFO`); older libcs keep only the permission bits. Until then Python's asyncio refuses a pipe for a subprocess (`Pipe transport is only for pipes, sockets and character devices`). They are read and written as pipes; socket calls on them fail with `ENOTSOCK`, and `isatty()` is false. Emscripten programs, bash included, see a FIFO. The same holds for any kernel descriptor that is neither seekable nor a terminal or device, such as captured output.
327
327
  - **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream or datagram pair, `bind`, `listen`, `connect`, `accept`, options, local and peer names) are the kernel's loopback sockets, and preview1's `sock_recv` / `sock_send` / `sock_shutdown` work on them and on inherited ones. Writing to a socket whose peer is gone fails with `EPIPE`, through `fd_write` as through `sock_send`, as if `MSG_NOSIGNAL` were set. A write to a pipe, FIFO or terminal whose reader is gone follows the program's `SIGPIPE` disposition, as the kernel knows it (#810): at the default it ends the program with status 141; ignored (also an ignore inherited across `exec` and spawn, as bash's `trap '' PIPE` passes on), the write fails with `EPIPE`; with a handler, the handler runs on the thread that wrote and then the write fails with `EPIPE` (#820); blocked, `SIGPIPE` stays pending and the write fails with `EPIPE`. An Emscripten program's write to a pipe, and its `send` without `MSG_NOSIGNAL`, follow the same rules, except that the kernel doesn't see an Emscripten program's signal mask, so a blocked `SIGPIPE` there still ends it. A WASIX program's libc reads an inherited ignore back through `slicc.sigaction_inherited` (wasix-sysroot -32 and later; see below). An Emscripten program's shim reads it from `Module.sliccKernel.sigInherited()` (#965, homescoop#973). That's a mask with bit n set for signal n (not bit n-1, as the WASIX import has it) holding only inherited `SIG_IGN`s, never `SIGKILL` or `SIGSTOP`. The shim seeds its `sigaction` table from it in a constructor that runs before any user constructor. The kernel puts the function on `Module.sliccKernel` before the glue runs constructors, ahead of the rest of `sliccKernel`. On a kernel without the function it is undefined, and the shim keeps the defaults. HTTP and HTTPS leave through the realm proxy that `https_proxy` names, as for any other program; `resolve` answers `/etc/hosts` names and literal addresses, and the [uplink](#uplink)'s names. `sock_open(SOCK_DGRAM)` on `AF_INET`/`AF_INET6` gives a [datagram socket](#datagram-sockets-udp); `sock_send_to` and `sock_recv_from` carry its addresses, and `RECV_DATA_TRUNCATED` is set in `ro_flags` when a datagram did not fit.
@@ -383,10 +383,19 @@ The module's default export (or its `main` export) is called with a context and
383
383
  - `status` is the exit status, or `null` with `signal` set when a signal ended the child. A non-zero status is not an error. A command that cannot start (it does not exist, or is not executable) answers `error: { code: 'ENOENT' }` (or the errno), `status: null` and `pid: 0` instead of throwing.
384
384
  - More than `maxBuffer` bytes on `stdout` or `stderr` (1 MiB by default) sends the child `killSignal` (`SIGTERM` by default), keeps the first `maxBuffer` bytes, and answers `error: { code: 'ENOBUFS' }`. The kernel keeps at most 64 MiB of each stream whatever `maxBuffer` says, `Infinity` included, since the buffers live in the kernel worker that all processes share. `timeout` (ms) sends `killSignal` when it runs out and answers `error: { code: 'ETIMEDOUT' }`. A child that ignores `killSignal` is still waited for, as in Node.
385
385
  - The child is in the program's process group, so `^C` reaches it. The kernel reaps it, so it leaves no zombie, and its CPU time counts as the program's children's. A signal the program catches does not cut the run short; its handler runs when the call returns. A signal that kills the program kills it as usual;
386
- - `sync.spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr })` starts a child as `spawn` below does, with the same options and the same child, but synchronously: it returns `{ pid, stdin, stdout, stderr, wait(), kill(signal) }` at once, so a runtime can set Node's `child.pid` before `spawn()` returns. A spawn error (`ENOENT`, `EACCES`, `ESRCH` or `EPERM` for a group, `EINVAL` for a bad option, `EBADF` for a descriptor the program doesn't have) is thrown at once, with its `code`. The streams, `wait()` and `kill()` are the asynchronous ones, used afterwards from async code; the child doesn't wait for the program to read, since its output goes to a kernel pipe, which holds what it writes until the program reads it (a child that fills the pipe waits, as with `spawn`);
386
+ - `sync.spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a child as `spawn` below does, with the same options and the same child, but synchronously: it returns `{ pid, stdin, stdout, stderr, wait(), kill(signal) }` at once, so a runtime can set Node's `child.pid` before `spawn()` returns. A spawn error (`ENOENT`, `EACCES`, `ESRCH` or `EPERM` for a group, `EINVAL` for a bad option, `EBADF` for a descriptor the program doesn't have) is thrown at once, with its `code`. The streams, `wait()` and `kill()` are the asynchronous ones, used afterwards from async code; the child doesn't wait for the program to read, since its output goes to a kernel pipe, which holds what it writes until the program reads it (a child that fills the pipe waits, as with `spawn`);
387
387
  - `sync.kill(pid, signal)`, a synchronous `kill`. When the signal reaches the program itself (its own pid, `0` for its process group, or `-pgid` of its own group) and its action is to end the program (no handler, not ignored), the call never returns: the program's writes reach their descriptors and its terminal modes are put back, as for `sync.exit`, then the rest of the group is signalled and the program ends by the signal, so a parent's `wait` and `sync.run` report `signal`, as Node does for `process.kill(process.pid)`. A signal with a handler runs it when the call returns, an ignored one does nothing, and a stop signal stops the program. A signal whose default action dumps core (`SIGQUIT`, `SIGABRT`, `SIGSEGV`, …) ends it the same way; the kernel writes no core and sets no core flag in the wait status. `-1` signals every process the program may signal except itself and pid 1, as on Linux, and returns (`ESRCH` when there was none, `EPERM` when there were some but it may signal none of them); a process that exec'd gets the signal once. Without users, every process runs as root, so `kill -9 -1` from any program ends every other process; with users it reaches only what the sender may signal (its own uid's processes, or all of them as root);
388
388
  - `sync.exit(status)`, which ends the process at once, as Node's `process.exit()` does: no `catch`, `finally`, microtask, timer or signal handler runs after it, because the call never returns. First the writes the program has made reach their descriptors, in order (a broken pipe drops the rest for that descriptor), and terminal modes the program set with `tty` are put back. Then the process ends the way any exit does: its descriptors are closed and written back, its parent gets `SIGCHLD`, and `wait` reports `status`. Children keep running. A JS program has no threads of its own (`Worker` is withheld), so nothing of it outlives the call. `exit(status)` (below) unwinds instead, so the program's `catch` and `finally` blocks run;
389
- - `spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`. `parentDeathSignal` (a number or `'SIGTERM'`, as Linux's `PR_SET_PDEATHSIG`) is sent to the child when the program ends, killed or not: to the child's whole group when it leads one (`pgid: 0` or `session: true`), else to the child alone. It's sent with the program's credentials, so a child the program may no longer signal (one that gained privilege) doesn't get it. If the program has already ended by the time the spawn completes, it's sent at once. A child of its own (not a group leader) that has been waited for gets nothing, and `sync.run` has no such option. A bad signal is `EINVAL`;
389
+ - `socketpair()` (and `sync.socketpair()`) answers `[a, b]`, two descriptors of the program on a connected `AF_UNIX` stream pair, and `pipe()` (and `sync.pipe()`) answers `[readFd, writeFd]` ([#1093](https://github.com/ai-ecoverse/slicc-kernel/issues/1093)).
390
+ - **The same descriptors either way:** the async and `sync` forms give the program's own kernel descriptors, which `read`, `write`, `close` and their `sync` forms all take, until the program closes them or ends.
391
+ - **Reading** a socket or pipe whose peer has closed gives an empty `Uint8Array` (end of file). A `read` still pending when the program `close`s that descriptor rejects with `EBADF` instead of waiting. A `read` and a `write` on one socket can be pending at the same time, as a socket is full duplex.
392
+ - **Writing to a socket whose peer is gone** fails with `EPIPE` and raises no `SIGPIPE`, at the default disposition too, as for WASIX and Emscripten programs (as if `MSG_NOSIGNAL` were set). A pipe's broken write follows `SIGPIPE` as described below.
393
+ - **`fds`**, an option of `spawn` and `sync.spawn`, gives the child more descriptors: `{ 3: f }` puts the program's descriptor `f` at the child's fd 3, on the same open file description, as `dup2` would.
394
+ - **Keys:** each key must be an integer of 3 or more, as stdio stays `stdin`, `stdout` and `stderr`. Each value must be an open descriptor of the program. Anything else is `EINVAL` before the child starts.
395
+ - **Only the named descriptors reach the child.** Others the program holds, the other end of a pair included, don't, and a grandchild gets fd 3 only if the child names it again.
396
+ - **The parent's copy:** the program keeps `f` and closes it itself when it no longer needs it, so the child sees end of file once it's the only one left.
397
+ - **Why a `Record`:** it can't name a child fd twice, and it reads like Node's `stdio` index, so it's this rather than a list of `{ fd, from }`.
398
+ - `spawn({ argv, argv0, env, cwd, pgid, session, parentDeathSignal, stdin, stdout, stderr, fds })` starts a command (found as `kernel.run` finds it) as a child process and resolves with `{ pid, stdin, stdout, stderr, wait(), kill(signal) }`. Each stdio is `'inherit'` (the default: the program's own), `'pipe'` (a web stream on a kernel pipe), `'null'`, or one of the program's descriptors. `env` is the child's whole environment (the program's by default), and `cwd` defaults to the program's. The command run is `argv[0]`; `argv0`, when given, is the `argv[0]` the child sees instead, as given (no basename; an empty string too), as with Node's `options.argv0`: `spawn({ argv: ['/bin/sh', '-c', 'echo $0'], argv0: '/bin/sh' })` prints `/bin/sh`, where without it the child sees the command's own name (`sh`). A command whose package manifest sets its `argv0` (a multi-call program's applets, such as coreutils') keeps that one. An `argv0` that isn't a string is `EINVAL`. The child is in the program's process group and session, as after `fork` (Node's `detached: false`), so the terminal's `^C` and `^Z` reach it along with the program. `pgid: 0` makes it lead a new group in the program's session, set before it runs, so a `kill(-pid)` right after the call reaches it and everything it starts (`npx`, then `node`); on the program's terminal that group is in the background, so a read from the terminal stops it with `SIGTTIN` (`EIO` when it ignores `SIGTTIN`), as on Linux. `pgid: N` joins group N, which must be in the program's session: `EPERM` for another session's group, `ESRCH` when there is none. `session: true` makes it lead a new session and a new group (`setsid`, what Node's `detached: true` does on POSIX): its pid is its `pgid` and its `sid`, it has no controlling terminal (`/dev/tty` is `ENXIO`) and the terminal's keys don't signal it. `session` together with `pgid`, or a `pgid` that isn't a non-negative integer, is `EINVAL`. A command that does not exist rejects with `ENOENT`. `parentDeathSignal` (a number or `'SIGTERM'`, as Linux's `PR_SET_PDEATHSIG`) is sent to the child when the program ends, killed or not: to the child's whole group when it leads one (`pgid: 0` or `session: true`), else to the child alone. It's sent with the program's credentials, so a child the program may no longer signal (one that gained privilege) doesn't get it. If the program has already ended by the time the spawn completes, it's sent at once. A child of its own (not a group leader) that has been waited for gets nothing, and `sync.run` has no such option. A bad signal is `EINVAL`;
390
399
  - `wait(pid, { untraced })` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; with `untraced: true` it also resolves when the child stops, with `{ status: 128 + signal, signal, stopped: true }`, as `waitpid` with `WUNTRACED` (a child's `wait({ untraced })` is the same), and options that aren't an object are `EINVAL`; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
391
400
  - `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;
392
401
  - `crontab({ action, user?, text? })`, what the [`crontab`](#cron) command calls: `action` is `'list'`, `'remove'` or `'install'` (with the table as `text`), on the caller's own table or, for an effective uid of 0, on `user`'s. It resolves with `{ text }` (the table for `list`, `''` otherwise) or `{ error }`, the message to print. A malformed call is `EINVAL`;
@@ -635,7 +644,7 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
635
644
 
636
645
  ## Filesystem
637
646
 
638
- `/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. Files directly in `/`, and directories created there after a process started, are OPFS as well, the same for Emscripten and WASI programs; only `/dev` and `/proc` live in each process's memory. A program's `mkdir` (WASI `path_create_directory`, Emscripten's `mkdir`) is exclusive, as on Linux: it fails with `EEXIST` when the name exists, and of several processes or threads making one name at once exactly one succeeds, so a directory works as a lock. Under a missing directory it fails with `ENOENT`, as on Linux (`mkdir -p` and `os.makedirs` make the parents in the program); a JS program's `ctx.fs.mkdir` makes parents itself. The kernel makes the system `PNPM_HOME` (`/usr/local/share/pnpm`, or the embedder's) when it starts, since pnpm makes only its `bin` directory there. A full origin storage (`QuotaExceededError`, which also comes early in a browser's private or off-the-record contexts, whatever `navigator.storage.estimate()` says) is `ENOSPC`, never retried. An OPFS or IndexedDB failure that has no POSIX counterpart is `EIO`; its message names the DOM error and the operation (`lookup`, `read`, `write`, `commit` or `sidecar`), and the kernel worker logs the first of each kind to its console. File contents are buffered per open file and written back on close, `fsync` and exit, so a refused write (`ENOSPC`, `EIO`) surfaces at that close or `fsync`, and a program that ignores close errors doesn't see it (`echo x > f` can exit 0 with `f` empty, as with any buffered write-back). A write-back error a close has already reported to a WASI program isn't raised again when it exits, so the program keeps its own exit status. Metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
647
+ `/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. Files directly in `/`, and directories created there after a process started, are OPFS as well, the same for Emscripten and WASI programs; only `/dev` and `/proc` live in each process's memory. A program's `mkdir` (WASI `path_create_directory`, Emscripten's `mkdir`) is exclusive, as on Linux: it fails with `EEXIST` when the name exists, and of several processes or threads making one name at once exactly one succeeds, so a directory works as a lock. Under a missing directory it fails with `ENOENT`, as on Linux (`mkdir -p` and `os.makedirs` make the parents in the program); a JS program's `ctx.fs.mkdir` makes parents itself. The kernel makes the system `PNPM_HOME` (`/usr/local/share/pnpm`, or the embedder's) when it starts, since pnpm makes only its `bin` directory there. A full origin storage (`QuotaExceededError`, which also comes early in a browser's private or off-the-record contexts, whatever `navigator.storage.estimate()` says) is `ENOSPC`, never retried. A read that meets `NotFoundError` while a writer outside the kernel swaps the file in with `createWritable` (and a `stat`, `lstat` or `utimes` that meets it) looks the name up again, up to three times, before it fails ([#1075](https://github.com/ai-ecoverse/slicc-kernel/issues/1075)), so a program whose package is overwritten in place keeps starting. An OPFS or IndexedDB failure that has no POSIX counterpart is `EIO`; its message names the DOM error and the operation (`lookup`, `read`, `write`, `commit` or `sidecar`), and the kernel worker logs the first of each kind to its console. File contents are buffered per open file and written back on close, `fsync` and exit, so a refused write (`ENOSPC`, `EIO`) surfaces at that close or `fsync`, and a program that ignores close errors doesn't see it (`echo x > f` can exit 0 with `f` empty, as with any buffered write-back). A write-back error a close has already reported to a WASI program isn't raised again when it exits, so the program keeps its own exit status. Metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
639
648
 
640
649
  A lookup follows at most 40 symlinks, as Linux does: the 41st is `ELOOP`, for WASI programs, JS programs and the kernel API alike, with or without `..` in a link's target. Emscripten programs resolve paths in their glue, which stops one link earlier, at 40.
641
650
 
@@ -1077,6 +1077,17 @@ var S_IFIFO = 4096;
1077
1077
  var PERM_MASK = 4095;
1078
1078
  var S_ISGID2 = 1024;
1079
1079
  var READ_ATTEMPTS = 3;
1080
+ async function swapped(op) {
1081
+ for (let attempt = 1; ; attempt++) {
1082
+ try {
1083
+ return await op();
1084
+ } catch (err) {
1085
+ if (err?.name !== "NotFoundError" || attempt >= READ_ATTEMPTS) {
1086
+ throw err;
1087
+ }
1088
+ }
1089
+ }
1090
+ }
1080
1091
  var MAX_LINKS2 = 40;
1081
1092
  var CHECK_MS = 1e3;
1082
1093
  var ABSENT_MS = 2;
@@ -1862,10 +1873,14 @@ var OpfsFs = class {
1862
1873
  return { ...stat, ...ownOf(meta) ?? inherit(entries) };
1863
1874
  }
1864
1875
  stat(path) {
1865
- return this.unreplaced(async () => this.describe(await this.located(path, true)));
1876
+ return this.unreplaced(
1877
+ () => swapped(async () => this.describe(await this.located(path, true)))
1878
+ );
1866
1879
  }
1867
1880
  lstat(path) {
1868
- return this.unreplaced(async () => this.describe(await this.located(path, false)));
1881
+ return this.unreplaced(
1882
+ () => swapped(async () => this.describe(await this.located(path, false)))
1883
+ );
1869
1884
  }
1870
1885
  async exists(path) {
1871
1886
  return this.locate(path, true).then(
@@ -1891,8 +1906,17 @@ var OpfsFs = class {
1891
1906
  }
1892
1907
  reading(path, op) {
1893
1908
  return this.unreplaced(async () => {
1894
- const handle = await this.existing(path);
1895
- return this.retrying(path, () => op(handle), false);
1909
+ for (let attempt = 1; ; attempt++) {
1910
+ const found = await this.locate(path, true);
1911
+ const handle = found.handle;
1912
+ if (handle.kind === "directory") throw fsError("EISDIR", path);
1913
+ try {
1914
+ return await this.retrying(path, () => op(handle), false);
1915
+ } catch (err) {
1916
+ if (err.code !== "ENOENT" || attempt >= READ_ATTEMPTS) throw err;
1917
+ this.files.delete(found.path);
1918
+ }
1919
+ }
1896
1920
  });
1897
1921
  }
1898
1922
  async existing(path, writing = false) {
@@ -2396,10 +2420,10 @@ var OpfsFs = class {
2396
2420
  }));
2397
2421
  }
2398
2422
  async utimes(path, atime, mtime) {
2399
- await this.setTimes(await this.locate(path, true), atime, mtime);
2423
+ await swapped(async () => this.setTimes(await this.locate(path, true), atime, mtime));
2400
2424
  }
2401
2425
  async lutimes(path, atime, mtime) {
2402
- await this.setTimes(await this.locate(path, false), atime, mtime);
2426
+ await swapped(async () => this.setTimes(await this.locate(path, false), atime, mtime));
2403
2427
  }
2404
2428
  async setTimes(found, atime, mtime) {
2405
2429
  this.refuse(found.path);
@@ -6632,7 +6656,7 @@ function satisfies(version, range2) {
6632
6656
  }
6633
6657
 
6634
6658
  // src/version.ts
6635
- var KERNEL_VERSION = true ? "1.125.9" : "0.0.0-dev";
6659
+ var KERNEL_VERSION = true ? "1.125.11" : "0.0.0-dev";
6636
6660
  var OS_RELEASE = "6.0.0-slicc";
6637
6661
  function procVersion() {
6638
6662
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -19330,6 +19354,7 @@ var MAX_READ = 1024 * 1024;
19330
19354
  var O_NONBLOCK = 2048;
19331
19355
  var WRITE_ACCESS = 3;
19332
19356
  var KEYBOARD = /* @__PURE__ */ new Set([SIG.INT, SIG.QUIT, SIG.TSTP]);
19357
+ var STUCK_ESCAPES = /* @__PURE__ */ new Set([SIG.INT, SIG.TERM, SIG.QUIT, SIG.HUP]);
19333
19358
  var DEFAULT_UMASK2 = 18;
19334
19359
  function words(value2) {
19335
19360
  if (!Array.isArray(value2) || !value2.every((w) => typeof w === "string")) {
@@ -19386,6 +19411,8 @@ var WasmProcess = class {
19386
19411
  cred;
19387
19412
  execChild;
19388
19413
  killed = false;
19414
+ calls = 0;
19415
+ pendingAt = /* @__PURE__ */ new Map();
19389
19416
  execTermsig;
19390
19417
  stopped = 0;
19391
19418
  stops = 0;
@@ -19484,9 +19511,9 @@ var WasmProcess = class {
19484
19511
  const action = defaultAction(sig);
19485
19512
  return action === "stop" ? this.stop(sig) : action;
19486
19513
  }
19487
- if (((this.options.pendingBits?.() ?? 0) & bit) !== 0 && defaultAction(sig) === "terminate") {
19488
- return "terminate";
19489
- }
19514
+ if (((this.options.pendingBits?.() ?? 0) & bit) !== 0) {
19515
+ if (STUCK_ESCAPES.has(sig) && this.pendingAt.get(sig) === this.calls) return "terminate";
19516
+ } else this.pendingAt.set(sig, this.calls);
19490
19517
  this.options.onPending?.(sig);
19491
19518
  if (this.options.jobs?.keyboard && KEYBOARD.has(sig) && this.waiting.size === 0)
19492
19519
  this.owed = true;
@@ -19609,6 +19636,7 @@ var WasmProcess = class {
19609
19636
  }
19610
19637
  async syscall(req, cancelled, block) {
19611
19638
  this.options.onSyscall?.(req);
19639
+ this.calls += 1;
19612
19640
  const started = performance.now();
19613
19641
  let restarted = false;
19614
19642
  for (; ; ) {