feat(tls): net.connect_tls/read_tls/write_tls builtins (rv2 9 F3c-net)

The outbound TLS 1.3 client wired into the VM (ids 115-117, WO_B_MAX->117):

- net.connect_tls(host,port)->Int: DNS + non-blocking connect+poll bounded
  by WO_TLS_HANDSHAKE_MS (decision 5), then a blocking, SO_*TIMEO-bounded
  hand-rolled handshake over the sans-io driver, then wo_tls_verify_chain
  (chain + host + validity + basicConstraints/EKU) against the shard's
  lazily-loaded read-only CA bundle (decision 4). Any failure traps WO_T_IO
  loudly (decision 3). Returns the fd.
- net.read_tls / net.write_tls: application data over the parked data plane
  (decision 1) — O_NONBLOCK + park on POLLIN/POLLOUT like net.read/write,
  with record reassembly + leftover-plaintext + in-flight-record buffers in
  the per-fd slot so a park/retry never re-seals or loses progress.
- per-shard wo_tls_conn slot table keyed by fd, no locks (one thread per
  shard, the wo_child pattern; decision 2); net.close frees the slot;
  wo_vm_destroy reaps all slots + the CA bundle. getrandom ephemeral.
- driver keeps the whole Certificate message + wo_tls_client_chain() so the
  trust walk sees the full chain, not just the leaf.
- wiring: wob.h, loader.c arities, builtin.c dispatch (second net range),
  types.ml (net.connect_tls/read_tls/write_tls), sysio.c impl.

Builds; full runtime suite 0 fail; woc builds. Live behaviour is the
Phase-4 gate (next commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 9a922b3245eaa9134efb60bfd46521952ed12a39)
This commit is contained in:
shoney.arickathil 2026-09-09 02:52:27 +02:00
parent 040ec9c189
commit 6d6c810695
9 changed files with 399 additions and 2 deletions

View file

@ -365,6 +365,9 @@ let stdlib_members : stdlib_member list =
m "net" "recv_fd" 1 106 (Some (TNullable (TScalar "Int"))) None; m "net" "recv_fd" 1 106 (Some (TNullable (TScalar "Int"))) None;
m "net" "connect_unix" 1 107 (Some (TScalar "Int")) None; m "net" "connect_unix" 1 107 (Some (TScalar "Int")) None;
m "net" "connect" 2 110 (Some (TScalar "Int")) None; m "net" "connect" 2 110 (Some (TScalar "Int")) None;
m "net" "connect_tls" 2 115 (Some (TScalar "Int")) None;
m "net" "read_tls" 2 116 (Some (TScalar "Text")) None;
m "net" "write_tls" 2 117 None None;
(* runtime-v2 6: resize's read twin (nil = not a tty), and a (* runtime-v2 6: resize's read twin (nil = not a tty), and a
codepoint's terminal cell width (libc wcwidth under C.UTF-8) *) codepoint's terminal cell width (libc wcwidth under C.UTF-8) *)
m "term" "size" 1 108 (Some (TNullable (TScalar termsize_record_name))) (Some termsize_record_name); m "term" "size" 1 108 (Some (TNullable (TScalar termsize_record_name))) (Some termsize_record_name);

View file

