feat(rt2): spawn_pty + resize — a child that believes it owns a terminal

- posix_openpt/grantpt/unlockpt/ptsname_r (plain libc, no -lutil); child
  setsid + opens the slave as its controlling terminal, initial
  TIOCSWINSZ from the call
- Child.stdin == Child.stdout = the master (caller's copy); the slot
  keeps a private dup so resize survives the caller closing theirs
- proc.resize -> TIOCSWINSZ; refuses by name on a pipe child
- legs: test -t proves a real tty; stty size reads "24 80" then "40 120"
  after a mid-sleep resize; refusal asserted; test_proc 193/0 ASan clean

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 9836c9cd5197153517c054b243cab3b453d130a0)
This commit is contained in:
shoney.arickathil 2026-09-01 23:54:46 +02:00
parent 803ff0b790
commit 0c7d0530e9
3 changed files with 270 additions and 0 deletions

View file

@ -340,6 +340,10 @@ let stdlib_members : stdlib_member list =
m "proc" "spawn" 2 97 (Some (TNullable (TScalar child_record_name))) (Some child_record_name);
m "proc" "wait_dl" 2 98 (Some (TNullable (TScalar "Int"))) None;
m "proc" "signal" 2 99 None None;
(* runtime-v2 2: the PTY child — stdin==stdout=master, stderr -1;
resize refuses by name on a pipe child *)
m "proc" "spawn_pty" 4 100 (Some (TNullable (TScalar child_record_name))) (Some child_record_name);
m "proc" "resize" 3 101 None None;
(* json — both members are lowered specially (emit.ml): encode needs its
argument's static kind, and decode has no type until an `as` names one,
so neither goes through the generic builtin path. They are listed here

View file

@ -21,7 +21,9 @@
#include <fcntl.h>
#include <poll.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <termios.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
@ -1275,6 +1277,107 @@ int wo_builtin_sys(wo_vm *vm, uint64_t *R, uint32_t ins, const char **msg) {
fb->park_done = 0;
return WO_SYS_PARKED;
}
/* ---- runtime-v2 2: the PTY child ---------------------------------- */
case WO_B_PROC_SPAWN_PTY: { /* Child: stdin==stdout=master (raw side —
* the line discipline lives on the slave),
* stderr -1. The slot keeps a private dup
* of the master so resize survives the
* caller closing its copy. */
char argbuf[62][512];
char *argv[64];
if (proc_argv(R[B], R[B + 1], path, sizeof path, argbuf, argv, msg))
return WO_T_BOUNDS;
int cols = (int)(int64_t)R[B + 2], rows = (int)(int64_t)R[B + 3];
if (cols <= 0) cols = 80;
if (rows <= 0) rows = 24;
wo_child *ch = proc_slot_claim(vm, msg);
if (!ch) return WO_T_IO;
int master = posix_openpt(O_RDWR | O_NOCTTY);
char sname[128];
if (master < 0 || grantpt(master) != 0 || unlockpt(master) != 0 ||
ptsname_r(master, sname, sizeof sname) != 0) {
if (master >= 0) close(master);
*msg = strerror(errno);
return WO_T_IO;
}
pid_t pid = fork();
if (pid < 0) {
close(master);
*msg = strerror(errno);
return WO_T_IO;
}
if (pid == 0) {
setsid(); /* the slave becomes the CONTROLLING terminal */
int slave = open(sname, O_RDWR);
if (slave < 0) _exit(127);
struct winsize ws;
memset(&ws, 0, sizeof ws);
ws.ws_col = (unsigned short)cols;
ws.ws_row = (unsigned short)rows;
ioctl(slave, TIOCSWINSZ, &ws);
dup2(slave, STDIN_FILENO);
dup2(slave, STDOUT_FILENO);
dup2(slave, STDERR_FILENO);
close(slave);
close(master);
execvp(path, argv);
_exit(127);
}
fcntl(master, F_SETFL, fcntl(master, F_GETFL, 0) | O_NONBLOCK);
int pidfd = (int)syscall(SYS_pidfd_open, pid, 0);
if (pidfd < 0) {
int e = errno;
kill(pid, SIGKILL);
int st;
while (waitpid(pid, &st, 0) < 0 && errno == EINTR) {}
close(master);
*msg = strerror(e);
return WO_T_IO;
}
memset(ch, 0, sizeof *ch);
ch->used = 1;
ch->streaming = 1;
ch->pid = (int)pid;
ch->pidfd = pidfd;
ch->epfd = -1;
ch->ofd = ch->efd = -1;
ch->master_dup = dup(master);
ch->gen = ++vm->proc_gen;
ch->owner_actor = vm->cur->actor;
vm->nchildren++;
wo_hdr *o = record_of(vm, R[B + 4], 4, msg);
if (!o) {
close(master);
proc_slot_kill(vm, ch);
return R[B + 4] >= vm->mod->class_cnt ? WO_T_BOUNDS : WO_T_OOM;
}
uint64_t *fp = wo_fields(o);
fp[0] = ((uint64_t)ch->gen << 6) | (uint64_t)(ch - vm->children);
fp[1] = (uint64_t)master;
fp[2] = (uint64_t)master;
fp[3] = (uint64_t)(int64_t)-1;
R[A] = (uint64_t)(uintptr_t)o;
return 0;
}
case WO_B_PROC_RESIZE: { /* (id, cols, rows) -> 0: TIOCSWINSZ through
* the slot's own master dup */
wo_child *ch = proc_slot_by_id(vm, R[B], msg);
if (!ch) return WO_T_IO;
if (ch->master_dup < 0) {
*msg = "child has no terminal to resize";
return WO_T_IO;
}
struct winsize ws;
memset(&ws, 0, sizeof ws);
ws.ws_col = (unsigned short)(int64_t)R[B + 1];
ws.ws_row = (unsigned short)(int64_t)R[B + 2];
if (ioctl(ch->master_dup, TIOCSWINSZ, &ws) != 0) {
*msg = strerror(errno);
return WO_T_IO;
}
R[A] = 0;
return 0;
}
case WO_B_PROC_SIGNAL: { /* (id, sig) -> 0 through the pidfd */
wo_child *ch = proc_slot_by_id(vm, R[B], msg);
if (!ch) return WO_T_IO;

View file

@ -881,6 +881,166 @@ static void test_stream_churn(void) {
free(img);
}
/* ---- runtime-v2 2: the PTY child ----------------------------------------
* pty module: spawn_pty `sh -c <script>` at cols x rows; optionally sleep
* then resize; read once from the master; close; return the text. */
static uint8_t *pty_module(const char *script, int64_t cols, int64_t rows,
int do_resize, int64_t rcols, int64_t rrows,
size_t *len) {
wb_t *b = wb_new();
uint32_t kchild = wb_const_text(b, "Child");
uint32_t km = wb_const_text(b, "main");
uint32_t kcmd = wb_const_text(b, "sh");
uint32_t kdc = wb_const_text(b, "-c");
uint32_t kscript = wb_const_text(b, script);
uint8_t kinds[4] = {WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR};
uint32_t cls = wb_class(b, kchild, 0, kinds, 4);
uint32_t kcls = wb_const_int(b, (int64_t)cls);
uint32_t kcols = wb_const_int(b, cols);
uint32_t krows = wb_const_int(b, rows);
uint32_t krc = wb_const_int(b, rcols);
uint32_t krr = wb_const_int(b, rrows);
uint32_t k100 = wb_const_int(b, 100);
uint32_t kms = wb_const_int(b, 4000);
uint32_t kmax = wb_const_int(b, 64);
uint32_t code[40];
uint32_t n = 0;
code[n++] = wo_ins_abx(WOP_LOADK, 1, (uint16_t)kcmd);
code[n++] = wo_ins_abc(WOP_BUILTIN, 2, WO_K_TEXT, WO_B_MULTI_NEW);
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kdc);
code[n++] = wo_ins_abc(WOP_BUILTIN, 6, 2, WO_B_MULTI_PUSH);
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kscript);
code[n++] = wo_ins_abc(WOP_BUILTIN, 6, 2, WO_B_MULTI_PUSH);
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kcols);
code[n++] = wo_ins_abx(WOP_LOADK, 4, (uint16_t)krows);
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)kcls);
code[n++] = wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PROC_SPAWN_PTY);
code[n++] = wo_ins_abc(WOP_DROP, 2, 0, 0);
code[n++] = wo_ins_abc(WOP_GETF, 9, 0, 1); /* the master */
code[n++] = wo_ins_abc(WOP_GETF, 4, 0, 0); /* the id */
if (do_resize) { /* let the child start, then resize */
code[n++] = wo_ins_abx(WOP_LOADK, 1, (uint16_t)k100);
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_TIME_SLEEP);
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)krc);
code[n++] = wo_ins_abx(WOP_LOADK, 6, (uint16_t)krr);
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 4, WO_B_PROC_RESIZE);
}
code[n++] = wo_ins_abc(WOP_MOVE, 4, 9, 0);
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)kmax);
code[n++] = wo_ins_abx(WOP_LOADK, 6, (uint16_t)kms);
code[n++] = wo_ins_abc(WOP_BUILTIN, 7, 4, WO_B_NET_READ_DL);
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 9, WO_B_NET_CLOSE);
code[n++] = wo_ins_abc(WOP_DROP, 0, 0, 0);
code[n++] = wo_ins_abc(WOP_RET, 7, 0, 0);
wb_method(b, km, WOB_NONE, 0, 10, code, n, NULL, 0, NULL, 0);
return wb_finish(b, len);
}
/* run a pty module, copy the returned text into out */
static void run_pty(const char *script, int64_t cols, int64_t rows,
int do_resize, int64_t rcols, int64_t rrows, char *out,
size_t outcap) {
size_t len;
uint8_t *img = pty_module(script, cols, rows, do_resize, rcols, rrows, &len);
wo_module mod;
char lerr[256];
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
uint64_t ret = 0;
wo_err err;
memset(&err, 0, sizeof err);
T_EQ(wo_vm_call(&VM, 0, NULL, 0, &ret, &err), 0);
out[0] = '\0';
const wo_str *s = (const wo_str *)(uintptr_t)ret;
if (ret != 0 && ret != WO_NIL_SCALAR && s) {
size_t c = s->len < outcap - 1 ? s->len : outcap - 1;
memcpy(out, s->data, c);
out[c] = '\0';
wo_str_free(&VM.rt, (wo_str *)s);
}
wo_vm_destroy(&VM);
int st;
T_EQ(waitpid(-1, &st, WNOHANG), -1);
T_EQ(errno, ECHILD);
wo_module_free(&mod);
free(img);
}
static void test_pty_is_a_tty(void) {
char out[128];
run_pty("test -t 0 && test -t 1 && echo yes-tty", 24, 80, 0, 0, 0, out,
sizeof out);
T_CHECK(strncmp(out, "yes-tty", 7) == 0); /* discipline adds \r\n */
}
static void test_pty_size_and_resize(void) {
char out[128];
/* the initial size is what spawn_pty stated */
run_pty("stty size", 80, 24, 0, 0, 0, out, sizeof out);
T_CHECK(strncmp(out, "24 80", 5) == 0);
/* a resize during the child's sleep is what stty then reports */
run_pty("sleep 0.3; stty size", 80, 24, 1, 120, 40, out, sizeof out);
T_CHECK(strncmp(out, "40 120", 6) == 0);
}
/* resize on a pipe child refuses by name */
static uint8_t *pipe_resize_module(size_t *len) {
wb_t *b = wb_new();
uint32_t kchild = wb_const_text(b, "Child");
uint32_t km = wb_const_text(b, "main");
uint32_t kcmd = wb_const_text(b, "sleep");
uint32_t karg = wb_const_text(b, "1");
uint8_t kinds[4] = {WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR, WO_K_SCALAR};
uint32_t cls = wb_class(b, kchild, 0, kinds, 4);
uint32_t kcls = wb_const_int(b, (int64_t)cls);
uint32_t k80 = wb_const_int(b, 80);
uint32_t k24 = wb_const_int(b, 24);
uint32_t code[24];
uint32_t n = 0;
code[n++] = wo_ins_abx(WOP_LOADK, 1, (uint16_t)kcmd);
code[n++] = wo_ins_abc(WOP_BUILTIN, 2, WO_K_TEXT, WO_B_MULTI_NEW);
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)karg);
code[n++] = wo_ins_abc(WOP_BUILTIN, 6, 2, WO_B_MULTI_PUSH);
code[n++] = wo_ins_abx(WOP_LOADK, 3, (uint16_t)kcls);
code[n++] = wo_ins_abc(WOP_BUILTIN, 0, 1, WO_B_PROC_SPAWN);
code[n++] = wo_ins_abc(WOP_DROP, 2, 0, 0);
code[n++] = wo_ins_abc(WOP_GETF, 1, 0, 1);
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_NET_CLOSE);
code[n++] = wo_ins_abc(WOP_GETF, 1, 0, 2);
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_NET_CLOSE);
code[n++] = wo_ins_abc(WOP_GETF, 1, 0, 3);
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 1, WO_B_NET_CLOSE);
code[n++] = wo_ins_abc(WOP_GETF, 4, 0, 0);
code[n++] = wo_ins_abc(WOP_DROP, 0, 0, 0);
code[n++] = wo_ins_abx(WOP_LOADK, 5, (uint16_t)k80);
code[n++] = wo_ins_abx(WOP_LOADK, 6, (uint16_t)k24);
code[n++] = wo_ins_abc(WOP_BUILTIN, 8, 4, WO_B_PROC_RESIZE);
code[n++] = wo_ins_abc(WOP_RET0, 0, 0, 0);
wb_method(b, km, WOB_NONE, 0, 9, code, n, NULL, 0, NULL, 0);
return wb_finish(b, len);
}
static void test_resize_refuses_on_pipe_child(void) {
size_t len;
uint8_t *img = pipe_resize_module(&len);
wo_module mod;
char lerr[256];
T_EQ(wo_load_buf(&mod, img, len, lerr, sizeof lerr), 0);
T_EQ(wo_vm_init(&VM, &mod, 1 << 20), 0);
uint64_t ret = 0;
wo_err err;
memset(&err, 0, sizeof err);
T_EQ(wo_vm_call(&VM, 0, NULL, 0, &ret, &err), -1);
T_EQ(err.code, WO_T_IO);
T_CHECK(strstr(err.msg, "terminal") != NULL);
wo_vm_destroy(&VM);
int st;
T_EQ(waitpid(-1, &st, WNOHANG), -1);
T_EQ(errno, ECHILD);
wo_module_free(&mod);
free(img);
}
static void on_alarm(int sig) { (void)sig; } /* interrupt, don't die */
static int64_t mono_ms(void) {
@ -931,6 +1091,9 @@ int main(void) {
test_stream_wait();
test_stream_one_waiter();
test_stream_churn();
test_pty_is_a_tty();
test_pty_size_and_resize();
test_resize_refuses_on_pipe_child();
test_stop_kills_child(); /* last: it latches the stop flag */
return t_report("test_proc");
}