feat(net): net.connect outbound TCP client (id 110)

- new builtin net.connect(host, port) -> Int: the outbound-socket gap
  language 38 named and jarvis surfaced; the client half of the net verbs
- getaddrinfo for DNS (v4/v6, numeric or hostname), blocking connect with
  the same EINTR/stop handling as net.connect_unix, then O_NONBLOCK for the
  park plane; returns the same fd-scalar accept yields
- wob.h enum + WO_B_MAX 110; types.ml registration; loader.c arity;
  builtin.c sysio dispatch range extended to WO_B_NET_CONNECT; sysio.c impl
- verified: numeric IP + hostname (DNS) connect to a local listener, closed
  port traps cleanly; ASan-clean; runtime battery 0 fail
- deferred (next slice): net.connect_dl deadline/park variant (no shard
  stall during handshake), on the accept_dl pattern

(cherry picked from commit 13c6f124428243b4956fbb4eceb1e7d0206d45f2)
This commit is contained in:
shoney.arickathil 2026-09-07 18:57:50 +02:00
parent b932e0cb87
commit e91a3704fe
5 changed files with 63 additions and 3 deletions

View file

@ -364,6 +364,7 @@ let stdlib_members : stdlib_member list =
m "net" "send_fd" 2 105 (Some (TScalar "Bool")) None; m "net" "send_fd" 2 105 (Some (TScalar "Bool")) None;
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;
(* 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,7 @@ 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_TERM_WIDTH)) || (C >= WO_B_NET_READ_DL && C <= WO_B_NET_CONNECT))
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)
return wo_builtin_crypto(vm, R, ins, msg); return wo_builtin_crypto(vm, R, ins, msg);

View file

@ -78,7 +78,7 @@ static const uint8_t b_arity[WO_B_MAX + 1] = {
[WO_B_PROC_SPAWN_PTY] = 5, [WO_B_PROC_RESIZE] = 3, [WO_B_PROC_SPAWN_PTY] = 5, [WO_B_PROC_RESIZE] = 3,
[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_TERM_SIZE] = 2, [WO_B_TERM_WIDTH] = 1, [WO_B_NET_CONNECT] = 2,
/* 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
id to build */ id to build */
[WO_B_JSON_ENCODE] = 2, [WO_B_JSON_DECODE] = 2, [WO_B_MAP_GET_OPT] = 2, [WO_B_JSON_ENCODE] = 2, [WO_B_JSON_DECODE] = 2, [WO_B_MAP_GET_OPT] = 2,

View file

@ -25,6 +25,7 @@
#include <sys/syscall.h> #include <sys/syscall.h>
#include <termios.h> #include <termios.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <signal.h> #include <signal.h>
#include <stdio.h> #include <stdio.h>
@ -1552,6 +1553,59 @@ 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: { /* (host, port) -> Int: outbound TCP client fd.
* DNS via getaddrinfo (v4 or v6), blocking
* connect with the same EINTR/stop handling as
* WO_B_NET_CONNECT_UNIX, then nonblocking for the
* plane. A _dl deadline/park variant is the next
* slice; this one can stall the shard during the
* handshake, tolerable while connect is rare. */
if (cstr_of(R[B], path, sizeof path, 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;
}
char portstr[8];
snprintf(portstr, sizeof portstr, "%u", (unsigned)port);
struct addrinfo hints;
struct addrinfo *res = NULL;
struct addrinfo *ai;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
int grc = getaddrinfo(path, portstr, &hints, &res);
if (grc != 0) {
*msg = gai_strerror(grc);
return WO_T_IO;
}
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;
int rc;
for (;;) {
rc = connect(fd, ai->ai_addr, ai->ai_addrlen);
if (rc == 0 || errno != EINTR) break;
if (stop_pending()) {
close(fd);
freeaddrinfo(res);
return WO_SYS_STOPPED;
}
}
if (rc == 0) break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0) {
*msg = strerror(errno);
return WO_T_IO;
}
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
R[A] = (uint64_t)fd;
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

@ -547,9 +547,14 @@ enum {
WO_B_TERM_WIDTH = 109, /* (codepoint) -> Int cell width via libc WO_B_TERM_WIDTH = 109, /* (codepoint) -> Int cell width via libc
* wcwidth under C.UTF-8: -1 control, * wcwidth under C.UTF-8: -1 control,
* 0 combining, 1, or 2 */ * 0 combining, 1, or 2 */
/* ---- language 38 / jarvis: the outbound-socket client side. TCP
* connect with DNS via getaddrinfo; blocking connect then nonblocking
* for the plane, mirroring WO_B_NET_CONNECT_UNIX. A _dl deadline/park
* variant is the next slice. Underneath runtime-v2 9's outbound TLS. */
WO_B_NET_CONNECT = 110, /* (host, port) -> Int: outbound TCP client fd */
}; };
#define WO_B_MAX 109u #define WO_B_MAX 110u
/* 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