// exec runs subprocesses. It owns argv/environment conversion, launch error // reporting, waiting, deadlines, and process-group cleanup; callers own build // and test policy. package exec; import os; import time; @symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; def SYS_FCNTL: i64 = 72i64; def F_DUPFD_CLOEXEC: i32 = 1030; export type termination = enum i32 { EXIT = 0, SIGNAL = 1, TIMEOUT = 2, ERROR = 3, }; export type result = struct { termination: termination, code: i32, errno: i32, cleanuperrno: i32, }; // command is deliberately concrete. argv includes argv[0], env is the full // environment, and an empty dir inherits the caller's working directory. // An empty stdinpath selects the null device; stdoutpath and stderrpath are // created exclusively with mode 0600. A zero deadline means no timeout. export type command = struct { path: str, argv: []str, env: []str, dir: str, stdinpath: str, stdoutpath: str, stderrpath: str, deadline: time.instant, grace: time.duration, }; type state = enum i32 { EMPTY = 0, RUNNING = 1, TERM = 2, KILL = 3, }; export type process = struct { pid: i32, state: i32, markerfd: i32, marker: [4]u8, markern: i32, markerclosed: bool, deadline: i64, termdeadline: i64, grace: time.duration, hasdeadline: bool, done: bool, reaped: bool, leaderdone: bool, reaptermination: termination, reapcode: i32, result: result, }; // interrupt is the small signalfd watch paired with child launch. start // restores the pre-watch mask in children before exec, so SIGINT/SIGTERM do // not remain blocked in executed programs. export type interrupt = struct { fd: i32, oldmask: u64, active: bool, errno: i32, }; let launchmaskactive: bool = false; let launcholdmask: u64 = 0u64; fn instantns(i: *time.instant) i64 = { return i.sec * (time.second: i64) + i.nsec; }; fn nowns() i64 = { let now: time.instant = time.now(time.clock.monotonic); return instantns(&now); }; fn reset(p: *process) void = { p.pid = -1; p.state = state.EMPTY as i32; p.markerfd = -1; p.markern = 0; p.markerclosed = false; p.deadline = 0i64; p.termdeadline = 0i64; p.grace = 0i64: time.duration; p.hasdeadline = false; p.done = false; p.reaped = false; p.leaderdone = false; p.reaptermination = termination.ERROR; p.reapcode = 0; p.result.termination = termination.ERROR; p.result.code = 0; p.result.errno = 0; p.result.cleanuperrno = 0; }; fn hasnul(s: str) bool = { let i: i32 = 0; for (i < s.len) { if (s[i] == 0u8) { return true; }; i += 1; }; return false; }; fn valid(c: *command) bool = { if (c.path.len == 0 || c.argv.len == 0 || c.argv[0].len == 0) { return false; }; if ((c.grace: i64) < 0i64) { return false; }; if (hasnul(c.path) || hasnul(c.dir) || hasnul(c.stdinpath) || hasnul(c.stdoutpath) || hasnul(c.stderrpath)) { return false; }; let i: i32 = 0; for (i < c.argv.len) { if (hasnul(c.argv[i])) { return false; }; i += 1; }; i = 0; for (i < c.env.len) { if (hasnul(c.env[i])) { return false; }; i += 1; }; return true; }; fn cstr(s: str) *u8 = { let b: []u8 = alloc([], (s.len + 1): u64)!; b.len = s.len + 1; let i: i32 = 0; for (i < s.len) { b[i] = s[i]; i += 1; }; b[s.len] = 0u8; return b.ptr; }; fn ctable(v: []str) []*u8 = { let r: []*u8 = alloc([], (v.len + 1): u64)!; let i: i32 = 0; for (i < v.len) { append(r, cstr(v[i])); i += 1; }; append(r, nil: *u8); return r; }; fn setcleanup(r: *result, rc: i32) void = { if (rc >= 0 || rc == -3 || r.cleanuperrno != 0) { return; }; r.cleanuperrno = -rc; }; fn closefd(r: *result, fd: i32) void = { if (fd >= 0) { setcleanup(r, os.close(fd)); }; }; // Capture and marker descriptors must not occupy stdin/stdout/stderr. The // caller may legitimately have closed any of those descriptors before run. fn safefd(r: *result, fd: i32) i32 = { if (fd > os.STDERR_FILENO) { return fd; }; let moved: i32 = syscall3(SYS_FCNTL, fd: i64, F_DUPFD_CLOEXEC: i64, (os.STDERR_FILENO + 1): i64): i32; closefd(r, fd); return moved; }; fn fail(p: *process, rc: i32) void = { p.result.termination = termination.ERROR; p.result.errno = rc; if (p.result.errno < 0) { p.result.errno = -p.result.errno; }; if (p.result.errno == 0) { p.result.errno = 5; }; p.done = true; }; fn childmark(fd: i32, rc: i32) void = { let ei: i32 = rc; if (ei < 0) { ei = -ei; }; if (ei == 0) { ei = 5; }; let e: u32 = ei: u32; let b: [4]u8; b[0] = e: u8; b[1] = (e >> 8u32): u8; b[2] = (e >> 16u32): u8; b[3] = (e >> 24u32): u8; for (true) { let n: i64 = os.write(fd, &b[0], size([4]u8)); if (n == size([4]u8): i64) { break; }; if (n != -4i64) { os.exit(126); }; }; os.close(fd); os.exit(127); }; fn childclose(fd: i32, markerfd: i32) void = { let rc: i32 = os.close(fd); if (rc < 0) { childmark(markerfd, rc); }; }; fn child(c: *command, av: []*u8, ep: []*u8, infd: i32, outfd: i32, errfd: i32, markerread: i32, markerwrite: i32) void = { let rc: i32 = os.setpgid(0, 0); if (rc < 0) { childmark(markerwrite, rc); }; if (launchmaskactive) { rc = os.sigprocmask(os.SIG_SETMASK, &launcholdmask, nil: *u64); if (rc < 0) { childmark(markerwrite, rc); }; }; childclose(markerread, markerwrite); rc = os.dup2(infd, os.STDIN_FILENO); if (rc < 0) { childmark(markerwrite, rc); }; childclose(infd, markerwrite); rc = os.dup2(outfd, os.STDOUT_FILENO); if (rc < 0) { childmark(markerwrite, rc); }; rc = os.dup2(errfd, os.STDERR_FILENO); if (rc < 0) { childmark(markerwrite, rc); }; childclose(outfd, markerwrite); childclose(errfd, markerwrite); if (c.dir.len != 0) { rc = os.chdir(c.dir); if (rc < 0) { childmark(markerwrite, rc); }; }; rc = os.execve(c.path, av.ptr, ep.ptr); childmark(markerwrite, rc); }; export fn start(p: *process, c: *command) void = { reset(p); p.deadline = instantns(&c.deadline); p.hasdeadline = c.deadline.sec != 0i64 || c.deadline.nsec != 0i64; p.grace = c.grace; if (!valid(c)) { fail(p, 22); return; }; let stdinpath: str = c.stdinpath; if (stdinpath.len == 0) { stdinpath = "/dev/null"; }; let infd: i32 = os.open(stdinpath, os.flag.RDONLY, 0i32); if (infd < 0) { fail(p, infd); return; }; infd = safefd(&p.result, infd); if (infd < 0) { fail(p, infd); return; }; let outfd: i32 = os.open(c.stdoutpath, os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); if (outfd < 0) { fail(p, outfd); closefd(&p.result, infd); return; }; outfd = safefd(&p.result, outfd); if (outfd < 0) { fail(p, outfd); closefd(&p.result, infd); return; }; let errfd: i32 = os.open(c.stderrpath, os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384); if (errfd < 0) { fail(p, errfd); closefd(&p.result, infd); closefd(&p.result, outfd); return; }; errfd = safefd(&p.result, errfd); if (errfd < 0) { fail(p, errfd); closefd(&p.result, infd); closefd(&p.result, outfd); return; }; let marker: [2]i32; let rc: i32 = os.pipe2(&marker, os.O_CLOEXEC | os.O_NONBLOCK); if (rc < 0) { fail(p, rc); closefd(&p.result, infd); closefd(&p.result, outfd); closefd(&p.result, errfd); return; }; marker[0] = safefd(&p.result, marker[0]); if (marker[0] < 0) { fail(p, marker[0]); closefd(&p.result, infd); closefd(&p.result, outfd); closefd(&p.result, errfd); closefd(&p.result, marker[1]); return; }; marker[1] = safefd(&p.result, marker[1]); if (marker[1] < 0) { fail(p, marker[1]); closefd(&p.result, infd); closefd(&p.result, outfd); closefd(&p.result, errfd); closefd(&p.result, marker[0]); return; }; let av: []*u8 = ctable(c.argv); let ep: []*u8 = ctable(c.env); let pid: i32 = os.fork(); if (pid < 0) { fail(p, pid); closefd(&p.result, infd); closefd(&p.result, outfd); closefd(&p.result, errfd); closefd(&p.result, marker[0]); closefd(&p.result, marker[1]); return; }; if (pid == 0) { child(c, av, ep, infd, outfd, errfd, marker[0], marker[1]); }; p.pid = pid; p.state = state.RUNNING as i32; p.markerfd = marker[0]; closefd(&p.result, infd); closefd(&p.result, outfd); closefd(&p.result, errfd); closefd(&p.result, marker[1]); // The child creates its group before user code. The parent repeats the // operation to close the cancel-before-child-runs race. let pgrc: i32 = os.setpgid(pid, pid); if (pgrc < 0 && pgrc != -13 && pgrc != -3) { p.result.termination = termination.ERROR; p.result.errno = -pgrc; setcleanup(&p.result, os.kill(-pid, os.SIGTERM)); p.termdeadline = nowns() + (p.grace: i64); p.state = state.TERM as i32; }; }; fn decodemarker(p: *process) void = { if (p.markern != 4) { if (p.result.termination != termination.TIMEOUT) { p.result.termination = termination.ERROR; }; if (p.result.errno == 0) { p.result.errno = 71; }; return; }; let e: u32 = p.marker[0]: u32 | ((p.marker[1]: u32) << 8u32) | ((p.marker[2]: u32) << 16u32) | ((p.marker[3]: u32) << 24u32); if (p.result.termination != termination.TIMEOUT) { p.result.termination = termination.ERROR; }; if (p.result.errno == 0) { p.result.errno = e: i32; }; if (p.result.errno == 0) { p.result.errno = 5; }; }; fn closemarker(p: *process) void = { if (p.markerfd < 0) { return; }; closefd(&p.result, p.markerfd); p.markerfd = -1; }; fn readmarker(p: *process) void = { if (p.markerfd < 0 || p.markerclosed || p.markern == 4) { return; }; for (true) { let n: i64 = os.read(p.markerfd, &p.marker[p.markern], (4 - p.markern): u64); if (n > 0) { p.markern += n: i32; if (p.markern == 4) { decodemarker(p); return; }; continue; }; if (n == 0) { p.markerclosed = true; return; }; if (n == -4i64) { continue; }; if (n == -11i64) { return; }; p.markerclosed = true; if (p.result.termination != termination.TIMEOUT) { p.result.termination = termination.ERROR; }; if (p.result.errno == 0) { p.result.errno = (-n): i32; }; return; }; }; fn groupsignal(p: *process, sig: i32) i32 = { let rc: i32 = os.kill(-p.pid, sig); setcleanup(&p.result, rc); return rc; }; fn leadersignal(p: *process, sig: i32) i32 = { let rc: i32 = os.kill(p.pid, sig); setcleanup(&p.result, rc); return rc; }; fn leaderterminal(p: *process) bool = { return p.reaped || p.leaderdone; }; // 0 means no group member remains, 1 means the group is live, and -1 is an // explicit cleanup failure. fn groupstate(p: *process) i32 = { let rc: i32 = os.kill(-p.pid, 0); if (rc == -3) { return 0; }; if (rc == 0) { return 1; }; setcleanup(&p.result, rc); return -1; }; fn finish(p: *process) void = { closemarker(p); p.state = state.EMPTY as i32; p.done = true; }; fn startterm(p: *process, now: i64) void = { groupsignal(p, os.SIGTERM); p.termdeadline = now + (p.grace: i64); p.state = state.TERM as i32; }; fn startkill(p: *process) void = { groupsignal(p, os.SIGKILL); if (!leaderterminal(p)) { leadersignal(p, os.SIGKILL); }; p.state = state.KILL as i32; }; fn advancecleanup(p: *process, now: i64) bool = { if (p.state == (state.RUNNING as i32)) { return false; }; let gs: i32 = groupstate(p); if (leaderterminal(p) && gs == 0) { finish(p); return true; }; if (p.state == (state.TERM as i32) && now >= p.termdeadline) { startkill(p); gs = groupstate(p); }; if (p.state == (state.KILL as i32)) { groupsignal(p, os.SIGKILL); if (!leaderterminal(p)) { leadersignal(p, os.SIGKILL); }; gs = groupstate(p); if (leaderterminal(p) && (gs == 0 || gs < 0)) { finish(p); return true; }; }; return false; }; fn reap(p: *process, status: i32) void = { p.reaped = true; p.leaderdone = true; if (os.wifexited(status)) { p.reaptermination = termination.EXIT; p.reapcode = os.wexitstatus(status); } else if (os.wifsignaled(status)) { p.reaptermination = termination.SIGNAL; p.reapcode = os.wtermsig(status); } else { if (p.result.errno == 0) { p.result.errno = 71; }; }; readmarker(p); if (p.markern == 0 && p.markerclosed) { if (p.result.termination != termination.TIMEOUT && p.result.errno == 0) { p.result.termination = p.reaptermination; p.result.code = p.reapcode; }; } else if (p.markern != 4 && p.result.errno == 0) { decodemarker(p); }; if (p.state == (state.RUNNING as i32)) { startterm(p, nowns()); }; }; fn waitleader(p: *process) bool = { if (p.reaped) { return true; }; let status: i32 = 0; for (true) { let pid: i32 = os.wait4(p.pid, &status, os.WNOHANG, nil: *void); if (pid == p.pid) { reap(p, status); return true; }; if (pid == 0) { return false; }; if (pid == -4) { continue; }; if (p.result.errno == 0) { p.result.errno = -pid; }; if (pid == -10) { p.leaderdone = true; }; if (p.result.termination != termination.TIMEOUT) { p.result.termination = termination.ERROR; }; if (p.state == (state.RUNNING as i32)) { startterm(p, nowns()); }; return false; }; return false; }; export fn poll(p: *process) bool = { if (p.done) { return true; }; if (p.pid <= 0 || p.state == (state.EMPTY as i32)) { return true; }; readmarker(p); waitleader(p); let now: i64 = nowns(); if (p.state == (state.RUNNING as i32) && p.hasdeadline && p.markerclosed && p.markern == 0 && p.result.errno == 0 && now >= p.deadline) { p.result.termination = termination.TIMEOUT; p.result.code = 0; startterm(p, now); }; advancecleanup(p, now); return p.done; }; export fn cancel(p: *process) void = { if (p.done || p.pid <= 0 || p.state == (state.EMPTY as i32)) { return; }; if (p.state == (state.RUNNING as i32)) { startterm(p, nowns()); }; }; export fn run(c: *command, out: *result) void = { let watch: interrupt; if (!interruptopen(&watch)) { out.termination = termination.ERROR; out.code = 0; out.errno = watch.errno; out.cleanuperrno = 0; return; }; let p: process; let interrupted: i32 = 0; start(&p, c); for (!p.done) { let signo: i32 = interruptpoll(&watch); if (signo != 0) { if (signo > 0 && interrupted == 0) { interrupted = signo; }; cancel(&p); }; if (poll(&p)) { break; }; time.sleep(time.millisecond, time.clock.monotonic); }; interruptclose(&watch); if (watch.errno != 0 && p.result.cleanuperrno == 0) { p.result.cleanuperrno = watch.errno; }; *out = p.result; if (interrupted != 0) { os.kill(os.getpid(), interrupted); }; }; // runstdio executes one child with the caller's stdin/stdout/stderr and waits // only for that child. It deliberately does not create or clean a process // group: commands such as a compiler driver or a user program own any // descendants they create. env is the complete environment; an empty slice // requests an empty environment. The close-on-exec marker keeps an execve // setup failure distinct from a real program exit 127. export fn runstdio(path: str, argv: []str, env: []str, out: *result) void = { out.termination = termination.ERROR; out.code = 0; out.errno = 0; out.cleanuperrno = 0; if (path.len == 0 || argv.len == 0 || argv[0].len == 0 || hasnul(path)) { out.errno = 22; return; }; let i: i32 = 0; for (i < argv.len) { if (hasnul(argv[i])) { out.errno = 22; return; }; i += 1; }; i = 0; for (i < env.len) { if (hasnul(env[i])) { out.errno = 22; return; }; i += 1; }; let marker: [2]i32; let rc: i32 = os.pipe2(&marker, os.O_CLOEXEC); if (rc < 0) { out.errno = -rc; return; }; marker[0] = safefd(out, marker[0]); if (marker[0] < 0) { out.errno = -marker[0]; closefd(out, marker[1]); return; }; marker[1] = safefd(out, marker[1]); if (marker[1] < 0) { out.errno = -marker[1]; closefd(out, marker[0]); return; }; let av: []*u8 = ctable(argv); let ep: []*u8 = ctable(env); let pid: i32 = os.fork(); if (pid < 0) { out.errno = -pid; closefd(out, marker[0]); closefd(out, marker[1]); return; }; if (pid == 0) { if (launchmaskactive) { rc = os.sigprocmask(os.SIG_SETMASK, &launcholdmask, nil: *u64); if (rc < 0) { childmark(marker[1], rc); }; }; childclose(marker[0], marker[1]); rc = os.execve(path, av.ptr, ep.ptr); childmark(marker[1], rc); }; closefd(out, marker[1]); let status: i32 = 0; let waited: i32 = 0; for (true) { waited = os.wait4(pid, &status, 0, nil: *void); if (waited == pid) { break; }; if (waited == -4) { continue; }; out.errno = -waited; if (out.errno <= 0) { out.errno = 5; }; break; }; let mark: [4]u8; let markn: i32 = 0; for (waited == pid && markn < 4) { let n: i64 = os.read(marker[0], &mark[markn], (4 - markn): u64); if (n > 0) { markn += n: i32; continue; }; if (n == 0) { break; }; if (n == -4i64) { continue; }; out.errno = (-n): i32; break; }; closefd(out, marker[0]); if (waited != pid || out.errno != 0) { return; }; if (markn != 0) { if (markn != 4) { out.errno = 71; return; }; let e: u32 = mark[0]: u32 | ((mark[1]: u32) << 8u32) | ((mark[2]: u32) << 16u32) | ((mark[3]: u32) << 24u32); out.errno = e: i32; if (out.errno == 0) { out.errno = 5; }; out.code = 127; return; }; if (os.wifexited(status)) { out.termination = termination.EXIT; out.code = os.wexitstatus(status); return; }; if (os.wifsignaled(status)) { out.termination = termination.SIGNAL; out.code = os.wtermsig(status); return; }; out.errno = 71; }; export fn interruptopen(w: *interrupt) bool = { w.fd = -1; w.oldmask = 0u64; w.active = false; w.errno = 0; if (launchmaskactive) { w.errno = 16; return false; }; let mask: u64 = (1u64 << ((os.SIGINT - 1): u64)) | (1u64 << ((os.SIGTERM - 1): u64)); let rc: i32 = os.sigprocmask(os.SIG_BLOCK, &mask, &w.oldmask); if (rc < 0) { w.errno = -rc; return false; }; w.fd = os.signalfd(-1, &mask, os.SFD_CLOEXEC | os.SFD_NONBLOCK); if (w.fd < 0) { w.errno = -w.fd; os.sigprocmask(os.SIG_SETMASK, &w.oldmask, nil: *u64); w.fd = -1; return false; }; launcholdmask = w.oldmask; launchmaskactive = true; w.active = true; return true; }; export fn interruptpoll(w: *interrupt) i32 = { if (!w.active || w.fd < 0) { w.errno = 22; return -22; }; let info: [128]u8; for (true) { let n: i64 = os.read(w.fd, &info[0], size([128]u8)); if (n == -4i64) { continue; }; if (n == -11i64) { return 0; }; if (n < 0) { w.errno = (-n): i32; return n: i32; }; if (n != size([128]u8): i64) { w.errno = 71; return -71; }; let signo: u32 = info[0]: u32 | ((info[1]: u32) << 8u32) | ((info[2]: u32) << 16u32) | ((info[3]: u32) << 24u32); return signo: i32; }; return 0; }; export fn interruptclose(w: *interrupt) bool = { if (!w.active) { return w.errno == 0; }; let ok: bool = true; let rc: i32 = os.close(w.fd); if (rc < 0) { w.errno = -rc; ok = false; }; rc = os.sigprocmask(os.SIG_SETMASK, &w.oldmask, nil: *u64); if (rc < 0) { if (w.errno == 0) { w.errno = -rc; }; ok = false; }; launchmaskactive = false; launcholdmask = 0u64; w.fd = -1; w.active = false; return ok; };