@ai-ecoverse/slicc-kernel 1.154.3 → 1.156.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
@@ -453,7 +453,7 @@ The module's default export (or its `main` export) is called with a context and
453
453
  - `wait(pid, { untraced })` resolves with `{ status }` once a child ends, or `{ status: 128 + signal, signal }` when a signal ended it, without blocking the program; with `untraced: true` it also resolves when the child stops, with `{ status: 128 + signal, signal, stopped: true }`, as `waitpid` with `WUNTRACED` (a child's `wait({ untraced })` is the same), and options that aren't an object are `EINVAL`; `kill(pid, signal)` signals any process, by number or as `'SIGTERM'` (the default);
454
454
  - `sudo(args)`, what the [`sudo`](#sudo) command calls: `args` are its arguments after `sudo`, and it resolves with `{ pid }`, the command the kernel started as the program's child, to `wait` for; `{ run }`, an argv to run as the caller (with `users: 'off'`); `{ list }`, the text of `sudo -l`; or `{ error, errno }`, the message to print;
455
455
  - `crontab({ action, user?, text? })`, what the [`crontab`](#cron) command calls: `action` is `'list'`, `'remove'` or `'install'` (with the table as `text`), on the caller's own table or, for an effective uid of 0, on `user`'s. It resolves with `{ text }` (the table for `list`, `''` otherwise) or `{ error }`, the message to print. A malformed call is `EINVAL`;
456
- - `net`: `listen({ port, host, backlog })` and `connect({ host, port, signal })` (`host` defaults to `127.0.0.1`) on the kernel's loopback, the same sockets WASIX programs use, so a JS server is reached by other processes, by the page as `<port>.kernel.localhost` (`loopbackFetch`, `dial`) and by attached clients. A listener has its `port` (as bound, so port `0` picks a free one), `accept({ signal })`, `close()` and `for await` over its connections. A connection has `local` and `remote` addresses, `readable` and `writable` web streams (closing the writable shuts down the sending side, so the peer reads the end), and `close()`. The loopback is IPv4 (`127.0.0.1` by default, or `0.0.0.0`), so an IPv6 host such as `::1` fails with `EADDRNOTAVAIL` for both `listen` and `connect`, and binding and connecting fail as they would for a WASIX program (`EADDRINUSE`, `ECONNREFUSED`, `EADDRNOTAVAIL`). `sync.net.listen` takes the same options and gives the same listener, bound and listening when it returns, so its `port` is known at once, as Node's `listen()` needs ([#1398](https://github.com/ai-ecoverse/slicc-kernel/issues/1398)); it needs the sync lane. A blocked accept, read or write waits without holding anything in the kernel, so aborting it or closing the socket leaves nothing behind. A write to a connection the peer closed fails with `EPIPE` and no `SIGPIPE`;
456
+ - `net`: `listen({ port, host, backlog })` and `connect({ host, port, signal })` (`host` defaults to `127.0.0.1`) on the kernel's loopback, the same sockets WASIX programs use, so a JS server is reached by other processes, by the page as `<port>.kernel.localhost` (`loopbackFetch`, `dial`) and by attached clients. A listener has its `port` (as bound, so port `0` picks a free one), `accept({ signal })`, `close()` and `for await` over its connections. A connection has `local` and `remote` addresses, `readable` and `writable` web streams (closing the writable shuts down the sending side, so the peer reads the end), `close()`, and `reset()`, which closes it with an RST as Node's `resetAndDestroy()` does ([#1530](https://github.com/ai-ecoverse/slicc-kernel/issues/1530)): on the kernel's loopback, the peer's pending and later reads and its writes fail with `ECONNRESET` instead of seeing the end, and bytes still in flight to it are dropped. A TLS connection (`secure()`, a `tls` connect on the loopback) resets the same way. On an uplink connection `reset()` closes at once with a FIN, as the transport can't send an RST: bytes still buffered for the peer are discarded instead of sent. The loopback is IPv4 (`127.0.0.1` by default, or `0.0.0.0`), so an IPv6 host such as `::1` fails with `EADDRNOTAVAIL` for both `listen` and `connect`, and binding and connecting fail as they would for a WASIX program (`EADDRINUSE`, `ECONNREFUSED`, `EADDRNOTAVAIL`). `sync.net.listen` takes the same options and gives the same listener, bound and listening when it returns, so its `port` is known at once, as Node's `listen()` needs ([#1398](https://github.com/ai-ecoverse/slicc-kernel/issues/1398)); it needs the sync lane. A blocked accept, read or write waits without holding anything in the kernel, so aborting it or closing the socket leaves nothing behind. A write to a connection the peer closed fails with `EPIPE` and no `SIGPIPE`;
457
457
  - `net.connect({ host, port, tls: { servername, alpn, rejectUnauthorized, ca, path, cert, key, passphrase, sni, checkHostname, keylog } })` takes one of two paths. By default it's **A1, TLS through the transport** ([#914](https://github.com/ai-ecoverse/slicc-kernel/issues/914)), described first below. For a destination the uplink routes it can also be **A2, a TLS handshake in the kernel** ([#922](https://github.com/ai-ecoverse/slicc-kernel/issues/922)), described after it. `conn.tls.path` (`'transport'` or `'kernel'`) and `conn.tls.substitutes` say which one was taken.
458
458
  - **What it does:** there's no handshake in the kernel. The connection carries plain HTTP/1.1 (and WebSocket upgrades) to the realm proxy's HTTP path for the origin `https://<servername or host>[:port]`, exactly as a `CONNECT` through `127.0.0.1:3128` does after the kernel terminates its TLS. The transport (the page's `fetch` or WebSocket, or the uplink's) makes the real TLS connection and verifies it. So `https.request` over a socket, keep-alive agents, `agent-base` and `wss://` clients work with the transport's semantics. That includes sealed placeholders in headers, which reach the transport exactly as from `ctx.fetch`.
459
459
  - **The `tls` property:** the connection's `tls` is `{ path: 'transport', substitutes: true, protocol: 'transport', alpnProtocol: 'http/1.1', authorized: true, servername }`. `authorized` means the transport verified the server. There's no peer certificate chain and no TLS version of the kernel's own, so don't read it as a real handshake's result.
@@ -501,6 +501,7 @@ The module's default export (or its `main` export) is called with a context and
501
501
  - Before dialling, `cert` without `key` (or the reverse) is `EINVAL`. A key that doesn't match the leaf is `ERR_OSSL_X509_KEY_VALUES_MISMATCH`, a wrong passphrase `ERR_OSSL_BAD_DECRYPT`, a missing one `ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED`, and a key the engine can't read `ERR_OSSL_UNSUPPORTED`.
502
502
  - `pfx` is `ENOTSUP`: pass `cert` and `key`.
503
503
  - A server that requires a certificate and gets none refuses with an alert. On TLS 1.2 that fails the connect (`ERR_SSL_HANDSHAKE_FAILURE`). On TLS 1.3 the client's side of the handshake is already done, so the next read or write fails with `ERR_SSL_FATAL_ALERT`, as Node's fails after `secureConnect`.
504
+ - **A reply that isn't TLS** ([#1548](https://github.com/ai-ecoverse/slicc-kernel/issues/1548)) is told apart by its first record's header, in OpenSSL's order, so a program gets Node's code: an SSLv2 version is `ERR_SSL_WRONG_VERSION_NUMBER`; a length over 16704 bytes `ERR_SSL_PACKET_LENGTH_TOO_LONG` (most plain-text replies, `HTTP/1.1 …` among them); a major version other than 3 `ERR_SSL_HTTP_REQUEST` for `GET ` or `PUT ` and `ERR_SSL_WRONG_VERSION_NUMBER` otherwise (`220 …`, `SSH-2.0…`, `+OK …`); a record over 16384 bytes `ERR_SSL_DATA_LENGTH_TOO_LONG`; and a handshake record that isn't a ServerHello `ERR_SSL_UNEXPECTED_MESSAGE`. Anything else stays `ERR_SSL_HANDSHAKE_FAILURE`, or `ECONNRESET` for a server that closes.
504
505
  - **A fatal alert from the server**, at the handshake or after it, is in the error: its `detail` ends in ` (alert <n>)`, and a JS program's error has `alert: n`, the alert's description number (116 certificate_required, 40 handshake_failure).
505
506
  - **`keylog: true`** ([#1504](https://github.com/ai-ecoverse/slicc-kernel/issues/1504), tls-engine 4.2.0-10) gives the session's NSS key log lines as `conn.tls.keylog`, a frozen list of strings: `CLIENT_RANDOM …` on TLS 1.2, the four `…_TRAFFIC_SECRET…` lines on TLS 1.3, the lines Node's `keylog` event and `SSLKEYLOGFILE` carry. **They are the session's secrets: anyone holding them and a capture of the traffic can read it.** So a keylogged session is never sealed (`substitutes: false`; a placeholder goes out as written), and `keylog` on the transport path (A1, where the page makes the TLS) is `ENOTSUP`. Without `keylog` the engine collects nothing. The kernel wipes its copies once the lines are in the reply (the engine zeroes its buffer as it hands them out); the lines reach the program as JS strings, which can't be wiped, so they live as long as the program keeps them, as with its own `key`. A TLS server takes `keylog` too (`listen({ tls })`, `secure(conn, { server: true })`), for every connection it serves.
506
507
  - **`sni: false`** sends no server name. The leaf is still checked against `servername` exactly as with SNI.
@@ -6097,8 +6097,8 @@ function link(a, b) {
6097
6097
  pipe.openRead();
6098
6098
  pipe.openWrite();
6099
6099
  }
6100
- a.attach({ rx: ba, tx: ab });
6101
- b.attach({ rx: ab, tx: ba });
6100
+ a.attach({ rx: ba, tx: ab, peer: b });
6101
+ b.attach({ rx: ab, tx: ba, peer: a });
6102
6102
  }
6103
6103
  var SERVE_CHUNK = 64 * 1024;
6104
6104
  var NEVER_ABORTED = new AbortController().signal;
@@ -6110,6 +6110,8 @@ var KernelSocket = class _KernelSocket {
6110
6110
  dialing;
6111
6111
  failed;
6112
6112
  reset;
6113
+ abortive = false;
6114
+ refused;
6113
6115
  local;
6114
6116
  peer;
6115
6117
  peerCred;
@@ -6146,20 +6148,25 @@ var KernelSocket = class _KernelSocket {
6146
6148
  }
6147
6149
  async read(max, signal) {
6148
6150
  const { rx } = this.connection();
6151
+ if (this.refused) throw this.refused;
6149
6152
  if (this.readShut || max <= 0) return new Uint8Array(0);
6150
6153
  const bytes = await pipeCall(() => rx.read(max, signal));
6154
+ if (this.refused) throw this.refused;
6151
6155
  if (bytes.length === 0 && this.reset) throw this.reset;
6152
6156
  return bytes;
6153
6157
  }
6154
6158
  async peek(max, signal) {
6155
6159
  const { rx } = this.connection();
6160
+ if (this.refused) throw this.refused;
6156
6161
  if (this.readShut || max <= 0) return new Uint8Array(0);
6157
6162
  const bytes = await pipeCall(() => rx.peek(max, signal));
6163
+ if (this.refused) throw this.refused;
6158
6164
  if (bytes.length === 0 && this.reset) throw this.reset;
6159
6165
  return bytes;
6160
6166
  }
6161
6167
  async write(bytes, signal) {
6162
6168
  const { tx } = this.connection();
6169
+ if (this.refused) throw this.refused;
6163
6170
  if (this.writeShut) throw new KernelError("EPIPE");
6164
6171
  if (this.reset?.code === "EPROTO") throw this.reset;
6165
6172
  return pipeCall(() => tx.write(bytes, signal));
@@ -6346,10 +6353,11 @@ var KernelSocket = class _KernelSocket {
6346
6353
  handshaking = false;
6347
6354
  signal.removeEventListener("abort", abandon);
6348
6355
  }
6349
- this.link = uplinkLink(made2.conn, (err) => {
6356
+ const plain3 = uplinkLink(made2.conn, (err) => {
6350
6357
  this.reset = err instanceof KernelError && err.code === "EPROTO" ? err : new KernelError("ECONNRESET");
6351
6358
  this.changes.wake();
6352
6359
  });
6360
+ this.link = { ...plain3, ...raw2.peer ? { peer: raw2.peer } : {} };
6353
6361
  this.changes.wake();
6354
6362
  return made2.info;
6355
6363
  }
@@ -6467,8 +6475,22 @@ var KernelSocket = class _KernelSocket {
6467
6475
  setOption(level, name2, value2) {
6468
6476
  this.options.set(`${level}:${name2}`, value2);
6469
6477
  }
6478
+ resetOnClose() {
6479
+ this.connection();
6480
+ this.abortive = true;
6481
+ }
6482
+ abort() {
6483
+ const link2 = this.link;
6484
+ if (link2.peer) link2.peer.resetBy();
6485
+ else link2.tx.closeRead();
6486
+ }
6487
+ resetBy() {
6488
+ this.refused ??= new KernelError("ECONNRESET");
6489
+ this.changes.wake();
6490
+ }
6470
6491
  close() {
6471
6492
  const was = this.state;
6493
+ if (this.abortive && was === "connected") this.abort();
6472
6494
  this.state = "closed";
6473
6495
  this.dialing?.abort(new KernelError("ECONNABORTED"));
6474
6496
  this.dialing = void 0;
@@ -8090,7 +8112,7 @@ function satisfies(version, range2) {
8090
8112
  }
8091
8113
 
8092
8114
  // src/version.ts
8093
- var KERNEL_VERSION = true ? "1.154.3" : "0.0.0-dev";
8115
+ var KERNEL_VERSION = true ? "1.156.0" : "0.0.0-dev";
8094
8116
  var OS_RELEASE = "6.0.0-slicc";
8095
8117
  function procVersion() {
8096
8118
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
@@ -19445,6 +19467,7 @@ function openSession(o) {
19445
19467
  var Ciphertext = class {
19446
19468
  queue = [];
19447
19469
  ended = false;
19470
+ opening = new Uint8Array(0);
19448
19471
  conn;
19449
19472
  session;
19450
19473
  constructor(conn, session) {
@@ -19453,11 +19476,19 @@ var Ciphertext = class {
19453
19476
  }
19454
19477
  hand(bytes) {
19455
19478
  if (bytes.length > 0) this.queue.push(bytes);
19479
+ this.note(bytes);
19480
+ }
19481
+ note(bytes) {
19482
+ if (this.opening.length >= OPENING) return;
19483
+ const next3 = new Uint8Array(Math.min(OPENING, this.opening.length + bytes.length));
19484
+ next3.set(this.opening);
19485
+ next3.set(bytes.subarray(0, next3.length - this.opening.length), this.opening.length);
19486
+ this.opening = next3;
19456
19487
  }
19457
19488
  async more() {
19458
19489
  if (this.queue.length === 0 && !this.ended) {
19459
19490
  const bytes = await this.conn.read();
19460
- if (bytes) this.queue.push(bytes);
19491
+ if (bytes) this.hand(bytes);
19461
19492
  else this.ended = true;
19462
19493
  }
19463
19494
  if (this.queue.length === 0) {
@@ -19487,11 +19518,38 @@ async function send(conn, session) {
19487
19518
  }
19488
19519
  }
19489
19520
  }
19490
- function refused2(engine, session, err) {
19521
+ var OPENING = 6;
19522
+ var PLAIN_MAX = 16384;
19523
+ var ENCRYPTED_MAX = PLAIN_MAX + 320;
19524
+ var HTTP_METHODS = ["GET ", "POST ", "HEAD ", "PUT ", "PATCH", "OPTIO", "DELET", "TRACE"];
19525
+ function headerClass(head) {
19526
+ const [type, major, minor, hi, lo] = head;
19527
+ if (major === 0 && minor === 2) return ["ERR_SSL_WRONG_VERSION_NUMBER", "wrong version number"];
19528
+ const length2 = hi << 8 | lo;
19529
+ if (length2 > ENCRYPTED_MAX) return ["ERR_SSL_PACKET_LENGTH_TOO_LONG", "packet length too long"];
19530
+ if (major !== 3) {
19531
+ const text = new TextDecoder("latin1").decode(head.subarray(0, 5));
19532
+ if (HTTP_METHODS.some((m) => text.startsWith(m)))
19533
+ return ["ERR_SSL_HTTP_REQUEST", "http request"];
19534
+ return ["ERR_SSL_WRONG_VERSION_NUMBER", "wrong version number"];
19535
+ }
19536
+ if (length2 > PLAIN_MAX) return ["ERR_SSL_DATA_LENGTH_TOO_LONG", "data length too long"];
19537
+ if (type === 22 && head.length > 5 && head[5] !== 2)
19538
+ return ["ERR_SSL_UNEXPECTED_MESSAGE", "unexpected message"];
19539
+ return void 0;
19540
+ }
19541
+ function recordRefusal(opening2) {
19542
+ if (opening2.length < 5) return void 0;
19543
+ const found = headerClass(opening2);
19544
+ return found && new KernelError("EPROTO", `${found[0]}: ${found[1]}`);
19545
+ }
19546
+ function refused2(engine, session, err, opening2) {
19491
19547
  const flags2 = session.verifyResult;
19492
19548
  const code = verifyCode(flags2);
19493
19549
  if (code && flags2 !== -1)
19494
19550
  return new KernelError("EPROTO", `${code}: ${engine.verifyInfo(flags2).trim()}`);
19551
+ const record2 = recordRefusal(opening2);
19552
+ if (record2) return record2;
19495
19553
  const text = err instanceof TlsError ? err.message : String(err);
19496
19554
  return new KernelError("EPROTO", `ERR_SSL_HANDSHAKE_FAILURE: ${text}${alertOf(session)}`);
19497
19555
  }
@@ -19506,15 +19564,12 @@ async function handshake(conn, o, session, inbox) {
19506
19564
  done5 = session.handshake();
19507
19565
  } catch (err) {
19508
19566
  await send(conn, session).catch(() => void 0);
19509
- throw refused2(o.engine, session, err);
19567
+ throw refused2(o.engine, session, err, inbox.opening);
19510
19568
  }
19511
19569
  await send(conn, session);
19512
19570
  if (done5) return;
19513
19571
  if (!await inbox.more()) {
19514
- throw new KernelError(
19515
- "ECONNRESET",
19516
- "the server closed the connection during the TLS handshake"
19517
- );
19572
+ throw recordRefusal(inbox.opening) ?? new KernelError("ECONNRESET", "the server closed the connection during the TLS handshake");
19518
19573
  }
19519
19574
  }
19520
19575
  }
@@ -23272,6 +23327,7 @@ var SOCKET_OPS = [
23272
23327
  "sock-accept",
23273
23328
  "sock-connect",
23274
23329
  "sock-shutdown",
23330
+ "sock-reset",
23275
23331
  "sock-name",
23276
23332
  "sock-getopt",
23277
23333
  "sock-setopt",
@@ -23339,6 +23395,9 @@ async function socketSyscall(req, proc) {
23339
23395
  case "sock-shutdown":
23340
23396
  anySocketAt(fds, req.fd).shutdown(req.how);
23341
23397
  return ok3();
23398
+ case "sock-reset":
23399
+ socketAt(fds, req.fd).resetOnClose();
23400
+ return ok3();
23342
23401
  case "sock-name": {
23343
23402
  const socket = anySocketAt(fds, req.fd);
23344
23403
  const addr = req.peer ? socket.peer : socket.local;