@ai-ecoverse/slicc-kernel 1.136.8 → 1.137.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 +22 -4
- package/dist/index.js +3 -1
- package/dist/index.js.map +2 -2
- package/dist/kernel-worker.js +666 -164
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +58 -5
- package/dist/node-process-worker.js.map +4 -4
- package/dist/node.js +653 -160
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +58 -5
- package/dist/process-worker.js.map +4 -4
- package/dist/testing.js +1 -0
- package/dist/testing.js.map +2 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/kernel/net/dns-stub.d.ts +65 -0
- package/dist/types/kernel/net/resolver.d.ts +4 -1
- package/dist/types/kernel/net/sealed-pg.d.ts +1 -0
- package/dist/types/kernel/net/uplink.d.ts +3 -1
- package/dist/types/kernel/proc-info.d.ts +2 -0
- package/dist/types/kernel/socket.d.ts +3 -1
- package/dist/types/launcher.d.ts +2 -0
- package/dist/types/node.d.ts +1 -0
- package/dist/types/process/procfs.d.ts +1 -1
- package/dist/types/process/wasi/wasi-procfs.d.ts +2 -1
- package/dist/types/serve.d.ts +2 -0
- package/dist/types/testing.d.ts +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ TypeScript declarations ship in `dist/types/`, and each entry (`.`, `./node`, `.
|
|
|
33
33
|
| `ca` | `'<metadata>-ca'`, or `'slicc-kernel-ca'` | the IndexedDB database for the proxy's CA (see [Network](#network)); `false` keeps it in memory |
|
|
34
34
|
| `media` | `'<metadata>:media'` | the IndexedDB database for the folder handles of `fsa` drives (see [Removable media](#removable-media-fsa)); `false` keeps them in memory, as `metadata: false` does |
|
|
35
35
|
| `worker` | `new URL('./kernel-worker.js', import.meta.url)` | the kernel worker script |
|
|
36
|
-
| `network` | none | `{ transport, uplink }`: how programs reach the outside world (see [Network](#network)
|
|
36
|
+
| `network` | none | `{ transport, uplink, doh }`: how programs reach the outside world (see [Network](#network), [Uplink](#uplink) and [DNS](#dns)) |
|
|
37
37
|
| `requestDirectory` | none | `() => Promise<FileSystemDirectoryHandle>`, typically `showDirectoryPicker`: how an `fsa` mount gets its folder (see [Removable media](#removable-media-fsa)) |
|
|
38
38
|
| `hostfs` | none | `(source, { readonly }) => Promise<{ url, token, capabilities? } \| { port, token?, capabilities? }>`: a grant for a `hostfs` mount from the local proxy, or a `MessagePort` the page serves the folder on (see [Host folders](#host-folders-hostfs)) |
|
|
39
39
|
| `onMountPending` | none | `({ target, source, insert }) => void`: an `fsa` mount needs a folder; call `insert()` from a user gesture |
|
|
@@ -225,12 +225,14 @@ A WASI preview1 or WASIX program (Zig, Rust, Go, wasi-libc or wasix-libc C) has
|
|
|
225
225
|
{ "slicc": { "abi": "wasi", "commands": { "rg": { "wasm": "bin/rg.wasm" }, "rustc": { "script": "bin/rustc" } } } }
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
A command's name is any single path component without whitespace or control characters, so coreutils' `[` is in `/usr/bin` too; a name that is empty, `.` or `..`, or has a `/`, a space, a tab, a newline, NUL or another control character is skipped. Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The catalog reads its roots in order, and the first root that provides a name wins it: a user's own global packages (for that user's processes), `/node_modules`, root's global packages, then each appended root. Within a root, packages are read by name and the first wins (compared as strings by UTF-16 code unit, so a scoped `@scope/name` comes before names that start with a letter, but after those that start with a digit), whatever order the file system lists them in; a `PNPM_HOME`'s install groups count as one root, read group by group (by the group directory's name, effectively arbitrary for pnpm's hashed names), each by package name. A variant can take over from the package it extends with `"slicc": { "replaces": ["@ai-ecoverse/wasm-imagemagick"] }`: when it and a package it names are in the same root, it wins every command name they share, whatever the order; names only the other provides stay the other's, a third package that provides the name and sorts first still wins it, and packages that replace each other fall back to the name order. A replace never reaches into an earlier root, so root's global packages can't take a name from `/node_modules` or a user's own packages. The field must be an array of package names; anything else is ignored. `sh` runs bash as `sh` unless a package provides its own. A package's `engines.slicc-kernel` is a floor the kernel holds it to: when the running kernel's version doesn't satisfy that npm range (comparators, `^`, `~`, x-ranges, hyphen ranges, `||`), or the range can't be read, its commands are refused. They stay in `/usr/bin`, and running one by name, by path or as a `#!` interpreter exits 126 with `<cmd>: <pkg>@<version> needs slicc-kernel <range>; this kernel is <version>` on stderr. Each refused package is logged once to the console. A non-string `engines.slicc-kernel`, or none, is no constraint. A development build reports the newest release tag as `<tag>+dev` and checks against that; a build of unknown version (`0.0.0`) checks nothing. The refusal follows the program, not the name: its glue or `.wasm` run by path (directly or through a symlink), and a byte-identical copy of its glue beside it named for one of its commands (#390, #899), exit 126 with the same message ([#1235](https://github.com/ai-ecoverse/slicc-kernel/issues/1235)). Only commands are refused: a package without commands (a `py-*` library, a side module) isn't, and a refused package that replaces base commands makes those commands fail with 126 rather than fall back to the package it replaces. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. An entry reads as its program, as a PATH search that opens what it finds expects (`ruby -S rake`, `perl -S`, Pod::Usage looking itself up, bundler checking that `/usr/bin/ruby` isn't empty). A script command reads as its script, a JS command as its module, and a WASI or Emscripten command as its `.wasm`, so `cat /usr/bin/hello` prints the script and `head -c4 /usr/bin/cat` gives `\0asm`. The entry is a plain file and no link: it stays a root-owned `0755` regular file with its own inode, `readlink` of it is `EINVAL`, and `which` and `command -v` print `/usr/bin/<name>`. Its size and times are the program's when the caller may read the program (anyone, for a kernel-internal look or with users off, only when everyone may), and 0 and the epoch otherwise. Running it still runs the catalog command, with its `env`, `args` and `argv0`. The read is the caller's: with users on, reading an entry also needs `r` on its program, and that includes a user's own commands in `~/.local/share/pnpm/bin`, so an entry whose program the caller can't read is `EACCES`. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program; when it is the `wasm` of a WASI command in the caller's catalog, the user's own packages included, it runs as that command, with its `env` defaults, `args` and `argv0`, the command whose name matches the file's, without `.wasm`, if several share it, while a copy elsewhere runs bare), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. A spawn by a bare name (no `/`) that the catalog doesn't have, and that didn't ask for no search (a WASIX `posix_spawn` or `execve`, as opposed to its `posix_spawnp`/`execvp`, keeps its bare name), searches the `PATH` of the spawn's own environment, in order, with an empty entry meaning the working directory; each candidate is run exactly as a spawn of that path would be. The first that runs wins; if one was found but refused and nothing else matched, the spawn fails with `EACCES` (126), otherwise `ENOENT` (127). A catalog command still wins over the same name on `PATH`, the reverse of a shell's order. A searched bare name is never looked up in the working directory itself: a file of that name there runs only through an empty `PATH` entry, as with `execvp` ([#1234](https://github.com/ai-ecoverse/slicc-kernel/issues/1234)). The `PATH` searched is the new process's, from the spawn's environment; POSIX `posix_spawnp` uses the caller's, which is the same unless the caller hands the child another one. This is
|
|
228
|
+
A command's name is any single path component without whitespace or control characters, so coreutils' `[` is in `/usr/bin` too; a name that is empty, `.` or `..`, or has a `/`, a space, a tab, a newline, NUL or another control character is skipped. Entries may also set `argv0`, `args` and `env`, and a package can set `slicc.env` for all of its commands. A WASI entry with `"preopenRoot": true` also gets `/` as a preopen (see [WASI programs](#wasi-programs)). An entry with `"argv0Path": true` gets the absolute path it was run by as `argv[0]` when that path is a link outside `/bin` and `/usr/bin` to the command, as a venv's `bin/python` is (the `#!` line of a script naming such a link included), so it can find what sits beside that link; other entries keep their `argv0`. In `env`, `${package}` is the package directory, `${cwd}` the process's working directory, and `${NAME}` the caller's `NAME` when the program starts (a default naming an unset variable is left out); a relative path that exists in the package (`lib/python3.14`, `./`) becomes that absolute path. The caller's environment wins over these defaults; a `null` value removes the variable for the command, after the caller's environment is applied (a host-networked program can drop the kernel's proxy settings that way), and a command entry can set a variable its package's `slicc.env` removes. The catalog reads its roots in order, and the first root that provides a name wins it: a user's own global packages (for that user's processes), `/node_modules`, root's global packages, then each appended root. Within a root, packages are read by name and the first wins (compared as strings by UTF-16 code unit, so a scoped `@scope/name` comes before names that start with a letter, but after those that start with a digit), whatever order the file system lists them in; a `PNPM_HOME`'s install groups count as one root, read group by group (by the group directory's name, effectively arbitrary for pnpm's hashed names), each by package name. A variant can take over from the package it extends with `"slicc": { "replaces": ["@ai-ecoverse/wasm-imagemagick"] }`: when it and a package it names are in the same root, it wins every command name they share, whatever the order; names only the other provides stay the other's, a third package that provides the name and sorts first still wins it, and packages that replace each other fall back to the name order. A replace never reaches into an earlier root, so root's global packages can't take a name from `/node_modules` or a user's own packages. The field must be an array of package names; anything else is ignored. `sh` runs bash as `sh` unless a package provides its own. A package's `engines.slicc-kernel` is a floor the kernel holds it to: when the running kernel's version doesn't satisfy that npm range (comparators, `^`, `~`, x-ranges, hyphen ranges, `||`), or the range can't be read, its commands are refused. They stay in `/usr/bin`, and running one by name, by path or as a `#!` interpreter exits 126 with `<cmd>: <pkg>@<version> needs slicc-kernel <range>; this kernel is <version>` on stderr. Each refused package is logged once to the console. A non-string `engines.slicc-kernel`, or none, is no constraint. A development build reports the newest release tag as `<tag>+dev` and checks against that; a build of unknown version (`0.0.0`) checks nothing. The refusal follows the program, not the name: its glue or `.wasm` run by path (directly or through a symlink), and a byte-identical copy of its glue beside it named for one of its commands (#390, #899), exit 126 with the same message ([#1235](https://github.com/ai-ecoverse/slicc-kernel/issues/1235)). Only commands are refused: a package without commands (a `py-*` library, a side module) isn't, and a refused package that replaces base commands makes those commands fail with 126 rather than fall back to the package it replaces. The installed commands appear as executables in a virtual `/usr/bin` and `/bin`, so `PATH` lookups, `command -v` and `ls /usr/bin` work without writing anything to OPFS. An entry reads as its program, as a PATH search that opens what it finds expects (`ruby -S rake`, `perl -S`, Pod::Usage looking itself up, bundler checking that `/usr/bin/ruby` isn't empty). A script command reads as its script, a JS command as its module, and a WASI or Emscripten command as its `.wasm`, so `cat /usr/bin/hello` prints the script and `head -c4 /usr/bin/cat` gives `\0asm`. The entry is a plain file and no link: it stays a root-owned `0755` regular file with its own inode, `readlink` of it is `EINVAL`, and `which` and `command -v` print `/usr/bin/<name>`. Its size and times are the program's when the caller may read the program (anyone, for a kernel-internal look or with users off, only when everyone may), and 0 and the epoch otherwise. Running it still runs the catalog command, with its `env`, `args` and `argv0`. The read is the caller's: with users on, reading an entry also needs `r` on its program, and that includes a user's own commands in `~/.local/share/pnpm/bin`, so an entry whose program the caller can't read is `EACCES`. A process can also run a program by the path of its glue when the `.wasm` sits next to it, a wasm module by its own path (as a WASI program; when it is the `wasm` of a WASI command in the caller's catalog, the user's own packages included, it runs as that command, with its `env` defaults, `args` and `argv0`, the command whose name matches the file's, without `.wasm`, if several share it, while a copy elsewhere runs bare), and a script through its `#!` line (including `#!/usr/bin/env name`). Executing any other file fails with `ENOEXEC`, so bash runs it as a shell script, as POSIX shells do. A spawn by a bare name (no `/`) that the catalog doesn't have, and that didn't ask for no search (a WASIX `posix_spawn` or `execve`, as opposed to its `posix_spawnp`/`execvp`, keeps its bare name), searches the `PATH` of the spawn's own environment, in order, with an empty entry meaning the working directory; each candidate is run exactly as a spawn of that path would be. The first that runs wins; if one was found but refused and nothing else matched, the spawn fails with `EACCES` (126), otherwise `ENOENT` (127). A catalog command still wins over the same name on `PATH`, the reverse of a shell's order. A searched bare name is never looked up in the working directory itself: a file of that name there runs only through an empty `PATH` entry, as with `execvp` ([#1234](https://github.com/ai-ecoverse/slicc-kernel/issues/1234)). The `PATH` searched is the new process's, from the spawn's environment; POSIX `posix_spawnp` uses the caller's, which is the same unless the caller hands the child another one. This is `posix_spawnp`/`execvp` semantics for every caller that asks for a search: JS programs' `ctx.spawn`, WASI programs' spawn import, WASIX `posix_spawnp`, and Emscripten programs built on homescoop's shim before its own `PATH` search (gmake, tar, git and others hand the kernel the bare name). wasm-coreutils from 9.12.0-11 searches `PATH` itself and hands the kernel a path ([#1165](https://github.com/ai-ecoverse/slicc-kernel/issues/1165), [#1168](https://github.com/ai-ecoverse/slicc-kernel/issues/1168)). A `..` in a path that is run is resolved physically, as the file system resolves it: `pkg/../tools/x` after a symlinked `pkg` is the `tools/x` beside the link's target, and its exec permission is checked there. The walk is the caller's: with users on, each directory it passes needs search (`x`) permission (`EACCES`, 126 in bash, as on Linux), and a sudo process follows only the links it trusts, as for any other path. Commands also come from pnpm's global directory. Every process gets a `PNPM_HOME` and has `$PNPM_HOME/bin` on `PATH`: root's is `/usr/local/share/pnpm` (unless the embedder sets its own), a user's `~/.local/share/pnpm` (see [Users](#users)). The kernel scans each of pnpm's global install groups (`$PNPM_HOME/global/*/<group>/node_modules`) after `/node_modules`, which wins when both provide a command. The catalog is re-read whenever anything under either changes, so `pnpm add -g @ai-ecoverse/wasi-ripgrep` makes `rg` work in the same shell, and `pnpm remove -g` removes it again.
|
|
229
229
|
|
|
230
230
|
**More roots:** `createKernel({ commands: { append: ['/opt/user/node_modules'] } })` (and `createNodeKernel`) scans more directories, each absolute and normalised (anything else throws a `TypeError`). A name is looked up in a user's own global packages first, then in `modules`, then in root's global packages, then in the appended roots in the order given, and the first match wins, so an appended root never shadows another command (or `sudo`). Their commands are in `/usr/bin` for every user, like root's global packages, and their `binfmt` and file system entries count too. A package in an appended root also gets a command for each `bin` entry of its `package.json` (a string `bin` is named after the package, without its scope), which runs as a `#!` script; a name its `slicc.commands` gives wins. A `bin` entry is skipped when its name isn't a command name (see above), or its file, with symlinks resolved, isn't a regular file inside the package. `/node_modules` and the global packages keep reading only `slicc.commands`. A missing root counts as empty until it appears, and an install or removal in a root is picked up as it happens. With `users: 'enforce'` a root counts only when it and every directory above it are root's and not writable by group or others; another root is skipped without a word. Under `'report'` such a root is still scanned, and reported once to `onDenial` and the report log as `{ pid: 0, uid: 0, op: 'commands', path, errno: 'EACCES' }`. Running a `bin` command needs `r` and `x` on its file under `users` (see [Permissions](#permissions)), checked when it runs, so a `0644` file is `EACCES` under `'enforce'`.
|
|
231
231
|
|
|
232
232
|
**Running a glue by path:** executing a package's Emscripten glue by its path runs the command that names it (the one named like the file, when several share it), even with no `.wasm` beside it. A byte-identical copy of the glue in the same directory, named for one of the package's commands, runs as that command too ([#899](https://github.com/ai-ecoverse/slicc-kernel/issues/899)). wasm-clang ships `bin/clang`, `bin/wasm-ld` and the rest as copies of `bin/llvm`, and emcc executes `$LLVM_ROOT/clang`. The caller must be able to read the copy. A copy elsewhere, or a same-named file whose bytes differ, is not the command. The comparison is cached by path, size, mtime and inode.
|
|
233
233
|
|
|
234
|
+
**A `.wasm` run by path:** a WebAssembly file that no package command owns, executed by its path (for example a venv's copied interpreter), runs as a WASI program with `argv[0]` as the caller passed it, as `execve` does: `/tmp/v/bin/python` or `./python`, or `python` when found through `PATH` (#1264). Earlier kernels gave it the file's base name without `.wasm`. Its `/proc/self/exe` names the file.
|
|
235
|
+
|
|
234
236
|
**Variants by environment** ([homescoop#357](https://github.com/ai-ecoverse/homescoop/issues/357)): one package can carry more than one build of a wasm command and choose between them by the environment. `"variants": [{ "env": { "PYTHON_FORK": "true" }, "wasm": "bin/python-fork.wasm" }]` on a command entry (Emscripten or WASI) is matched in order against the environment the new process gets, at spawn and at exec. Every key must equal its value exactly, and the first match runs its `wasm`; no match runs the command's own `wasm`. This holds however the kernel reaches the entry: by name, by its path (`/usr/bin/python3`, as `sys.executable` gives it to `subprocess` and `multiprocessing`), or through a symlink to that path (a venv's `bin/python`). So `PYTHON_FORK=true python3`, `PYTHON_FORK=true exec /usr/bin/python3` and a child that inherits `PYTHON_FORK=true` all get the variant, and `PYTHON_FORK=1` doesn't. Commands of one package that share a build (`python`, `python3`, `pip`, `pip3`) each carry the same `variants`. A variant replaces only the module: the `abi`, the glue, `env`, `argv0`, `argv0Path` and the `/usr/bin` entry stay the command's. For an Emscripten command that means a variant swaps only the `.wasm` and keeps the command's JS glue, so it must be built against that same glue; a WASI or WASIX command has no glue, so there's no such constraint. Only the chosen module is read, checked and compiled, and compiled modules are cached by its path. A read or `stat` of `/usr/bin/<name>` still shows the default `wasm`. A `variants` that isn't an array, or has an entry without an `env` object of strings and a `wasm` inside the package, is ignored as a whole, with one warning per command, and the command runs its `wasm`. An older kernel ignores the field.
|
|
235
237
|
|
|
236
238
|
A package can also name a command that runs files by their suffix, as `binfmt_misc` does: `"slicc": { "binfmt": { ".jsh": "jsh" } }`. Executing a file that has no `#!` line and is not a wasm module, whose name ends in a mapped suffix, runs that command with the file's path as invoked and the arguments after it, as a `#!` line naming the command would; a `#!` line still wins. The longest matching suffix wins, the whole name is never a suffix (`.jsh` alone is not one), and of two packages that map one suffix, the one whose name sorts first wins, as for commands; the kernel warns once (`console.warn`) when another package maps that suffix to a different command. A suffix starts with a dot and the command must be a name the catalog resolves when the file runs.
|
|
@@ -505,6 +507,8 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
505
507
|
- `AUTH LOGIN`: the password line (the second line, or the first after `AUTH LOGIN <user>`) is decoded, substituted (`auth-login`) and re-encoded; the user line goes out as written.
|
|
506
508
|
- Every other line goes out as written. After the first `AUTH`, or once `MAIL`, `RCPT`, `DATA` or `BDAT` is seen, so is the rest of the session. A line still unfinished after 2 s, or longer than 4 KiB, is sent as written, and so is the rest.
|
|
507
509
|
- **pg:** a session that starts with a protocol 3 `StartupMessage` (after `sslmode`'s `SSLRequest` upgrade, or with direct TLS) is followed message by message. The only position is the **`PasswordMessage` answering `AuthenticationCleartextPassword` (`R` 3)**, as the program read it from the server. The password is substituted (`password-cleartext`) and the message is framed again. **SCRAM-SHA-256** (`R` 10, the pg 14+ default) works with an unmodified client:
|
|
510
|
+
- **Only where a value can be substituted:** the kernel follows SCRAM only in a session where a sealed value could reach this host: one the bundled roots verified, or one a vault anchor verified (`sessionAnchors` found an anchor for this destination; it's asked once, when the pg session starts). In any other session, such as one verified by the program's own `ca` (a private-CA server, `sslrootcert`) or not verified at all (`sslmode=require`), the mechanisms, the GS2 header and `c=` pass through byte for byte, so a program's own password with any `channel_binding` works as it does without the kernel ([slicc-jsh#584](https://github.com/ai-ecoverse/slicc-jsh/issues/584)).
|
|
511
|
+
- **In an anchored session:** `-PLUS` is not offered and the server side is `n,,`, so a program's own password with `channel_binding=prefer` against a `-PLUS` server is refused there, and `require` fails closed, until the binding is taken from the anchor-verified leaf ([#1259](https://github.com/ai-ecoverse/slicc-kernel/issues/1259)).
|
|
508
512
|
- **What the program is offered:** the server's mechanisms. `SCRAM-SHA-256-PLUS` is offered only when the kernel can bind to the server: a verified session whose leaf certificate's signature hash is SHA-256, SHA-384 or SHA-512, with MD5 and SHA-1 counting as SHA-256 (RFC 5929 §4.1). With any other hash (RSA-PSS or Ed25519, say), `-PLUS` is dropped. So libpq's default `channel_binding=prefer`, `require`, and node-postgres' `enableChannelBinding` all work.
|
|
509
513
|
- **Channel binding (`tls-server-end-point`):** the kernel changes only what the server would refuse.
|
|
510
514
|
- `n,,` goes out as written.
|
|
@@ -592,6 +596,15 @@ WASIX programs get `AF_INET`/`AF_INET6` `SOCK_DGRAM` sockets, and `AF_UNIX` ones
|
|
|
592
596
|
- **Flow control:** a receiver queue holds 512 messages or 256 KiB. A full one blocks a sender, or fails a non-blocking send with `EAGAIN`, as on Linux.
|
|
593
597
|
- **Not supported:** passing descriptors or credentials (`SCM_RIGHTS`, `SCM_CREDENTIALS`) over datagrams, and `AF_UNIX` datagrams for Emscripten and JS programs.
|
|
594
598
|
|
|
599
|
+
#### DNS
|
|
600
|
+
|
|
601
|
+
The kernel answers DNS itself, so DNS clients (BIND's `dig`, `host` and `nslookup`) see the same names as `getaddrinfo` (#1244).
|
|
602
|
+
- **Where it listens:** UDP and TCP port 53 on the kernel's loopback; `/etc/resolv.conf` names `127.0.0.53`, as systemd-resolved does, but any loopback address reaches it. TCP takes several queries on one connection (2-byte length framing), at most 64 connections at once, each closed after 10 s without a query or when an answer can't be written within 10 s (a client that stops reading). Over UDP at most 64 questions are answered at once; while that many wait (on the forwarder, say), more are dropped, as by a full receive queue. Port 53 is the kernel's, as the proxy's and CDP's ports are: a program's `bind` to it, root's included, is `EADDRINUSE` on UDP and TCP, so no program can answer other users' questions, and `kernel.dial` refuses it.
|
|
603
|
+
- **What it answers itself,** from the [kernel's resolver](#node-name): `A` and `AAAA` for `/etc/hosts` names, the node name, `localhost` and `*.localhost`, `host.slicc.internal`, and the [uplink](#uplink)'s names (a tray's `<device>.slicc.internal`, MagicDNS); `PTR` from `/etc/hosts` and the built-in entries (`127.0.0.0/8` is `localhost`). TTLs are 30 s. Answers for `localhost` and `slicc.internal` are authoritative (`aa`), and a name there that nothing knows is `NXDOMAIN`. Other types for a name the kernel knows (`MX` for `host.slicc.internal`) get an empty answer.
|
|
604
|
+
- **Everything else** (other names, other types, other classes, address literals, an `AAAA` the uplink has no answer to, a name whose uplink lookup failed) goes to a forwarder, and there is none by default: those questions are `SERVFAIL`, and nothing leaves the kernel. An [uplink](#uplink) whose `nameserver` names its DNS server (an address it routes, such as Tailscale's `100.100.100.100`) gets them over its own TCP, length-framed as RFC 1035 has it, so they go where that uplink and its exit send them. Without one, `network: { doh }` names a DNS over HTTPS endpoint (RFC 8484, `POST`, id 0) that the [transport](#network) is asked; `doh: false` or none is no forwarder. A forwarder's answer must repeat the question (name, type and class) or it is `SERVFAIL`, as are a failed connection, an HTTP error or a non-`application/dns-message` answer, and no answer within 5 s. The answer goes back as the forwarder gave it, with the query's id. Over UDP, an answer longer than 512 bytes (or the query's EDNS size, at most 4096) comes back truncated (`tc`, no records), so the client asks again over TCP.
|
|
605
|
+
- **What a forwarder sees:** the question's bytes as the program wrote them (id set to 0), EDNS options included, so a program can carry data to the forwarder in DNS names, as anywhere DNS is resolved. That gives no process more than it has: every process may already use the proxy and the transport, and connect to what the uplink routes; the kernel has no per-user network policy that the stub would bypass.
|
|
606
|
+
- **`/etc/resolv.conf`:** at boot, the kernel writes `nameserver 127.0.0.53` and `options edns0` there when there is no such file (a symlink counts as one), owned by root with mode `0644`. A file root wrote stays, and so does none at all when `/etc/resolv.conf` is a [protected path](#protected-paths). One that root removes comes back at the next boot. It has no `search` line, because musl's `getaddrinfo` (WASIX programs) would then try every short name in that domain first.
|
|
607
|
+
|
|
595
608
|
### Node name
|
|
596
609
|
|
|
597
610
|
The kernel has one node name, `slicc` unless `createKernel({ hostname })` (or `createNodeKernel`) names another; it must be a valid host name, and not `localhost`, a `*.localhost` name, `host.slicc.internal`, `wasmer.sh` or an IPv4 address, or `createKernel` rejects. `no_proxy` lists it (and `emscripten`), so HTTP clients reach the kernel's own servers by it directly.
|
|
@@ -607,6 +620,7 @@ The kernel has one node name, `slicc` unless `createKernel({ hostname })` (or `c
|
|
|
607
620
|
|
|
608
621
|
- `traits`: `{ tcp: true, udp, ipv6 }`. Without `ipv6: true`, the kernel asks only for, and keeps only, IPv4 addresses. With `udp: true` and a `udp` function, routed datagrams go through it.
|
|
609
622
|
- `routes`: the first route table, `{ prefixes, exit }`, which `kernel.setRoutes` replaces when it changes. The kernel matches it itself, so a destination outside it costs no round trip.
|
|
623
|
+
- `nameserver` (optional): an address the uplink routes where a DNS server answers on TCP port 53 (Tailscale: `100.100.100.100`). The kernel's [DNS](#dns) stub forwards the questions it can't answer there.
|
|
610
624
|
- `resolve(name, family, signal)`: resolves with the name's addresses, as a list or as `{ addresses, ttl }` (seconds). `[]` means the name is not the uplink's. `family` is `4`, `6` or `0` (either), and `signal` aborts when the kernel stops waiting.
|
|
611
625
|
- `dial({ network: 'tcp', host, port, signal })`: opens a TCP connection and resolves with `{ localAddr, remoteAddr, read(), write(bytes), closeWrite(), close() }`.
|
|
612
626
|
- `read()` resolves with the next bytes, or `null` at the end.
|
|
@@ -646,7 +660,7 @@ An uplink connection is an ordinary socket fd: it survives `dup`, `fork` and `ex
|
|
|
646
660
|
|
|
647
661
|
In the browser, the page serves the uplink to the kernel worker over the same port as the transport, with buffers copied and transferred. `terminate()` closes every connection and cancels whatever is pending, and the page refuses to dial loopback, `10.0.2.2` or reserved addresses itself as well.
|
|
648
662
|
|
|
649
|
-
`@ai-ecoverse/slicc-kernel/testing` is test support, kept out of the main entry so it never reaches a page bundle. It runs with the Node kernel and needs no wasm of its own. `fakeUplink({ names, routes, ipv6, peers, address })` is an uplink to pass as `network.uplink`:
|
|
663
|
+
`@ai-ecoverse/slicc-kernel/testing` is test support, kept out of the main entry so it never reaches a page bundle. It runs with the Node kernel and needs no wasm of its own. `fakeUplink({ names, routes, ipv6, peers, address, nameserver })` is an uplink to pass as `network.uplink`:
|
|
650
664
|
- **Names:** `resolve` answers from `names` (a list or `{ addresses, ttl }` per name) and records each question in `asked`.
|
|
651
665
|
- **Routes:** `routes` is the first table; `kernel.setRoutes` changes it as for a real uplink.
|
|
652
666
|
- **Dials:** each dial is recorded in `dialled`. `localAddr` is `address` (default `100.100.100.100`) with a fresh port. `peers`, keyed `'host:port'` (or `'[v6]:port'`), decides how a dial ends:
|
|
@@ -1105,7 +1119,7 @@ A character device (`0666`), there in every `users` mode, through which programs
|
|
|
1105
1119
|
|
|
1106
1120
|
### `/proc`
|
|
1107
1121
|
|
|
1108
|
-
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`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. 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:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) 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, from the moment the call actually starts to wait (a call that could wait but answers at once, such as a `select` with a zero timeout or a ready descriptor, a `waitpid` with `WNOHANG`, or a futex wait whose value has already changed, 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 the times of the children a process has waited for (theirs and their own waited-for children's, as POSIX counts them) show as its `cutime` and `cstime` in `/proc/<pid>/stat`; a stopped process is `T` there, in `/proc/<pid>/status` (`T (stopped)`) and in `ps()`; a WASI or WASIX program's `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` (`clock_time_get`) count only the time its thread runs, its own waits and blocked kernel calls left out (a sleep is not CPU time), per thread for now, so a threaded program's process clock is its calling thread's; 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). WASI and WASIX programs also have `/proc/<pid>/task/<tid>/stat` for each of a process's threads (the main thread is tid 1), whose state is `S` while the thread is blocked in the kernel (a `sem_wait`, a blocking read, a sleep, a futex) and `R` otherwise (`T` and `Z` as for the process). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
|
|
1122
|
+
Every process of the kernel has `/proc/<pid>/` with `cmdline`, `comm`, `stat`, `statm` and `status`, in the formats procps reads, and `exe`, a link to the file the process was started from: the glue or `.wasm` that ran, as resolved (a copy run by its own path names the copy, a symlink its target) (#1264). As on Linux without ptrace access, reading another process's `exe` link is `EACCES` unless the reader is root or its uid and gid match all of the target's real, effective and saved ids; `cmdline` and `status` stay readable, 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`. `/proc/version` names the kernel, Linux-shaped, with the slicc-kernel version that built it: `Linux version 6.0.0-slicc (slicc-kernel <version>) #1 SMP PREEMPT_DYNAMIC`. `/proc/sys/kernel/osrelease` is `6.0.0-slicc` and `/proc/sys/kernel/ostype` is `Linux`. The version is the one npm publishes, set when the package is built (`0.0.0-dev` when run from the sources). A JS program reads it as `ctx.kernel.version`. 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:`; a process with a tag, see `kernel.run`, also has `Tag:\t<tag>` after `Gid:`, shown to root and to the same uid only, with newlines, tabs and carriage returns written `\n`, `\t` and `\r`) 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, from the moment the call actually starts to wait (a call that could wait but answers at once, such as a `select` with a zero timeout or a ready descriptor, a `waitpid` with `WNOHANG`, or a futex wait whose value has already changed, 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 the times of the children a process has waited for (theirs and their own waited-for children's, as POSIX counts them) show as its `cutime` and `cstime` in `/proc/<pid>/stat`; a stopped process is `T` there, in `/proc/<pid>/status` (`T (stopped)`) and in `ps()`; a WASI or WASIX program's `CLOCK_PROCESS_CPUTIME_ID` and `CLOCK_THREAD_CPUTIME_ID` (`clock_time_get`) count only the time its thread runs, its own waits and blocked kernel calls left out (a sleep is not CPU time), per thread for now, so a threaded program's process clock is its calling thread's; 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). WASI and WASIX programs also have `/proc/<pid>/task/<tid>/stat` for each of a process's threads (the main thread is tid 1), whose state is `S` while the thread is blocked in the kernel (a `sem_wait`, a blocking read, a sleep, a futex) and `R` otherwise (`T` and `Z` as for the process). JS programs read the same `/proc` through `ctx.fs` and `ctx.open` (see [JS programs](#js-programs)). Neither has `/proc/<pid>/fd` yet.
|
|
1109
1123
|
|
|
1110
1124
|
The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#users)). While the root has none of them, or no `/etc/mtab`, reading them gets per-process ones: root is uid 0 and the realm user, uid 1000, is named after `USER`, or `web_user` (Emscripten's default) without one, so it matches `$USER`, with `HOME` as its home, so `id`, `whoami`, `ls -l` and `ps` show names. Nothing is written to OPFS, and a real file there wins.
|
|
1111
1125
|
|
|
@@ -1276,6 +1290,10 @@ Not part of the run yet: the rest of bios's setup (the `rest` shard, the upgrade
|
|
|
1276
1290
|
|
|
1277
1291
|
The WASIX fixtures of `test/integration/epoll.test.mjs` (#674), under `test/integration/fixtures/`, are committed with their sources, build scripts and built wasm.
|
|
1278
1292
|
|
|
1293
|
+
### `dnstest`
|
|
1294
|
+
|
|
1295
|
+
`test/integration/fixtures/dnstest`: a C WASIX program, built by `build.sh` with `wasixcc`, for the [DNS](#dns) rows (#1244). `dnstest udp|tcp|tcp2 <server> <name> <type> [edns]` sends one question to `<server>:53` (`tcp2` sends it twice on one connection), then prints the reply's id check, rcode and `aa`/`tc`/`ra` flags and section counts, and each answer as `<name> <ttl> <type> <data>`. `dnstest flood <server> <name> <type> <n>` sends `n` questions over UDP without waiting, `dnstest bind` tries to bind `127.0.0.1:53` over UDP and TCP and prints each result, and `dnstest stall <server> <name> <type> <s>` writes questions over TCP until the connection is full, waits `s` seconds, then counts the answers it can read (`closed` when the stub hung up before answering them all).
|
|
1296
|
+
|
|
1279
1297
|
### `pg-fixture` with `plus`
|
|
1280
1298
|
|
|
1281
1299
|
`test/integration/pg-fixture.mjs`, `pgFixture(t, cert, password, { plus: true })`: pg-gateway's SCRAM-SHA-256 server, which offers `SCRAM-SHA-256-PLUS` too and refuses as PostgreSQL's `auth-scram.c` does (jsh#531):
|
package/dist/index.js
CHANGED
|
@@ -2278,7 +2278,8 @@ function dialer(call) {
|
|
|
2278
2278
|
function uplinkInit(uplink) {
|
|
2279
2279
|
if (!uplink) return {};
|
|
2280
2280
|
const traits = { tcp: true, udp: carriesUdp(uplink), ipv6: uplink.traits.ipv6 === true };
|
|
2281
|
-
|
|
2281
|
+
const nameserver = typeof uplink.nameserver === "string" ? { nameserver: uplink.nameserver } : {};
|
|
2282
|
+
return { uplink: { traits, ...uplink.routes ? { routes: uplink.routes } : {}, ...nameserver } };
|
|
2282
2283
|
}
|
|
2283
2284
|
function initCall(options, transport) {
|
|
2284
2285
|
return {
|
|
@@ -2292,6 +2293,7 @@ function initCall(options, transport) {
|
|
|
2292
2293
|
...options.ca !== void 0 ? { ca: options.ca } : {},
|
|
2293
2294
|
...transport ? { transport: transport.traits } : {},
|
|
2294
2295
|
...uplinkInit(options.network?.uplink),
|
|
2296
|
+
...options.network?.doh !== void 0 ? { doh: options.network.doh } : {},
|
|
2295
2297
|
...options.hostfs ? { hostfs: true } : {},
|
|
2296
2298
|
...options.cdp ? { cdp: true } : {},
|
|
2297
2299
|
...facadesInit(options.facades),
|