@ai-ecoverse/slicc-kernel 1.70.0 → 1.71.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
@@ -43,6 +43,7 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
43
43
  | `sealed` | none | `(req, { signal }) => Promise<object>`: answers requests written to [`/dev/sealed`](#devsealed). `createNodeKernel` takes it too |
44
44
  | `approve` | none | `(req, { signal }) => 'once' \| 'always' \| 'deny'` (or a promise of one): decides a `sudo` request that needs approval (see [Sudo](#sudo)); without it, such a request is denied. `createNodeKernel` takes it too |
45
45
  | `cdp` | none | `({ runtime }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
46
+ | `protect` | none | `{ paths }`: directories no process may change, root included, in every `users` mode (see [Protected paths](#protected-paths)). `createNodeKernel` takes it too |
46
47
 
47
48
  ### `kernel.run(argv, options?) → Promise<{ status, stdout, stderr }>`
48
49
 
@@ -600,6 +601,26 @@ A character device (`0666`, major 10, minor 250) through which a program talks t
600
601
  - **Reading:** the reply comes from the same descriptor, as JSON (`{ v: 1, … }`). A read waits for it, drains it, then returns end-of-file; with no request outstanding it returns end-of-file at once. A signal ends the wait with `EINTR` and the request stays outstanding, `O_NONBLOCK` gives `EAGAIN`, and `poll`/`select` report the descriptor readable once the reply is there. A new write drops an unread reply.
601
602
  - **The hook:** `signal` aborts with `'timeout'` after 10 s (the reply is then `{ v: 1, error: { code: 'ETIMEDOUT' } }`), `'replaced'` when a new write on the descriptor comes first, and `'withdrawn'` when the writer exits or the descriptor is closed. A throw or a reply that isn't an object becomes an `EIO` error reply, with the first line of the message; no hook is `ENOTCONN`. On the page, `createKernel` relays the request from the kernel worker and back, and the abort too.
602
603
 
604
+ ### Protected paths
605
+
606
+ `createKernel({ protect: { paths: ['/node_modules', '/os'] } })` makes those directories read-only for every process, root included, in every `users` mode. They hold code every later process runs, which only the embedder writes, straight to OPFS.
607
+ - **What fails:** any change under a path fails with `EROFS` ("Read-only file system"): a write, a create, a truncate, `mkdir`, removing or renaming (from or to), a hard or symbolic link created there, `chmod`, `chown` and setting times. So does removing or renaming a directory that holds one (`rm -rf /`). A missing working directory for `run` or `openTerminal` isn't created there either. Reads are unaffected. `'/'` protects the whole file system.
608
+ - **Stats and symlinks:** files under a path show as on a read-only mount, so opens for writing fail at once. A write through a symlink, or through a symlinked directory, that leads into a path fails too.
609
+ - **Where it's checked:** the root file system itself checks every change, from the moment the kernel is constructed, inside the change and against the path it really lands on. So a symlink swapped mid-way can't redirect a write. The kernel's own writes there are refused too, so don't protect what the kernel writes after it starts:
610
+ - `/etc` (`passwd`, `group` and the proxy's CA);
611
+ - `/home` and `/root` (new users' homes);
612
+ - `/tmp`;
613
+ - the system `PNPM_HOME` (`/usr/local/share/pnpm`);
614
+ - `/.slicc-unlinked` (files removed while open);
615
+ - `/` itself, which holds all of these.
616
+ Paths in the layout the kernel stamps with root's owner at boot are skipped when protected.
617
+ - **Symlinks as paths:** a path that is itself a symlink protects the directory it names too, by its real path, once the kernel has prepared.
618
+ - **Mounts:** nothing can be mounted at, under or above a path, not even by the embedder (`kernel.mount`, `/etc/fstab`). So a path always lies on the root file system, where that check holds.
619
+ - **Clients:** a client's (`kernel.connect`) file calls, mounts and unmounts see the paths read-only, as its processes do.
620
+ - **Not affected:** the page's own writes to OPFS.
621
+ - **Writers:** a `writer` that lets chosen processes change the paths needs the kernel-side process tag ([#405](https://github.com/ai-ecoverse/slicc-kernel/pull/405)). Until then, `protect.writer` is refused.
622
+ - **Where it is a boundary:** it's one in a browser, where programs' own code can't reach OPFS around the kernel (see [JS programs](#js-programs)). Under the Node entry it isn't, until [#407](https://github.com/ai-ecoverse/slicc-kernel/issues/407), and there `kernel.writeFile` under a path fails too, so write those files into `root` before creating the kernel.
623
+
603
624
  ### `/proc`
604
625
 
605
626
  Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `/proc/self` is a link to the `/proc/<pid>` of the process that reads it, as on Linux (`readlink /proc/self` is its pid, the one it was exec'd under included). They come from the kernel's process table when they are opened, so a terminal sees the processes of every client. A process that exits while a program reads the `/proc` directory doesn't fail or cut short the listing: its pid is still listed, and opening its files fails with `ENOENT`, which `ps`, `pgrep` and `pidof` skip, as on Linux; an exec'd program shows under the pid its parent knows. `/proc/uptime`, `/proc/loadavg`, `/proc/stat` and `/proc/meminfo` are there too, and `/proc/mounts`. A process with no parent in the kernel (one a page, a client or the Node entry started) has parent 1, as children of init do, in `getppid()`, `/proc/<pid>/stat` and `ps` alike, and so does a process whose parent has ended, waited for or not (`getppid()` asks the kernel each time; a shell's `$PPID` keeps the value it started with, as on Linux). Pids, parents, process groups, sessions, command lines, terminals, start and boot times are real, and so are the credentials in `status` (`Uid:`, `Gid:`, `Groups:`) and each process's memory: the size of its wasm memory (`VSZ` and `RSS` alike, as wasm has no paging), which `/proc/meminfo` counts as used. CPU times are measured per process, its threads and the images it exec'd included: time a worker spends waiting for the kernel's answer is system time (at most 20 ms of each call; the rest of a longer one, such as a write into a full pipe, counts as blocked), time it spends blocked (a read, a `poll` or `select` that waits, a sleep, `wait`, a futex, while stopped) is neither (a readiness check that never waits is system time), and the rest is user time; a process's times stop when it ends, a call still in flight then adds nothing, so a loop that never calls the kernel shows as user time while it runs; they show in `/proc/<pid>/stat`, `ps` and `ps()`, and `/proc/stat` counts every process of the kernel, the reaped ones included, and `/proc/loadavg` averages the processes not blocked, sampled every 5 seconds. A JS program's time isn't measured yet. `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) is a placeholder. A process that isn't root sees only its own user's processes (see [Users](#users)). WASI and WASIX programs see the same `/proc`, read-only (opening a file in it for writing, or linking one of its files elsewhere, fails with `EROFS`), from every thread, also through a symlink that points into it (where every change, `mkdir`, `rename`, `unlink`, `chmod`, a hard link from it or into it, and so on, fails too: with `EROFS` when the root file system has a physical `/proc`, which stays hidden and untouched, and otherwise with the `ENOENT` of the missing directory, since that check is only paid for when a physical `/proc` exists; with one, a symlink whose target passes `..` after a file or a missing name, such as `/f/../proc`, fails with `ENOTDIR` or `ENOENT`, as on Linux), and `/` lists it, so a WASI program gets a preopen for it; `/proc/self` is the program's own pid there too (the pid an exec'd program took over). Neither has `/proc/<pid>/fd` yet.
package/dist/index.js CHANGED
@@ -287,6 +287,9 @@ async function loopbackFetch(dial, input, options) {
287
287
  }
288
288
  }
289
289
 
290
+ // src/fs/protect.ts
291
+ var PROTECT_WRITER = "slicc-kernel: protect.writer needs the process tag (#405), which this kernel does not have yet";
292
+
290
293
  // src/kernel/net/loopback-names.ts
291
294
  var HOST_LOOPBACK = "host.slicc.internal";
292
295
  var HOST_LOOPBACK_ADDRESS = [10, 0, 2, 2];
@@ -1693,7 +1696,7 @@ function localProxyTransport(options) {
1693
1696
  }
1694
1697
 
1695
1698
  // src/mount/driver.ts
1696
- function fsError(code, message = code) {
1699
+ function fsError2(code, message = code) {
1697
1700
  return Object.assign(new Error(message), { code });
1698
1701
  }
1699
1702
 
@@ -1775,7 +1778,7 @@ function serveHostfsPort(port, transport) {
1775
1778
  );
1776
1779
  return send2({ id });
1777
1780
  }
1778
- if (op !== "call") throw fsError("EINVAL", `unknown hostfs port op ${op}`);
1781
+ if (op !== "call") throw fsError2("EINVAL", `unknown hostfs port op ${op}`);
1779
1782
  send2({ id, result: await answer(data.name, data.body) });
1780
1783
  } catch (err) {
1781
1784
  send2({ id, error: errorOf(err) });
@@ -1842,6 +1845,7 @@ function initCall(options, transport) {
1842
1845
  ...options.onDenial ? { denials: true } : {},
1843
1846
  ...options.sealed ? { sealed: true } : {},
1844
1847
  ...options.approve ? { approve: true } : {},
1848
+ ...options.protect ? { protect: { paths: [...options.protect.paths] } } : {},
1845
1849
  ...options.processMounts !== void 0 ? {
1846
1850
  processMounts: typeof options.processMounts === "function" ? "ask" : options.processMounts
1847
1851
  } : {}
@@ -1936,6 +1940,7 @@ function checkOptions(options) {
1936
1940
  }
1937
1941
  async function createKernel(options = {}) {
1938
1942
  checkOptions(options);
1943
+ if (options.protect?.writer) throw new Error(PROTECT_WRITER);
1939
1944
  const url = options.worker ?? new URL("./kernel-worker.js", import.meta.url);
1940
1945
  const worker = new Worker(url, { type: "module", name: "slicc-kernel" });
1941
1946
  const pending = /* @__PURE__ */ new Map();