/* * 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 #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; }