@ -172,7 +172,8 @@ int wo_builtin(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
if (C == WO_B_JSON_ENCODE || C == WO_B_JSON_DECODE) if (C == WO_B_JSON_ENCODE || C == WO_B_JSON_DECODE)
return wo_builtin_json(vm, R, ins, msg); return wo_builtin_json(vm, R, ins, msg);
if ((C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) || C == WO_B_TIME_TICKS if ((C >= WO_B_SYS_FIRST && C <= WO_B_PROC_RUN) || C == WO_B_TIME_TICKS
|| (C >= WO_B_NET_READ_DL && C <= WO_B_NET_CONNECT)) || (C >= WO_B_NET_READ_DL && C <= WO_B_NET_CONNECT)
|| (C >= WO_B_NET_CONNECT_TLS && C <= WO_B_NET_WRITE_TLS))
return wo_builtin_sys(vm, R, ins, msg); return wo_builtin_sys(vm, R, ins, msg);
if ((C >= WO_B_SHA1 && C <= WO_B_HMAC_SHA256) if ((C >= WO_B_SHA1 && C <= WO_B_HMAC_SHA256)
|| (C >= WO_B_CHACHA20POLY1305_SEAL && C <= WO_B_AES_GCM_OPEN)) || (C >= WO_B_CHACHA20POLY1305_SEAL && C <= WO_B_AES_GCM_OPEN))

View file

@ -79,6 +79,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
[WO_B_SIGNAL_ON] = 3, [WO_B_TERM_RAW] = 1, [WO_B_TERM_RESTORE] = 1, [WO_B_SIGNAL_ON] = 3, [WO_B_TERM_RAW] = 1, [WO_B_TERM_RESTORE] = 1,
[WO_B_NET_SEND_FD] = 2, [WO_B_NET_RECV_FD] = 1, [WO_B_NET_CONNECT_UNIX] = 1, [WO_B_NET_SEND_FD] = 2, [WO_B_NET_RECV_FD] = 1, [WO_B_NET_CONNECT_UNIX] = 1,
[WO_B_TERM_SIZE] = 2, [WO_B_TERM_WIDTH] = 1, [WO_B_NET_CONNECT] = 2, [WO_B_TERM_SIZE] = 2, [WO_B_TERM_WIDTH] = 1, [WO_B_NET_CONNECT] = 2,
[WO_B_NET_CONNECT_TLS] = 2, [WO_B_NET_READ_TLS] = 2, [WO_B_NET_WRITE_TLS] = 2,
[WO_B_CHACHA20POLY1305_SEAL] = 4, [WO_B_CHACHA20POLY1305_OPEN] = 4, [WO_B_CHACHA20POLY1305_SEAL] = 4, [WO_B_CHACHA20POLY1305_OPEN] = 4,
[WO_B_AES_GCM_SEAL] = 4, [WO_B_AES_GCM_OPEN] = 4, [WO_B_AES_GCM_SEAL] = 4, [WO_B_AES_GCM_OPEN] = 4,
/* json (json.c): encode takes the value's static kind, decode the class /* json (json.c): encode takes the value's static kind, decode the class

View file

@ -32,6 +32,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <locale.h> #include <locale.h>
#include <sys/random.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <sys/un.h> #include <sys/un.h>
@ -42,7 +43,9 @@
#include "builtin.h" #include "builtin.h"
#include "cont.h" #include "cont.h"
#include "crypto.h"
#include "gc.h" #include "gc.h"
#include "tls.h"
/* ---- shared helpers -------------------------------------------------- */ /* ---- shared helpers -------------------------------------------------- */
@ -376,6 +379,176 @@ static int proc_drain_fd(int *fd, char **buf, size_t *len, size_t *alloc,
return 0; return 0;
} }
/* ---- runtime-v2 9 F3c-net: outbound TLS client ---------------------------
* Per-shard, one thread: no locks. A connection's state — the sans-io driver
* (tls.c) plus the socket-side record-reassembly and in-flight buffers — lives
* in a malloc'd wo_tls_conn kept in vm->tls[], keyed by fd. The handshake is
* blocking and deadline-bounded (decision 5); application read/write park the
* fiber (decision 1). The trust anchors are the shard's lazily-loaded, read-only
* CA bundle (decision 4). */
#define WO_TLS_REC_MAX (5u + 16384u + 256u) /* max TLSCiphertext on the wire */
typedef struct wo_tls_conn {
int fd;
wo_tls_client cli;
char host[256];
uint8_t rbuf[WO_TLS_REC_MAX]; size_t rbn; /* partial inbound record */
uint8_t pbuf[WO_TLS_BUF_MAX]; size_t pboff, pbn; /* decrypted, unconsumed */
uint8_t wbuf[WO_TLS_REC_MAX]; size_t wblen, wboff; /* outbound record in flight */
} wo_tls_conn;
static wo_tls_conn *tls_find(wo_vm *vm, int fd) {
for (uint32_t i = 0; i < WO_TLS_MAX; i++)
if (vm->tls[i] && vm->tls[i]->fd == fd) return vm->tls[i];
return NULL;
}
static wo_tls_conn *tls_claim(wo_vm *vm, int fd) {
for (uint32_t i = 0; i < WO_TLS_MAX; i++)
if (!vm->tls[i]) {
wo_tls_conn *c = calloc(1, sizeof *c);
if (!c) return NULL;
c->fd = fd; vm->tls[i] = c; vm->ntls++;
return c;
}
return NULL; /* full = the ceiling */
}
static void tls_free(wo_vm *vm, int fd) {
for (uint32_t i = 0; i < WO_TLS_MAX; i++)
if (vm->tls[i] && vm->tls[i]->fd == fd) {
free(vm->tls[i]); vm->tls[i] = NULL; vm->ntls--;
return;
}
}
/* Shard teardown: close every live TLS fd, free the slots and the CA bundle. */
void wo_tls_reap_all(wo_vm *vm) {
for (uint32_t i = 0; i < WO_TLS_MAX; i++)
if (vm->tls[i]) { close(vm->tls[i]->fd); free(vm->tls[i]); vm->tls[i] = NULL; }
vm->ntls = 0;
free(vm->ca_arena); vm->ca_arena = NULL;
free((void *)vm->ca_certs); vm->ca_certs = NULL;
free(vm->ca_lens); vm->ca_lens = NULL;
vm->ca_count = 0; vm->ca_loaded = 0;
}
static int tls_rand(uint8_t *buf, size_t n) {
size_t off = 0;
while (off < n) {
ssize_t r = getrandom(buf + off, n - off, 0);
if (r < 0) { if (errno == EINTR) continue; return -1; }
off += (size_t)r;
}
return 0;
}
static void tls_now14(char out[15]) {
time_t t = time(NULL);
struct tm tmv;
gmtime_r(&t, &tmv);
strftime(out, 15, "%Y%m%d%H%M%S", &tmv);
}
/* Lazily load the shard's read-only CA anchors from the system PEM bundle
* (WO_CA_BUNDLE overrides the path). 0 ok, -1 on failure (cached either way). */
static int tls_ca_ensure(wo_vm *vm) {
if (vm->ca_loaded) return vm->ca_count > 0 ? 0 : -1;
vm->ca_loaded = 1;
const char *path = getenv("WO_CA_BUNDLE");
if (!path) path = "/etc/ssl/certs/ca-certificates.crt";
FILE *f = fopen(path, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END); long sz = ftell(f); fseek(f, 0, SEEK_SET);
if (sz <= 0) { fclose(f); return -1; }
char *pem = malloc((size_t)sz);
size_t got = pem ? fread(pem, 1, (size_t)sz, f) : 0;
fclose(f);
if (!pem) return -1;
enum { MAXC = 1024 };
uint8_t *arena = malloc((size_t)sz); /* DER < PEM */
const uint8_t **certs = malloc(MAXC * sizeof *certs);
size_t *lens = malloc(MAXC * sizeof *lens);
long n = (arena && certs && lens)
? wo_tls_pem_to_ders(pem, got, arena, (size_t)sz, certs, lens, MAXC) : -1;
free(pem);
if (n <= 0) { free(arena); free((void *)certs); free(lens); return -1; }
vm->ca_arena = arena; vm->ca_certs = certs; vm->ca_lens = lens;
vm->ca_count = (size_t)n;
return 0;
}
/* Blocking whole-buffer send (handshake path; the socket carries SO_SNDTIMEO). */
static int tls_send_all(int fd, const uint8_t *b, size_t n) {
size_t off = 0;
while (off < n) {
ssize_t w = send(fd, b + off, n - off, MSG_NOSIGNAL);
if (w < 0) { if (errno == EINTR) continue; return -1; }
off += (size_t)w;
}
return 0;
}
static int tls_recv_exact(int fd, uint8_t *b, size_t n) {
size_t off = 0;
while (off < n) {
ssize_t r = recv(fd, b + off, n - off, 0);
if (r < 0) { if (errno == EINTR) continue; return -1; }
if (r == 0) return -1; /* EOF mid-handshake */
off += (size_t)r;
}
return 0;
}
/* Read one whole TLS record (blocking) into buf; returns its length or -1. */
static int tls_recv_record(int fd, uint8_t *buf, size_t cap) {
if (tls_recv_exact(fd, buf, 5) != 0) return -1;
size_t body = ((size_t)buf[3] << 8) | buf[4];
if (5 + body > cap) return -1;
if (tls_recv_exact(fd, buf + 5, body) != 0) return -1;
return (int)(5 + body);
}
/* Drive the blocking handshake on conn->fd to ESTABLISHED, then validate the
* chain against the shard anchors. 0 ok, -1 on any failure (*msg set). */
static int tls_handshake(wo_vm *vm, wo_tls_conn *conn, size_t hostlen,
const char **msg) {
uint8_t priv[32], pub[32], rnd[32], sid[32], base9[32] = { 9 };
uint8_t ch[512]; size_t chlen = 0;
if (tls_rand(priv, 32) || tls_rand(rnd, 32) || tls_rand(sid, 32)) {
*msg = "tls: getrandom failed"; return -1;
}
wo_x25519(pub, priv, base9);
if (wo_tls_build_client_hello(conn->host, hostlen, pub, rnd, sid, ch, sizeof ch, &chlen) != 0
|| wo_tls_client_start_with(&conn->cli, ch, chlen, priv) != 0) {
*msg = "tls: ClientHello build failed"; return -1;
}
wo_tls_client_set_host(&conn->cli, conn->host, hostlen);
uint8_t rec[WO_TLS_REC_MAX]; size_t rn;
rn = wo_tls_client_take_output(&conn->cli, rec, sizeof rec);
if (rn == 0 || tls_send_all(conn->fd, rec, rn) != 0) {
*msg = "tls: sending ClientHello failed"; return -1;
}
for (;;) {
int rl = tls_recv_record(conn->fd, rec, sizeof rec);
if (rl < 0) { *msg = "tls: handshake read failed or timed out"; return -1; }
wo_tls_status st = wo_tls_client_push_record(&conn->cli, rec, (size_t)rl);
if (st == WO_TLS_FAILED) { *msg = "tls: handshake verification failed"; return -1; }
rn = wo_tls_client_take_output(&conn->cli, rec, sizeof rec);
if (rn > 0 && tls_send_all(conn->fd, rec, rn) != 0) {
*msg = "tls: handshake write failed"; return -1;
}
if (st == WO_TLS_ESTABLISHED) break;
}
/* trust: full chain + host + validity + basicConstraints/EKU vs anchors */
const uint8_t *chain[16]; size_t clens[16];
size_t nchain = wo_tls_client_chain(&conn->cli, chain, clens, 16);
char now[15]; tls_now14(now);
if (nchain == 0 ||
!wo_tls_verify_chain(chain, clens, nchain, vm->ca_certs, vm->ca_lens,
vm->ca_count, conn->host, hostlen, now)) {
*msg = "tls: certificate chain not trusted"; return -1;
}
return 0;
}
int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) { int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
wo_rt *rt = &vm->rt; wo_rt *rt = &vm->rt;
uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins); uint8_t A = wo_ins_a(ins), B = wo_ins_b(ins), C = wo_ins_c(ins);
@ -727,6 +900,7 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
return 0; return 0;
} }
case WO_B_NET_CLOSE: { case WO_B_NET_CLOSE: {
tls_free(vm, (int)R[B]); /* frees the TLS slot if this was one (else no-op) */
close((int)R[B]); close((int)R[B]);
R[A] = 0; R[A] = 0;
return 0; return 0;
@ -1606,6 +1780,165 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
R[A] = (uint64_t)fd; R[A] = (uint64_t)fd;
return 0; return 0;
} }
case WO_B_NET_CONNECT_TLS: { /* (host, port) -> Int: TLS client fd */
char host[256];
if (cstr_of(R[B], host, sizeof host, msg)) return WO_T_BOUNDS;
int64_t port = (int64_t)R[B + 1];
if (port < 0 || port > 65535) { *msg = "port out of range"; return WO_T_BOUNDS; }
if (tls_ca_ensure(vm) != 0) { *msg = "tls: cannot load CA trust store"; return WO_T_IO; }
long dl_ms = 10000;
const char *denv = getenv("WO_TLS_HANDSHAKE_MS");
if (denv) { long v = atol(denv); if (v > 0) dl_ms = v; }
char portstr[8]; snprintf(portstr, sizeof portstr, "%u", (unsigned)port);
struct addrinfo hints, *res = NULL, *ai;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host, portstr, &hints, &res) != 0) { *msg = "tls: DNS failure"; return WO_T_IO; }
/* non-blocking connect + poll, bounded by the deadline (decision 5) */
int fd = -1;
for (ai = res; ai; ai = ai->ai_next) {
fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
if (fd < 0) continue;
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
int rc = connect(fd, ai->ai_addr, ai->ai_addrlen);
if (rc == 0) break;
if (errno != EINPROGRESS) { close(fd); fd = -1; continue; }
struct pollfd pfd = { fd, POLLOUT, 0 };
int pr = poll(&pfd, 1, (int)dl_ms);
if (pr == 1) {
int soe = 0; socklen_t sl = sizeof soe;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &soe, &sl) == 0 && soe == 0) break;
}
close(fd); fd = -1;
}
freeaddrinfo(res);
if (fd < 0) { *msg = "tls: connect failed"; return WO_T_IO; }
/* handshake runs blocking, each syscall bounded by SO_*TIMEO */
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
struct timeval tv = { dl_ms / 1000, (dl_ms % 1000) * 1000 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
wo_tls_conn *conn = tls_claim(vm, fd);
if (!conn) { close(fd); *msg = "tls: too many connections"; return WO_T_IO; }
size_t hl = strlen(host);
if (hl >= sizeof conn->host) hl = sizeof conn->host - 1;
memcpy(conn->host, host, hl); conn->host[hl] = 0;
if (tls_handshake(vm, conn, hl, msg) != 0) {
tls_free(vm, fd); close(fd); return WO_T_IO;
}
/* established: the data plane is non-blocking + parked (decision 1) */
struct timeval z = { 0, 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &z, sizeof z);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &z, sizeof z);
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
R[A] = (uint64_t)fd;
return 0;
}
case WO_B_NET_READ_TLS: { /* (fd, max) -> Text; empty = EOF */
wo_tls_conn *conn = tls_find(vm, (int)R[B]);
if (!conn) { *msg = "tls: not a TLS connection"; return WO_T_IO; }
int64_t max = (int64_t)R[B + 1];
if (max < 0) max = 0;
for (;;) {
/* serve leftover decrypted plaintext first */
if (conn->pbn > conn->pboff) {
size_t avail = conn->pbn - conn->pboff;
size_t give = (size_t)max < avail ? (size_t)max : avail;
wo_str *s = wo_str_new(rt, (const char *)conn->pbuf + conn->pboff, (uint32_t)give);
if (!s) { *msg = "out of memory"; return WO_T_OOM; }
conn->pboff += give;
if (conn->pboff >= conn->pbn) conn->pboff = conn->pbn = 0;
R[A] = (uint64_t)(uintptr_t)s;
return 0;
}
/* fill rbuf until a full record; park on EAGAIN */
for (;;) {
size_t need = conn->rbn < 5 ? 5
: 5 + (((size_t)conn->rbuf[3] << 8) | conn->rbuf[4]);
if (conn->rbn >= 5 && need > sizeof conn->rbuf) {
*msg = "tls: oversize record"; return WO_T_IO;
}
if (conn->rbn >= need) break; /* full record buffered */
ssize_t n = recv((int)R[B], conn->rbuf + conn->rbn,
need - conn->rbn, 0);
if (n < 0 && errno == EINTR) {
if (stop_pending()) return WO_SYS_STOPPED;
continue;
}
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
if (stop_pending()) return WO_SYS_STOPPED;
vm->cur->park_fd = (int)R[B]; vm->cur->park_deadline = 0;
vm->cur->park_events = POLLIN; vm->cur->park_done = 0;
return WO_SYS_PARKED;
}
if (n == 0) { /* EOF -> empty Text */
wo_str *e = wo_str_new(rt, "", 0);
if (!e) { *msg = "out of memory"; return WO_T_OOM; }
R[A] = (uint64_t)(uintptr_t)e; return 0;
}
if (n < 0) { *msg = strerror(errno); return WO_T_IO; }
conn->rbn += (size_t)n;
}
size_t rlen = 5 + (((size_t)conn->rbuf[3] << 8) | conn->rbuf[4]);
uint8_t ct = 0;
int dn = wo_tls_client_decrypt(&conn->cli, conn->rbuf, rlen,
conn->pbuf, sizeof conn->pbuf, &ct);
memmove(conn->rbuf, conn->rbuf + rlen, conn->rbn - rlen);
conn->rbn -= rlen;
if (dn < 0) { *msg = "tls: bad record (auth failure)"; return WO_T_IO; }
if (ct == WO_TLS_CT_ALERT) { /* close_notify etc -> EOF */
wo_str *e = wo_str_new(rt, "", 0);
if (!e) { *msg = "out of memory"; return WO_T_OOM; }
R[A] = (uint64_t)(uintptr_t)e; return 0;
}
if (ct != WO_TLS_CT_APPLICATION_DATA) /* NewSessionTicket etc */
continue; /* ignore, read the next */
conn->pbn = (size_t)dn; conn->pboff = 0; /* serve on next loop */
}
}
case WO_B_NET_WRITE_TLS: { /* (fd, text) -> 0 */
wo_tls_conn *conn = tls_find(vm, (int)R[B]);
if (!conn) { *msg = "tls: not a TLS connection"; return WO_T_IO; }
const wo_str *body = (const wo_str *)(uintptr_t)R[B + 1];
if (!body || body->h.class_id != WO_CLS_STR) {
*msg = "not a text value"; return WO_T_BOUNDS;
}
if (body->len > 16384) { *msg = "tls: write too large (max 16384/record)"; return WO_T_BOUNDS; }
/* seal once; the sealed record survives parks in conn->wbuf so a retry
* neither re-seals (which would advance the record seq twice) nor loses
* a partial write's progress. */
if (conn->wblen == 0) {
int sn = wo_tls_client_encrypt(&conn->cli, (const uint8_t *)body->data,
body->len, conn->wbuf, sizeof conn->wbuf);
if (sn < 0) { *msg = "tls: encrypt failed"; return WO_T_IO; }
conn->wblen = (size_t)sn; conn->wboff = 0;
}
while (conn->wboff < conn->wblen) {
ssize_t n = send((int)R[B], conn->wbuf + conn->wboff,
conn->wblen - conn->wboff, MSG_NOSIGNAL);
if (n < 0 && errno == EINTR) {
if (stop_pending()) return WO_SYS_STOPPED;
continue;
}
if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
if (stop_pending()) return WO_SYS_STOPPED;
vm->cur->park_fd = (int)R[B]; vm->cur->park_deadline = 0;
vm->cur->park_events = POLLOUT; vm->cur->park_done = 0;
return WO_SYS_PARKED;
}
if (n < 0) { *msg = strerror(errno); return WO_T_IO; }
conn->wboff += (size_t)n;
}
conn->wblen = 0; conn->wboff = 0;
R[A] = 0;
return 0;
}
case WO_B_NET_SEND_FD: { /* (conn, fd) -> Bool: SCM_RIGHTS, one fd */ case WO_B_NET_SEND_FD: { /* (conn, fd) -> Bool: SCM_RIGHTS, one fd */
int conn = (int)(int64_t)R[B]; int conn = (int)(int64_t)R[B];
int pass = (int)(int64_t)R[B + 1]; int pass = (int)(int64_t)R[B + 1];

View file

@ -496,6 +496,10 @@ static int on_flight_msg(wo_tls_client *c, const uint8_t *msg, size_t mlen) {
p += 3; p += 3;
if (p + clen > mlen || clen > sizeof c->leaf) return -1; if (p + clen > mlen || clen > sizeof c->leaf) return -1;
memcpy(c->leaf, msg + p, clen); c->leaflen = clen; memcpy(c->leaf, msg + p, clen); c->leaflen = clen;
if (mlen <= sizeof c->certmsg) { /* keep the whole msg for */
memcpy(c->certmsg, msg, mlen); /* the trust-chain walk */
c->certmsg_len = mlen;
}
/* hostname check (when a host was set): a leaf whose SAN does not match /* hostname check (when a host was set): a leaf whose SAN does not match
* the target host is a refused connection, not a warning. */ * the target host is a refused connection, not a warning. */
if (c->host && !wo_x509_check_host(c->leaf, c->leaflen, c->host, c->hostlen)) if (c->host && !wo_x509_check_host(c->leaf, c->leaflen, c->host, c->hostlen))
@ -739,3 +743,25 @@ long wo_tls_pem_to_ders(const char *pem, size_t pemlen, uint8_t *arena,
} }
return (long)count; return (long)count;
} }
/* Parse the stored Certificate message into leaf-first DER cert spans. */
size_t wo_tls_client_chain(const wo_tls_client *c, const uint8_t **certs,
size_t *lens, size_t max) {
if (c->certmsg_len < 8) return 0;
const uint8_t *m = c->certmsg; size_t mlen = c->certmsg_len;
size_t p = 4 + 1 + m[4]; /* skip hdr + ctx */
if (p + 3 > mlen) return 0;
p += 3; /* cert_list length */
size_t n = 0;
while (p + 3 <= mlen && n < max) {
size_t clen = ((size_t)m[p] << 16) | ((size_t)m[p + 1] << 8) | m[p + 2];
p += 3;
if (p + clen > mlen) return 0; /* malformed */
certs[n] = m + p; lens[n] = clen; n++;
p += clen;
if (p + 2 > mlen) break; /* per-entry extensions len */
size_t extl = ((size_t)m[p] << 8) | m[p + 1];
p += 2 + extl;
}
return n;
}

