@ai-ecoverse/slicc-kernel 1.91.0 → 1.92.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 +4 -2
- package/dist/driver-worker.js +9 -7
- package/dist/driver-worker.js.map +2 -2
- package/dist/kernel-worker.js +30 -8
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +79 -24
- package/dist/node-process-worker.js.map +3 -3
- package/dist/node.js +30 -8
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +79 -24
- package/dist/process-worker.js.map +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -275,6 +275,7 @@ A WASI command can name an ES module of its package that provides imports of its
|
|
|
275
275
|
- `fs`: the synchronous filesystem the WASI calls use (absolute paths);
|
|
276
276
|
- `fds`: `open(path, { read, write, append, create, exclusive, truncate, directory, nofollow, mode })` returns a WASI descriptor of the program; `close`, `fstat` (`{ kind, path, mode, uid, gid, size, ino, mtimeMs }`), `fchmod`, and `tryLock(fd, exclusive)` / `unlock(fd)`: advisory locks held by the kernel across processes and threads, keyed by path, released on unlock, on closing the descriptor and at exit (`tryLock` returns 0, or 6 when another process holds the lock);
|
|
277
277
|
- `syscall(req)`: a kernel syscall, synchronously;
|
|
278
|
+
- `net.connect({ host, port, nonblock })` connects a TCP socket to the kernel's loopback and returns its kernel descriptor. `host` is `localhost` or `127.x.y.z`; anything else, or a port outside 1–65535, is `EINVAL`. A refused connection throws `ECONNREFUSED`. The connection carries the program's own identity, as any connect from its process does, so a [page facade](#page-facades) such as `SLICC_LINK_URL` sees the program's stamp. With `nonblock` it returns at once, and `net.ready(fd)` answers 0 once a connect is complete, `EINPROGRESS` (26) while it isn't, or the connect's WASI errno (`SO_ERROR`). On the loopback a connect completes at once, so `ready` is 0 straight away. `ready` is meant for a descriptor from `net.connect`. The descriptor is read and written with `fd-read` and `fd-write` and closed with `fd-close`, as below, and `fd-select` (`{ op: 'fd-select', read: [fds], write: [fds], timeoutMs }`, answering `{ read, write }` with the ready ones; `timeoutMs: -1` waits without a limit) waits until it's readable or writable;
|
|
278
279
|
- `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`;
|
|
279
280
|
- `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;
|
|
280
281
|
- `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;
|
|
@@ -302,6 +303,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
302
303
|
- `signals`: `on(signal, handler)` catches a signal (by number or as `'SIGINT'`), `ignore(signal)` ignores it and `reset(signal)` restores its default action. A call blocked in a read or write is restarted after a handler runs; signals without a handler keep their default actions, so an uncaught `SIGINT` ends the program with 130 and `SIGKILL` always does;
|
|
303
304
|
- `sync`: `read`, `write`, `close`, `open` (whose files' methods are synchronous too) and `fs`, made synchronously, for code that cannot `await`, such as a compiled program's file system or a `readFileSync`. A synchronous call holds the worker until the kernel answers, so nothing else of the program runs meanwhile, timers and callbacks included; a signal that arrives during a blocked read or write runs its handler, and the call is then restarted. Descriptors are shared with the rest of the context, so a file opened with `sync.open` can be closed with `close`, and the other way around; `sync.run` and `sync.exit` are described next;
|
|
304
305
|
- `sync.cred()` returns the process's credentials, `{ ruid, euid, suid, rgid, egid, sgid, groups }`, and `sync.setcred(change)` changes them, in the shape of the Emscripten call (see [Users](#users)): each field is optional, `-1` keeps an id, and `groups` sets the group list. They are synchronous, so a runtime can build Node's `process.setuid`, `seteuid`, `setgid`, `setegid`, `setgroups` and `initgroups` on them, and follow the same rules as every other program: root may set anything, anyone else only its own real, effective and saved ids (`EPERM` otherwise), and only root may set the groups; a bad value is `EINVAL`. The new ids count at once for the program's own files and for every child it starts, with any `users` mode (with `'off'`, only the ids change);
|
|
306
|
+
- `umask(mask?)` (and `sync.umask(mask?)`) returns the process's umask and, given a mask from `0` to `0o777`, sets it (anything else is `EINVAL`). It's the kernel's umask, so `/proc/self/status` shows it and a child inherits it. The kernel applies it when the program creates: `fs.writeFile(path, data, { mode })`, `fs.mkdir(path, { mode })` and `open(path, { create, mode })` (and their `sync` forms) create with `mode` less the umask, `0o666` for a file and `0o777` for a directory without one, as `open(2)` and `mkdir(2)` do, with `users` off too. A `mode` outside `0` to `0o7777` is `EINVAL`;
|
|
305
307
|
- `sync.run({ argv, argv0, env, cwd, pgid, session, input, stdin, stdout, stderr, maxBuffer, timeout, killSignal })` runs a command to its end and answers `{ pid, status, signal, stdout, stderr, error }`, as Node's `spawnSync` does. `pgid` and `session` work as for `spawn` below; a group that doesn't exist or is another session's answers `error: { code: 'ESRCH' }` or `'EPERM'`, and a bad option throws `EINVAL`. It suits `execSync`, `spawnSync` and `execFileSync`. `argv0` works as for `spawn` below. The kernel does the whole run while the program's worker waits; other processes go on.
|
|
306
308
|
- Each stdio is `'pipe'` (the default), `'ignore'`, `'inherit'` (the program's own, a terminal included), or one of the program's descriptors by number, which the child gets as it is (two slots on one descriptor share it, so their writes keep their order). `stderr: 'stdout'` sends stderr to whatever stdout is, the stdout capture included, in the order the child writes. With `'pipe'`, stdin gets `input` (bytes, or a string as UTF-8) and then its end, so `cat` stops; `stdout` and `stderr` come back as bytes and are absent for the other choices. Both are drained while the child runs, so a child that writes more than a pipe holds never stalls. As in Node, the call returns once the child has exited and every captured stream has reached its end, so output a background grandchild writes later is still collected; `timeout` bounds that wait too, so, as with Node's `spawnSync`, a run blocks for as long as a background process keeps a captured stream open unless `timeout` is set; pass one for a command that may leave something in the background. Once the outcome is decided (the end of output, an overflow or a timeout), the captured streams are closed, so a writer still left on them gets `EPIPE` (and `SIGPIPE`) instead of running on.
|
|
307
309
|
- `status` is the exit status, or `null` with `signal` set when a signal ended the child. A non-zero status is not an error. A command that cannot start (it does not exist, or is not executable) answers `error: { code: 'ENOENT' }` (or the errno), `status: null` and `pid: 0` instead of throwing.
|
|
@@ -323,7 +325,7 @@ The module's default export (or its `main` export) is called with a context and
|
|
|
323
325
|
|
|
324
326
|
A program that throws, or leaves an error or a rejected promise unhandled with no listener for it, ends with status 1 and the message on stderr; one whose module cannot be loaded, or exports no function, ends with 126. A write to a pipe whose reader is gone ends the program with 141, unless it ignores or catches `SIGPIPE` (an ignore inherited from its parent counts), in which case the write fails with `EPIPE`. Errors carry a POSIX `code`. Reads and writes of pipes, terminals and sockets go through the kernel's asynchronous operations, so a program blocked reading stdin can still write and handle signals; other calls of one process are made one at a time. A JS program cannot `fork`, and threads it starts itself are not the kernel's.
|
|
325
327
|
|
|
326
|
-
The module has the trust of an Emscripten program's glue or a WASI program's imports module: it comes from a package that was installed, and it runs in the worker of its own process. The kernel withholds the worker's own ways out before it loads the module: `XMLHttpRequest`, `WebSocketStream`, `WebTransport`, `EventSource`, `importScripts`, `indexedDB`, `caches`, `cookieStore`, `Worker`, `SharedWorker`, `
|
|
328
|
+
The module has the trust of an Emscripten program's glue or a WASI program's imports module: it comes from a package that was installed, and it runs in the worker of its own process. The kernel withholds the worker's own ways out before it loads the module: `XMLHttpRequest`, `WebSocketStream`, `WebTransport`, `EventSource`, `importScripts`, `indexedDB`, `caches`, `cookieStore`, `Worker`, `SharedWorker`, `postMessage`, `close`, Chrome's old file system API (`webkitRequestFileSystem`, `webkitRequestFileSystemSync`, `webkitResolveLocalFileSystemURL` and `webkitResolveLocalFileSystemSyncURL`, which reach the origin's OPFS), and `navigator.storage`, `navigator.storageBuckets`, `navigator.locks`, `navigator.serviceWorker`, `navigator.usb`, `navigator.hid` and `navigator.serial` are `undefined`, also through their prototypes, and a worker where one of them cannot be removed refuses to run the program (126). `fetch`, `WebSocket` and `BroadcastChannel` are `undefined` too, but stay writable and configurable, so a program can install its own over `ctx.fetch` and `ctx.websocket`, or a `BroadcastChannel` within the program (as slicc-jsh does). The platform's `BroadcastChannel`, which would reach the origin's other contexts around the kernel, is gone from the scope and its prototypes before the program runs, so deleting the program's own brings back `undefined`, not it; a worker where it can't be removed refuses to run the program (126), as for the others. A program's files are the kernel's, through the context; it reaches the kernel only through its own process's system calls, as the context makes them, and no other process's. This is a guard against programs going around the kernel, not a sandbox: `import()` of a URL is a request the kernel cannot withhold. In a browser, every process worker gets the same lockdown before any program code runs, so an Emscripten program's glue and a WASI program's imports module (any user can write either) see the same globals `undefined`, and a worker where one cannot be removed ends the process with 126. Under the Node entry, glue and imports modules keep Node's globals, which Emscripten's Node glue needs, so there users are no boundary against a user's own glue ([#407](https://github.com/ai-ecoverse/slicc-kernel/issues/407)).
|
|
327
329
|
|
|
328
330
|
Under the Node entry, the kernel also withholds the host's Node from the program before its module loads ([#297](https://github.com/ai-ecoverse/slicc-kernel/issues/297)). `require`, `module`, `exports`, `__filename`, `__dirname`, `localStorage` and `sessionStorage` are `undefined` for good. `process`, `global`, `setImmediate` and `clearImmediate` are `undefined` too, but stay writable, so a program can install its own (as slicc-jsh does). `Buffer` stays Node's own, since Node's `Response`, `Request`, `Blob` and stream bodies read it when they run, and a program can replace it. Process workers share Node's memory allocator, so its `allocUnsafe` and `allocUnsafeSlow` zero-fill and its pool is off (`Buffer.poolSize` is 0): a program never sees memory another process freed. A host can also start Node with `--zero-fill-buffers`. Imports of Node's builtin modules (`node:fs`, `fs`, `node:worker_threads`, …) and of anything but a `data:` URL, `file:` included, fail with `ERR_ACCESS_DENIED`, whether static, dynamic or through `import.meta.resolve`. A process worker that exits or crashes on its own ends its process with 70. This is the same guard, not a sandbox: a host that runs code it does not trust, such as an agent's, needs its own isolation around the Node process as well.
|
|
329
331
|
|
|
@@ -578,7 +580,7 @@ An exec'd program takes over the pid of the process it replaces, as on Linux: `g
|
|
|
578
580
|
|
|
579
581
|
### File modes and the umask
|
|
580
582
|
|
|
581
|
-
Every process has a umask in the kernel, `022` for a process the embedder starts, inherited across fork, spawn and exec and shown in `/proc/<pid>/status` (`Umask:`). The kernel applies it when a program creates a file or directory, whatever its libc does: an Emscripten program's `open` with `O_CREAT`, `mkdir` and `mknod` (so `umask 077; touch f` gives `600` in a child too), and a WASI program's `path_open` with `O_CREAT` and `path_create_directory` (`0666` and `0777` less the umask). An Emscripten program's `umask()` sets and reads the kernel's umask through the glue's `__syscall_umask` import, so `umask 077` in bash shows in `/proc` and reaches every child. The kernel finds these imports by the glue's own functions (`___syscall_openat`, `___syscall_mkdirat`, `___syscall_mknodat`, `___syscall_umask`, and `___syscall_faccessat` for `access(2)`), so it also does so for glue whose imports are minified to short names, as wasm-curl's and wasm-clang's are; a glue that has one of those functions but doesn't import it under any name is refused. A build that resolves `umask` inside its wasm, as wasm-bash 5.3.0-8 to 5.3.0-10 do, keeps a umask of its own that the kernel never sees.
|
|
583
|
+
Every process has a umask in the kernel, `022` for a process the embedder starts, inherited across fork, spawn and exec and shown in `/proc/<pid>/status` (`Umask:`). The kernel applies it when a program creates a file or directory, whatever its libc does: a JS program's `writeFile`, `mkdir` and `open` with `create` (see [JS programs](#js-programs)), an Emscripten program's `open` with `O_CREAT`, `mkdir` and `mknod` (so `umask 077; touch f` gives `600` in a child too), and a WASI program's `path_open` with `O_CREAT` and `path_create_directory` (`0666` and `0777` less the umask). An Emscripten program's `umask()` sets and reads the kernel's umask through the glue's `__syscall_umask` import, so `umask 077` in bash shows in `/proc` and reaches every child. The kernel finds these imports by the glue's own functions (`___syscall_openat`, `___syscall_mkdirat`, `___syscall_mknodat`, `___syscall_umask`, and `___syscall_faccessat` for `access(2)`), so it also does so for glue whose imports are minified to short names, as wasm-curl's and wasm-clang's are; a glue that has one of those functions but doesn't import it under any name is refused. A build that resolves `umask` inside its wasm, as wasm-bash 5.3.0-8 to 5.3.0-10 do, keeps a umask of its own that the kernel never sees.
|
|
582
584
|
|
|
583
585
|
WASI and WASIX have no call for file modes, so the kernel adds the import module `slicc_fs`, whose functions return a preview1 errno (kernels without them answer `ENOSYS`):
|
|
584
586
|
|
package/dist/driver-worker.js
CHANGED
|
@@ -1236,7 +1236,6 @@ var BLOCKED_GLOBALS = [
|
|
|
1236
1236
|
"cookieStore",
|
|
1237
1237
|
"Worker",
|
|
1238
1238
|
"SharedWorker",
|
|
1239
|
-
"BroadcastChannel",
|
|
1240
1239
|
"postMessage",
|
|
1241
1240
|
"close",
|
|
1242
1241
|
"webkitRequestFileSystem",
|
|
@@ -1253,7 +1252,7 @@ var BLOCKED_NAVIGATOR = [
|
|
|
1253
1252
|
"hid",
|
|
1254
1253
|
"serial"
|
|
1255
1254
|
];
|
|
1256
|
-
var OPEN_SLOTS = ["fetch", "WebSocket"];
|
|
1255
|
+
var OPEN_SLOTS = ["fetch", "WebSocket", "BroadcastChannel"];
|
|
1257
1256
|
function chain(target) {
|
|
1258
1257
|
const out = [];
|
|
1259
1258
|
for (let at = target; at && at !== Object.prototype; ) {
|
|
@@ -1284,17 +1283,20 @@ function hide(target, names) {
|
|
|
1284
1283
|
}
|
|
1285
1284
|
function vacate(target, names) {
|
|
1286
1285
|
for (const name of names) {
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1286
|
+
const holders = chain(target);
|
|
1287
|
+
for (const holder of holders) {
|
|
1288
|
+
if (Object.getOwnPropertyDescriptor(holder, name)?.configurable) {
|
|
1289
|
+
Reflect.deleteProperty(holder, name);
|
|
1290
|
+
}
|
|
1290
1291
|
}
|
|
1291
|
-
|
|
1292
|
+
const kept = holders.slice(1).some((holder) => Object.hasOwn(holder, name));
|
|
1293
|
+
const opened = Reflect.defineProperty(target, name, {
|
|
1292
1294
|
value: void 0,
|
|
1293
1295
|
writable: true,
|
|
1294
1296
|
enumerable: false,
|
|
1295
1297
|
configurable: true
|
|
1296
1298
|
});
|
|
1297
|
-
if (Reflect.get(target, name) !== void 0) {
|
|
1299
|
+
if (kept || !opened || Reflect.get(target, name) !== void 0) {
|
|
1298
1300
|
throw new Error(`slicc-kernel: cannot withhold ${name} from a JS program`);
|
|
1299
1301
|
}
|
|
1300
1302
|
}
|