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
+15
View File
@@ -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))
+13
View File
@@ -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. */
+167 -2
View File
@@ -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
* <fcntl.h> 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 <vlibc/features.h>
@@ -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 <fcntl.h>) 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
+111
View File
@@ -0,0 +1,111 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#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();
}
}
}
+67
View File
@@ -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 <stddef.h>
#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 */
+50
View File
@@ -0,0 +1,50 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdarg.h>
#include <stdlib.h>
#include <unistd.h>
#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)
+50
View File
@@ -0,0 +1,50 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdarg.h>
#include <unistd.h>
#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)
+47
View File
@@ -0,0 +1,47 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdarg.h>
#include <unistd.h>
#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)
+19
View File
@@ -0,0 +1,19 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdlib.h>
#include <unistd.h>
#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);
}
+33
View File
@@ -0,0 +1,33 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#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));
}
+104
View File
@@ -0,0 +1,104 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#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)
+77
View File
@@ -0,0 +1,77 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#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) */
+34
View File
@@ -0,0 +1,34 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stddef.h>
#include <unistd.h>
#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) */
+27
View File
@@ -0,0 +1,27 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#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));
}
+109
View File
@@ -0,0 +1,109 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#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) */
+145
View File
@@ -0,0 +1,145 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stdlib.h>
#include <unistd.h>
#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 <signal.h>
* 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;
}
+70
View File
@@ -0,0 +1,70 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <unistd.h>
#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));
}
+201
View File
@@ -0,0 +1,201 @@
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#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;
}
}
+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;
}