@ai-ecoverse/slicc-kernel 1.88.0 → 1.88.1
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 +1 -1
- package/dist/kernel-worker.js +116 -40
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +116 -40
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -659,7 +659,7 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
|
|
|
659
659
|
- `interpreters` is the chain of `#!` and `binfmt` interpreters the kernel will really run, outermost first, each absolute with its symlinks resolved, and empty for a program of the catalog (Emscripten, WASI or JS) and the `/usr/bin/<name>` entries for one. A plain `#!/usr/bin/env X` (or `env -S X …`) is reported as `X`: a command of the catalog as `/usr/bin/X`, a script command by its file. `env` with any other option or a `NAME=value` assignment is run and reported as `/usr/bin/env` itself;
|
|
660
660
|
- `trusted` is true when the program and every interpreter are regular files owned by root and not writable by group or others, under directories that are all the same, up to `/` (for a command of the catalog, its package files). It's false unless `users` is `'enforce'`: under `'off'` files have no owners, and under `'report'` owners don't stop a write. The reply must be `{ values, placeholders }` (string maps) within 10 s; anything else is `EACCES`. Each variable of the child's environment whose value is exactly `placeholders[NAME]` gets `values[NAME]`; nothing else changes. The kernel keeps no copy and logs nothing of either. Like any environment, the values are inherited by whatever the child itself spawns or execs, as on Linux. A ticket isn't carried across `sudo`, which builds its own environment: a `sudo` run with `SLICC_SEALED_TICKET` set fails with `EACCES`, and burns the ticket.
|
|
661
661
|
|
|
662
|
-
**The child's output is scrubbed (S2).** The kernel compiles a [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub) scrubber (1.0.0, bundled; the published package has no runtime dependency) over exactly the values it swapped in, and gives the child new open file descriptions for fd 1 and fd 2, for every other descriptor it holds on the same descriptions (a `dup` of them), and for every descriptor on a terminal (fd 0 of a terminal session is one), that pass every write through it into the originals (one wrapper per original description, so `2>&1` keeps its order). A value appears as its placeholder, and a fragment or encoded form of it (JSON, percent, HTML, base64, hex, wrapped) as `[redacted:NAME]`. Bytes go through at once unless they may still be the start of a match; such a tail is let out after 50 ms without a write when it's shorter than 16 bytes, and otherwise at the next write or at close. The child's descendants inherit the scrubbed descriptions through fork, spawn, exec and `dup`, and a descendant that opens its controlling terminal (`/dev/tty`, or a terminal by name) gets one scrubbed too, as does a pseudo-terminal slave (`/dev/pts/N`) or [`/dev/clipboard`](#devclipboard) it opens, and a `/dev/clipboard` descriptor it inherited is wrapped at the redeem like fd 1 and 2, so the page's clipboard gets placeholders, never the values. Files, sockets and pipes the child or a descendant opens itself are not scrubbed, so `printf %s "$T" > f` writes the value. A scrubbed fd 1 or 2 is a stream: it doesn't seek. A descendant that redeems a ticket of its own adds its values: its output is scrubbed for both sets. A value under 8 bytes is swapped but can't be scrubbed. The scrubber goes when the last process and description holding it do.
|
|
662
|
+
**The child's output is scrubbed (S2).** The kernel compiles a [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub) scrubber (1.0.0, bundled; the published package has no runtime dependency) over exactly the values it swapped in, and gives the child new open file descriptions for fd 1 and fd 2, for every other descriptor it holds on the same descriptions (a `dup` of them), and for every descriptor on a terminal (fd 0 of a terminal session is one), that pass every write through it into the originals (one wrapper per original description, so `2>&1` keeps its order). A value appears as its placeholder, and a fragment or encoded form of it (JSON, percent, HTML, base64, hex, wrapped) as `[redacted:NAME]`. Bytes go through at once unless they may still be the start of a match; such a tail is let out after 50 ms without a write when it's shorter than 16 bytes, and otherwise at the next write or at close. The child's descendants inherit the scrubbed descriptions through fork, spawn, exec and `dup`, and a descendant that opens its controlling terminal (`/dev/tty`, or a terminal by name) gets one scrubbed too, as does a pseudo-terminal slave (`/dev/pts/N`) or [`/dev/clipboard`](#devclipboard) it opens, and a `/dev/clipboard` descriptor it inherited is wrapped at the redeem like fd 1 and 2, so the page's clipboard gets placeholders, never the values. Files, sockets and pipes the child or a descendant opens itself are not scrubbed, so `printf %s "$T" > f` writes the value. A scrubbed fd 1 or 2 is a stream: it doesn't seek. Under `O_NONBLOCK` it gives `EAGAIN` when the pipe or terminal under it would. A write that finds room returns at once, taking at most 64 KiB, and the scrubbed bytes the file couldn't take yet go out before anything written later. Until they have, `poll` reports it unwritable and a nonblocking write gets `EAGAIN`, taking nothing. A blocking write that waits for them, or for room, ends with `EINTR` at a signal, taking nothing; one interrupted after it began writing returns its whole count, and the rest follows. A descendant that redeems a ticket of its own adds its values: its output is scrubbed for both sets. A value under 8 bytes is swapped but can't be scrubbed. The scrubber goes when the last process and description holding it do.
|
|
663
663
|
|
|
664
664
|
### `/dev/clipboard`
|
|
665
665
|
|
package/dist/kernel-worker.js
CHANGED
|
@@ -4580,12 +4580,12 @@ async function interruptible(work, signal) {
|
|
|
4580
4580
|
if (signal.aborted) throw new KernelError("EINTR");
|
|
4581
4581
|
let stop = () => {
|
|
4582
4582
|
};
|
|
4583
|
-
const
|
|
4583
|
+
const interrupted3 = new Promise((_, reject) => {
|
|
4584
4584
|
stop = () => reject(new KernelError("EINTR"));
|
|
4585
4585
|
signal.addEventListener("abort", stop, { once: true });
|
|
4586
4586
|
});
|
|
4587
4587
|
try {
|
|
4588
|
-
return await Promise.race([work,
|
|
4588
|
+
return await Promise.race([work, interrupted3]);
|
|
4589
4589
|
} finally {
|
|
4590
4590
|
signal.removeEventListener("abort", stop);
|
|
4591
4591
|
}
|
|
@@ -9594,11 +9594,12 @@ var MountFs = class {
|
|
|
9594
9594
|
});
|
|
9595
9595
|
}
|
|
9596
9596
|
async chmod(path, mode) {
|
|
9597
|
-
const
|
|
9598
|
-
|
|
9599
|
-
this.
|
|
9597
|
+
const real = await this.follow(path);
|
|
9598
|
+
const found = this.at(real);
|
|
9599
|
+
if (!found) return this.base.chmod(real, mode);
|
|
9600
|
+
this.writable(found.mount, real);
|
|
9600
9601
|
if (!found.mount.caps.chmod) return;
|
|
9601
|
-
this.table.forget(found.mount,
|
|
9602
|
+
this.table.forget(found.mount, real);
|
|
9602
9603
|
await found.mount.conn.call({ op: "setattr", path: found.rel, mode });
|
|
9603
9604
|
}
|
|
9604
9605
|
async utimes(path, atime, mtime) {
|
|
@@ -10615,7 +10616,7 @@ function mountTable(mounts = []) {
|
|
|
10615
10616
|
}
|
|
10616
10617
|
|
|
10617
10618
|
// src/version.ts
|
|
10618
|
-
var KERNEL_VERSION = true ? "1.88.
|
|
10619
|
+
var KERNEL_VERSION = true ? "1.88.1" : "0.0.0-dev";
|
|
10619
10620
|
var OS_RELEASE = "6.0.0-slicc";
|
|
10620
10621
|
function procVersion() {
|
|
10621
10622
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -12182,16 +12183,16 @@ function interruption(signal) {
|
|
|
12182
12183
|
}
|
|
12183
12184
|
async function selectFds(fds, read, write, timeoutMs, interrupt) {
|
|
12184
12185
|
const deadline = timeoutMs < 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
|
|
12185
|
-
const
|
|
12186
|
-
|
|
12186
|
+
const interrupted3 = interruption(interrupt);
|
|
12187
|
+
interrupted3.promise.catch(() => {
|
|
12187
12188
|
});
|
|
12188
12189
|
try {
|
|
12189
|
-
return await waitReady(fds, read, write, deadline,
|
|
12190
|
+
return await waitReady(fds, read, write, deadline, interrupted3.promise);
|
|
12190
12191
|
} finally {
|
|
12191
|
-
|
|
12192
|
+
interrupted3.done();
|
|
12192
12193
|
}
|
|
12193
12194
|
}
|
|
12194
|
-
async function waitReady(fds, read, write, deadline,
|
|
12195
|
+
async function waitReady(fds, read, write, deadline, interrupted3) {
|
|
12195
12196
|
for (; ; ) {
|
|
12196
12197
|
const now = ready(fds, read, write);
|
|
12197
12198
|
if (now.read.length > 0 || now.write.length > 0 || Date.now() >= deadline) return now;
|
|
@@ -12203,7 +12204,7 @@ async function waitReady(fds, read, write, deadline, interrupted2) {
|
|
|
12203
12204
|
let timer;
|
|
12204
12205
|
const expiry = deadline === Number.POSITIVE_INFINITY ? [] : [new Promise((resolve2) => timer = setTimeout(resolve2, deadline - Date.now()))];
|
|
12205
12206
|
try {
|
|
12206
|
-
await Promise.race([...changes, ...expiry,
|
|
12207
|
+
await Promise.race([...changes, ...expiry, interrupted3]);
|
|
12207
12208
|
} finally {
|
|
12208
12209
|
round.abort();
|
|
12209
12210
|
clearTimeout(timer);
|
|
@@ -15132,63 +15133,137 @@ function compile(secrets, options = {}) {
|
|
|
15132
15133
|
|
|
15133
15134
|
// src/kernel/scrub-file.ts
|
|
15134
15135
|
var SCRUB_IDLE_MS = 50;
|
|
15136
|
+
var SCRUB_NONBLOCK_MAX = 64 * 1024;
|
|
15135
15137
|
var encoder5 = new TextEncoder();
|
|
15136
15138
|
function scrubberOf(secrets) {
|
|
15137
15139
|
const usable = secrets.filter((s) => encoder5.encode(s.value).length >= MIN_VALUE);
|
|
15138
15140
|
return usable.length > 0 ? compile(usable, { idleMs: SCRUB_IDLE_MS }) : void 0;
|
|
15139
15141
|
}
|
|
15140
|
-
async function writeAll2(file, bytes
|
|
15142
|
+
async function writeAll2(file, bytes) {
|
|
15141
15143
|
const write = file.write;
|
|
15142
|
-
for (let at = 0; at < bytes.length; )
|
|
15143
|
-
at += await write.call(file, bytes.subarray(at), at === 0 ? signal : void 0);
|
|
15144
|
-
}
|
|
15144
|
+
for (let at = 0; at < bytes.length; ) at += await write.call(file, bytes.subarray(at));
|
|
15145
15145
|
}
|
|
15146
15146
|
function bound(file, key) {
|
|
15147
15147
|
const method = file[key];
|
|
15148
15148
|
return typeof method === "function" ? { [key]: method.bind(file) } : {};
|
|
15149
15149
|
}
|
|
15150
|
+
function interrupted2(err) {
|
|
15151
|
+
const code = err?.code;
|
|
15152
|
+
return code === "EINTR" || code === "EAGAIN";
|
|
15153
|
+
}
|
|
15150
15154
|
function scrubbed(inner, scrubber, idleMs = SCRUB_IDLE_MS) {
|
|
15151
15155
|
const file = inner.file;
|
|
15152
15156
|
const stream = scrubber.stream();
|
|
15153
|
-
let
|
|
15157
|
+
let tail = Promise.resolve();
|
|
15158
|
+
let pending = 0;
|
|
15159
|
+
let busy = false;
|
|
15154
15160
|
let timer;
|
|
15155
15161
|
let failed3;
|
|
15162
|
+
const waiters = /* @__PURE__ */ new Set();
|
|
15163
|
+
const quiet = () => !busy && pending === 0;
|
|
15164
|
+
const wake = () => {
|
|
15165
|
+
for (const waiter of [...waiters]) waiter();
|
|
15166
|
+
};
|
|
15156
15167
|
const raise = () => {
|
|
15157
15168
|
if (failed3 === void 0) return;
|
|
15158
15169
|
const err = failed3;
|
|
15159
15170
|
failed3 = void 0;
|
|
15160
15171
|
throw err;
|
|
15161
15172
|
};
|
|
15162
|
-
const
|
|
15163
|
-
|
|
15164
|
-
|
|
15165
|
-
|
|
15173
|
+
const later = (bytes) => {
|
|
15174
|
+
if (bytes.length === 0) return;
|
|
15175
|
+
pending += 1;
|
|
15176
|
+
tail = tail.then(() => writeAll2(file, bytes)).catch((err) => {
|
|
15177
|
+
if (failed3 === void 0) failed3 = err;
|
|
15178
|
+
}).finally(() => {
|
|
15179
|
+
pending -= 1;
|
|
15180
|
+
if (quiet()) wake();
|
|
15181
|
+
});
|
|
15182
|
+
};
|
|
15183
|
+
const settling = (signal) => new Promise((resolve2, reject) => {
|
|
15184
|
+
const done4 = () => {
|
|
15185
|
+
waiters.delete(done4);
|
|
15186
|
+
signal?.removeEventListener("abort", stop);
|
|
15187
|
+
resolve2();
|
|
15188
|
+
};
|
|
15189
|
+
const stop = () => {
|
|
15190
|
+
waiters.delete(done4);
|
|
15191
|
+
reject(new KernelError("EINTR"));
|
|
15192
|
+
};
|
|
15193
|
+
if (signal?.aborted) return stop();
|
|
15194
|
+
waiters.add(done4);
|
|
15195
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
15196
|
+
});
|
|
15197
|
+
const settled = async () => {
|
|
15198
|
+
while (!quiet()) await settling();
|
|
15199
|
+
};
|
|
15200
|
+
const changed = async (signal) => {
|
|
15201
|
+
const round = new AbortController();
|
|
15202
|
+
const stop = signal ? AbortSignal.any([signal, round.signal]) : round.signal;
|
|
15203
|
+
try {
|
|
15204
|
+
const own = settling(stop);
|
|
15205
|
+
await Promise.race(file.changed ? [own, file.changed(stop)] : [own]);
|
|
15206
|
+
} catch (err) {
|
|
15207
|
+
throw signal?.aborted ? new KernelError("EINTR") : err;
|
|
15208
|
+
} finally {
|
|
15209
|
+
round.abort();
|
|
15210
|
+
}
|
|
15211
|
+
};
|
|
15212
|
+
const turn = async (signal) => {
|
|
15213
|
+
for (let waited = false; ; waited = true) {
|
|
15214
|
+
if (quiet() && (pollFile(file).writable || !file.changed)) {
|
|
15215
|
+
busy = true;
|
|
15216
|
+
return;
|
|
15217
|
+
}
|
|
15218
|
+
if (signal?.aborted) throw new KernelError(waited ? "EINTR" : "EAGAIN");
|
|
15219
|
+
await changed(signal);
|
|
15220
|
+
}
|
|
15221
|
+
};
|
|
15222
|
+
const send = async (out, signal) => {
|
|
15223
|
+
const write = file.write;
|
|
15224
|
+
for (let at = 0; at < out.length; ) {
|
|
15225
|
+
try {
|
|
15226
|
+
at += await write.call(file, out.subarray(at), signal);
|
|
15227
|
+
} catch (err) {
|
|
15228
|
+
if (!signal?.aborted || !interrupted2(err)) throw err;
|
|
15229
|
+
}
|
|
15230
|
+
if (at < out.length && signal?.aborted) return later(out.subarray(at));
|
|
15231
|
+
}
|
|
15166
15232
|
};
|
|
15167
15233
|
const release = () => {
|
|
15168
15234
|
timer = void 0;
|
|
15169
|
-
|
|
15170
|
-
failed3 = err;
|
|
15171
|
-
});
|
|
15235
|
+
later(stream.idle());
|
|
15172
15236
|
};
|
|
15173
15237
|
return new OpenFile({
|
|
15174
15238
|
...bound(file, "read"),
|
|
15175
15239
|
...bound(file, "peek"),
|
|
15176
|
-
...bound(file, "poll"),
|
|
15177
|
-
...bound(file, "changed"),
|
|
15178
15240
|
...bound(file, "stat"),
|
|
15241
|
+
poll: () => {
|
|
15242
|
+
const state = pollFile(file);
|
|
15243
|
+
return quiet() ? state : { ...state, writable: false };
|
|
15244
|
+
},
|
|
15245
|
+
changed,
|
|
15179
15246
|
...file.tty ? { tty: file.tty } : {},
|
|
15180
15247
|
...file.clipboard ? { clipboard: file.clipboard } : {},
|
|
15181
15248
|
...file.write ? {
|
|
15182
15249
|
write: async (bytes, signal) => {
|
|
15183
|
-
|
|
15184
|
-
|
|
15185
|
-
|
|
15250
|
+
await turn(signal);
|
|
15251
|
+
const taken = signal?.aborted && bytes.length > SCRUB_NONBLOCK_MAX ? bytes.subarray(0, SCRUB_NONBLOCK_MAX) : bytes;
|
|
15252
|
+
try {
|
|
15253
|
+
raise();
|
|
15254
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
15255
|
+
timer = void 0;
|
|
15256
|
+
await send(stream.push(taken), signal);
|
|
15257
|
+
} finally {
|
|
15258
|
+
busy = false;
|
|
15259
|
+
if (quiet()) wake();
|
|
15260
|
+
}
|
|
15186
15261
|
timer = setTimeout(release, idleMs);
|
|
15187
|
-
return
|
|
15262
|
+
return taken.length;
|
|
15188
15263
|
}
|
|
15189
15264
|
} : {},
|
|
15190
15265
|
flush: async () => {
|
|
15191
|
-
await
|
|
15266
|
+
await settled();
|
|
15192
15267
|
raise();
|
|
15193
15268
|
await file.flush?.();
|
|
15194
15269
|
},
|
|
@@ -15196,7 +15271,9 @@ function scrubbed(inner, scrubber, idleMs = SCRUB_IDLE_MS) {
|
|
|
15196
15271
|
if (timer !== void 0) clearTimeout(timer);
|
|
15197
15272
|
let failure2;
|
|
15198
15273
|
try {
|
|
15199
|
-
await
|
|
15274
|
+
await settled();
|
|
15275
|
+
later(stream.end());
|
|
15276
|
+
await settled();
|
|
15200
15277
|
raise();
|
|
15201
15278
|
} catch (err) {
|
|
15202
15279
|
failure2 = { err };
|
|
@@ -15372,12 +15449,12 @@ var SealedChannel = class {
|
|
|
15372
15449
|
if (signal.aborted) throw new KernelError("EINTR");
|
|
15373
15450
|
let stop = () => {
|
|
15374
15451
|
};
|
|
15375
|
-
const
|
|
15452
|
+
const interrupted3 = new Promise((_, reject) => {
|
|
15376
15453
|
stop = () => reject(new KernelError("EINTR"));
|
|
15377
15454
|
signal.addEventListener("abort", stop, { once: true });
|
|
15378
15455
|
});
|
|
15379
15456
|
try {
|
|
15380
|
-
await Promise.race([flight.done,
|
|
15457
|
+
await Promise.race([flight.done, interrupted3]);
|
|
15381
15458
|
} finally {
|
|
15382
15459
|
signal.removeEventListener("abort", stop);
|
|
15383
15460
|
}
|
|
@@ -18340,11 +18417,9 @@ var Launcher = class {
|
|
|
18340
18417
|
groupSession(pgid) {
|
|
18341
18418
|
return this.jobs.sessionOf(pgid);
|
|
18342
18419
|
}
|
|
18343
|
-
treeScrubs(
|
|
18420
|
+
treeScrubs(req) {
|
|
18344
18421
|
const inherited2 = req.ppid !== void 0 ? this.scrubs.get(req.ppid) : void 0;
|
|
18345
|
-
|
|
18346
|
-
if (scrubs) this.scrubs.set(pid, scrubs);
|
|
18347
|
-
return scrubs;
|
|
18422
|
+
return req.scrub ? [...inherited2 ?? [], req.scrub] : inherited2;
|
|
18348
18423
|
}
|
|
18349
18424
|
start(req) {
|
|
18350
18425
|
this.sampling();
|
|
@@ -18352,7 +18427,7 @@ var Launcher = class {
|
|
|
18352
18427
|
throw new SpawnError("ESRCH");
|
|
18353
18428
|
}
|
|
18354
18429
|
const pid = req.pid ?? this.nextPid++;
|
|
18355
|
-
const scrubs = this.treeScrubs(
|
|
18430
|
+
const scrubs = this.treeScrubs(req);
|
|
18356
18431
|
const stack = this.stackFor(req.tag);
|
|
18357
18432
|
const terminal = req.fds.stdioTerminal();
|
|
18358
18433
|
const decided = req.decided ?? Promise.resolve(true);
|
|
@@ -18422,6 +18497,7 @@ var Launcher = class {
|
|
|
18422
18497
|
...this.shownIds(req)
|
|
18423
18498
|
});
|
|
18424
18499
|
this.processes.set(pid, handle);
|
|
18500
|
+
if (scrubs) this.scrubs.set(pid, scrubs);
|
|
18425
18501
|
const { cred } = req;
|
|
18426
18502
|
this.creds.set(pid, () => handle.cred?.() ?? copyCred(cred));
|
|
18427
18503
|
if (req.tag !== void 0) this.tags.set(pid, req.tag);
|