prepare()-ing for execution

3 min read Original article ↗
#include <errno.h> #include <stdbool.h> #include <stdlib.h> #include <stdnoreturn.h> #include <unistd.h> #include <ucontext.h> #include "prepare.h" #define thread_local _Thread_local /* preparation state machine */ enum { PREP_START, /* initial state */ PREP_LEAVING, /* controlled termination / exec of child */ PREP_WINDDOWN, /* back in parent after controlled termination */ PREP_FUBAR, /* child has terminated unexepectedly */ }; struct prepper { int state; int child_pid; ucontext_t vfork_ctx; ucontext_t ret_ctx; void (*reaper)(int); char vfork_stack[8192]; }; static thread_local struct prepper *prepper; static void prepare1(void); static int unprepare(void); /* * Enter preparation state by calling vfork() and transferring control * to the nascent child. The preparation state cannot be entered * when it is already active. Following a succesful prepare() call, the * caller should set up the process as needed and call prep_execve() to * execute a new process or prep_exit() to exit the preparation state, * terminating the nascent child. * * If the forked child dies before calling prep_exit() or prep_execve() * succesfully, the function reaper is called with the pid of the now * dead child as its sole argument. If reaper is a null pointer or * after it returns, abort() is called. * * Returns 0 on success, -1 on error. Function fails with EBUSY * when the thread is already in preparation state (i.e. is a vfork-ed * child). Function can also fail for all reasons vfork() can fail. */ int prepare(void (*reaper)(int)) { /* TODO: block signals in forked child */ if (prepper != NULL) { errno = EBUSY; return (-1); } prepper = malloc(sizeof *prepper); if (prepper == NULL) return (-1); prepper->state = PREP_START; prepper->child_pid = -1; prepper->reaper = reaper; getcontext(&prepper->vfork_ctx); prepper->vfork_ctx.uc_stack.ss_sp = prepper->vfork_stack; prepper->vfork_ctx.uc_stack.ss_size = sizeof(prepper->vfork_stack); prepper->vfork_ctx.uc_link = NULL; makecontext(&prepper->vfork_ctx, prepare1, 0); swapcontext(&prepper->ret_ctx, &prepper->vfork_ctx); if (prepper->state == PREP_WINDDOWN) return (unprepare()); return (0); } /* * Helper function running in vfork_ctx. */ static void prepare1(void) { int pid; pid = vfork(); switch (pid) { case 0: break; default: /* in parent */ prepper->child_pid = pid; if (prepper->state != PREP_LEAVING) { /* something went very wrong */ prepper->state = PREP_FUBAR; if (prepper->reaper != NULL) prepper->reaper(pid); abort(); } /* fallthrough */ case -1: prepper->state = PREP_WINDDOWN; } setcontext(&prepper->ret_ctx); } /* * Exit from the nascent child. The preparation state is left * when this function has returned and control returns as parent. * Returns pid of zombie on success, -1 on error. Function fails * if the thread is not currently in the preparation state, * setting errno to ECHILD. */ int prep_exit(int status) { if (prepper == NULL) { errno = ECHILD; return (-1); } prepper->state = PREP_LEAVING; getcontext(&prepper->ret_ctx); if (prepper->state == PREP_WINDDOWN) return (unprepare()); _exit(status); } /* * Replace nascent child with program image path using argument * vector argv and environment envp. On success, leave preparation * state and return as parent with pid of executed child. On failure * remain in preparation state and return -1. If thread is not in * preparation state, errno is set to ECHILD. Function can fail for * any of the reasons execve() can fail. */ int prep_execve(const char *path, char *const argv[], char *const envp[]) { int res; if (prepper == NULL) { errno = ECHILD; return (-1); } prepper->state = PREP_LEAVING; getcontext(&prepper->ret_ctx); if (prepper->state == PREP_WINDDOWN) return (unprepare()); res = execve(path, argv, envp); prepper->state = PREP_START; return (res); } int ispreparing(void) { return (prepper != NULL); } /* * Leave the preparation state. Assumes we are in PREP_WINDDOWN state. * Return the pid of the child process. */ static int unprepare(void) { int pid; pid = prepper->child_pid; free(prepper); prepper = NULL; return (pid); }