ffi/libc: phase 1 syscalls — waitpid + raw fd I/O + kill
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) <noreply@anthropic.com>
This commit is contained in:
+84
-8
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user