From 14c6fd6cf61d2935294b5d41d65f1fc15a789692 Mon Sep 17 00:00:00 2001 From: huntedbytheirs Date: Sat, 5 Sep 2026 16:55:14 -0400 Subject: [PATCH] feat(process): fork/exec/id/session + system/popen --- include/stdio.h | 15 + include/stdlib.h | 13 + include/unistd.h | 169 ++++++++- src/process/atfork.c | 111 ++++++ src/process/atfork_impl.h | 67 ++++ src/process/execl.c | 50 +++ src/process/execle.c | 50 +++ src/process/execlp.c | 47 +++ src/process/execv.c | 19 + src/process/execve.c | 33 ++ src/process/execvp.c | 104 +++++ src/process/fork.c | 77 ++++ src/process/groups.c | 34 ++ src/process/pid.c | 27 ++ src/process/session.c | 109 ++++++ src/process/system.c | 145 +++++++ src/process/uid.c | 70 ++++ src/stdio/popen.c | 201 ++++++++++ tests/test_process.c | 779 ++++++++++++++++++++++++++++++++++++++ 19 files changed, 2118 insertions(+), 2 deletions(-) create mode 100644 src/process/atfork.c create mode 100644 src/process/atfork_impl.h create mode 100644 src/process/execl.c create mode 100644 src/process/execle.c create mode 100644 src/process/execlp.c create mode 100644 src/process/execv.c create mode 100644 src/process/execve.c create mode 100644 src/process/execvp.c create mode 100644 src/process/fork.c create mode 100644 src/process/groups.c create mode 100644 src/process/pid.c create mode 100644 src/process/session.c create mode 100644 src/process/system.c create mode 100644 src/process/uid.c create mode 100644 src/stdio/popen.c create mode 100644 tests/test_process.c diff --git a/include/stdio.h b/include/stdio.h index e251cf6..ab20c2a 100644 --- a/include/stdio.h +++ b/include/stdio.h @@ -261,6 +261,21 @@ tmpfile(void); __attribute__((pure)) int fileno(FILE *stream); +/* + * Run command in a subshell ("sh -c command") with a pipe attached to + * its standard output (mode "r") or standard input (mode "w"). Only the + * two POSIX modes are accepted (the glibc "re"/"we" close-on-exec + * extension is not). Returns the stream, or NULL with errno set. + * pclose closes the stream, waits for the shell, and returns its + * termination status (the raw wait status, e.g. 0 for "exit 0"), or -1 + * when the stream was not opened by popen or the wait failed. + */ +FILE * +popen(const char *command, const char *mode); + +int +pclose(FILE *stream); + /* getc/putc/getchar/putchar as macros over fgetc/fputc (see above). */ #define getc(stream) fgetc(stream) #define putc(c, stream) fputc((c), (stream)) diff --git a/include/stdlib.h b/include/stdlib.h index 8a265ec..45ce218 100644 --- a/include/stdlib.h +++ b/include/stdlib.h @@ -463,6 +463,19 @@ mkstemp(char *); char * mkdtemp(char *); +/* process control (todo 20) */ + +/* + * Pass string to the command language interpreter: "sh -c string". + * Returns the shell's wait status (e.g. 768 for "exit 3"), 1 when + * string is NULL (a shell is always available), or -1 with errno set + * when the child cannot be created or reaped. During the run, SIGCHLD + * is blocked and SIGINT/SIGQUIT are ignored in the caller, as POSIX + * requires. + */ +int +system(const char *string); + #if VLIBC_LEVEL_GE(2) /* Level 2 (muslmimic): XSI and BSD environment extras. */ diff --git a/include/unistd.h b/include/unistd.h index deecc51..2019141 100644 --- a/include/unistd.h +++ b/include/unistd.h @@ -15,6 +15,15 @@ * Level 2 (muslmimic): dup3, pipe2 (Linux extensions), truncate (XSI), * lseek64 (glibc LFS alias of lseek on x86_64). * + * Process control (todo 20) is declared further down: + * + * Level 1 (onlyposix): fork, exec family (execl/execlp/execle/execv/ + * execvp/execve/fexecve), getpid, getppid, getuid, + * geteuid, getgid, getegid, setuid, seteuid, setgid, + * setegid, getgroups, setpgid, getpgrp, setsid. + * Level 2 (muslmimic): vfork, setpgrp (obsolescent), setgroups, getpgid, + * getsid, tcgetpgrp, tcsetpgrp (XSI). + * * The open-flag constants (O_RDONLY, O_CREAT, O_CLOEXEC, ...) belong to * and are deliberately not defined here; the oflag arguments below * are plain int and take their values from that header. The optional mode @@ -23,7 +32,10 @@ * * None of these declarations carry an intent attribute: every function * performs I/O with side effects and reports failures through errno, so - * const/pure would be unsound. + * const/pure would be unsound. The process functions additionally must not + * be const/pure because their results are process state — marking getpid + * const, for example, would let the compiler hoist it across fork() and + * observe the parent's pid inside the child. */ #include @@ -174,8 +186,119 @@ access(const char *path, int amode); int faccessat(int fd, const char *path, int amode, int flag); +/* Level 1 (POSIX base): process control. */ + +/* + * Create a child process that is a copy of the caller: fork() returns 0 + * in the child, the child's pid in the parent, and -1 with errno set on + * failure. + * + * Around the fork, the atfork hook table runs: the prepare handlers in + * reverse registration order before the fork, then the child handlers in + * the child and the parent handlers in the parent, both in registration + * order (POSIX pthread_atfork protocol). The table is registered by + * pthread_atfork() (todo 45) and is empty — a no-op — in a process + * without threads. + */ +pid_t +fork(void); + +/* + * Replace the calling process image. Success never returns; -1 with + * errno set otherwise. execl/execle/execlp take the arguments as a + * varargs list terminated by (char *)NULL (execle is followed by one + * final char *const envp[] argument); execv/execvp take an argv array. + * execvp and execlp search the PATH environment variable (default + * "/bin:/usr/bin") when file contains no '/'. execv/execvp/execl/execlp + * use the caller's environment; execve/execle/fexecve take envp. + */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +execl(const char *path, const char *arg0, ...); + +int +execle(const char *path, const char *arg0, ...); + +int +execlp(const char *file, const char *arg0, ...); +// NOLINTEND(bugprone-easily-swappable-parameters) + +int +execv(const char *path, char *const argv[]); + +int +execvp(const char *file, char *const argv[]); + +int +execve(const char *path, char *const argv[], char *const envp[]); + +/* + * Like execve, but the image is the open descriptor fd. + */ +int +fexecve(int fd, char *const argv[], char *const envp[]); + +/* + * Process ids. getpid/getppid/getuid/geteuid/getgid/getegid read the + * kernel's per-process ids; setuid/seteuid/setgid/setegid change them. + * seteuid/setegid change only the effective id (setresuid/setresgid + * underneath: Linux has no seteuid/setegid syscall). + */ +pid_t +getpid(void); + +pid_t +getppid(void); + +uid_t +getuid(void); + +uid_t +geteuid(void); + +gid_t +getgid(void); + +gid_t +getegid(void); + +int +setuid(uid_t uid); + +int +seteuid(uid_t euid); + +int +setgid(gid_t gid); + +int +setegid(gid_t egid); + +/* + * Supplementary groups: getgroups fills grouplist with up to gidsetsize + * group ids and returns the total count (0 can be passed to count only); + * setgroups (level 2, XSI) installs the list. + */ +int +getgroups(int gidsetsize, gid_t grouplist[]); + +/* + * Session control: setpgid moves pid into process group pgid (0 means + * the caller / the caller's pid); getpgrp returns the caller's process + * group; setsid creates a new session with the caller as leader and + * returns the new session id. + */ +int +setpgid(pid_t pid, pid_t pgid); + +pid_t +getpgrp(void); + +pid_t +setsid(void); + #if VLIBC_LEVEL_GE(2) -/* Level 2 (muslmimic): Linux extensions + XSI. */ +/* Level 2 (muslmimic): Linux extensions + XSI + obsolescent. */ /* * Like dup2(), but with descriptor flags (O_CLOEXEC from ) applied @@ -205,6 +328,48 @@ truncate(const char *path, off_t length); */ off_t lseek64(int fildes, off_t offset, int whence); + +/* + * vfork (obsolescent): like fork(), but the child borrows the parent's + * address space until it execs or exits. + */ +pid_t +vfork(void); + +/* + * setpgrp (obsolescent): setpgid(0, 0) — the caller becomes the leader + * of its own process group. + */ +int +setpgrp(void); + +/* + * setgroups (XSI): install the supplementary group list of gidsetsize + * entries. Requires privilege. + */ +int +setgroups(size_t gidsetsize, const gid_t *grouplist); + +/* + * getpgid/getsid (XSI): the process group / session of pid (0 = the + * caller). + */ +pid_t +getpgid(pid_t pid); + +pid_t +getsid(pid_t pid); + +/* + * Controlling-terminal foreground process group (XSI): tcgetpgrp reads + * the foreground group of the terminal on fildes; tcsetpgrp makes pgid + * the foreground group. + */ +pid_t +tcgetpgrp(int fildes); + +int +tcsetpgrp(int fildes, pid_t pgid); #endif /* VLIBC_LEVEL_GE(2) */ #ifdef __cplusplus diff --git a/src/process/atfork.c b/src/process/atfork.c new file mode 100644 index 0000000..68e0d01 --- /dev/null +++ b/src/process/atfork.c @@ -0,0 +1,111 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "atfork_impl.h" + +#include "../internal/malloc.h" + +/* + * The atfork hook table (todo 20). + * + * The list is a doubly linked chain in registration order (head = oldest + * registration). fork() runs the phases through __vlibc_atfork_prepare/ + * parent/child around the syscall; pthread_atfork (todo 45) appends + * entries via __vlibc_atfork_register. Nodes live on the malloc heap: + * registration happens long after the allocator is up, and fork() itself + * never allocates — the phases only walk the existing chain. + * + * Phase order (POSIX 1003.1-2008 pthread_atfork): + * - prepare: reverse registration order (tail to head); + * - parent and child: registration order (head to tail). + */ + +struct vlibc_atfork_entry *volatile __vlibc_atfork_list = 0; + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +__vlibc_atfork_register(void (*prepare)(void), void (*parent)(void), void (*child)(void)) +{ + struct vlibc_atfork_entry *node; + struct vlibc_atfork_entry *tail; + + node = (struct vlibc_atfork_entry *)__libc_malloc(sizeof(*node)); + if (node == 0) + { + errno = ENOMEM; + return -1; + } + node->prepare = prepare; + node->parent = parent; + node->child = child; + node->prev = 0; + node->next = 0; + + /* Append at the tail so the chain stays in registration order. */ + if (__vlibc_atfork_list == 0) + { + __vlibc_atfork_list = node; + return 0; + } + tail = __vlibc_atfork_list; + while (tail->next != 0) + { + tail = tail->next; + } + tail->next = node; + node->prev = tail; + return 0; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +void +__vlibc_atfork_prepare(void) +{ + struct vlibc_atfork_entry *node = __vlibc_atfork_list; + + /* Reverse registration order: walk to the tail, then back. */ + while (node != 0 && node->next != 0) + { + node = node->next; + } + for (; node != 0; node = node->prev) + { + if (node->prepare != 0) + { + node->prepare(); + } + } +} + +void +__vlibc_atfork_parent(void) +{ + struct vlibc_atfork_entry *node; + + /* Registration order. */ + for (node = __vlibc_atfork_list; node != 0; node = node->next) + { + if (node->parent != 0) + { + node->parent(); + } + } +} + +void +__vlibc_atfork_child(void) +{ + struct vlibc_atfork_entry *node; + + /* Registration order. */ + for (node = __vlibc_atfork_list; node != 0; node = node->next) + { + if (node->child != 0) + { + node->child(); + } + } +} diff --git a/src/process/atfork_impl.h b/src/process/atfork_impl.h new file mode 100644 index 0000000..378d0b9 --- /dev/null +++ b/src/process/atfork_impl.h @@ -0,0 +1,67 @@ +#ifndef VLIBC_PROCESS_ATFORK_IMPL_H +#define VLIBC_PROCESS_ATFORK_IMPL_H + +/* + * vlibc — internal atfork hook table (todo 20). + * + * fork() runs three hook phases around the clone operation: + * + * 1. prepare — in the parent, before the fork, in REVERSE registration + * order (POSIX 1003.1-2008 pthread_atfork: "the prepare handlers are + * called in the reverse order in which they were registered"); + * 2. parent — in the parent, after the fork, in registration order; + * 3. child — in the child, after the fork, in registration order + * (POSIX: "the parent and child handlers are called in the order in + * which they were registered"). + * + * The list is a doubly linked chain in registration order (head = oldest + * registration); the phases walk it head-to-tail or tail-to-head as the + * ordering above demands. It starts NULL — with no threads and no + * registrations every phase is a no-op, so fork() keeps working before + * todo 45 (pthread_atfork) lands. Todo 45 appends entries through + * __vlibc_atfork_register(); nothing else needs to change here. + * + * The names sit in the implementation-reserved namespace (this is the + * library's private seam, never public API), so the corresponding + * bugprone checks are waived per the house NOLINT convention. + */ + +#include + +#include "../internal/libc.h" + +// NOLINTBEGIN(bugprone-reserved-identifier) + +struct vlibc_atfork_entry +{ + void (*prepare)(void); + void (*parent)(void); + void (*child)(void); + struct vlibc_atfork_entry *prev; /* toward older registrations */ + struct vlibc_atfork_entry *next; /* toward newer registrations */ +}; + +/* Head of the registration list; NULL until the first registration. */ +hidden extern struct vlibc_atfork_entry *volatile __vlibc_atfork_list; + +/* + * Append a handler set to the list. Returns 0, or -1 with errno ENOMEM + * when the node cannot be allocated (pthread_atfork maps that to its + * error return). Only pthread_atfork (todo 45) calls this. + */ +hidden int +__vlibc_atfork_register(void (*prepare)(void), void (*parent)(void), void (*child)(void)); + +/* The three phases; fork() calls these around the syscall. */ +hidden void +__vlibc_atfork_prepare(void); + +hidden void +__vlibc_atfork_parent(void); + +hidden void +__vlibc_atfork_child(void); + +// NOLINTEND(bugprone-reserved-identifier) + +#endif /* VLIBC_PROCESS_ATFORK_IMPL_H */ diff --git a/src/process/execl.c b/src/process/execl.c new file mode 100644 index 0000000..a3194c1 --- /dev/null +++ b/src/process/execl.c @@ -0,0 +1,50 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include "../internal/syscall.h" + +/* + * execl: execve with the arguments collected from the varargs list, which + * is terminated by (char *)NULL. POSIX sets no argument-count limit, so + * the list is counted first and staged on a VLA; GCC supports VLA in + * every mode vlibc targets (the C23 auto-VLA optionality does not bite + * here). The POSIX signature is fixed; the adjacent-parameter check is + * waived per the house NOLINT convention. + */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +execl(const char *path, const char *arg0, ...) +{ + va_list ap; + int argc; + int i; + int ret; + + va_start(ap, arg0); + argc = 1; + while (va_arg(ap, const char *) != 0) + { + argc++; + } + va_end(ap); + + { + char *argv[argc + 1]; + + argv[0] = (char *)arg0; + va_start(ap, arg0); + for (i = 1; i <= argc; i++) + { + argv[i] = va_arg(ap, char *); + } + va_end(ap); + ret = execve(path, argv, environ); + } + return ret; +} +// NOLINTEND(bugprone-easily-swappable-parameters) diff --git a/src/process/execle.c b/src/process/execle.c new file mode 100644 index 0000000..583a135 --- /dev/null +++ b/src/process/execle.c @@ -0,0 +1,50 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * execle: like execl, but the varargs list is terminated by (char *)NULL + * followed by one final char *const envp[] argument. The argv staging + * uses a VLA (see execl.c); envp is read directly from the varargs. The + * POSIX signature is fixed; the adjacent-parameter check is waived per + * the house NOLINT convention. + */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +execle(const char *path, const char *arg0, ...) +{ + va_list ap; + int argc; + int i; + char **envp; + int ret; + + va_start(ap, arg0); + argc = 1; + while (va_arg(ap, const char *) != 0) + { + argc++; + } + envp = va_arg(ap, char **); + va_end(ap); + + { + char *argv[argc + 1]; + + argv[0] = (char *)arg0; + va_start(ap, arg0); + for (i = 1; i <= argc; i++) + { + argv[i] = va_arg(ap, char *); + } + va_end(ap); + ret = execve(path, argv, envp); + } + return ret; +} +// NOLINTEND(bugprone-easily-swappable-parameters) diff --git a/src/process/execlp.c b/src/process/execlp.c new file mode 100644 index 0000000..9743547 --- /dev/null +++ b/src/process/execlp.c @@ -0,0 +1,47 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * execlp: like execl, but the file is located through PATH (see + * execvp.c). The argv staging uses a VLA; the POSIX signature is fixed + * and the adjacent-parameter check is waived per the house NOLINT + * convention. + */ +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +int +execlp(const char *file, const char *arg0, ...) +{ + va_list ap; + int argc; + int i; + int ret; + + va_start(ap, arg0); + argc = 1; + while (va_arg(ap, const char *) != 0) + { + argc++; + } + va_end(ap); + + { + char *argv[argc + 1]; + + argv[0] = (char *)arg0; + va_start(ap, arg0); + for (i = 1; i <= argc; i++) + { + argv[i] = va_arg(ap, char *); + } + va_end(ap); + ret = execvp(file, argv); + } + return ret; +} +// NOLINTEND(bugprone-easily-swappable-parameters) \ No newline at end of file diff --git a/src/process/execv.c b/src/process/execv.c new file mode 100644 index 0000000..2131f41 --- /dev/null +++ b/src/process/execv.c @@ -0,0 +1,19 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * execv: execve with the calling process's environment. environ is the + * global installed by the startup code (src/start/environ.c); the internal + * syscall layer is not needed here beyond execve itself. + */ +int +execv(const char *path, char *const argv[]) +{ + return execve(path, argv, environ); +} diff --git a/src/process/execve.c b/src/process/execve.c new file mode 100644 index 0000000..fae6ba7 --- /dev/null +++ b/src/process/execve.c @@ -0,0 +1,33 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* AT_EMPTY_PATH for execveat (fexecve): resolve the fd itself. */ +#define VLIBC_PROC_AT_EMPTY_PATH 0x1000 + +/* + * execve: replace the process image with the file at path. Success never + * returns; -1 with errno set otherwise. + */ +int +execve(const char *path, char *const argv[], char *const envp[]) +{ + return syscall_ret(__syscall3(SYS_execve, (long)path, (long)argv, (long)envp)); +} + +/* + * fexecve: like execve, but the image is the open descriptor fd. Runs the + * kernel's execveat with an empty path plus AT_EMPTY_PATH. A descriptor + * opened with O_PATH cannot be exec'd on kernels before 6.3; a regular + * open works everywhere. + */ +int +fexecve(int fd, char *const argv[], char *const envp[]) +{ + return syscall_ret( + __syscall5(SYS_execveat, fd, (long)"", (long)argv, (long)envp, VLIBC_PROC_AT_EMPTY_PATH)); +} diff --git a/src/process/execvp.c b/src/process/execvp.c new file mode 100644 index 0000000..c015a9f --- /dev/null +++ b/src/process/execvp.c @@ -0,0 +1,104 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include "../internal/syscall.h" + +/* + * execvp: search PATH for file and execve it with the caller's + * environment. If file contains '/', execve is tried directly. + * + * PATH is read via getenv(); a missing PATH defaults to "/bin:/usr/bin" + * (the traditional pathconf default). An empty entry — a leading, + * trailing, or doubled colon — means the current directory, per POSIX. + * + * Error policy (POSIX XBD exec): ENOENT and ENOTDIR from a candidate are + * skipped while the rest of PATH is searched, and EACCES is remembered; + * any other error aborts immediately. When every candidate fails, errno + * is EACCES if any candidate failed with EACCES, else ENOENT. + */ +// NOLINTBEGIN(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) +int +execvp(const char *file, char *const argv[]) +{ + const char *path; + const char *p; + size_t flen; + int saw_eacces; + + if (file == 0 || file[0] == '\0') + { + errno = ENOENT; + return -1; + } + if (strchr(file, '/') != 0) + { + return execve(file, argv, environ); + } + path = getenv("PATH"); + if (path == 0) + { + path = "/bin:/usr/bin"; + } + flen = strlen(file); + saw_eacces = 0; + + for (p = path; *p != '\0';) + { + const char *end = p; + size_t dlen; + + while (*end != '\0' && *end != ':') + { + end++; + } + dlen = (size_t)(end - p); + + { + char candidate[dlen + flen + 2]; /* dir + '/' + file + NUL */ + long r; + + if (dlen == 0) + { + /* Empty PATH entry: the current directory. */ + memcpy(candidate, file, flen + 1); + } + else + { + memcpy(candidate, p, dlen); + candidate[dlen] = '/'; + memcpy(candidate + dlen + 1, file, flen + 1); + } + r = __syscall3(SYS_execve, (long)candidate, (long)argv, (long)environ); + if (r < 0) + { + int e = (int)-r; + + if (e == EACCES) + { + saw_eacces = 1; + } + else if (e != ENOENT && e != ENOTDIR) + { + /* A real error: stop the search and report it. */ + errno = e; + return -1; + } + } + } + if (*end == '\0') + { + break; + } + p = end + 1; + } + + errno = saw_eacces ? EACCES : ENOENT; + return -1; +} +// NOLINTEND(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling) diff --git a/src/process/fork.c b/src/process/fork.c new file mode 100644 index 0000000..55a7dbb --- /dev/null +++ b/src/process/fork.c @@ -0,0 +1,77 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +#include "atfork_impl.h" + +/* + * fork(): create a child that is a copy of the caller. + * + * Hook invocation order (documented for todo 45, pthread_atfork): + * + * 1. __vlibc_atfork_prepare() — reverse registration order (POSIX), + * before the syscall; + * 2. SYS_fork; + * 3. __vlibc_atfork_child() in the child, __vlibc_atfork_parent() in + * the parent — both in registration order (POSIX). + * + * The hook list is NULL by default (no threads, no registrations), so a + * bare fork() costs exactly the three no-op walker calls; with a static + * link the compiler folds them to nothing. + * + * SYS_fork is sufficient here: it is the kernel's plain + * clone(SIGCHLD, 0). There is no thread runtime yet (todo 45); once there + * is, fork must switch to SYS_clone with a clearable child tid so that + * pthread_join can reap without SIGCHLD races. + */ +pid_t +fork(void) +{ + pid_t ret; + + __vlibc_atfork_prepare(); + ret = syscall_ret(__syscall0(SYS_fork)); + if (ret == 0) + { + /* Child: run the child phase (registration order). */ + __vlibc_atfork_child(); + } + else if (ret > 0) + { + /* Parent: run the parent phase (registration order). */ + __vlibc_atfork_parent(); + } + return ret; +} + +#if VLIBC_LEVEL_GE(2) + +/* + * vfork(): like fork(), but POSIX lets the child borrow the parent's + * address space until it execs or exits (POSIX [OB], level 2). + * + * Implemented as fork(). Rationale (glibc makes the identical choice on + * Linux): a C vfork cannot honor the "no function call between vfork and + * exec" contract. The child's rsp is the parent's suspended rsp + 8 + * (the child has already popped vfork's return address), so the child's + * first call — even into this library's own execve wrapper — writes its + * return address exactly over the parent's suspended return-address + * slot; when the parent resumes it returns through the clobbered slot + * into the child's continuation. Verified empirically (the resumed + * parent landed in the child's exec-failure path). The kernel's COW + * fork already makes fork() as cheap as vfork for all practical + * purposes, and the [OB] semantics differences (no parent suspension, + * no shared stack) are explicitly tolerated by the plan. The atfork + * hooks run, as they do for glibc's fork-based vfork. + */ +pid_t +vfork(void) +{ + return fork(); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/process/groups.c b/src/process/groups.c new file mode 100644 index 0000000..35a4971 --- /dev/null +++ b/src/process/groups.c @@ -0,0 +1,34 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "../internal/syscall.h" + +/* + * Supplementary group access. + * + * getgroups (POSIX base): fill grouplist with up to gidsetsize + * supplementary group ids and return the count; with gidsetsize 0 only + * the count is returned and nothing is written. + * + * setgroups (XSI, level 2): install the given supplementary group list. + * Requires privilege (CAP_SETGID). + */ +int +getgroups(int gidsetsize, gid_t grouplist[]) +{ + return syscall_ret(__syscall2(SYS_getgroups, gidsetsize, (long)grouplist)); +} + +#if VLIBC_LEVEL_GE(2) + +int +setgroups(size_t gidsetsize, const gid_t *grouplist) +{ + return syscall_ret(__syscall2(SYS_setgroups, (long)gidsetsize, (long)grouplist)); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/process/pid.c b/src/process/pid.c new file mode 100644 index 0000000..63d2901 --- /dev/null +++ b/src/process/pid.c @@ -0,0 +1,27 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * getpid/getppid: plain kernel reads. + * + * No const/pure attribute: the result is process state, not a pure + * function of the arguments. In particular, const would let the compiler + * hoist a getpid call across fork() and observe the parent's pid inside + * the child. + */ +pid_t +getpid(void) +{ + return (pid_t)syscall_ret(__syscall0(SYS_getpid)); +} + +pid_t +getppid(void) +{ + return (pid_t)syscall_ret(__syscall0(SYS_getppid)); +} diff --git a/src/process/session.c b/src/process/session.c new file mode 100644 index 0000000..f023905 --- /dev/null +++ b/src/process/session.c @@ -0,0 +1,109 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* Foreground-process-group ioctls (x86_64 _IO/_IOR derivations). */ +#define VLIBC_PROC_TIOCGPGRP 0x540F +#define VLIBC_PROC_TIOCSPGRP 0x5410 + +/* + * Process group and session control (POSIX base): setpgid, getpgrp, + * setsid. The XSI/obsolescent extras (setpgrp, getpgid, getsid, + * tcgetpgrp, tcsetpgrp) are gated at level 2 below. + */ + +/* + * setpgid: move pid (0 = the caller) into process group pgid (0 = pid's + * own value). A process can change groups for itself and its children; + * session leaders cannot change groups. + */ +int +setpgid(pid_t pid, pid_t pgid) +{ + return syscall_ret(__syscall2(SYS_setpgid, pid, pgid)); +} + +/* + * getpgrp: the caller's process group. + */ +pid_t +getpgrp(void) +{ + return (pid_t)syscall_ret(__syscall0(SYS_getpgrp)); +} + +/* + * setsid: make the caller the leader of a new session and process group, + * detaching it from the controlling terminal. Returns the new session id + * (the caller's pid), or -1 with errno EPERM when the caller is already + * a process group leader. + */ +pid_t +setsid(void) +{ + return (pid_t)syscall_ret(__syscall0(SYS_setsid)); +} + +#if VLIBC_LEVEL_GE(2) + +/* Level 2 (muslmimic): XSI and obsolescent session extras. */ + +/* + * setpgrp (obsolescent): setpgid(0, 0) — the caller becomes the leader + * of its own process group. + */ +int +setpgrp(void) +{ + return setpgid(0, 0); +} + +/* + * getpgid (XSI): the process group of pid (0 = the caller). + */ +pid_t +getpgid(pid_t pid) +{ + return (pid_t)syscall_ret(__syscall1(SYS_getpgid, pid)); +} + +/* + * getsid (XSI): the session id of pid (0 = the caller). + */ +pid_t +getsid(pid_t pid) +{ + return (pid_t)syscall_ret(__syscall1(SYS_getsid, pid)); +} + +/* + * tcgetpgrp (XSI): the foreground process group of the terminal on + * fildes, or -1 with errno set (ENOTTY when fildes is not a terminal). + */ +pid_t +tcgetpgrp(int fildes) +{ + int pgrp = 0; + + if (syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_PROC_TIOCGPGRP, (long)&pgrp)) < 0) + { + return (pid_t)-1; + } + return (pid_t)pgrp; +} + +/* + * tcsetpgrp (XSI): make pgid the foreground process group of the + * terminal on fildes. + */ +int +tcsetpgrp(int fildes, pid_t pgid) +{ + return syscall_ret(__syscall3(SYS_ioctl, fildes, VLIBC_PROC_TIOCSPGRP, pgid)); +} + +#endif /* VLIBC_LEVEL_GE(2) */ diff --git a/src/process/system.c b/src/process/system.c new file mode 100644 index 0000000..e2868b4 --- /dev/null +++ b/src/process/system.c @@ -0,0 +1,145 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include + +#include "../internal/syscall.h" + +/* + * system(cmd): run "sh -c cmd" and return the shell's wait status. + * + * - NULL cmd: report whether a command processor exists. /bin/sh is + * assumed here, as everywhere in POSIX, so this always returns 1. + * - otherwise: fork, exec /bin/sh with {"sh", "-c", cmd}, wait for it, + * and return the raw wait4 status (e.g. 768 == "exit 3"). + * - -1 with errno set when the child cannot be created or reaped. + * + * POSIX requires the parent to block SIGCHLD and ignore SIGINT/SIGQUIT + * while the shell runs. There is no signal.h yet (todo 28): the mask and + * dispositions are manipulated with raw rt_sigprocmask/rt_sigaction + * syscalls on the kernel's one-word sigset_t, so this file takes no + * dependency on the signal todo. The child restores the saved mask and + * dispositions before exec, matching glibc/musl behavior. + * + * The child runs only async-signal-safe code between fork and exec: the + * raw signal syscalls, execve, and the raw exit syscall. fork()'s atfork + * hooks run (prepare/child), which is what pthread_atfork (todo 45) + * wants. This fork-based implementation matches glibc; POSIX does not + * require system() to be async-signal-safe itself. + */ + +/* + * Kernel sigaction layout (x86_64): handler, flags, restorer, and the + * one-word sigset_t mask. This is the syscall ABI, NOT the + * struct — todo 28 owns that one. + */ +struct vlibc_sys_sigaction +{ + void (*handler)(int); + unsigned long flags; + void (*restorer)(void); + unsigned long mask; +}; + +#define VLIBC_PROC_SIG_IGN ((void (*)(int))1) +#define VLIBC_PROC_SIG_BLOCK 0 +#define VLIBC_PROC_SIG_SETMASK 2 +#define VLIBC_PROC_SIGINT 2 +#define VLIBC_PROC_SIGQUIT 3 +#define VLIBC_PROC_SIGCHLD 17 + +int +system(const char *string) +{ + struct vlibc_sys_sigaction ignore; + struct vlibc_sys_sigaction old_int; + struct vlibc_sys_sigaction old_quit; + unsigned long block_mask = 1UL << (VLIBC_PROC_SIGCHLD - 1); + unsigned long old_mask = 0; + pid_t pid; + int status = 0; + + if (string == 0) + { + /* A command processor is always available. */ + return 1; + } + + ignore.handler = VLIBC_PROC_SIG_IGN; + ignore.flags = 0; + ignore.restorer = 0; + ignore.mask = 0; + + /* 1. Parent: ignore SIGINT/SIGQUIT, block SIGCHLD (all saved). */ + if (syscall_ret( + __syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGINT, (long)&ignore, (long)&old_int, 8)) < 0) + { + return -1; + } + if (syscall_ret(__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGQUIT, (long)&ignore, (long)&old_quit, + 8)) < 0) + { + /* Undo the SIGINT change above. */ + (void)syscall_ret(__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGINT, (long)&old_int, 0, 8)); + return -1; + } + if (syscall_ret(__syscall4(SYS_rt_sigprocmask, VLIBC_PROC_SIG_BLOCK, (long)&block_mask, + (long)&old_mask, 8)) < 0) + { + (void)syscall_ret(__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGINT, (long)&old_int, 0, 8)); + (void)syscall_ret(__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGQUIT, (long)&old_quit, 0, 8)); + return -1; + } + + /* 2. Fork. */ + pid = fork(); + if (pid == 0) + { + char *sh_argv[4]; + + /* Child: restore the saved state, then exec the shell. */ + (void)__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGINT, (long)&old_int, 0, 8); + (void)__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGQUIT, (long)&old_quit, 0, 8); + (void)__syscall4(SYS_rt_sigprocmask, VLIBC_PROC_SIG_SETMASK, (long)&old_mask, 0, 8); + sh_argv[0] = "sh"; + sh_argv[1] = "-c"; + sh_argv[2] = (char *)string; + sh_argv[3] = 0; + execve("/bin/sh", sh_argv, environ); + __syscall1(SYS_exit_group, 127); /* exec failed */ + } + if (pid < 0) + { + /* Restore the parent state; errno was set by fork(). */ + (void)__syscall4(SYS_rt_sigprocmask, VLIBC_PROC_SIG_SETMASK, (long)&old_mask, 0, 8); + (void)__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGINT, (long)&old_int, 0, 8); + (void)__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGQUIT, (long)&old_quit, 0, 8); + return -1; + } + + /* 3. Parent: wait for the shell (raw SYS_wait4; todo 23 owns the + * public wrapper). */ + for (;;) + { + long r = __syscall4(SYS_wait4, pid, (long)&status, 0, 0); + + if (r < 0 && -r == EINTR) + { + continue; + } + if (r < 0) + { + status = syscall_ret(r); /* -1 + errno */ + } + break; + } + + /* 4. Parent: restore the saved state. */ + (void)__syscall4(SYS_rt_sigprocmask, VLIBC_PROC_SIG_SETMASK, (long)&old_mask, 0, 8); + (void)__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGINT, (long)&old_int, 0, 8); + (void)__syscall4(SYS_rt_sigaction, VLIBC_PROC_SIGQUIT, (long)&old_quit, 0, 8); + return status; +} diff --git a/src/process/uid.c b/src/process/uid.c new file mode 100644 index 0000000..6c55c52 --- /dev/null +++ b/src/process/uid.c @@ -0,0 +1,70 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include "../internal/syscall.h" + +/* + * Real/effective uid and gid accessors and mutators. None of the getters + * carries a const/pure attribute: the values are process state, not a + * pure function of the arguments, and the set functions change what the + * get functions read. + */ +uid_t +getuid(void) +{ + return (uid_t)syscall_ret(__syscall0(SYS_getuid)); +} + +uid_t +geteuid(void) +{ + return (uid_t)syscall_ret(__syscall0(SYS_geteuid)); +} + +gid_t +getgid(void) +{ + return (gid_t)syscall_ret(__syscall0(SYS_getgid)); +} + +gid_t +getegid(void) +{ + return (gid_t)syscall_ret(__syscall0(SYS_getegid)); +} + +/* + * setuid: set the real, effective, and saved ids (kernel SYS_setuid). + * When unprivileged, uid must match the current real or effective id. + */ +int +setuid(uid_t uid) +{ + return syscall_ret(__syscall1(SYS_setuid, uid)); +} + +/* + * seteuid: set only the effective id. Linux has no seteuid syscall; the + * kernel contract is setresuid(-1, euid, -1), which is what glibc and + * musl issue too. + */ +int +seteuid(uid_t euid) +{ + return syscall_ret(__syscall3(SYS_setresuid, -1, euid, -1)); +} + +int +setgid(gid_t gid) +{ + return syscall_ret(__syscall1(SYS_setgid, gid)); +} + +int +setegid(gid_t egid) +{ + return syscall_ret(__syscall3(SYS_setresgid, -1, egid, -1)); +} diff --git a/src/stdio/popen.c b/src/stdio/popen.c new file mode 100644 index 0000000..b12e586 --- /dev/null +++ b/src/stdio/popen.c @@ -0,0 +1,201 @@ +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include + +#include "stdio_impl.h" + +#include "../internal/syscall.h" + +/* + * popen/pclose (POSIX base, todo 20): run "sh -c cmd" with a pipe + * attached to its stdin ("w") or stdout ("r"). + * + * The stream is a todo-15 FILE built on the FILE core. popen does NOT go + * through the public fdopen(): todo 15's fdopen validates the requested + * mode against the descriptor's F_GETFL access mode by masking with + * O_RDWR, so a plain O_WRONLY descriptor is misread as O_RDONLY and a + * "w" fdopen is rejected — and the pipe write end is exactly that case + * (F_SETFL cannot promote a pipe end to O_RDWR). popen knows the access + * mode of its own freshly created pipe end, so it goes straight to the + * FILE core: stdio_parse_mode + stdio_alloc_file. Only the POSIX modes + * "r" and "w" are accepted; the glibc "re"/"we" close-on-exec extension + * is not. There is no sys/wait.h yet (todo 23): pclose waits via the + * raw SYS_wait4, and the child exits via the raw SYS_exit_group when + * the exec fails. + * + * Registry: a static table mapping FILE* to the shell pid. The todo-15 + * FILE struct has no spare field for the pid, so pclose looks the + * stream up here. FOPEN_MAX (16) slots; a full table fails with EMFILE. + * Not thread-safe — fine while there is no thread runtime (todo 45). + * + * pclose closes the stream first, then waits: for "w" streams the close + * flushes the buffered writes and delivers EOF, letting the shell exit; + * waiting first could deadlock on a full pipe. A stream not opened by + * popen is still closed, and pclose returns -1 as POSIX requires. A wait + * failure (ECHILD) returns -1 with errno set. + */ + +struct vlibc_popen_entry +{ + FILE *stream; + pid_t pid; +}; + +static struct vlibc_popen_entry popen_table[FOPEN_MAX]; + +// NOLINTBEGIN(bugprone-easily-swappable-parameters) +FILE * +popen(const char *command, const char *mode) +{ + int fds[2] = {0}; /* written by SYS_pipe; the analyzer cannot model it */ + int parent_fd; + int child_fd; + int read_mode; + int m; + int oflags; + pid_t pid; + FILE *f; + int i; + + if (mode[0] == 'r' && mode[1] == '\0') + { + read_mode = 1; + } + else if (mode[0] == 'w' && mode[1] == '\0') + { + read_mode = 0; + } + else + { + errno = EINVAL; + return 0; + } + (void)stdio_parse_mode(mode, &m, &oflags); /* "r"/"w" always parse */ + + if (syscall_ret(__syscall1(SYS_pipe, (long)fds)) < 0) + { + return 0; + } + if (read_mode) + { + parent_fd = fds[0]; /* parent reads the shell's stdout */ + child_fd = fds[1]; + } + else + { + parent_fd = fds[1]; /* parent writes the shell's stdin */ + child_fd = fds[0]; + } + + pid = fork(); + if (pid == 0) + { + char *sh_argv[4]; + int target = read_mode ? 1 : 0; + + /* + * Child: wire the pipe end onto the shell's fd (0 for "w", 1 + * for "r"), close everything else. The guards cover the + * (theoretical) case where the pipe handed us fd 0 or 1. + */ + if (parent_fd != target) + { + (void)__syscall1(SYS_close, parent_fd); + } + if (child_fd != target) + { + (void)__syscall2(SYS_dup2, child_fd, target); + (void)__syscall1(SYS_close, child_fd); + } + sh_argv[0] = "sh"; + sh_argv[1] = "-c"; + sh_argv[2] = (char *)command; + sh_argv[3] = 0; + execve("/bin/sh", sh_argv, environ); + __syscall1(SYS_exit_group, 127); /* exec failed */ + } + + /* Parent. */ + (void)__syscall1(SYS_close, child_fd); + if (pid < 0) + { + (void)__syscall1(SYS_close, parent_fd); + return 0; + } + + f = stdio_alloc_file(parent_fd, m); + if (f == 0) + { + /* Drop the descriptor and reap the shell we just spawned. */ + (void)__syscall1(SYS_close, parent_fd); + (void)__syscall4(SYS_wait4, pid, 0, 0, 0); + return 0; + } + + for (i = 0; i < FOPEN_MAX; i++) + { + if (popen_table[i].stream == 0) + { + popen_table[i].stream = f; + popen_table[i].pid = pid; + return f; + } + } + + /* Registry full: drop the stream and reap the shell. */ + (void)fclose(f); + (void)__syscall4(SYS_wait4, pid, 0, 0, 0); + errno = EMFILE; + return 0; +} +// NOLINTEND(bugprone-easily-swappable-parameters) + +int +pclose(FILE *stream) +{ + pid_t pid = 0; + int found = 0; + int status = 0; + int i; + + for (i = 0; i < FOPEN_MAX; i++) + { + if (popen_table[i].stream == stream) + { + pid = popen_table[i].pid; + popen_table[i].stream = 0; + popen_table[i].pid = 0; + found = 1; + break; + } + } + + (void)fclose(stream); + + if (!found) + { + /* Not a popen stream: POSIX wants -1. The stream is still + * closed. */ + return -1; + } + + for (;;) + { + long r = __syscall4(SYS_wait4, pid, (long)&status, 0, 0); + + if (r < 0 && -r == EINTR) + { + continue; + } + if (r < 0) + { + return syscall_ret(r); /* -1 + errno (e.g. ECHILD) */ + } + return status; + } +} diff --git a/tests/test_process.c b/tests/test_process.c new file mode 100644 index 0000000..98df345 --- /dev/null +++ b/tests/test_process.c @@ -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 + +#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; +}