@ai-ecoverse/slicc-kernel 1.135.5 → 1.135.7

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
@@ -317,7 +317,7 @@ On top of preview1 a WASI program gets:
317
317
  - `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.
318
318
  - **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.
319
319
  - **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).
320
- - **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, unless the main thread blocks it and another thread does not. Then, as on Linux, it goes to that thread: its handler runs there, or with no handler its default action ends (or stops) the whole process at once. So the "signal thread" pattern works: block in main, unblock in a worker (#520). It waits on the main thread only when every thread blocks it. 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. `slicc.sigmask_epoll_wait(set_lo, set_hi, epfd, events_ptr, maxevents, timeout_ns, nevents_ptr)` is `epoll_pwait` (#771). It swaps the thread's mask to the given set in the same way. A pending signal the new mask unblocks runs its handler and makes the call return `EINTR` with no events, without waiting. Otherwise the call waits exactly as `wasix_32v1.epoll_wait` with the same arguments does (the same event layout and errors; a `timeout_ns` of all bits set is forever, 0 doesn't wait), under the new mask, so a signal it unblocks ends the wait with `EINTR` after the handler. The old mask is back before the call returns, on every path, and `*nevents_ptr` is always written. A kernel without it answers `ENOSYS`, and the libc keeps its non-atomic swap around `epoll_wait`. For both masked waits, a signal that came while the program computed and was delivered as the call began (its handler ran under the old mask, as on Linux, where it would have run before the call) ends the call with `EINTR` only if the new mask leaves it unblocked; when the new mask blocks every signal delivered then, the call waits as if none had come (#799). `slicc.sigwait(set_lo, set_hi, timeout_ns, sig_ptr)` is `sigwait`, `sigwaitinfo` and `sigtimedwait` (#520). If a signal of the set is pending for the thread, or else for the process, it takes it: the lowest-numbered first, with no handler run. It writes the number as a `u32` at `sig_ptr` and returns 0. Otherwise it waits until one is pending. `timeout_ns` of all bits set is forever and 0 doesn't wait; when the time runs out it answers `EAGAIN` (`*sig_ptr = 0`). A signal outside the set that the thread has unblocked and handles runs its handler and ends the call with `EINTR` (also one that came while the program computed and was delivered as the call began, as for the other waits), and so does a job-control stop, once continued. While a thread waits, the kernel counts it as able to take the signals of its set: a process-directed signal that every thread blocks goes to it (the main thread first, if it is the one waiting), so the signal-thread pattern works with `sigwait` too. Only kernel signals 1 to 31 can be waited for. A kernel without it answers `ENOSYS`. `slicc.sigaction_inherited(ign_ptr)` (#820) writes, as a little-endian `u64` at `ign_ptr`, the signals the program arrived with set to `SIG_IGN`, inherited across `exec` or spawn (bit `sig - 1`; never `SIGKILL` or `SIGSTOP`; kernel signals are 1 to 31, so the high word is 0), and returns 0. A libc calls it once before constructors and `main` and seeds its `sigaction` table with `SIG_IGN` for each bit, so `sigaction(sig, NULL, &old)` reads an inherited ignore back (wasix-sysroot 2025.9.30-32 and later); a kernel without it answers `ENOSYS`, and the libc keeps its defaults. **A handler and its own signal** (#971): while a handler runs, the same signal waits, as if `sa_mask` held it: it arrives once after the handler returns, not nested inside it, unless the handler was set with `SA_NODEFER`. A program that is sent its fatal signal twice, or raises it from its own handler, therefore no longer recurses (gpg's `got_fatal_signal` under `timeout` overflowed the stack). Other signals still run nested, as with an empty `sa_mask`. Handlers are never inherited. **The sender** (#892): `slicc.sigwait_info(set_lo, set_hi, timeout_ns, info_ptr)` is `slicc.sigwait` that also says who sent the signal it took, for `sigwaitinfo` and `sigtimedwait`. `slicc.siginfo_take(sig, info_ptr)` fills the same struct with the sender of the instance of `sig` just delivered to this thread's handler, for an `SA_SIGINFO` handler's `siginfo_t`, and consumes it; with none recorded it answers `ENOENT` and leaves the struct alone, and libc fills `SI_USER` with pid and uid 0. Both write `struct { u32 size; i32 signo; i32 code; i32 pid; u32 uid; i32 status; }`, little-endian, 24 bytes:
320
+ - **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, unless the main thread blocks it and another thread does not. Then, as on Linux, it goes to that thread: its handler runs there, or with no handler its default action ends (or stops) the whole process at once. So the "signal thread" pattern works: block in main, unblock in a worker (#520). It waits on the main thread only when every thread blocks it. 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. `slicc.sigmask_epoll_wait(set_lo, set_hi, epfd, events_ptr, maxevents, timeout_ns, nevents_ptr)` is `epoll_pwait` (#771). It swaps the thread's mask to the given set in the same way. A pending signal the new mask unblocks runs its handler and makes the call return `EINTR` with no events, without waiting. Otherwise the call waits exactly as `wasix_32v1.epoll_wait` with the same arguments does (the same event layout and errors; a `timeout_ns` of all bits set is forever, 0 doesn't wait), under the new mask, so a signal it unblocks ends the wait with `EINTR` after the handler. The old mask is back before the call returns, on every path, and `*nevents_ptr` is always written. A kernel without it answers `ENOSYS`, and the libc keeps its non-atomic swap around `epoll_wait`. For both masked waits, a signal that came while the program computed and was delivered as the call began (its handler ran under the old mask, as on Linux, where it would have run before the call) ends the call with `EINTR` only if the new mask leaves it unblocked; when the new mask blocks every signal delivered then, the call waits as if none had come (#799). `slicc.sigwait(set_lo, set_hi, timeout_ns, sig_ptr)` is `sigwait`, `sigwaitinfo` and `sigtimedwait` (#520). If a signal of the set is pending for the thread, or else for the process, it takes it: the lowest-numbered first, with no handler run. It writes the number as a `u32` at `sig_ptr` and returns 0. Otherwise it waits until one is pending. `timeout_ns` of all bits set is forever and 0 doesn't wait; when the time runs out it answers `EAGAIN` (`*sig_ptr = 0`). A signal outside the set that the thread has unblocked and handles runs its handler and ends the call with `EINTR` (also one that came while the program computed and was delivered as the call began, as for the other waits), and so does a job-control stop, once continued. While a thread waits, the kernel counts it as able to take the signals of its set: a process-directed signal that every thread blocks goes to it (the main thread first, if it is the one waiting), so the signal-thread pattern works with `sigwait` too. Only kernel signals 1 to 31 can be waited for. A kernel without it answers `ENOSYS`. `slicc.sigaction_inherited(ign_ptr)` (#820) writes, as a little-endian `u64` at `ign_ptr`, the signals the program arrived with set to `SIG_IGN`, inherited across `exec` or spawn (bit `sig - 1`; never `SIGKILL` or `SIGSTOP`; kernel signals are 1 to 31, so the high word is 0), and returns 0. A libc calls it once before constructors and `main` and seeds its `sigaction` table with `SIG_IGN` for each bit, so `sigaction(sig, NULL, &old)` reads an inherited ignore back (wasix-sysroot 2025.9.30-32 and later); a kernel without it answers `ENOSYS`, and the libc keeps its defaults. **A handler and its own signal** (#971): while a handler runs, the same signal waits, as if `sa_mask` held it: it arrives once after the handler returns, not nested inside it, unless the handler was set with `SA_NODEFER`. The hold is part of the handler's mask, so a handler that unblocks its own signal (`pthread_sigmask(SIG_UNBLOCK, …)` or a `SIG_SETMASK` without it) lifts it: a signal raised after that nests at once, as on Linux (#1236). A program that is sent its fatal signal twice, or raises it from its own handler, therefore no longer recurses (gpg's `got_fatal_signal` under `timeout` overflowed the stack). Other signals still run nested, as with an empty `sa_mask`. Handlers are never inherited. **The sender** (#892): `slicc.sigwait_info(set_lo, set_hi, timeout_ns, info_ptr)` is `slicc.sigwait` that also says who sent the signal it took, for `sigwaitinfo` and `sigtimedwait`. `slicc.siginfo_take(sig, info_ptr)` fills the same struct with the sender of the instance of `sig` just delivered to this thread's handler, for an `SA_SIGINFO` handler's `siginfo_t`, and consumes it; with none recorded it answers `ENOENT` and leaves the struct alone, and libc fills `SI_USER` with pid and uid 0. Both write `struct { u32 size; i32 signo; i32 code; i32 pid; u32 uid; i32 status; }`, little-endian, 24 bytes:
321
321
  - the caller puts in `size` how many bytes it has, and the kernel fills the fields that fit and writes back how many it filled, so later fields can follow without a new import;
322
322
  - `kill()`/`raise()` by a process give `SI_USER` (0), the sender's pid as `getpid()` shows it and its real uid; a `SIGPIPE` from a write is `SI_USER` from the process itself, as on Linux;
323
323
  - `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;
