@ai-ecoverse/slicc-kernel 1.130.1 → 1.131.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -487,13 +487,13 @@ The module's default export (or its `main` export) is called with a context and
487
487
  - `AUTH PLAIN <blob>`, or the line after a bare `AUTH PLAIN`: the kernel decodes the blob, and asks the vault (`auth-plain`) only when the placeholders are in the **password field**. A placeholder in the user or authorization identity goes out as written. It substitutes there and re-encodes.
488
488
  - `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.
489
489
  - 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.
490
+ - **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. Anything else goes out as written: the startup, an MD5 (`R` 5) or SCRAM (`R` 10) answer, every later message, and a frame over 16 KiB. MD5 and SCRAM need the kernel to compute the answer from the password, so they aren't supported yet. A server whose `pg_hba` asks for them gets the placeholder, and refuses.
490
491
  - **What goes out:** the placeholders the vault answers with a value of at least 8 bytes go out as that value (`Basic` decoded, substituted and re-encoded). Everything else goes out as written:
491
492
  - the request line and the body;
492
493
  - other headers;
493
494
  - a head with placeholders in two header lines;
494
495
  - later requests on the same connection;
495
496
  - anything once the head passes 16 KiB, or once the program pauses for 2 s in the middle of it;
496
- - pg, which comes next.
497
497
  - **What's read back:** once a process has had a value substituted for a host, everything its whole process tree reads from any A2 session to that host is scrubbed: the process itself, its ancestors and every descendant (forked, spawned or exec'd, before or after the substitution; a forked child's own new session included) with [slicc-scrub](https://github.com/ai-ecoverse/slicc-scrub), as S2 does: the value comes back as its placeholder, and a fragment or an encoded form (JSON, percent, HTML, base64, hex) as `[redacted:sealed]`. The scrub stays until the whole tree has ended. A process outside the tree isn't covered. A value shorter than 8 bytes is never substituted, as it couldn't be scrubbed.
498
498
  - **The bound:** the server the value goes to is the one the vault rule names, verified against the kernel's own roots. Substitution trusts that server not to reflect the value back in a form the scrub doesn't know (encrypted under a key the program chose, say). That's the same residual as A1's.
499
499
  - **Elsewhere:** a placeholder written into any other A2 session reaches the server as written. It isn't the secret, so nothing leaks.
@@ -986,7 +986,7 @@ The kernel then asks the hook `{ v: 1, op: 'materializeConsume', ticket, stamp,
986
986
  **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.
987
987
 
988
988
  **A2 sessions (`sessionMaterialize`, [#1090](https://github.com/ai-ecoverse/slicc-kernel/issues/1090)).** For a kernel-terminated TLS session (see **Sealed values in an A2 session** under JS programs' `ctx.net`), the kernel asks the hook `{ v: 1, op: 'sessionMaterialize', caller, stamp, session, host, port, protocol, position, verified, handshakeAt, exit, placeholders, head? }` (slicc-vault 1.10.0, Sessions), once per session:
989
- - `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by, as the uplink reported it (`tailscale` or `device:<id>`, the id of `[A-Za-z0-9._-]`);
989
+ - `stamp` (also `caller`) is the stamp of the process that owns the socket; `session` the kernel's id for it (16 to 128 of `A-Za-z0-9_-`); `host` the name the leaf was verified against, lower-cased without a trailing dot; `handshakeAt` when the handshake finished (epoch ms); `exit` the exit the session leaves by, as the uplink reported it (`tailscale` or `device:<id>`, the id of `[A-Za-z0-9._:-]`);
990
990
  - `protocol` is `http1`, `smtp` or `pg`, and `position` one of its own: `header:<lower-cased name>`, `auth-plain` or `auth-login`, or `password-cleartext`; `head` (`{ method, path }`) is there for `http1` only, and required there;
991
991
  - `placeholders` are the 1 to 16 distinct `fnox_proxy_<24 hex>` placeholders found at that position.
992
992
 
@@ -7022,7 +7022,7 @@ function satisfies(version, range2) {
7022
7022
  }
7023
7023
 
7024
7024
  // src/version.ts
7025
- var KERNEL_VERSION = true ? "1.130.1" : "0.0.0-dev";
7025
+ var KERNEL_VERSION = true ? "1.131.0" : "0.0.0-dev";
7026
7026
  var OS_RELEASE = "6.0.0-slicc";
7027
7027
  function procVersion() {
7028
7028
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -17746,7 +17746,7 @@ var TOKEN5 = "[!#$%&'*+.^_`|~0-9a-z-]+";
17746
17746
  var HEADER = new RegExp(`^header:${TOKEN5}$`);
17747
17747
  var METHOD = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
17748
17748
  var TARGET = /^[\x21-\x7e]+$/;
17749
- var SESSION_EXIT = /^(tailscale|device:[A-Za-z0-9._-]+)$/;
17749
+ var SESSION_EXIT = /^(tailscale|device:[A-Za-z0-9._:-]+)$/;
17750
17750
  var HOST = /^[a-z0-9.:-]+$/;
17751
17751
  var UNSAFE = /[\r\n\0]/;
17752
17752
  var POSITIONS = {
@@ -18495,6 +18495,90 @@ async function substituteHead(head, ask) {
18495
18495
  `, values };
18496
18496
  }
18497
18497
 
18498
+ // src/kernel/net/sealed-pg.ts
18499
+ var PG_MESSAGE_MAX = 16 * 1024;
18500
+ var PROTOCOL_3 = 196608;
18501
+ var CLEARTEXT = 3;
18502
+ function int32(bytes, at) {
18503
+ return bytes[at] << 24 | bytes[at + 1] << 16 | bytes[at + 2] << 8 | bytes[at + 3];
18504
+ }
18505
+ function sniffPg(start2) {
18506
+ const bytes = Uint8Array.from(start2, (c) => c.charCodeAt(0));
18507
+ if (bytes.length === 0 || bytes[0] !== 0) return bytes.length === 0 ? "more" : "no";
18508
+ if (bytes.length < 8) return "more";
18509
+ const length2 = int32(bytes, 0);
18510
+ return length2 >= 8 && length2 <= PG_MESSAGE_MAX && int32(bytes, 4) === PROTOCOL_3 ? "yes" : "no";
18511
+ }
18512
+ var PgServer = class {
18513
+ buffer = new Uint8Array(0);
18514
+ skip = 0;
18515
+ lost = false;
18516
+ cleartext = false;
18517
+ feed(bytes) {
18518
+ if (this.lost) return;
18519
+ const data = new Uint8Array(this.buffer.length + bytes.length);
18520
+ data.set(this.buffer);
18521
+ data.set(bytes, this.buffer.length);
18522
+ let at = 0;
18523
+ for (; ; ) {
18524
+ const left = data.length - at;
18525
+ if (this.skip > 0) {
18526
+ const n = Math.min(this.skip, left);
18527
+ this.skip -= n;
18528
+ at += n;
18529
+ if (this.skip > 0) break;
18530
+ continue;
18531
+ }
18532
+ if (left < 5) break;
18533
+ const type = data[at];
18534
+ const whole = 1 + int32(data, at + 1);
18535
+ if (whole < 5 || type === 82 && whole > PG_MESSAGE_MAX) {
18536
+ this.lost = true;
18537
+ this.cleartext = false;
18538
+ this.buffer = new Uint8Array(0);
18539
+ return;
18540
+ }
18541
+ if (type !== 82) {
18542
+ this.skip = whole;
18543
+ continue;
18544
+ }
18545
+ if (left < whole) break;
18546
+ this.cleartext = whole === 9 && int32(data, at + 5) === CLEARTEXT;
18547
+ at += whole;
18548
+ }
18549
+ this.buffer = data.slice(at);
18550
+ }
18551
+ };
18552
+ function frame3(type, body) {
18553
+ const out = new Uint8Array(5 + body.length);
18554
+ out[0] = type;
18555
+ const length2 = 4 + body.length;
18556
+ out.set([length2 >>> 24 & 255, length2 >>> 16 & 255, length2 >>> 8 & 255, length2 & 255], 1);
18557
+ for (let i = 0; i < body.length; i++) out[5 + i] = body.charCodeAt(i);
18558
+ return out;
18559
+ }
18560
+ function pgLength(bytes, started) {
18561
+ const header3 = started ? 5 : 4;
18562
+ if (bytes.length < header3) return void 0;
18563
+ const length2 = int32(bytes, started ? 1 : 0);
18564
+ return started ? 1 + length2 : length2;
18565
+ }
18566
+ async function pgPassword(message3, server, ask) {
18567
+ if (message3[0] !== 112 || !server.cleartext) return { bytes: message3, done: true };
18568
+ server.cleartext = false;
18569
+ let body = "";
18570
+ for (const b of message3.subarray(5)) body += String.fromCharCode(b);
18571
+ if (!body.endsWith("\0") || body.indexOf("\0") !== body.length - 1)
18572
+ return { bytes: message3, done: true };
18573
+ const password = body.slice(0, -1);
18574
+ const placeholders = placeholdersIn(password);
18575
+ if (placeholders.length === 0 || placeholders.length > SESSION_PLACEHOLDERS)
18576
+ return { bytes: message3, done: true };
18577
+ const values = usable(await ask(placeholders));
18578
+ if (Object.keys(values).length === 0) return { bytes: message3, done: true };
18579
+ return { bytes: frame3(112, `${replaced(password, values)}\0`), values, done: true };
18580
+ }
18581
+
18498
18582
  // src/kernel/net/sealed-smtp.ts
18499
18583
  var SMTP_LINE_MAX = 4096;
18500
18584
  var GREETING = ["EHLO ", "HELO "];
@@ -18616,11 +18700,14 @@ function headEnd2(bytes) {
18616
18700
  return -1;
18617
18701
  }
18618
18702
  function modeOf(start2) {
18619
- const http = sniffHttp1(start2);
18620
- const smtp = sniffSmtp(start2);
18621
- if (http === "yes") return "http1";
18622
- if (smtp === "yes") return "smtp";
18623
- return http === "no" && smtp === "no" ? "pass" : void 0;
18703
+ const sniffs = [
18704
+ ["http1", sniffHttp1(start2)],
18705
+ ["smtp", sniffSmtp(start2)],
18706
+ ["pg", sniffPg(start2)]
18707
+ ];
18708
+ const hit = sniffs.find(([, v]) => v === "yes");
18709
+ if (hit) return hit[0];
18710
+ return sniffs.every(([, v]) => v === "no") ? "pass" : void 0;
18624
18711
  }
18625
18712
  function crlf(bytes) {
18626
18713
  for (let i = 0; i + 1 < bytes.length; i++) if (bytes[i] === 13 && bytes[i + 1] === 10) return i;
@@ -18634,12 +18721,15 @@ var SessionWriter = class {
18634
18721
  failed;
18635
18722
  session = sessionId();
18636
18723
  smtp;
18724
+ pgStarted = false;
18637
18725
  conn;
18638
18726
  s;
18727
+ pg;
18639
18728
  scope;
18640
- constructor(conn, s, scope) {
18729
+ constructor(conn, s, pg, scope) {
18641
18730
  this.conn = conn;
18642
18731
  this.s = s;
18732
+ this.pg = pg;
18643
18733
  this.scope = scope;
18644
18734
  this.smtp = new SmtpClient(
18645
18735
  (position, placeholders) => this.ask("smtp", position, placeholders)
@@ -18688,6 +18778,7 @@ var SessionWriter = class {
18688
18778
  if (this.mode === "pass") await this.release();
18689
18779
  else if (this.mode === "http1") await this.http1();
18690
18780
  else if (this.mode === "smtp") await this.lines();
18781
+ else if (this.mode === "pg") await this.messages();
18691
18782
  if (this.mode !== "pass" && this.buffer.length > 0)
18692
18783
  this.timer = setTimeout(() => this.idle(), HEAD_IDLE_MS);
18693
18784
  return bytes.length;
@@ -18718,6 +18809,33 @@ var SessionWriter = class {
18718
18809
  }
18719
18810
  if (this.buffer.length > SMTP_LINE_MAX) await this.release();
18720
18811
  }
18812
+ async messages() {
18813
+ for (; ; ) {
18814
+ const whole = pgLength(this.buffer, this.pgStarted);
18815
+ if (whole === void 0 || whole < 4 || whole > PG_MESSAGE_MAX) {
18816
+ if (whole !== void 0) await this.release();
18817
+ return;
18818
+ }
18819
+ if (this.buffer.length < whole) return;
18820
+ const message3 = this.buffer.slice(0, whole);
18821
+ this.buffer = this.buffer.slice(whole);
18822
+ const started = this.pgStarted;
18823
+ this.pgStarted = true;
18824
+ const out = started ? await pgPassword(
18825
+ message3,
18826
+ this.pg,
18827
+ (placeholders) => this.ask("pg", "password-cleartext", placeholders)
18828
+ ).catch(() => ({ bytes: message3, done: true })) : { bytes: message3, done: false };
18829
+ if ("values" in out && out.values) this.remember(out.values);
18830
+ try {
18831
+ await writeAll4(this.conn, out.bytes);
18832
+ } finally {
18833
+ out.bytes.fill(0);
18834
+ message3.fill(0);
18835
+ }
18836
+ if (out.done) return this.release();
18837
+ }
18838
+ }
18721
18839
  idle() {
18722
18840
  this.chain = this.chain.then(() => this.release()).catch((err) => {
18723
18841
  this.failed ??= err;
@@ -18831,9 +18949,18 @@ function idle() {
18831
18949
  return new Promise((resolve2) => setTimeout(() => resolve2(IDLE2), SCRUB_IDLE_MS));
18832
18950
  }
18833
18951
  function sealedConn(conn, s) {
18952
+ const pg = new PgServer();
18834
18953
  const scope = s.broker.sessions.scope(s.pid, s.host);
18835
- const writer = substitutes(s) ? new SessionWriter(conn, s, scope) : void 0;
18836
- const reader = new ScrubReader(conn, scope);
18954
+ const writer = substitutes(s) ? new SessionWriter(conn, s, pg, scope) : void 0;
18955
+ const observed = {
18956
+ ...conn,
18957
+ read: async () => {
18958
+ const bytes = await conn.read();
18959
+ if (bytes) pg.feed(bytes);
18960
+ return bytes;
18961
+ }
18962
+ };
18963
+ const reader = new ScrubReader(writer ? observed : conn, scope);
18837
18964
  return {
18838
18965
  localAddr: conn.localAddr,
18839
18966
  remoteAddr: conn.remoteAddr,
@@ -24091,6 +24218,7 @@ var SETUP_OPS = /* @__PURE__ */ new Set([
24091
24218
  "proc-identity",
24092
24219
  "fd-info",
24093
24220
  "sig-mask",
24221
+ "sig-inherited",
24094
24222
  "proc-setpgid",
24095
24223
  "tty-pgrp-set",
24096
24224
  "proc-exec",