@ai-ecoverse/slicc-kernel 1.146.1 → 1.146.3

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
@@ -1128,7 +1128,7 @@ A character device (`0666`), there in every `users` mode, through which programs
1128
1128
  - **No hash known:** when `lookup` has none (`undefined`), `unknown: 'allow'` (the default) runs the file and reports `{ op: 'exec-unverified', errno: 'ENODATA', … }` once per path, while `unknown: 'deny'` refuses it as a mismatch.
1129
1129
  - **The cache:** a result is kept per real path and the file's version: what OPFS itself reports (its `lastModified` and `size`), plus the count of writes kernels made to the file, which the metadata store keeps (Chrome doesn't move `lastModified` for a write through a sync access handle). A kernel with `integrity` set counts a write to a protected file before its bytes change, and again once the write is over (also when it failed), at wherever the file is by then; the count follows a rename, and no `utimes` turns it back. The version also holds the count of moves into protection: a rename or a symlink at or above a protected path counts one before and after it, so a move into a protected path re-verifies every protected program once. So a change made around the kernel, same size included, is checked again at the next start. A kernel without `integrity` counts nothing (so writes into protected paths cost no metadata update), and its writes don't move the version another kernel on the origin reads: give every kernel on one origin the same `integrity`. The cache is the kernel's own, in memory, so nothing verified outlives it. The compiled module is cached by the same key, so a program that hasn't changed costs nothing more to start.
1130
1130
  - **Revoking:** a result is kept until the file changes, so when `lookup`'s answers change (an updated hash list, a package revoked or reinstalled), call `await kernel.integrity.revoke(path?)` (on the page and in Node). It drops what the kernel verified for that path, or for everything under it when it's a directory (its real path and the name a link reached it by both count), or for every path without one, and rebuilds the command catalog from the manifests as checked again. A load that was under way when `revoke` was called keeps nothing. It takes effect at the next start: running processes keep what they loaded. `path` must be absolute; without `integrity` it does nothing.
1131
- - **Where `lookup` gets its hashes:** from a source independent of the installed files, such as the per-file hashes the embedder derives from the certified package tarball when it builds its catalog. Hashes read back from the installed files prove nothing.
1131
+ - **Where `lookup` gets its hashes:** from a source independent of the installed files, such as the per-file hashes the embedder derives from the published package tarball when it builds its catalog. Hashes read back from the installed files prove nothing.
1132
1132
  - **Links out of the paths:** a file reached through a protected path but whose real path lies outside every path (a symlink at or below a protected path, or one of its directories, pointing elsewhere, a command of the catalog included) is still checked, under the last protected name on the way: the kernel asks `lookup(name)` for the bytes of the real file, `unknown` applies when it has no hash, and `onDenial` reports the name as `path`. Writes outside the paths aren't counted, so such a result isn't cached: the file is hashed again at every start.