@@ -1424,6 +1424,8 @@ Up to -35, `sigsetjmp` was an ordinary libc function rather than the `setjmp` th
1424
1424
 
1425
1425
  A WASIX C program for `test/integration/handler-defer.test.mjs` (#971), built on wasix-sysroot 2025.9.30-33. Its `SIGTERM` handler counts how often and how deeply it runs. On its first run it sends itself `SIGTERM` again and then writes to stderr. `handlertest` installs it with no flags, and `handlertest nodefer` with `SA_NODEFER`. Either way it prints `the handler ran N times, at most D deep`.
1426
1426
 
1427
+ **`nesttest`:** a WASIX C program for `test/integration/handler-defer.test.mjs` (#1236, homescoop#1058), built on wasix-sysroot 2025.9.30-28. Its `SIGUSR1` handler, on its first run, unblocks `SIGUSR1` with `pthread_sigmask` and raises it again. It prints the order of handler entries and exits (`<` and `>`) and the deepest nesting; Linux prints `order=<<>> maxdepth=2`.
1428
+
1427
1429
  **Build:** `WASIX_SYSROOT_33=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` on `PATH`. `bin/handlertest.wasm` is 40,458 bytes, sha256 `b0e8e841c053ff133b734de336198410e7705b536e155166fa35970ad1fbb9c2`.
1428
1430
 
1429
1431
  ### `fifotest`
@@ -7098,7 +7098,7 @@ function satisfies(version, range2) {
7098
7098
  }
7099
7099
 
7100
7100
  // src/version.ts
7101
- var KERNEL_VERSION = true ? "1.135.5" : "0.0.0-dev";
7101
+ var KERNEL_VERSION = true ? "1.135.7" : "0.0.0-dev";
7102
7102
  var OS_RELEASE = "6.0.0-slicc";
7103
7103
  function procVersion() {
7104
7104
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -5008,6 +5008,9 @@ var init_wasi_host = __esm({
5008
5008
  else if (!o.bare) this.fds.setup(o.cwd, o.inherited ?? [], o.preopenRoot === true);
5009
5009
  this.listening = o.shared ? [] : this.fds.sockets();
5010
5010
  }
5011
+ fromCwd(raw) {
5012
+ return this.fds.cwd() === void 0 ? this.fds.resolve(3, `${this.cwd}/${raw}`) : this.fds.resolve(3, raw);
5013
+ }
5011
5014
  get cwd() {
5012
5015
  return this.fds.cwd() ?? this.startCwd;
5013
5016
  }
@@ -5020,7 +5023,7 @@ var init_wasi_host = __esm({
5020
5023
  const raw = mem.string(p, l);
5021
5024
  if (raw === "") throw new WasiError("ENOENT");
5022
5025
  if (raw.startsWith("/")) return fds.resolve(3, raw);
5023
- return dirfd === AT_FDCWD ? fds.resolve(3, `${this.cwd}/${raw}`) : fds.resolve(dirfd, raw);
5026
+ return dirfd === AT_FDCWD ? this.fromCwd(raw) : fds.resolve(dirfd, raw);
5024
5027
  };
5025
5028
  return (...args) => {
5026
5029
  const [srcDirfd, sp, sl, dstDirfd, dp, dl, mode, flags] = args;
@@ -5180,7 +5183,7 @@ var init_wasi_host = __esm({
5180
5183
  if (!mkfifo) return E.NOSYS;
5181
5184
  const raw = mem.string(p, l);
5182
5185
  if (raw === "") throw new WasiError("ENOENT");
5183
- const path = dirfd === AT_FDCWD && !raw.startsWith("/") ? fds.resolve(3, `${this.cwd}/${raw}`) : at(dirfd, p, l);
5186
+ const path = dirfd === AT_FDCWD && !raw.startsWith("/") ? this.fromCwd(raw) : at(dirfd, p, l);
5184
5187
  const umask = this.umasks.get();
5185
5188
  const want = mode & 4095 & ~umask;
5186
5189
  mkfifo.call(this.o.fs, path, want);
@@ -5285,12 +5288,18 @@ var init_wasi_host = __esm({
5285
5288
  if (errno2 === E.SUCCESS && disposition === SIG_IGN) this.sigmask?.discard(sig);
5286
5289
  return errno2;
5287
5290
  },
5288
- sigmask_set: (how, lo, hi, old) => this.sigmask ? this.sigmask.set(how, lo, hi, (oldLo, oldHi) => {
5289
- if (old === 0) return;
5290
- const view = mem.view();
5291
- view.setUint32(old, oldLo, true);
5292
- view.setUint32(old + 4, oldHi, true);
5293
- }) : E.NOSYS,
5291
+ sigmask_set: (how, lo, hi, old) => {
5292
+ if (!this.sigmask) return E.NOSYS;
5293
+ if (how === SIG_UNBLOCK || how === SIG_SETMASK) {
5294
+ this.signals?.unblocking(how === SIG_UNBLOCK ? lo << 1 : ~(lo << 1));
5295
+ }
5296
+ return this.sigmask.set(how, lo, hi, (oldLo, oldHi) => {
5297
+ if (old === 0) return;
5298
+ const view = mem.view();
5299
+ view.setUint32(old, oldLo, true);
5300
+ view.setUint32(old + 4, oldHi, true);
5301
+ });
5302
+ },
5294
5303
  sigmask_poll: (lo, hi, inPtr, outPtr, n, nevents) => this.sigmask ? this.maskedPoll(this.sigmask, [lo, hi], [inPtr, outPtr, n, nevents]) : E.NOSYS,
5295
5304
  sigmask_epoll_wait: (lo, hi, epfd, events, max, timeout, nevents) => this.sigmask ? this.maskedEpoll(
5296
5305
  this.sigmask,
@@ -6690,7 +6699,7 @@ var KERNEL_VERSION, OS_RELEASE;
6690
6699
  var init_version = __esm({
6691
6700
  "src/version.ts"() {
6692
6701
  "use strict";
6693
- KERNEL_VERSION = true ? "1.135.5" : "0.0.0-dev";
6702
+ KERNEL_VERSION = true ? "1.135.7" : "0.0.0-dev";
6694
6703
  OS_RELEASE = "6.0.0-slicc";
6695
6704
  }
6696
6705
  });
@@ -15867,6 +15876,10 @@ var init_wasi_signals = __esm({
15867
15876
  nodefer = 0;
15868
15877
  running = 0;
15869
15878
  deferred = 0;
15879
+ lifted = 0;
15880
+ unblocking(bits) {
15881
+ this.lifted |= bits & this.running;
15882
+ }
15870
15883
  defaults = 0;
15871
15884
  nocldwait = false;
15872
15885
  fallBack;
@@ -15937,15 +15950,18 @@ var init_wasi_signals = __esm({
15937
15950
  return;
15938
15951
  }
15939
15952
  const bit = sigbit(sig);
15940
- if ((this.running & bit) !== 0 && (this.nodefer & bit) === 0) {
15953
+ if ((this.running & bit) !== 0 && ((this.nodefer | this.lifted) & bit) === 0) {
15941
15954
  this.deferred |= bit;
15942
15955
  return;
15943
15956
  }
15957
+ const outer = { running: this.running & bit, lifted: this.lifted & bit };
15944
15958
  this.running |= bit;
15959
+ this.lifted &= ~bit;
15945
15960
  try {
15946
15961
  this.run(handler, sig);
15947
15962
  } finally {
15948
- this.running &= ~bit;
15963
+ this.running = this.running & ~bit | outer.running;
15964
+ this.lifted = this.lifted & ~bit | outer.lifted;
15949
15965
  }
15950
15966
  if ((this.deferred & bit) === 0) return;
15951
15967
  this.deferred &= ~bit;