@ai-ecoverse/slicc-kernel 1.61.2 → 1.62.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 +19 -5
- package/dist/index.js +12 -9
- package/dist/index.js.map +2 -2
- package/dist/kernel-worker.js +616 -260
- package/dist/kernel-worker.js.map +4 -4
- package/dist/node-process-worker.js +14 -4
- package/dist/node-process-worker.js.map +2 -2
- package/dist/node.js +711 -355
- package/dist/node.js.map +4 -4
- package/dist/process-worker.js +14 -4
- package/dist/process-worker.js.map +2 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -38,7 +38,8 @@ const { status, stdout, stderr } = await kernel.run(['bash', '-c', 'echo hi > he
|
|
|
38
38
|
| `onMountPending` | none | `({ target, source, insert }) => void`: an `fsa` mount needs a folder; call `insert()` from a user gesture |
|
|
39
39
|
| `processMounts` | `true` | whether programs may call `mount(2)` and `umount2(2)`: `false`, or `(req) => boolean \| Promise<boolean>` to decide each call (see [Mounting from a program](#mounting-from-a-program)) |
|
|
40
40
|
| `hostname` | `'slicc'` | the kernel's node name (see [Node name](#node-name)) |
|
|
41
|
-
| `users` | `'off'` | `'off'`, `'report'` or `'enforce'`: whether files have owners (see [File owners](#file-owners)). `'off'` behaves exactly as kernels before owners did; any other value rejects. `createNodeKernel` takes it too |
|
|
41
|
+
| `users` | `'off'` | `'off'`, `'report'` or `'enforce'`: whether files have owners and their modes are checked (see [File owners](#file-owners) and [Permissions](#permissions)). `'off'` behaves exactly as kernels before owners did; any other value rejects. `createNodeKernel` takes it too |
|
|
42
|
+
| `onDenial` | none | `({ pid, uid, op, path, errno }) => void`: called for every permission check that fails, under `'report'` and `'enforce'` (see [Permissions](#permissions)). `createNodeKernel` takes it too |
|
|
42
43
|
| `cdp` | none | `({ runtime }) => Promise<CdpConnection>`: a browser-level DevTools connection for programs that drive a browser (see [Browser automation](#browser-automation-cdp)) |
|
|
43
44
|
|
|
44
45
|
### `kernel.run(argv, options?) → Promise<{ status, stdout, stderr }>`
|
|
@@ -160,7 +161,7 @@ const { status, stdout } = await kernel.run(['bash', '-c', 'echo hi'], { cwd: '/
|
|
|
160
161
|
kernel.terminate();
|
|
161
162
|
```
|
|
162
163
|
|
|
163
|
-
`createNodeKernel({ root, modules, env, network, worker, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), which keeps each file in 1 MiB chunks, and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal`, `users`, `setRoutes` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results; like the `fs` of an attached client, they go through the mounts, as the processes see them. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect({ user })` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
|
|
164
|
+
`createNodeKernel({ root, modules, env, network, worker, processMounts, fstabRetries, cdp, hostfs, hostfsOrigin, hostfsFetch, users, onDenial })` takes the options of `createKernel` except `metadata`, and `fstabRetries`, the delays in ms between tries of a failing `/etc/fstab` line (default `[1000, 4000, 16000]`). `root` is an in-memory directory by default (`memoryRoot()` makes another), which keeps each file in 1 MiB chunks, and POSIX metadata stays in memory. Processes and threads run on `worker_threads`. The kernel has `run`, `openTerminal`, `users`, `setRoutes` and `terminate` as above, plus `root`, `writeFile(path, data)` (creating the parent directories) and `readFile(path)` to put files in place and read results; like the `fs` of an attached client, they go through the mounts, as the processes see them. `nodeTransport()` is `fetchTransport()` with Node's `fetch`, which no CORS binds (`crossOrigin: 'any'`). `connect({ user })` and `attachKernel` work as in the browser: the port is a `worker_threads` `MessagePort`, which a worker thread can attach with, and `terminate()` ends every attached client.
|
|
164
165
|
|
|
165
166
|
## Commands
|
|
166
167
|
|
|
@@ -228,7 +229,8 @@ A WASI command can name an ES module of its package that provides imports of its
|
|
|
228
229
|
- `spawn({ argv, env, cwd, stdin, stdout, stderr })` starts a command (resolved like `kernel.run`) as a child and returns `{ pid, stdin, stdout, stderr }`. `env` is the child's whole environment (the program's own by default), `cwd` the program's by default. Each stdio is `'inherit'` (the default: the program's own), `'null'`, or `'pipe'`: a kernel pipe whose end the result holds as a descriptor, read and written with `fd-read` (`{ op: 'fd-read', fd, max }`, an empty read at the end), `fd-write` (`{ op: 'fd-write', fd, body }`) and `fd-close`, through `syscall` or `async.submit`; the child's ends are closed in the program. A command that does not exist throws `ENOENT`;
|
|
229
230
|
- `wait(pid)` blocks until a child ends and returns `{ status }`, or `{ status: 128 + signal, signal }` when a signal ended it; `kill(pid, sig)` signals it;
|
|
230
231
|
- `async`: operations of the process that any of its threads can wait for: `submit(req)` runs a kernel syscall without blocking the caller, `resolve(value)` completes at once, `hold()` never completes, and each returns an id; `wait(timeoutMs?)` returns the id of a completed one (0 after the timeout, -1 once `close()` was called), `take(id)` its `{ value }` or `{ error }` (`undefined` while it is pending), and `cancel(id)` drops one;
|
|
231
|
-
- `errno(err)`: the WASI errno of an error
|
|
232
|
+
- `errno(err)`: the WASI errno of an error;
|
|
233
|
+
- `uid` and `gid`, with `users: 'report'` or `'enforce'`: the process's effective uid and gid, read again at each use, so they follow a `setresuid`. `fstat` then shows a file's real owner, and the process's ids for a descriptor with no path. With `users: 'off'`, `ctx` has no `uid` or `gid`, and `fstat` answers 1000 for both, as before.
|
|
232
234
|
|
|
233
235
|
### JS programs
|
|
234
236
|
|
|
@@ -520,7 +522,7 @@ Lowering a soft resource limit (`ulimit -S -n 64`) still fails with `EPERM` in E
|
|
|
520
522
|
|
|
521
523
|
### Users
|
|
522
524
|
|
|
523
|
-
Every process has credentials in the kernel, as on Linux: real, effective and saved user and group ids, and supplementary groups. A process gets them from the user it was started as (`run`, `openTerminal`, a client's `spawn`, or the client's own user; root by default) and keeps its parent's across fork, spawn and exec. Setuid and setgid bits are never honoured on exec. Files may have owners (see [File owners](#file-owners))
|
|
525
|
+
Every process has credentials in the kernel, as on Linux: real, effective and saved user and group ids, and supplementary groups. A process gets them from the user it was started as (`run`, `openTerminal`, a client's `spawn`, or the client's own user; root by default) and keeps its parent's across fork, spawn and exec. Setuid and setgid bits are never honoured on exec. Files may have owners (see [File owners](#file-owners)), checked against the credentials with `users: 'enforce'` (see [Permissions](#permissions)); the credentials also decide who may signal whom, and what `/proc` and `ps` show.
|
|
524
526
|
|
|
525
527
|
- **The database:** `/etc/passwd` and `/etc/group` are real files. On first start the kernel writes them when they're missing, with `root` (uid 0, home `/root`, created `0700`) and `nobody` (65534), and the groups `root`, `sudo` (27), `users` (100) and `nogroup` (65534); files already there are kept as they are. The kernel reads them for every lookup, so an edit counts at once. [`kernel.users`](#kernelusers) adds and removes users.
|
|
526
528
|
- **Environment:** a process started as a user gets `HOME`, `USER`, `LOGNAME`, `PNPM_HOME`, `NPM_CONFIG_PREFIX` and `PATH`. A user's `PNPM_HOME` is `~/.local/share/pnpm` and its npm prefix `~/.local`; root's are `/usr/local/share/pnpm` (or the embedder's `env.PNPM_HOME`) and `/usr/local`. `PATH` is `$PNPM_HOME/bin:/usr/local/bin:/usr/bin:/bin`.
|
|
@@ -536,7 +538,7 @@ Programs ask the kernel for their ids:
|
|
|
536
538
|
|
|
537
539
|
### File owners
|
|
538
540
|
|
|
539
|
-
With `createKernel({ users: 'report' })` or `'enforce'`, files have owners
|
|
541
|
+
With `createKernel({ users: 'report' })` or `'enforce'`, files have owners, and [Permissions](#permissions) checks them against a process's credentials. With `'off'`, the default, nothing below happens: the kernel stores no owner, and every file looks owned by the caller as described under [Metadata](#metadata).
|
|
540
542
|
|
|
541
543
|
- **Where owners live:** on the root file system, in the sidecar record's `uid` and `gid` (see [Metadata](#metadata)). A path with no owner of its own has the owner of its nearest ancestor that has one, and `/` belongs to root, so a root written before owners existed, or by the BIOS through the OPFS API, is root's throughout. A directory whose owner changes keeps the owner its unowned contents had, in `heir`, so `chown cone /old` gives `cone` the directory and leaves the files below it to their old owner. A rename keeps the owner of what it moves.
|
|
542
544
|
- **New files:** a file, directory or symlink a process creates belongs to its effective uid, and to its effective gid, or to the directory's group when the directory has the setgid bit; a new directory in a setgid directory gets the setgid bit too. Every file and directory a process creates gets the mode it asked for (`0666` and `0777` when it can't ask, as a JS program's `ctx.fs` can't) less the process's umask, whichever runtime made it; an Emscripten program's file no longer gets `0644` whatever it asked for. A file written through a client's `fs` belongs to the client's user (root for a page) and gets `0666`, a directory `0777`, less the umask `kernel.users.add` gave that user (`022` for root and by default); the kernel's own files, such as `/etc/passwd`, have no owner of their own.
|
|
@@ -545,6 +547,18 @@ With `createKernel({ users: 'report' })` or `'enforce'`, files have owners. Noth
|
|
|
545
547
|
- **What programs see:** `stat` shows the owners to Emscripten programs, whatever their libc reports for their own ids, so a program built before H1 sees real owners too, and its owner checks can fail. WASI programs read them with `slicc_fs.fd_owner` and `path_owner`. The commands in `/usr/bin` and `/bin` are root's, and a user's own commands in `~/.local/share/pnpm/bin` belong to the owner of the closest real directory above them, normally the user.
|
|
546
548
|
- **Mounts:** the options `uid=` and `gid=` (decimal ids) give a mount its owner; without them it belongs to whoever mounted it: root for `kernel.mount`, `/etc/fstab` and a client, the process's effective ids for `mount(2)`, which then shows them in its options. A mount whose driver keeps owners (`tmpfs`) starts with its root owned so, and its files get owners as on the root file system, `chown` included. On `hostfs`, `fsa` and any driver without `owners`, every file shows as the mount's owner and `chown` fails with `EPERM`, root's too, as on a FAT drive. An embedder that mounts a folder for a user (bios's linked and picked folders) passes that user's `uid` and `gid`, or the user's programs see root's files there, and git calls the repository's ownership dubious.
|
|
547
549
|
|
|
550
|
+
### Permissions
|
|
551
|
+
|
|
552
|
+
With `users: 'enforce'` the kernel checks every file call that reaches it against the caller's effective uid, effective gid and supplementary groups, as Linux does: the owner bits for the file's owner, else the group bits for a member of its group, else the other bits. Root (effective uid 0) passes every check except executing a file with no `x` bit at all. A failed check is `EACCES`; changing what only the owner may change is `EPERM`. With `users: 'report'` the kernel makes the same checks and refuses nothing: it logs each denial once per user, operation, errno and path, with `console.warn`, up to 10,000 of them, then says that more were suppressed. With both, `onDenial` gets each denial as `{ pid, uid, op, path, errno }` (`pid` 0 for a client's `fs`); the shape is stable. With `users: 'off'` nothing is checked and no call is added.
|
|
553
|
+
|
|
554
|
+
- **Paths:** a path needs `x` on every directory it goes through, the targets of symlinks on the way included, resolved as the file system resolves them (a `..` after a symlinked directory goes to the target's parent). `stat`, `lstat`, `readlink` and `chown` need only that; `exists` answers `false` where it's refused. Listing a directory needs `r` and `x` on it.
|
|
555
|
+
- **Creating and removing:** `mkdir`, creating a file and `symlink` need `w` and `x` on the directory; `mkdir -p` needs them on the closest directory that exists, and nothing for one that exists already. Unlinking, `rmdir` and `rename` need `w` and `x` on both directories, and a directory moved to another needs `w` on itself. A recursive remove (`ctx.fs.rm`, a client's `rm`) needs `r`, `w` and `x` on every directory in the tree.
|
|
556
|
+
- **The sticky bit:** in a directory with mode `1000` set, such as `/tmp` at `1777`, only the file's owner, the directory's owner or root may unlink or rename a file (`EPERM` otherwise).
|
|
557
|
+
- **Hard links:** `link` needs `w` and `x` on the target's directory. As with Linux's `protected_hardlinks`, a user who doesn't own a file may link it only when it may read and write it (`EPERM` otherwise); root may link anything.
|
|
558
|
+
- **Modes and times:** only the owner or root may `chmod` (`EPERM`), and a `chmod` by a user that isn't in the file's group clears its setgid bit. Only the owner or root may set a file's times (`EPERM`).
|
|
559
|
+
- **A client's `fs`:** checked as the client's user; a page's client (root) passes.
|
|
560
|
+
- **Not yet checked:** opening a file and reading or writing it, executing it, and `access(2)`; until they are, `users: 'enforce'` isn't a security boundary, and no release turns it on by default before `sudo` exists.
|
|
561
|
+
|
|
548
562
|
### `/proc`
|
|
549
563
|
|
|
550
564
|
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, load and `MemTotal` (from `navigator.deviceMemory`, else 4 GiB) are placeholders. 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
|
@@ -1798,6 +1798,7 @@ function initCall(options, transport) {
|
|
|
1798
1798
|
...options.cdp ? { cdp: true } : {},
|
|
1799
1799
|
...options.hostname !== void 0 ? { hostname: options.hostname } : {},
|
|
1800
1800
|
...options.users !== void 0 ? { users: options.users } : {},
|
|
1801
|
+
...options.onDenial ? { denials: true } : {},
|
|
1801
1802
|
...options.processMounts !== void 0 ? {
|
|
1802
1803
|
processMounts: typeof options.processMounts === "function" ? "ask" : options.processMounts
|
|
1803
1804
|
} : {}
|
|
@@ -1843,6 +1844,15 @@ function kernelUsers(call, mode = "off") {
|
|
|
1843
1844
|
list: async () => await call({ op: "users-list" })
|
|
1844
1845
|
};
|
|
1845
1846
|
}
|
|
1847
|
+
async function mountPolicy(options, worker, { id, req }) {
|
|
1848
|
+
const decide = options.processMounts;
|
|
1849
|
+
let allowed = false;
|
|
1850
|
+
try {
|
|
1851
|
+
allowed = typeof decide === "function" ? await decide(req) : decide !== false;
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
worker.postMessage({ mountAllowed: { id, allowed } });
|
|
1855
|
+
}
|
|
1846
1856
|
async function createKernel(options = {}) {
|
|
1847
1857
|
if (!globalThis.crossOriginIsolated) throw new Error(ISOLATION);
|
|
1848
1858
|
if (options.hostname !== void 0 && !isHostname(options.hostname)) {
|
|
@@ -1886,15 +1896,7 @@ async function createKernel(options = {}) {
|
|
|
1886
1896
|
worker.postMessage({ hostfsGrant: { id, error: err.message ?? String(err) } });
|
|
1887
1897
|
}
|
|
1888
1898
|
}
|
|
1889
|
-
|
|
1890
|
-
const decide = options.processMounts;
|
|
1891
|
-
let allowed = false;
|
|
1892
|
-
try {
|
|
1893
|
-
allowed = typeof decide === "function" ? await decide(req) : decide !== false;
|
|
1894
|
-
} catch {
|
|
1895
|
-
}
|
|
1896
|
-
worker.postMessage({ mountAllowed: { id, allowed } });
|
|
1897
|
-
}
|
|
1899
|
+
const policy = (asked) => mountPolicy(options, worker, asked);
|
|
1898
1900
|
const pendingMedium = ({ target, source, handle }) => options.onMountPending?.({
|
|
1899
1901
|
target,
|
|
1900
1902
|
source,
|
|
@@ -1905,6 +1907,7 @@ async function createKernel(options = {}) {
|
|
|
1905
1907
|
({
|
|
1906
1908
|
data
|
|
1907
1909
|
}) => {
|
|
1910
|
+
if ("denial" in data) return void options.onDenial?.(data.denial);
|
|
1908
1911
|
if ("net" in data) return bridge?.answer(data);
|
|
1909
1912
|
if ("uplink" in data) return uplinks?.answer(data);
|
|
1910
1913
|
if ("cdpOpen" in data) return cdp.open(data);
|