1133
1133
  - **Sources a program reads itself** ([#454](https://github.com/ai-ecoverse/slicc-kernel/issues/454)): a runtime that loads code on its own is checked too, when a process reads the file through the kernel. That covers jsh's and slicc-node's modules and the `package.json` files whose `main`/`exports` they follow, python's `.py`, `.pyc`, `.pth` and zip imports, `dlopen`'s side modules, and a script's interpreter reading the script after its exec check (a file swapped in between is refused then).
1134
1134
  - **Which files:** those under a path whose name ends in one of `integrity.sources`, plus every regular file with an execute bit (a `#!` script without a suffix, a sourced script). The default is `['.js', '.mjs', '.cjs', '.json', '.py', '.pyc', '.pth', '.zip', '.so', '.wasm', '.sh']`. An embedder can replace the list, and `sources: []` turns these checks off and leaves only the ones above. Data files aren't checked, since a package can hold files no `lookup` knows (lock files, caches), which `unknown: 'deny'` would make unreadable.
@@ -7728,7 +7728,7 @@ function satisfies(version, range2) {
7728
7728
  }
7729
7729
 
7730
7730
  // src/version.ts
7731
- var KERNEL_VERSION = true ? "1.146.1" : "0.0.0-dev";
7731
+ var KERNEL_VERSION = true ? "1.146.3" : "0.0.0-dev";
7732
7732
  var OS_RELEASE = "6.0.0-slicc";
7733
7733
  function procVersion() {
7734
7734
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -8600,6 +8600,55 @@ var Crontabs = class {
8600
8600
 
8601
8601
  // src/fs/unlinked.ts
8602
8602
  var UNLINKED_DIR = ".slicc-unlinked";
8603
+ var PathMarks = class {
8604
+ gen = 0;
8605
+ floor = 0;
8606
+ touched = /* @__PURE__ */ new Map();
8607
+ flying = /* @__PURE__ */ new Map();
8608
+ generation() {
8609
+ return this.gen;
8610
+ }
8611
+ touch(target) {
8612
+ this.gen += 1;
8613
+ this.touched.delete(target);
8614
+ this.touched.set(target, this.gen);
8615
+ if (this.touched.size <= TOUCHED_MAX) return;
8616
+ const [oldest, at] = this.touched.entries().next().value;
8617
+ this.touched.delete(oldest);
8618
+ this.floor = at;
8619
+ }
8620
+ changedSince(path, gen) {
8621
+ if (gen < this.floor) return true;
8622
+ for (let p = path; this.touched.size > 0; p = parentOf5(p)) {
8623
+ if ((this.touched.get(p) ?? 0) > gen) return true;
8624
+ if (p === "/") return false;
8625
+ }
8626
+ return false;
8627
+ }
8628
+ async around(paths, op) {
8629
+ for (const p of paths) {
8630
+ this.flying.set(p, (this.flying.get(p) ?? 0) + 1);
8631
+ this.touch(p);
8632
+ }
8633
+ try {
8634
+ return await op();
8635
+ } finally {
8636
+ for (const p of paths) {
8637
+ const n = (this.flying.get(p) ?? 1) - 1;
8638
+ if (n > 0) this.flying.set(p, n);
8639
+ else this.flying.delete(p);
8640
+ this.touch(p);
8641
+ }
8642
+ }
8643
+ }
8644
+ inFlight(path) {
8645
+ for (let p = path; this.flying.size > 0; p = parentOf5(p)) {
8646
+ if (this.flying.has(p)) return true;
8647
+ if (p === "/") return false;
8648
+ }
8649
+ return false;
8650
+ }
8651
+ };
8603
8652
  function webLocks() {
8604
8653
  return globalThis.navigator?.locks;
8605
8654
  }
@@ -8653,14 +8702,13 @@ var UnlinkedKeeper = class {
8653
8702
  queue = Promise.resolve();
8654
8703
  count = 0;
8655
8704
  replacing = /* @__PURE__ */ new Map();
8656
- gen = 0;
8657
- floor = 0;
8658
- touched = /* @__PURE__ */ new Map();
8705
+ marks;
8659
8706
  replaced = 0;
8660
8707
  links = /* @__PURE__ */ new Map();
8661
8708
  linked = /* @__PURE__ */ new Map();
8662
8709
  dirOwner;
8663
8710
  constructor(fs, holders, rootOf, options = {}) {
8711
+ this.marks = options.marks ?? new PathMarks();
8664
8712
  const { locks, owner } = options;
8665
8713
  this.dirOwner = options.dirOwner;
8666
8714
  this.fs = fs;
@@ -8682,7 +8730,6 @@ var UnlinkedKeeper = class {
8682
8730
  const { replacing } = this;
8683
8731
  const replace = async (target, op) => {
8684
8732
  const { promise, resolve: resolve2 } = Promise.withResolvers();
8685
- keeper.touch(target);
8686
8733
  let mine;
8687
8734
  const gated = () => {
8688
8735
  const before = replacing.get(target);
@@ -8693,10 +8740,9 @@ var UnlinkedKeeper = class {
8693
8740
  if (mine) mine.sealed = true;
8694
8741
  };
8695
8742
  try {
8696
- await op(gated, sealed);
8743
+ await keeper.marks.around([target], () => op(gated, sealed));
8697
8744
  } finally {
8698
8745
  keeper.replaced += 1;
8699
- keeper.touch(target);
8700
8746
  resolve2();
8701
8747
  if (mine && replacing.get(target) === mine) replacing.delete(target);
8702
8748
  }
@@ -8747,25 +8793,15 @@ var UnlinkedKeeper = class {
8747
8793
  const target = fs.resolvePath("/", to);
8748
8794
  const source = fs.resolvePath("/", from);
8749
8795
  if (target === source) return fs.rename(from, to);
8750
- keeper.touch(source);
8751
- try {
8752
- await replace(target, async (gated, sealed) => {
8796
+ await keeper.marks.around(
8797
+ [source],
8798
+ () => replace(target, async (gated, sealed) => {
8753
8799
  const pinned = await keeper.pin(target, gated, sealed);
8754
8800
  await keeper.settle(pinned, () => fs.rename(from, to));
8755
- });
8756
- } finally {
8757
- keeper.touch(source);
8758
- }
8759
- },
8760
- async symlink(target, path, owner) {
8761
- const at = fs.resolvePath("/", path);
8762
- keeper.touch(at);
8763
- try {
8764
- await fs.symlink(target, path, owner);
8765
- } finally {
8766
- keeper.touch(at);
8767
- }
8801
+ })
8802
+ );
8768
8803
  },
8804
+ symlink: (target, path, owner) => keeper.marks.around([fs.resolvePath("/", path)], () => fs.symlink(target, path, owner)),
8769
8805
  readdir: async (path) => visible(await fs.readdir(path)),
8770
8806
  readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR).map(([name2, st]) => [name2, st && counted2(st)])
8771
8807
  };
@@ -8777,24 +8813,13 @@ var UnlinkedKeeper = class {
8777
8813
  }
8778
8814
  }
8779
8815
  generation() {
8780
- return this.gen;
8816
+ return this.marks.generation();
8781
8817
  }
8782
8818
  changedSince(path, gen) {
8783
- if (gen < this.floor) return true;
8784
- for (let p = path; this.touched.size > 0; p = parentOf5(p)) {
8785
- if ((this.touched.get(p) ?? 0) > gen) return true;
8786
- if (p === "/") return false;
8787
- }
8788
- return false;
8819
+ return this.marks.changedSince(path, gen);
8789
8820
  }
8790
- touch(target) {
8791
- this.gen += 1;
8792
- this.touched.delete(target);
8793
- this.touched.set(target, this.gen);
8794
- if (this.touched.size <= TOUCHED_MAX) return;
8795
- const [oldest, at] = this.touched.entries().next().value;
8796
- this.touched.delete(oldest);
8797
- this.floor = at;
8821
+ inFlight(path) {
8822
+ return this.marks.inFlight(path);
8798
8823
  }
8799
8824
  missed(path) {
8800
8825
  for (let p = path; this.replacing.size > 0; p = parentOf5(p)) {
@@ -13610,13 +13635,24 @@ async function holdOp(entry, path, req) {
13610
13635
  const real = await entry.fs.realpath?.(path, true).catch(() => path);
13611
13636
  await entry.source?.open(path, real);
13612
13637
  await stillThere(entry, path, req);
13613
- if (!r || !r.missed(path) && !r.changedSince(path, gen))
13638
+ if (req.nofollow && !await ownEntry(entry, path, real ?? path)) continue;
13639
+ if (!r || !r.missed(path) && !r.changedSince(path, gen) && !r.inFlight?.(path))
13614
13640
  return json2(entry.hold?.(path, on, open2, real) ?? null);
13615
13641
  }
13616
13642
  }
13617
13643
  entry.hold?.(path, on, open2, void 0, req.handle);
13618
13644
  return done2;
13619
13645
  }
13646
+ async function ownEntry(entry, path, real) {
13647
+ const own = await entry.fs.realpath?.(path, false).catch((err) => {
13648
+ if (err.code !== "ENOENT") throw err;
13649
+ return path;
13650
+ });
13651
+ if (own !== void 0 && own !== real) return false;
13652
+ const st = await entry.fs.lstat(path);
13653
+ if (st.isSymbolicLink) throw syncError("ELOOP", path);
13654
+ return true;
13655
+ }
13620
13656
  async function followed(g, path) {
13621
13657
  const st = await g.fs.lstat(path).catch(() => void 0);
13622
13658
  if (st?.isSymbolicLink && !trustedLink(permOf(st), g.links)) deny(g, "follow", path);
@@ -28971,6 +29007,7 @@ var Launcher = class {
28971
29007
  fstabRetries;
28972
29008
  inserted = /* @__PURE__ */ new Map();
28973
29009
  openFiles = /* @__PURE__ */ new Set();
29010
+ pathMarks = new PathMarks();
28974
29011
  deciding = /* @__PURE__ */ new Map();
28975
29012
  identities = { asked: 0, timedOut: 0 };
28976
29013
  nodes;
@@ -29105,6 +29142,7 @@ var Launcher = class {
29105
29142
  locks: webLocks(),
29106
29143
  busy: (path) => [...this.openFiles].some((nodes) => nodes.holds(path)),
29107
29144
  owner: options.unlinkOwner,
29145
+ marks: this.pathMarks,
29108
29146
  ...this.owners ? { dirOwner: { uid: 0, gid: 0, mode: 448 } } : {}
29109
29147
  }
29110
29148
  );
@@ -31026,6 +31064,7 @@ var Launcher = class {
31026
31064
  locks: webLocks(),
31027
31065
  busy: (path) => [...this.openFiles].some((open2) => open2.holds(path)),
31028
31066
  owner: this.unlinkOwnerOption,
31067
+ marks: this.pathMarks,
31029
31068
  ...this.owners ? { dirOwner: { uid: 0, gid: 0, mode: 448 } } : {}
31030
31069
  }
31031
31070
  );