From 113f87125a07eb0f8e15f54a23f5fad79a6985bc Mon Sep 17 00:00:00 2001 From: Markus Fritsche Date: Sun, 10 May 2026 18:58:35 +0000 Subject: [PATCH] =?UTF-8?q?ffi/libc:=20phase=201=20syscalls=20=E2=80=94=20?= =?UTF-8?q?waitpid=20+=20raw=20fd=20I/O=20+=20kill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends Phase 0's chdir/errno/strerror with the syscalls that ffi/pty needs to drive a forkpty'd child: waitpid (with WIFEXITED / WEXITSTATUS / WIFSIGNALED / WTERMSIG decoders), read, write, close, kill. Status-word macros are reproduced from glibc bits/waitstatus.h using the LuaJIT `bit` library. M.waitpid returns a structured (kind, value) rather than the raw status word — callers don't have to know the encoding: "exit", N — normal exit, N is exit code "signal", N — killed by signal N "other", raw — stopped/continued (Phase 1 doesn't trace those) nil, err — syscall failure M.read / M.write / M.close / M.kill mirror their syscall return shape with errno-string surfacing on failure. Read uses a shared 4 KiB buffer for the common case; larger reads allocate a fresh buffer. Smoke covers the chdir regression (still works), all four status decoders against known status words, pipe round-trip for read/write/ close, EOF -> ("", 0), invalid-fd close -> false, kill(self, 0) success, kill(bogus, 0) failure. waitpid is not exercised by the smoke (needs a real child); that arrives with ffi/pty. Co-Authored-By: Claude Opus 4.7 (1M context) --- ffi/libc.lua | 92 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/ffi/libc.lua b/ffi/libc.lua index d473315..ac3742d 100644 --- a/ffi/libc.lua +++ b/ffi/libc.lua @@ -1,20 +1,34 @@ --- ffi/libc.lua — shared libc bindings: chdir, errno, strerror. --- Phase 0: just enough to make `cd` interception work in executor.lua. --- See docs/PHASE0.md §7. +-- ffi/libc.lua — shared libc bindings. +-- Phase 0: chdir, errno, strerror — enough for `cd` interception in executor. +-- Phase 1: waitpid + WEXITSTATUS, raw fd I/O (read/write/close), kill — the +-- syscalls ffi/pty needs to drive a forkpty'd child. +-- See docs/PHASE0.md §7 and docs/PHASE1.md §3. local ffi = require("ffi") +local bit = require("bit") ffi.cdef[[ int chdir(const char *path); int *__errno_location(void); char *strerror(int errnum); + +typedef int pid_t; +typedef long ssize_t; +typedef unsigned long size_t; + +pid_t waitpid(pid_t pid, int *wstatus, int options); +ssize_t read (int fd, void *buf, size_t count); +ssize_t write (int fd, const void *buf, size_t count); +int close (int fd); +int kill (pid_t pid, int sig); ]] local C = ffi.C local M = {} --- Apply chdir per PHASE0.md §7 (intercepts `cd` so wd persists across popen). +-- ---------------------------------------------------------------- chdir / errno +-- Phase 0 invariants. Apply chdir per PHASE0.md §7. -- Returns: true on success; false, errmsg on failure. function M.chdir(path) local rc = C.chdir(path) @@ -22,12 +36,74 @@ function M.chdir(path) return false, ffi.string(C.strerror(C.__errno_location()[0])) end -function M.errno() - return C.__errno_location()[0] +function M.errno() return C.__errno_location()[0] end +function M.strerror(en) return ffi.string(C.strerror(en)) end + +-- ---------------------------------------------------------------- waitpid +-- Mirrors glibc's WIFEXITED / WEXITSTATUS / WIFSIGNALED / WTERMSIG macros. +local function WIFEXITED (status) return bit.band(status, 0x7f) == 0 end +local function WEXITSTATUS(status) return bit.band(bit.rshift(status, 8), 0xff) end +local function WIFSIGNALED(status) + -- signal-killed iff low 7 bits in 1..126 + local s = bit.band(status, 0x7f) + return s ~= 0 and s ~= 0x7f +end +local function WTERMSIG (status) return bit.band(status, 0x7f) end + +M.WIFEXITED = WIFEXITED +M.WEXITSTATUS = WEXITSTATUS +M.WIFSIGNALED = WIFSIGNALED +M.WTERMSIG = WTERMSIG + +-- waitpid wrapper. Returns (kind, value): +-- "exit", exit_code on normal exit (WIFEXITED -> WEXITSTATUS) +-- "signal", signum on signal kill (WIFSIGNALED -> WTERMSIG) +-- nil, errmsg on waitpid syscall failure +local status_buf = ffi.new("int[1]") +function M.waitpid(pid, options) + status_buf[0] = 0 + local rc = C.waitpid(pid, status_buf, options or 0) + if rc < 0 then + return nil, ffi.string(C.strerror(C.__errno_location()[0])) + end + local status = status_buf[0] + if WIFEXITED(status) then return "exit", WEXITSTATUS(status) end + if WIFSIGNALED(status) then return "signal", WTERMSIG(status) end + return "other", status end -function M.strerror(en) - return ffi.string(C.strerror(en)) +-- ---------------------------------------------------------------- raw fd I/O +-- Used by ffi/pty for master-fd transfer. Errors return nil + errmsg so +-- callers can decide between EAGAIN/EINTR retry and abort. EOF on read is +-- represented as ("", 0) — empty string, zero bytes. +local READ_BUF = ffi.new("char[?]", 4096) + +function M.read(fd, count) + count = count or 4096 + local buf = (count <= 4096) and READ_BUF or ffi.new("char[?]", count) + local n = C.read(fd, buf, count) + if n < 0 then + return nil, ffi.string(C.strerror(C.__errno_location()[0])), M.errno() + end + return ffi.string(buf, n), tonumber(n) +end + +function M.write(fd, data) + local n = C.write(fd, data, #data) + if n < 0 then + return nil, ffi.string(C.strerror(C.__errno_location()[0])), M.errno() + end + return tonumber(n) +end + +function M.close(fd) + return C.close(fd) == 0 +end + +function M.kill(pid, sig) + local rc = C.kill(pid, sig) + if rc == 0 then return true end + return false, ffi.string(C.strerror(C.__errno_location()[0])) end return M