feat(process): fork/exec/id/session + system/popen

This commit is contained in:
2026-09-05 16:55:14 -04:00
parent c9e9676d38
commit 14c6fd6cf6
19 changed files with 2118 additions and 2 deletions
+779
View File
@@ -0,0 +1,779 @@
/*
* vlibc — process control test (todo 20).
*
* Exercises fork/vfork/exec family/ids/session + system/popen end to
* end:
*
* 1. getpid/getppid/getuid/geteuid/getgid/getegid return live values.
* 2. fork: the child writes a marker and its pid through a pipe and
* exits 7 via the raw syscall; the parent sees 0 + the pid round
* trip and reaps a 7<<8 status via raw SYS_wait4.
* 3. fork + execve("/bin/true") exits 0.
* 4. fork + execve of a nonexistent binary: the child exits 127, the
* parent reaps 127<<8.
* 5. system("exit 3") returns 768 (WEXITSTATUS 3).
* 6. popen("echo hi","r") reads "hi\n" and pclose returns 0;
* popen("cat","w") writes through and pclose returns 0.
* 7. setuid(getuid()) etc. round trip to 0; getgroups counts and
* fills consistently.
* 8. (L2) session control in fork children: setsid/getpgrp/getsid and
* setpgid/setpgrp/getpgid; tcgetpgrp/tcsetpgrp fail with -1 on a
* pipe (ENOTTY, no errno read).
* 9. one fork+exec each for execl/execlp/execv/execvp/execle/fexecve.
* 10. PATH search: a helper script in a private directory is found via
* a custom PATH (exit 5 → 5<<8), an empty PATH entry means cwd,
* and execvp falls back to the default PATH when environ is NULL.
* 11. (L2) vfork: the child execs /bin/true, the parent reaps 0.
*
* sys/wait.h is todo 23 and does not exist: the test waits via raw
* SYS_wait4 in its own helper, and children exit via the raw syscalls.
*
* The negative paths make the LIBRARY write errno (syscall_ret), which
* under a host-linked binary targets glibc's private dtv slot at
* %fs:0+8. In the default mode each such call is bracketed with a
* save/restore of that slot (task 13 technique) — only vlibc/raw-syscall
* code runs between the write and the restore, so host state is intact
* when host code runs again. The test itself NEVER reads errno; every
* negative is asserted on the return value. The -f mode runs the
* failure scenarios and exits via raw SYS_exit_group (house pattern,
* tests/test_malloc.c).
*
* All diagnostics go through raw SYS_write (no host stdio): under
* -Iinclude the vlibc public headers shadow GCC's internal ones, so a
* host header would not compile. Only vlibc headers are included.
*/
#include <stddef.h>
#include "../include/stdio.h"
#include "../include/stdlib.h"
#include "../include/unistd.h"
#include "../src/internal/syscall.h"
/* Kernel-UAPI constants local to this test (fcntl.h is todo 21). */
#define T20_O_RDONLY 0x0
#define T20_O_WRONLY 0x1
#define T20_O_CREAT 0x40
#define T20_O_TRUNC 0x200
#define T20_AT_FDCWD (-100)
#define T20_EINTR 4
static int failures;
/* Write a NUL-terminated string to fd via the raw syscall layer. The
* optimize attribute keeps GCC from lowering the length loop into a
* strlen call, which would leave a vlibc-owned symbol undefined in this
* host-linked standalone binary (house idiom, see src/string). */
static __attribute__((optimize("no-tree-loop-distribute-patterns"))) void
say(int fd, const char *s)
{
long n = 0;
while (s[n] != '\0')
{
n++;
}
__syscall3(SYS_write, fd, (long)s, n);
}
/* Write v in decimal to fd. */
static void
say_dec(int fd, unsigned long v) // NOLINT(bugprone-easily-swappable-parameters)
{
char buf[24];
int i = (int)sizeof(buf);
buf[--i] = '\0';
do
{
buf[--i] = (char)('0' + (v % 10));
v /= 10;
} while (v != 0);
__syscall3(SYS_write, fd, (long)(buf + i), (long)(sizeof(buf) - 1 - i));
}
static void
check(int cond, const char *what)
{
if (cond)
{
say(1, "PASS: ");
say(1, what);
say(1, "\n");
}
else
{
say(2, "FAIL: ");
say(2, what);
say(2, "\n");
failures++;
}
}
/*
* Host-TCB slot-1 bracket: the library's errno write on a negative path
* lands at %fs:0+8, glibc's dtv pointer. Save and restore it around each
* such call; only vlibc/raw-syscall code runs in between (task 13
* technique).
*/
static unsigned long
tcb_slot1(void)
{
return *(unsigned long *)((char *)__builtin_thread_pointer() + 8);
}
static void
tcb_slot1_set(unsigned long value)
{
*(unsigned long *)((char *)__builtin_thread_pointer() + 8) = value;
}
/* Wait for pid via the raw syscall (sys/wait.h is todo 23) and return
* the raw wait status, or -1 when wait4 failed for a reason other than
* EINTR. */
static int
raw_wait(pid_t pid)
{
int status = 0;
for (;;)
{
long r = __syscall4(SYS_wait4, pid, (long)&status, 0, 0);
if (r < 0 && -r == T20_EINTR)
{
continue;
}
if (r < 0)
{
return -1;
}
return status;
}
}
/* Run fn(argv) in a fork child (via a public exec entry point) and
* return the child's wait status; the child exits 127 through the raw
* syscall when the exec entry point returns. */
typedef int (*t20_exec_fn)(void);
static int
exec_one(const char *label, t20_exec_fn fn)
{
pid_t pid = fork();
int status;
if (pid < 0)
{
check(0, label);
return -1;
}
if (pid == 0)
{
/* Child: exec (or raw-exit 127 on failure). */
(void)fn();
__syscall1(SYS_exit_group, 127);
}
status = raw_wait(pid);
check(status == 0, label);
return status;
}
/* The per-entry-point payloads. Each runs in the fork child. */
static char *av_true[] = {"true", 0};
static char *env_le[] = {"VLIBC_T20=le", 0};
static char *env_ve[] = {"VLIBC_T20=ve", 0};
static int
run_execve(void)
{
static char *envp_ok[] = {"VLIBC_T20=yes", 0};
return execve("/bin/true", av_true, envp_ok);
}
static int
run_fexecve(void)
{
int fd = open("/bin/true", T20_O_RDONLY);
if (fd < 0)
{
return -1;
}
return fexecve(fd, av_true, environ);
}
/* 1. Process ids. */
static void
id_scenarios(void)
{
check(getpid() > 0, "getpid returns a positive pid");
check(getppid() > 0, "getppid returns a positive pid");
check(getuid() == geteuid(), "getuid and geteuid agree");
check(getgid() == getegid(), "getgid and getegid agree");
}
/* 2. fork: marker + pid round trip through a pipe, child exits 7. */
static void
fork_scenario(void)
{
int fds[2] = {0};
unsigned char buf[5] = {0}; /* filled by SYS_read; the analyzer cannot model it */
pid_t pid;
int status;
if (syscall_ret(__syscall1(SYS_pipe, (long)fds)) < 0)
{
check(0, "fork scenario pipe");
return;
}
pid = fork();
if (pid < 0)
{
check(0, "fork returns a pid");
check(0, "child writes the marker and its pid");
check(0, "parent reaps the child with status 7<<8");
(void)__syscall1(SYS_close, fds[0]);
(void)__syscall1(SYS_close, fds[1]);
return;
}
if (pid == 0)
{
int mypid = getpid();
/* Child: marker + pid, then exit 7 via the raw syscall. */
buf[0] = 'c';
buf[1] = (unsigned char)(mypid & 0xff);
buf[2] = (unsigned char)((mypid >> 8) & 0xff);
buf[3] = (unsigned char)((mypid >> 16) & 0xff);
buf[4] = (unsigned char)((mypid >> 24) & 0xff);
(void)__syscall3(SYS_write, fds[1], (long)buf, 5);
__syscall1(SYS_exit_group, 7);
}
check(pid > 0, "fork returns the child pid in the parent");
check(__syscall3(SYS_read, fds[0], (long)buf, 5) == 5, "parent reads the five marker bytes");
{
int child_pid = (int)((unsigned int)buf[1] | ((unsigned int)buf[2] << 8) |
((unsigned int)buf[3] << 16) | ((unsigned int)buf[4] << 24));
check(buf[0] == 'c' && child_pid == pid,
"the child's getpid matches the parent's fork return value");
}
(void)__syscall1(SYS_close, fds[0]);
(void)__syscall1(SYS_close, fds[1]);
status = raw_wait(pid);
check(status == 7 << 8, "the child exited 7 (status 7<<8 == 1792)");
}
/* 3+4. execve of a good and of a nonexistent binary. */
static void
exec_scenarios(void)
{
pid_t pid;
int status;
pid = fork();
if (pid < 0)
{
check(0, "execve /bin/true child exits 0");
return;
}
if (pid == 0)
{
(void)run_execve();
__syscall1(SYS_exit_group, 127);
}
status = raw_wait(pid);
check(status == 0, "fork + execve(\"/bin/true\") exits 0");
pid = fork();
if (pid < 0)
{
check(0, "execve of a nonexistent binary gives 127<<8");
return;
}
if (pid == 0)
{
static char *av_bad[] = {"vlibc-no-such-binary", 0};
(void)execve("/bin/nonexistent-vlibc-xyz", av_bad, environ);
__syscall1(SYS_exit_group, 127);
}
status = raw_wait(pid);
check(status == 127 << 8, "failed execve: the child exited 127 (status 127<<8)");
}
/* 5. system. */
// NOLINTBEGIN(bugprone-command-processor)
static void
system_scenario(void)
{
check(system("exit 3") == 768, "system(\"exit 3\") returns 768 (WEXITSTATUS 3)");
}
// NOLINTEND(bugprone-command-processor)
/* 6. popen/pclose. */
// NOLINTBEGIN(bugprone-command-processor)
static void
popen_scenario(void)
{
FILE *f;
char rbuf[8];
long n = 0;
int rc;
f = popen("echo hi", "r");
check(f != 0, "popen(\"echo hi\", \"r\") returns a stream");
if (f == 0)
{
return;
}
n = (long)fread(rbuf, 1, sizeof(rbuf), f);
check(n == 3, "fread on the popen stream returns three bytes");
n = 0;
while (n < 3 && rbuf[n] == "hi\n"[n])
{
n++;
}
check(n == 3, "the stream reads \"hi\\n\"");
rc = pclose(f);
check(rc == 0, "pclose of the \"r\" stream returns 0");
f = popen("cat >/dev/null", "w");
check(f != 0, "popen(\"cat >/dev/null\", \"w\") returns a stream");
if (f == 0)
{
return;
}
check(fwrite("bye", 1, 3, f) == 3, "three bytes are written to the \"w\" stream");
rc = pclose(f);
check(rc == 0, "pclose of the \"w\" stream returns 0");
}
// NOLINTEND(bugprone-command-processor)
/* 7. uid/gid round trip + supplementary groups. */
static void
uid_scenarios(void)
{
gid_t gids[64];
int n;
int m;
check(setuid(getuid()) == 0, "setuid(getuid()) returns 0");
check(seteuid(geteuid()) == 0, "seteuid(geteuid()) returns 0");
check(setgid(getgid()) == 0, "setgid(getgid()) returns 0");
check(setegid(getegid()) == 0, "setegid(getegid()) returns 0");
n = getgroups(0, 0);
check(n >= 0, "getgroups(0, NULL) returns a non-negative count");
if (n >= 0)
{
m = getgroups(64, gids);
check(m == n, "getgroups(64, list) returns the same count");
}
}
#if VLIBC_LEVEL_GE(2)
/* 8. Session control in fork children (never in the parent: leaving the
* parent's process group would trigger SIGTTOU on terminal writes). */
static void
session_scenarios(void)
{
int fds[2] = {0};
pid_t pid;
int status;
unsigned long saved;
/* Child A: setsid makes it a session and group leader. */
pid = fork();
if (pid < 0)
{
check(0, "setsid child");
return;
}
if (pid == 0)
{
pid_t sid = setsid();
if (sid != getpid())
{
__syscall1(SYS_exit_group, 1);
}
if (getpgrp() != getpid())
{
__syscall1(SYS_exit_group, 2);
}
if (getsid(0) != getpid())
{
__syscall1(SYS_exit_group, 3);
}
__syscall1(SYS_exit_group, 0);
}
status = raw_wait(pid);
check(status == 0, "child: setsid() == getpid(), getpgrp() == pid, getsid(0) == pid");
/* Child B: setpgid(0,0) + setpgrp() + getpgid. */
pid = fork();
if (pid < 0)
{
check(0, "setpgid child");
return;
}
if (pid == 0)
{
if (setpgid(0, 0) != 0)
{
__syscall1(SYS_exit_group, 1);
}
if (setpgrp() != 0)
{
__syscall1(SYS_exit_group, 2);
}
if (getpgrp() != getpid())
{
__syscall1(SYS_exit_group, 3);
}
if (getpgid(0) != getpid())
{
__syscall1(SYS_exit_group, 4);
}
__syscall1(SYS_exit_group, 0);
}
status = raw_wait(pid);
check(status == 0, "child: setpgid(0,0), setpgrp(), getpgrp(), getpgid(0) all agree");
/* tcgetpgrp/tcsetpgrp on a pipe fail with -1 (ENOTTY). */
if (syscall_ret(__syscall1(SYS_pipe, (long)fds)) < 0)
{
check(0, "tcgetpgrp on a pipe");
check(0, "tcsetpgrp on a pipe");
return;
}
saved = tcb_slot1();
check(tcgetpgrp(fds[0]) == (pid_t)-1, "tcgetpgrp on a pipe returns -1");
tcb_slot1_set(saved);
saved = tcb_slot1();
check(tcsetpgrp(fds[0], 0) == -1, "tcsetpgrp on a pipe returns -1");
tcb_slot1_set(saved);
(void)__syscall1(SYS_close, fds[0]);
(void)__syscall1(SYS_close, fds[1]);
}
/* 11. vfork: the child execs /bin/true while the parent is suspended. */
// NOLINTBEGIN(clang-analyzer-security.insecureAPI.vfork, clang-analyzer-unix.Vfork,
// bugprone-unsafe-functions)
static void
vfork_scenario(void)
{
pid_t pid = vfork();
int status;
if (pid == 0)
{
/* Child: exec (the only sanctioned follow-up) or raw-exit. */
(void)execve("/bin/true", av_true, environ);
__syscall1(SYS_exit_group, 126);
}
check(pid >= 0, "vfork returns a pid in the parent (0 in the child)");
if (pid < 0)
{
return;
}
status = raw_wait(pid);
check(status == 0, "vfork child exec'd /bin/true and exited 0");
}
// NOLINTEND(clang-analyzer-security.insecureAPI.vfork, clang-analyzer-unix.Vfork,
// bugprone-unsafe-functions)
#endif /* VLIBC_LEVEL_GE(2) */
/* 9. One fork+exec per entry point. */
static int
run_execv(void)
{
return execv("/bin/true", av_true);
}
static int
run_execvp(void)
{
return execvp("true", av_true);
}
static int
run_execl(void)
{
return execl("/bin/true", "true", (char *)0);
}
static int
run_execlp(void)
{
return execlp("true", "true", (char *)0);
}
static int
run_execle(void)
{
return execle("/bin/true", "true", (char *)0, env_le);
}
static void
exec_variants(void)
{
(void)exec_one("execv(\"/bin/true\")", run_execv);
(void)exec_one("execvp(\"true\") via the default PATH", run_execvp);
(void)exec_one("execl(\"/bin/true\")", run_execl);
(void)exec_one("execlp(\"true\") via the default PATH", run_execlp);
(void)exec_one("execle(\"/bin/true\") with a custom envp", run_execle);
(void)exec_one("fexecve of an open /bin/true", run_fexecve);
}
/* 10. PATH search with a private helper script. */
static void
path_search_scenario(void)
{
const char *dir = "/tmp/vlibc-t20-bin";
const char *script_path = "/tmp/vlibc-t20-bin/vlibc-t20-helper";
const char script[] = "#!/bin/sh\nexit 5\n";
static char *path_env[] = {"PATH=/tmp/vlibc-t20-bin", 0};
static char *path_env_cwd[] = {"PATH=:/tmp/vlibc-t20-bin", 0};
char **saved_environ;
unsigned long saved;
int fd;
(void)__syscall2(SYS_mkdir, (long)dir, 0755); /* EEXIST is fine */
fd = open(script_path, T20_O_WRONLY | T20_O_CREAT | T20_O_TRUNC, 0755);
check(fd >= 0, "the helper script is created");
if (fd >= 0)
{
check(write(fd, script, sizeof(script) - 1) == (ssize_t)(sizeof(script) - 1),
"the helper script body is written");
check(close(fd) == 0, "the helper script descriptor is closed");
}
saved_environ = environ;
environ = path_env;
{
pid_t pid = fork();
int status;
if (pid < 0)
{
check(0, "execvp finds the helper via the custom PATH");
environ = saved_environ;
return;
}
if (pid == 0)
{
static char *av[] = {"vlibc-t20-helper", 0};
(void)execvp("vlibc-t20-helper", av);
__syscall1(SYS_exit_group, 127);
}
status = raw_wait(pid);
check(status == 5 << 8, "execvp finds the helper via PATH and it exits 5");
}
{
pid_t pid = fork();
int status;
if (pid < 0)
{
check(0, "execlp finds the helper via the custom PATH");
environ = saved_environ;
return;
}
if (pid == 0)
{
(void)execlp("vlibc-t20-helper", "vlibc-t20-helper", (char *)0);
__syscall1(SYS_exit_group, 127);
}
status = raw_wait(pid);
check(status == 5 << 8, "execlp finds the helper via PATH and it exits 5");
}
/* An empty PATH entry means the current directory (skipped here:
* the helper is not in cwd), and the search still reaches the
* second entry. */
environ = path_env_cwd;
{
pid_t pid = fork();
int status;
if (pid < 0)
{
check(0, "empty PATH entry is skipped");
environ = saved_environ;
return;
}
if (pid == 0)
{
static char *av[] = {"vlibc-t20-helper", 0};
(void)execvp("vlibc-t20-helper", av);
__syscall1(SYS_exit_group, 127);
}
status = raw_wait(pid);
check(status == 5 << 8, "an empty PATH entry means cwd and the search continues");
}
/* environ NULL -> the built-in default PATH finds /bin/true. */
environ = 0;
(void)exec_one("execvp(\"true\") with environ NULL (default PATH)", run_execvp);
/* Not found anywhere: -1 (errno written, bracketed). */
saved = tcb_slot1();
check(execvp("vlibc-no-such-helper-xyz", av_true) == -1,
"execvp of a missing helper returns -1");
tcb_slot1_set(saved);
environ = saved_environ;
(void)__syscall3(SYS_unlinkat, T20_AT_FDCWD, (long)script_path, 0);
(void)__syscall1(SYS_rmdir, (long)dir);
}
/*
* Failure scenarios (-f): every assertion is on the return value only,
* and the process exits through raw SYS_exit_group because the library
* writes errno on these paths (host-TCB hazard).
*/
// NOLINTBEGIN(bugprone-command-processor)
static int
failure_scenarios(void)
{
unsigned long saved;
int rc = 0;
saved = tcb_slot1();
if (execve("/bin/nonexistent-vlibc-xyz", av_true, env_ve) != -1)
{
say(2, "FAIL: execve of a nonexistent binary did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: execve of a nonexistent binary -> -1\n");
}
tcb_slot1_set(saved);
if (system(0) != 1)
{
say(2, "FAIL: system(NULL) did not return 1\n");
rc = 1;
}
else
{
say(1, "PASS: system(NULL) -> 1\n");
}
saved = tcb_slot1();
if (popen("echo hi", "x") != 0)
{
say(2, "FAIL: popen with an invalid mode did not return NULL\n");
rc = 1;
}
else
{
say(1, "PASS: popen with an invalid mode -> NULL\n");
}
tcb_slot1_set(saved);
saved = tcb_slot1();
if (fexecve(-1, av_true, env_ve) != -1)
{
say(2, "FAIL: fexecve(-1) did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: fexecve(-1) -> -1\n");
}
tcb_slot1_set(saved);
saved = tcb_slot1();
if (execvp("", av_true) != -1)
{
say(2, "FAIL: execvp(\"\") did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: execvp(\"\") -> -1\n");
}
tcb_slot1_set(saved);
saved = tcb_slot1();
if (getgroups(-1, 0) != -1)
{
say(2, "FAIL: getgroups(-1) did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: getgroups(-1) -> -1\n");
}
tcb_slot1_set(saved);
#if VLIBC_LEVEL_GE(2)
{
gid_t one = 0;
saved = tcb_slot1();
if (setgroups(1, &one) != -1)
{
say(2, "FAIL: setgroups without privilege did not return -1\n");
rc = 1;
}
else
{
say(1, "PASS: setgroups without privilege -> -1\n");
}
tcb_slot1_set(saved);
}
#endif
return rc;
}
// NOLINTEND(bugprone-command-processor)
int
main(int argc, char **argv)
{
if (argc == 2 && argv[1][0] == '-' && argv[1][1] == 'f')
{
/*
* The failure scenarios write errno inside the library; under
* the host libc that slot is glibc's private TLS state, so leave
* via the raw syscall without running host cleanup.
*/
int rc = failure_scenarios();
__syscall1(SYS_exit_group, rc);
return rc; /* not reached */
}
id_scenarios();
fork_scenario();
exec_scenarios();
system_scenario();
popen_scenario();
uid_scenarios();
exec_variants();
path_search_scenario();
#if VLIBC_LEVEL_GE(2)
session_scenarios();
vfork_scenario();
#endif
if (failures > 0)
{
say(2, "FAILED (");
say_dec(2, (unsigned long)failures);
say(2, " check(s))\n");
return 1;
}
say(1, "all process tests passed\n");
return 0;
}