@ai-ecoverse/slicc-kernel 1.50.0 → 1.51.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
@@ -186,8 +186,8 @@ A descriptor a program inherits at one of those numbers (from bash's `3<file`, o
186
186
  On top of preview1 a WASI program gets:
187
187
 
188
188
  - **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; `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.
189
- - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), 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.
190
- - **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`); without it, missing arguments are zero 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`, which puts a function into such a slot, is not there yet and answers `ENOSYS`. A `fork` or a `longjmp` (both through Asyncify) inside a function reached through `call_dynamic` cannot unwind through it.
189
+ - **WASIX** (a module importing `wasix_32v1`): fork and setjmp/longjmp through Asyncify (the module is built with `wasm-opt --asyncify`), 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. `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); `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`.
190
+ - **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`); without it, missing arguments are zero 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.
191
191
  - **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. 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 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.
192
192
  - **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.
193
193
  - **Sockets**: the WASIX socket calls (`sock_open`, `sock_pair` for an `AF_UNIX` stream 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, since a WASI program cannot ignore `SIGPIPE`; a pipe or terminal whose reader is gone still ends the program with status 141. 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.
@@ -6451,6 +6451,7 @@ var MountTable = class {
6451
6451
  mounts = /* @__PURE__ */ new Map();
6452
6452
  sessions = /* @__PURE__ */ new Map();
6453
6453
  committing = /* @__PURE__ */ new Map();
6454
+ epoch = 0;
6454
6455
  devices = 0;
6455
6456
  pending = /* @__PURE__ */ new Set();
6456
6457
  notes = /* @__PURE__ */ new Map();
@@ -6610,6 +6611,7 @@ var MountTable = class {
6610
6611
  const session = this.sessions.get(real);
6611
6612
  if (!session) return this.committing.get(real)?.done;
6612
6613
  this.sessions.delete(real);
6614
+ this.epoch++;
6613
6615
  const done4 = (async () => {
6614
6616
  try {
6615
6617
  await session.mount.conn.call({ op: "release", fh: await session.fh });
@@ -6662,19 +6664,29 @@ var MountTable = class {
6662
6664
  this.deps.changed?.(...full);
6663
6665
  }
6664
6666
  forget(mount, path) {
6667
+ this.epoch++;
6665
6668
  for (const key of [...mount.attrs.keys()]) if (within4(key, path)) mount.attrs.delete(key);
6666
6669
  mount.lists.delete(path);
6667
6670
  mount.lists.delete(parentOf5(path));
6668
6671
  for (const key of [...mount.lists.keys()]) if (within4(key, path)) mount.lists.delete(key);
6669
6672
  }
6670
- async getattr(mount, path, rel2) {
6671
- if (mount.conn.failure) throw mount.conn.failure;
6672
- const cached = mount.attrs.get(path);
6673
- if (cached && cached.expires > Date.now()) return cached.value;
6674
- const attr = await mount.conn.call({ op: "getattr", path: rel2 });
6675
- const ttl = mount.caps.attrTtl ?? 1e3;
6676
- if (ttl > 0) mount.attrs.set(path, { value: attr, expires: Date.now() + ttl });
6677
- return attr;
6673
+ async getattr(mount, path, rel2, settled = false) {
6674
+ for (; ; ) {
6675
+ if (mount.conn.failure) throw mount.conn.failure;
6676
+ if (settled) await this.committing.get(path)?.done.catch(() => void 0);
6677
+ const cached = mount.attrs.get(path);
6678
+ if (cached && cached.expires > Date.now()) return cached.value;
6679
+ const seen = this.epoch;
6680
+ const session = this.sessions.get(path);
6681
+ const attr = await mount.conn.call({ op: "getattr", path: rel2 });
6682
+ if (this.epoch !== seen) {
6683
+ if (settled && (this.sessions.get(path) !== session || this.committing.has(path))) continue;
6684
+ return attr;
6685
+ }
6686
+ const ttl = mount.caps.attrTtl ?? 1e3;
6687
+ if (ttl > 0) mount.attrs.set(path, { value: attr, expires: Date.now() + ttl });
6688
+ return attr;
6689
+ }
6678
6690
  }
6679
6691
  async readdir(mount, path, rel2) {
6680
6692
  if (mount.conn.failure) throw mount.conn.failure;
@@ -6955,7 +6967,7 @@ var MountFs = class {
6955
6967
  const real = normalizePath(path);
6956
6968
  if (this.table.writing(real)?.truncated) await this.table.commit(real);
6957
6969
  return this.statWriting(
6958
- await this.table.getattr(found.mount, real, found.rel),
6970
+ await this.table.getattr(found.mount, real, found.rel, true),
6959
6971
  real,
6960
6972
  found.mount
6961
6973
  );