@ai-ecoverse/slicc-kernel 1.98.3 → 1.98.4

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`), 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`.
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, 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`.
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`).
@@ -919,6 +919,20 @@ WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh
919
919
  - `wasixcc` (0.4.7, WASIX clang 21.1.2) comes from `PATH`, or is installed by `install-wasixcc.sh` as for `dlfork`.
920
920
  - The build is reproducible: `bin/epolltest.wasm` is 78,799 bytes, sha256 `9ff8d336a261ebb9076b5b9c760e9ba50fa17053b1d66ab89cfe05395c8e1deb`.
921
921
 
922
+ ### `piefork`
923
+
924
+ A position-independent C program, built like `dlfork` (wasixcc with `WASIXCC_PIC=yes` and legacy exceptions, against `@ai-ecoverse/wasix-sysroot` 2025.9.30-28, then `wasm-opt --asyncify` with `asyncify-imports@wasix_32v1.proc_fork`). It's the fixture of `test/integration/pie-fork.test.mjs` (#708).
925
+
926
+ What it does:
927
+ 1. It recurses 300 frames, each holding stack buffers.
928
+ 2. At the bottom it snapshots the first 64 KiB of memory, from address 16, where the linker puts its data, and forks.
929
+ 3. Parent and child then compare that memory with the snapshot and scribble 64 KiB of stack.
930
+ 4. On the way back up, each frame checks its own buffers.
931
+
932
+ Each side prints `pattern` (a static array's damage), `low` (bytes of low memory changed) and `frames` (damaged frames).
933
+
934
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` (version 132) on `PATH`. `bin/piefork.wasm` is 823,983 bytes, sha256 `0477598c71b02356145d1725548ee0f2b9e53ca0d43c7349f17acc2ff12b59f8`.
935
+
922
936
  ## License
923
937
 
924
938
  Apache-2.0
@@ -12082,7 +12082,7 @@ var PATH_OPS = new Set(SYNC_FS_OPS);
12082
12082
  var EARLY_WHOLE_FILE = 64 * 1024;
12083
12083
 
12084
12084
  // src/version.ts
12085
- var KERNEL_VERSION = true ? "1.98.3" : "0.0.0-dev";
12085
+ var KERNEL_VERSION = true ? "1.98.4" : "0.0.0-dev";
12086
12086
  var OS_RELEASE = "6.0.0-slicc";
12087
12087
  function procVersion() {
12088
12088
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -4591,7 +4591,7 @@ var KERNEL_VERSION, OS_RELEASE;
4591
4591
  var init_version = __esm({
4592
4592
  "src/version.ts"() {
4593
4593
  "use strict";
4594
- KERNEL_VERSION = true ? "1.98.3" : "0.0.0-dev";
4594
+ KERNEL_VERSION = true ? "1.98.4" : "0.0.0-dev";
4595
4595
  OS_RELEASE = "6.0.0-slicc";
4596
4596
  }
4597
4597
  });
@@ -13276,16 +13276,18 @@ function mutableGlobals(exports) {
13276
13276
  }
13277
13277
  return out;
13278
13278
  }
13279
- var UNWINDING, REWINDING, AsyncifyDriver;
13279
+ var STACK_POINTER, UNWINDING, REWINDING, AsyncifyDriver;
13280
13280
  var init_wasix_fork = __esm({
13281
13281
  "src/process/wasi/wasix-fork.ts"() {
13282
13282
  "use strict";
13283
13283
  init_wasi_abi();
13284
+ STACK_POINTER = "__stack_pointer";
13284
13285
  UNWINDING = 1;
13285
13286
  REWINDING = 2;
13286
13287
  AsyncifyDriver = class {
13287
13288
  exports = {};
13288
13289
  stackUpper = 0;
13290
+ linked;
13289
13291
  pending;
13290
13292
  answer = 0;
13291
13293
  snapshots = /* @__PURE__ */ new Map();
@@ -13294,10 +13296,12 @@ var init_wasix_fork = __esm({
13294
13296
  constructor(mem) {
13295
13297
  this.mem = mem;
13296
13298
  }
13299
+ useStack(pointer, low) {
13300
+ this.linked = { pointer, low };
13301
+ }
13297
13302
  bind(exports) {
13298
13303
  this.exports = exports;
13299
- const sp = this.exports.__stack_pointer;
13300
- this.stackUpper = sp instanceof WebAssembly.Global ? sp.value : 0;
13304
+ this.stackUpper = this.stackPointer();
13301
13305
  }
13302
13306
  get supported() {
13303
13307
  return typeof this.exports.asyncify_start_unwind === "function";
@@ -13332,7 +13336,7 @@ var init_wasix_fork = __esm({
13332
13336
  }
13333
13337
  fork(fork) {
13334
13338
  const sp = this.stackPointer();
13335
- const globals = mutableGlobals(this.exports);
13339
+ const globals = this.globals();
13336
13340
  return this.unwind((data) => {
13337
13341
  this.answer = fork(data, globals, sp);
13338
13342
  });
@@ -13343,7 +13347,7 @@ var init_wasix_fork = __esm({
13343
13347
  v.setBigUint64(snapPtr, BigInt(retPtr), true);
13344
13348
  v.setBigUint64(snapPtr + 8, BigInt(id), true);
13345
13349
  v.setBigUint64(snapPtr + 16, 0x51cc5117n, true);
13346
- const globals = mutableGlobals(this.exports);
13350
+ const globals = this.globals();
13347
13351
  return this.unwind((data) => {
13348
13352
  const end = this.mem.view().getUint32(data, true);
13349
13353
  this.snapshots.set(id, { frames: this.mem.bytes(data + 8, end - data - 8).slice(), globals });
@@ -13361,11 +13365,24 @@ var init_wasix_fork = __esm({
13361
13365
  this.answer = Number(val) || 1;
13362
13366
  });
13363
13367
  }
13368
+ stackGlobal() {
13369
+ const g = this.exports[STACK_POINTER];
13370
+ return g instanceof WebAssembly.Global ? g : this.linked?.pointer;
13371
+ }
13364
13372
  stackPointer() {
13365
- const g = this.exports.__stack_pointer;
13366
- return g instanceof WebAssembly.Global ? g.value : 0;
13373
+ return this.stackGlobal()?.value ?? 0;
13374
+ }
13375
+ globals() {
13376
+ const out = mutableGlobals(this.exports);
13377
+ if (this.linked && !out.some(([name2]) => name2 === STACK_POINTER)) {
13378
+ out.push([STACK_POINTER, this.linked.pointer.value]);
13379
+ }
13380
+ return out;
13367
13381
  }
13368
13382
  dataPointer() {
13383
+ if (this.linked && !(this.exports[STACK_POINTER] instanceof WebAssembly.Global)) {
13384
+ return this.linked.low + 15 & ~15;
13385
+ }
13369
13386
  const g = this.exports.__data_end;
13370
13387
  const dataEnd = g instanceof WebAssembly.Global ? g.value : 0;
13371
13388
  return dataEnd < this.stackUpper ? dataEnd + 15 & ~15 : 16;
@@ -13385,7 +13402,7 @@ var init_wasix_fork = __esm({
13385
13402
  }
13386
13403
  restoreGlobals(globals) {
13387
13404
  for (const [name2, value2] of globals) {
13388
- const g = this.exports[name2];
13405
+ const g = name2 === STACK_POINTER ? this.stackGlobal() : this.exports[name2];
13389
13406
  if (g instanceof WebAssembly.Global) g.value = value2;
13390
13407
  }
13391
13408
  }
@@ -15135,6 +15152,7 @@ async function instantiate(host, module, memory, threads, {
15135
15152
  }
15136
15153
  }
15137
15154
  if (sync) bindLinker(host, sync, instance, { thread, forked, path });
15155
+ if (sync && !thread) driver.useStack(sync.linker.stackPointer, sync.linker.stackLow);
15138
15156
  return { instance, driver };
15139
15157
  }
15140
15158
  function bindLinker(host, sync, instance, at) {