@ai-ecoverse/slicc-kernel 1.116.2 → 1.117.0

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
@@ -618,6 +618,13 @@ Directories are renamed with `FileSystemHandle.move()` where available, else by
618
618
  - **F_GETLK:** with a conflicting lock, it writes that lock back (`l_whence` 0, its absolute start, its length, the holder's pid). Without one, it sets only `l_type` to `2`.
619
619
  - **Errors (WASI numbers):** `EAGAIN` for a conflict on `F_SETLK` (never `EACCES`), `EINTR` when a signal with a handler ends an `F_SETLKW` wait, `EDEADLK` for a wait that would close a cycle, `EBADF` for a bad descriptor or a lock type the descriptor's access mode doesn't allow, and `EINVAL` for a bad `l_type` or `l_whence` or a start before 0.
620
620
  - **Semantics:** the same as Emscripten's, above. A libc that finds no import on an older kernel gets `ENOSYS` and keeps its local answer.
621
+ - **Any kind of descriptor** ([#893](https://github.com/ai-ecoverse/slicc-kernel/issues/893)): a pipe, a socket or a terminal takes record locks too, as on Linux, keyed by the object (both ends of a pipe are one object, each socket is its own, a terminal is one object however often it's opened). `l_whence` `CUR` and `END` count from 0 there.
622
+ - **`slicc.flock(fd, op) -> errno`** ([#893](https://github.com/ai-ecoverse/slicc-kernel/issues/893)): BSD `flock(2)` locks, with Linux's semantics, for a WASIX program on any descriptor: a regular file, a directory, a pipe, a socket, a terminal or a `/dev` device.
623
+ - **op:** Linux's bits, `LOCK_SH` `1`, `LOCK_EX` `2` or `LOCK_UN` `8`, optionally with `LOCK_NB` `4`. Anything else is `EINVAL`.
624
+ - **Owner:** the open file description, not the process. A `dup`'d descriptor and one a forked child or a spawned program inherits share it, so the child's `LOCK_EX|LOCK_NB` is granted and its `LOCK_UN` releases the lock for the parent too. So children that `flock(STDOUT)` an inherited STDOUT don't exclude each other, on Linux either: for that, each child opens a lock file itself. A second open of the same object is another description and conflicts: the file opened again (by any name of it), `/dev/tty` opened again, the other end of a pipe.
625
+ - **Rules:** shared locks coexist, an exclusive one excludes every other description, and any access mode takes either kind, so `LOCK_EX` on a read-only open excludes (homescoop#758). A conversion drops the old lock before it takes the new one, as on Linux, so a refused `LOCK_NB` conversion leaves no lock. The lock goes with `LOCK_UN` or when the description's last descriptor closes, at exit or kill too. flock locks and `fcntl` record locks are separate and don't block each other.
626
+ - **Errors (WASI numbers):** `EAGAIN` for a conflict with `LOCK_NB`, `EINTR` when a signal with a handler ends a wait (as for `F_SETLKW`: a libc that wants `SA_RESTART` asks again), `EBADF` for a bad descriptor, `EINVAL` for a bad op. There's no deadlock detection, as on Linux.
627
+ - **Limits:** locks are per kernel ([#675](https://github.com/ai-ecoverse/slicc-kernel/issues/675)). A file the program buffers itself is handed to the kernel at its first `flock`, with every other descriptor the program has open on it. A libc that finds no import on an older kernel gets `ENOSYS`; wasix-libc's `flock()` then keeps its old answer. Emscripten programs and the imports module's `tryLock` are unchanged.
621
628
  - **Rules on a mount:**
622
629
  - a rename across mounts is `EXDEV`, so `mv` copies;
623
630
  - a symlink on the root file system that leads onto a mount, such as `~/proj` → `/mnt/drive/proj`, or a chain from a mount to the root and back, is followed onto the mount by every operation, in every runtime: reads, writes, `stat`, listings, `mkdir`, removals, renames, `chmod`, `chown` and times. Removing the link itself removes the link. Before [#547](https://github.com/ai-ecoverse/slicc-kernel/issues/547) was fixed, the kernel resolved such a link against the directory under the mount point for JS and WASI programs, the embedder's `fs` and attached clients (Emscripten programs weren't affected): reads failed with `ENOENT`, and writes went into that hidden directory, which shows again once the mount is gone;
@@ -970,7 +977,7 @@ Releases are cut by semantic-release on every push to `main`, in two jobs, and o
970
977
  Every change is certified in its pull request, before it merges (`cert-review-pr.yml`, next section), and `cert-review` is a required check of the merge queue, so every commit on `main`, and every release built from it, was certified first. A release is then **verified on npm** (`release / verify on npm` in `release.yml`, `tools/cert-review/verify.mjs`), in code, for every tag the run made:
971
978
 
972
979
  - It waits up to an hour for npm to serve the version (an accepted publish can take a while to appear), then downloads the tarball and checks `dist.integrity` (sha512), the SLSA v1 provenance (the tarball's sha512 is its subject; `release.yml` on `refs/heads/main`, a GitHub-hosted builder; `gitCommit` = `gitHead` = the tag, on `main`), and `npm audit signatures`. A version that `release-recover.mjs` skipped because npm's `latest` is newer (#837) has nothing to verify.
973
- - It then writes `certified @ai-ecoverse/slicc-kernel@<v> sha256=<64 hex> (covers <first>–<v>; ci: <run>; pr-certified: <each merged pull request's cert-review>; known: <open known-issue issues>)` into the GitHub release's notes, between `<!-- cert-verify -->` markers. homescoop's ci reads exactly that prefix and re-hashes the npm tarball against it. `covers` starts after the last version npm serves, so a skipped version is covered by the next. Notes that already carry the identical line stay unchanged.
980
+ - It then writes `certified @ai-ecoverse/slicc-kernel@<v> sha256=<64 hex> (covers <first>–<v>; ci: <run>; pr-certified: <each merged pull request's cert-review at its merge>; known: <open known-issue issues>)` into the GitHub release's notes, between `<!-- cert-verify -->` markers. homescoop's ci reads exactly that prefix and re-hashes the npm tarball against it. `covers` starts after the last version npm serves, so a skipped version is covered by the next. Notes that already carry the identical line stay unchanged. A pull request's `cert-review` is the newest finished one (success, failure or error) on its head at or before its merge, or the newest pending one if none had finished. A re-review that a later `merge-ok` re-label starts on the same head, still pending at the merge or ending after it, doesn't change what the merge queue saw, and a status posted after the merge can't change a release's line.
974
981
  - A mismatch writes no line. The job is red with a `supply-chain alert`, and so is a line already in the notes with another sha256.
975
982
 
976
983
  The tools in `tools/cert-review/`:
@@ -1241,6 +1248,16 @@ A WASIX C program for `test/integration/wasix-locks.test.mjs` (#722). It calls `
1241
1248
 
1242
1249
  **Build:** `INSTALL_WASIXCC=<homescoop>/scripts/install-wasixcc.sh WASIX_SYSROOT_28=<unpacked @ai-ecoverse/wasix-sysroot 2025.9.30-28> ./build.sh`, with homescoop's pinned wasixcc 0.4.7 (LLVM 21.1.206, binaryen 133) and Homebrew's binaryen `wasm-opt` 132 on `PATH` for the Asyncify pass (`proc_fork`). Two builds give the same bytes: `bin/wlocktest.wasm` is 56,958 bytes, sha256 `31308a54e13e7dcac497ad9d8a05799008c8beaa4ae3a36c76e218af7235ec23`.
1243
1250
 
1251
+ ### `flocktest`
1252
+
1253
+ A WASIX C program for `test/integration/flock.test.mjs` (#893). It calls `slicc.flock` and `slicc.fcntl_lock` directly (`import_module("slicc")`). Its arguments are a sequence of steps, run in order:
1254
+ - `open SLOT r|w|rw PATH`, `pipe SLOT SLOT`, `pair SLOT SLOT` (a `socketpair`), `dup FROM TO`, `fd SLOT N` (names an inherited descriptor), `close SLOT`;
1255
+ - `flock SLOT OP`, where OP is made of `sh`, `ex`, `un` and `nb` (`exnb` is `LOCK_EX|LOCK_NB`) or is a number; `badfd`; `fcntl SLOT r|w|u` (`F_SETLK` on the whole file);
1256
+ - `fork … end` (the child runs the steps up to the matching `end`, its lines start with `child `, and the parent waits for it before it goes on after `end`);
1257
+ - `touch PATH`, `await PATH`, `sleep MS`, `say TEXT`, `trap` (a `SIGUSR1` handler).
1258
+
1259
+ **Build:** as `wlocktest`. Two builds give the same bytes: `bin/flocktest.wasm` is 58,442 bytes, sha256 `687ecf21c836adda4ef2e328b6032eed28ca7d37efc8e88d3aaefb8fc117115a`.
1260
+
1244
1261
  ## License
1245
1262
 
1246
1263
  Apache-2.0
@@ -3797,7 +3797,8 @@ function openPipe(capacity) {
3797
3797
  },
3798
3798
  close: () => pipe.closeRead(),
3799
3799
  poll: () => ({ readable: pipe.readReady, writable: false, hangup: pipe.writersGone }),
3800
- changed: (signal) => pipe.changed(signal)
3800
+ changed: (signal) => pipe.changed(signal),
3801
+ identity: pipe
3801
3802
  }),
3802
3803
  write: new OpenFile({
3803
3804
  write: async (bytes, signal) => {
@@ -3809,7 +3810,8 @@ function openPipe(capacity) {
3809
3810
  },
3810
3811
  close: () => pipe.closeWrite(),
3811
3812
  poll: () => ({ readable: false, writable: pipe.writeReady, hangup: pipe.readersGone }),
3812
- changed: (signal) => pipe.changed(signal)
3813
+ changed: (signal) => pipe.changed(signal),
3814
+ identity: pipe
3813
3815
  })
3814
3816
  };
3815
3817
  }
@@ -6258,7 +6260,7 @@ function satisfies(version, range2) {
6258
6260
  }
6259
6261
 
6260
6262
  // src/version.ts
6261
- var KERNEL_VERSION = true ? "1.116.2" : "0.0.0-dev";
6263
+ var KERNEL_VERSION = true ? "1.117.0" : "0.0.0-dev";
6262
6264
  var OS_RELEASE = "6.0.0-slicc";
6263
6265
  function procVersion() {
6264
6266
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -9624,6 +9626,7 @@ var FifoTable = class {
9624
9626
  let open = true;
9625
9627
  return new OpenFile({
9626
9628
  fifo: path,
9629
+ identity: fifo,
9627
9630
  ...reads ? {
9628
9631
  read: async (max, signal) => {
9629
9632
  try {
@@ -13057,7 +13060,71 @@ function lockRange(start2, len) {
13057
13060
  if (from < 0) throw new KernelError("EINVAL");
13058
13061
  return { start: from, end };
13059
13062
  }
13063
+ function changedAt(wakers, key, signal) {
13064
+ return new Promise((resolve2, reject) => {
13065
+ const set3 = wakers.get(key) ?? /* @__PURE__ */ new Set();
13066
+ wakers.set(key, set3);
13067
+ const done4 = () => {
13068
+ set3.delete(wake);
13069
+ if (set3.size === 0 && wakers.get(key) === set3) wakers.delete(key);
13070
+ signal.removeEventListener("abort", abort);
13071
+ };
13072
+ const wake = () => {
13073
+ done4();
13074
+ resolve2();
13075
+ };
13076
+ const abort = () => {
13077
+ done4();
13078
+ reject(new KernelError("EINTR"));
13079
+ };
13080
+ set3.add(wake);
13081
+ signal.addEventListener("abort", abort, { once: true });
13082
+ });
13083
+ }
13084
+ function wakeAt(wakers, key) {
13085
+ for (const wake of [...wakers.get(key) ?? []]) wake();
13086
+ }
13087
+ var FlockTable = class {
13088
+ held = /* @__PURE__ */ new Map();
13089
+ keys = /* @__PURE__ */ new WeakMap();
13090
+ wakers = /* @__PURE__ */ new Map();
13091
+ free(key, owner, kind) {
13092
+ for (const [other, has] of this.held.get(key) ?? [])
13093
+ if (other !== owner && (kind === "exclusive" || has === "exclusive")) return false;
13094
+ return true;
13095
+ }
13096
+ set(key, owner, kind) {
13097
+ const has = this.held.get(key)?.get(owner);
13098
+ if (has === kind) return true;
13099
+ if (has !== void 0) this.drop(owner);
13100
+ if (kind === "unlock") return true;
13101
+ if (!this.free(key, owner, kind)) return false;
13102
+ const holders = this.held.get(key) ?? /* @__PURE__ */ new Map();
13103
+ holders.set(owner, kind);
13104
+ this.held.set(key, holders);
13105
+ this.keys.set(owner, key);
13106
+ return true;
13107
+ }
13108
+ async wait(key, owner, kind, blocking) {
13109
+ if (this.set(key, owner, kind)) return;
13110
+ const signal = blocking();
13111
+ while (!this.set(key, owner, kind)) {
13112
+ if (signal.aborted) throw new KernelError("EINTR");
13113
+ await changedAt(this.wakers, key, signal);
13114
+ }
13115
+ }
13116
+ drop(owner) {
13117
+ const key = this.keys.get(owner);
13118
+ if (key === void 0) return;
13119
+ this.keys.delete(owner);
13120
+ const holders = this.held.get(key);
13121
+ holders?.delete(owner);
13122
+ if (holders?.size === 0) this.held.delete(key);
13123
+ wakeAt(this.wakers, key);
13124
+ }
13125
+ };
13060
13126
  var LockTable = class {
13127
+ flocks = new FlockTable();
13061
13128
  records = /* @__PURE__ */ new Map();
13062
13129
  blocked = /* @__PURE__ */ new Map();
13063
13130
  wakers = /* @__PURE__ */ new Map();
@@ -13115,28 +13182,10 @@ var LockTable = class {
13115
13182
  return false;
13116
13183
  }
13117
13184
  changed(key, signal) {
13118
- return new Promise((resolve2, reject) => {
13119
- const set3 = this.wakers.get(key) ?? /* @__PURE__ */ new Set();
13120
- this.wakers.set(key, set3);
13121
- const done4 = () => {
13122
- set3.delete(wake);
13123
- if (set3.size === 0 && this.wakers.get(key) === set3) this.wakers.delete(key);
13124
- signal.removeEventListener("abort", abort);
13125
- };
13126
- const wake = () => {
13127
- done4();
13128
- resolve2();
13129
- };
13130
- const abort = () => {
13131
- done4();
13132
- reject(new KernelError("EINTR"));
13133
- };
13134
- set3.add(wake);
13135
- signal.addEventListener("abort", abort, { once: true });
13136
- });
13185
+ return changedAt(this.wakers, key, signal);
13137
13186
  }
13138
13187
  wake(key) {
13139
- for (const wake of [...this.wakers.get(key) ?? []]) wake();
13188
+ wakeAt(this.wakers, key);
13140
13189
  }
13141
13190
  store(key, locks) {
13142
13191
  if (locks.length === 0) this.records.delete(key);
@@ -13201,6 +13250,7 @@ var HOST_OPS = [
13201
13250
  "lk-set",
13202
13251
  "lk-get",
13203
13252
  "lk-close",
13253
+ "flock",
13204
13254
  "async-submit",
13205
13255
  "async-wait",
13206
13256
  "async-take",
@@ -13353,6 +13403,8 @@ async function hostSyscall(req, ctx) {
13353
13403
  case "lk-get":
13354
13404
  case "lk-close":
13355
13405
  return recordLock2(req, ctx);
13406
+ case "flock":
13407
+ return flock(req, ctx);
13356
13408
  case "async-submit":
13357
13409
  if (!ctx.valid(req.req)) throw new KernelError("EINVAL");
13358
13410
  if (req.ack) ctx.ops.ack(req.ack);
@@ -13375,6 +13427,17 @@ async function hostSyscall(req, ctx) {
13375
13427
  return done2;
13376
13428
  }
13377
13429
  }
13430
+ var FLOCK_KINDS = /* @__PURE__ */ new Set(["shared", "exclusive", "unlock"]);
13431
+ async function flock(req, ctx) {
13432
+ const { locks, flockOf } = ctx;
13433
+ if (!locks || !flockOf) throw new KernelError("ENOLCK");
13434
+ if (!FLOCK_KINDS.has(req.kind)) throw new KernelError("EINVAL");
13435
+ const { key, owner } = await flockOf(req.fd);
13436
+ if (req.kind !== "unlock" && req.wait)
13437
+ await locks.flocks.wait(key, owner, req.kind, ctx.blocking);
13438
+ else if (!locks.flocks.set(key, owner, req.kind)) throw new KernelError("EAGAIN");
13439
+ return done2;
13440
+ }
13378
13441
  async function recordLock2(req, ctx) {
13379
13442
  const { locks, fileKey } = ctx;
13380
13443
  if (!locks || !fileKey) throw new KernelError("ENOLCK");
@@ -17503,6 +17566,18 @@ var fileKeys = /* @__PURE__ */ new WeakMap();
17503
17566
  function identityKey(st, path) {
17504
17567
  return st.ino === void 0 ? path : `${st.dev ?? 0}:${st.ino}`;
17505
17568
  }
17569
+ var objectIds = /* @__PURE__ */ new WeakMap();
17570
+ var nextObjectId = 1;
17571
+ function objectKey(file) {
17572
+ const at = file.tty ?? file.identity ?? file.pty ?? file;
17573
+ let id2 = objectIds.get(at);
17574
+ if (id2 === void 0) {
17575
+ id2 = nextObjectId++;
17576
+ objectIds.set(at, id2);
17577
+ }
17578
+ return `object:${id2}`;
17579
+ }
17580
+ var flockHooked = /* @__PURE__ */ new WeakSet();
17506
17581
  var DYING = { ok: false, errno: "EINTR", message: "the process is being killed" };
17507
17582
  var HOSTS_ONLY = new Resolver({ routes: new Routes() });
17508
17583
  function timeLeft(req, started) {
@@ -17803,6 +17878,7 @@ var WasmProcess = class {
17803
17878
  pid: this.pid,
17804
17879
  owner: () => this.lockOwner(),
17805
17880
  fileKey: (target, kind, setting) => this.lockKey(target, kind, setting),
17881
+ flockOf: (fd) => this.flockOf(fd),
17806
17882
  locks: this.options.locks,
17807
17883
  ops: this.asyncOps,
17808
17884
  blocking: () => this.blockingSignal(cancelled),
@@ -17871,6 +17947,7 @@ var WasmProcess = class {
17871
17947
  throw new KernelError("EBADF");
17872
17948
  const known = file ? fileKeys.get(file) : this.pathKeys.get(target.path ?? "");
17873
17949
  if (kind === "close" && !file && target.path !== void 0) this.pathKeys.delete(target.path);
17950
+ if (access2 && !access2.stat) return objectKey(access2);
17874
17951
  const want = kind === "read" ? R_OK : kind === "write" ? W_OK : 0;
17875
17952
  const path = await this.lockPath(target, want);
17876
17953
  if (known !== void 0) return known;
@@ -17885,6 +17962,33 @@ var WasmProcess = class {
17885
17962
  else if (this.pathKeys.size < 4096) this.pathKeys.set(path, key);
17886
17963
  return key;
17887
17964
  }
17965
+ async flockOf(fd) {
17966
+ const open = this.fds.get(fd);
17967
+ const { locks } = this.options;
17968
+ if (locks && !flockHooked.has(open)) {
17969
+ flockHooked.add(open);
17970
+ open.onClose(() => locks.flocks.drop(open));
17971
+ }
17972
+ return { key: await this.objectKeyOf(open), owner: open };
17973
+ }
17974
+ async objectKeyOf(open) {
17975
+ const { file } = open;
17976
+ const meta = file.heldMeta;
17977
+ if (meta && "device" in meta) return `device:${meta.device}`;
17978
+ if (meta && "dir" in meta) {
17979
+ const st2 = await this.options.fs.stat?.(meta.dir).catch(() => void 0);
17980
+ return st2 ? identityKey(st2, meta.dir) : objectKey(file);
17981
+ }
17982
+ const known = fileKeys.get(open);
17983
+ if (known !== void 0) return known;
17984
+ const vfs = await file.stat?.().catch(() => void 0);
17985
+ if (!vfs || vfs.orphan) return objectKey(file);
17986
+ const st = await this.options.fs.stat?.(vfs.path).catch(() => void 0);
17987
+ if (!st) return objectKey(file);
17988
+ const key = identityKey(st, vfs.path);
17989
+ fileKeys.set(open, key);
17990
+ return key;
17991
+ }
17888
17992
  async dropRecordLocks(fd) {
17889
17993
  const { locks } = this.options;
17890
17994
  const owner = this.options.jobs?.shown(this.pid) ?? this.pid;
@@ -20165,6 +20269,7 @@ function scrubbed(inner, scrubber, idleMs = SCRUB_IDLE_MS) {
20165
20269
  },
20166
20270
  changed,
20167
20271
  ...file.tty ? { tty: file.tty } : {},
20272
+ identity: file.identity ?? file,
20168
20273
  ...file.clipboard ? { clipboard: file.clipboard } : {},
20169
20274
  ...file.write ? {
20170
20275
  write: async (bytes, signal) => {