View file

@ -164,6 +164,7 @@ typedef struct {
uint8_t transcript[WO_TLS_BUF_MAX]; size_t tlen; uint8_t transcript[WO_TLS_BUF_MAX]; size_t tlen;
uint8_t hsbuf[WO_TLS_BUF_MAX]; size_t hsn; /* reassembled handshake bytes */ uint8_t hsbuf[WO_TLS_BUF_MAX]; size_t hsn; /* reassembled handshake bytes */
uint8_t leaf[WO_TLS_LEAF_MAX]; size_t leaflen; uint8_t leaf[WO_TLS_LEAF_MAX]; size_t leaflen;
uint8_t certmsg[WO_TLS_BUF_MAX]; size_t certmsg_len; /* whole Certificate msg */
uint16_t cv_scheme; uint16_t cv_scheme;
uint8_t out[1024]; size_t outn; /* bytes for the caller to send */ uint8_t out[1024]; size_t outn; /* bytes for the caller to send */
const char *host; size_t hostlen; /* if set, leaf SAN is enforced */ const char *host; size_t hostlen; /* if set, leaf SAN is enforced */
@ -177,6 +178,13 @@ typedef struct {
* string must outlive the handshake (not copied). */ * string must outlive the handshake (not copied). */
void wo_tls_client_set_host(wo_tls_client *c, const char *host, size_t hostlen); void wo_tls_client_set_host(wo_tls_client *c, const char *host, size_t hostlen);
/* After the handshake, parse the server's Certificate message into leaf-first
* DER cert spans (pointers into the driver's own buffer, valid for the client's
* lifetime). Returns the count (0 if none / more than max), for
* wo_tls_verify_chain. */
size_t wo_tls_client_chain(const wo_tls_client *c, const uint8_t **certs,
size_t *lens, size_t max);
/* Start a handshake from a caller-built ClientHello handshake message and a /* Start a handshake from a caller-built ClientHello handshake message and a
* fixed X25519 private key (production passes fresh randomness; the KAT injects * fixed X25519 private key (production passes fresh randomness; the KAT injects
* the RFC's). Frames the ClientHello into a plaintext record in c->out for the * the RFC's). Frames the ClientHello into a plaintext record in c->out for the

View file

@ -769,6 +769,7 @@ int wo_vm_init(wo_vm *vm, const wo_module *mod, size_t heap_cap) {
void wo_vm_destroy(wo_vm *vm) { void wo_vm_destroy(wo_vm *vm) {
wo_proc_reap_all(vm); /* iteration 42: no child outlives its shard */ wo_proc_reap_all(vm); /* iteration 42: no child outlives its shard */
wo_tls_reap_all(vm); /* runtime-v2 9: no TLS fd/bundle outlives it either */
wo_term_restore_all(vm); /* runtime-v2 4: no wrecked tty either */ wo_term_restore_all(vm); /* runtime-v2 4: no wrecked tty either */
/* iteration 35: the fiber pool dies with the vm */ /* iteration 35: the fiber pool dies with the vm */
while (vm->fib_pool) { while (vm->fib_pool) {

View file

@ -193,6 +193,10 @@ typedef struct wo_child {
struct wo_actor *owner_actor; /* NULL = the program owns it */ struct wo_actor *owner_actor; /* NULL = the program owns it */
} wo_child; } wo_child;
#define WO_PROC_MAX 32u #define WO_PROC_MAX 32u
/* runtime-v2 9 F3c-net: this shard's live TLS connections (net.connect_tls),
* capped like the child slots. sysio.c owns struct wo_tls_conn. */
#define WO_TLS_MAX 64u
struct wo_tls_conn;
/* sysio.c: kill+reap the fiber's in-flight child, if any (fib_reap), and /* sysio.c: kill+reap the fiber's in-flight child, if any (fib_reap), and
* every live child on the shard (wo_vm_destroy / engine stop). * every live child on the shard (wo_vm_destroy / engine stop).
@ -202,6 +206,9 @@ struct wo_vm;
void wo_proc_abandon(struct wo_vm *vm, wo_fiber *fb); void wo_proc_abandon(struct wo_vm *vm, wo_fiber *fb);
void wo_proc_abandon_actor(struct wo_vm *vm, struct wo_actor *a); void wo_proc_abandon_actor(struct wo_vm *vm, struct wo_actor *a);
void wo_proc_reap_all(struct wo_vm *vm); void wo_proc_reap_all(struct wo_vm *vm);
/* runtime-v2 9 F3c-net: close every live TLS connection + free the CA bundle
* on shard teardown (sysio.c owns it; wo_vm_destroy calls it). */
void wo_tls_reap_all(struct wo_vm *vm);
/* runtime-v2 3 (sysio.c): turn latched signals into Signal-record sends. /* runtime-v2 3 (sysio.c): turn latched signals into Signal-record sends.
* Cheap when nothing arrived; called from wo_io_wait and the inbox * Cheap when nothing arrived; called from wo_io_wait and the inbox
@ -301,6 +308,16 @@ typedef struct wo_vm {
* submits landed at garbage offsets and parked fibers lost their * submits landed at garbage offsets and parked fibers lost their
* wakes. Per-vm storage ends the race by construction. */ * wakes. Per-vm storage ends the race by construction. */
unsigned char io_params[256]; unsigned char io_params[256];
/* runtime-v2 9 F3c-net: live TLS connections keyed by fd, and the shard's
* lazily-loaded read-only CA trust anchors — per-shard, no locks (one
* thread), mirroring `children` above. sysio.c owns the lifecycle. */
struct wo_tls_conn *tls[WO_TLS_MAX];
uint32_t ntls;
int ca_loaded;
uint8_t *ca_arena;
const uint8_t **ca_certs;
size_t *ca_lens;
size_t ca_count;
} wo_vm; } wo_vm;
/* arc: the spawn/send builtins' runtime halves (vm.c owns the scheduler). */ /* arc: the spawn/send builtins' runtime halves (vm.c owns the scheduler). */

View file

@ -561,9 +561,16 @@ enum {
/* rv2 8 phase B: AES-GCM (AES-128/256 by key length), AES-NI hardware. */ /* rv2 8 phase B: AES-GCM (AES-128/256 by key length), AES-NI hardware. */
WO_B_AES_GCM_SEAL = 113, /* (key, nonce, aad, plaintext) -> Bytes */ WO_B_AES_GCM_SEAL = 113, /* (key, nonce, aad, plaintext) -> Bytes */
WO_B_AES_GCM_OPEN = 114, /* (key, nonce, aad, ct||tag) -> ?Bytes */ WO_B_AES_GCM_OPEN = 114, /* (key, nonce, aad, ct||tag) -> ?Bytes */
/* runtime-v2 9 F3c-net: outbound TLS 1.3 client. connect_tls dials + runs
* the handshake (blocking, deadline-bounded) and validates the chain;
* read_tls/write_tls carry application data over the parked data plane. */
WO_B_NET_CONNECT_TLS = 115, /* (host, port) -> Int: TLS client fd */
WO_B_NET_READ_TLS = 116, /* (fd, max) -> Text; empty = EOF */
WO_B_NET_WRITE_TLS = 117, /* (fd, text) -> 0 (all bytes sealed + sent) */
}; };
#define WO_B_MAX 114u #define WO_B_MAX 117u
/* ids at or above this one live in sysio.c, not builtin.c */ /* ids at or above this one live in sysio.c, not builtin.c */
#define WO_B_SYS_FIRST WO_B_FS_EXISTS #define WO_B_SYS_FIRST WO_B_FS_EXISTS