From b6c8f412a2a89326519d689b01f3859a3f548d65 Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Wed, 28 Jan 2026 18:49:02 +0100 Subject: [PATCH] libstore: generalize linux vfork infra to also allow exec'ing we'll use this to run programs from a vforked child instead of using runProgram. ideally we would like to have runProgram be as efficient as possible, but our mount namespace handling makes this harder than is worth dealing with right now. the linux derivation goal is a good place to prototype functionality like this, especially since we need fine control over credentials and file descriptor tables of children Change-Id: Ibc356613ae10b06ea81de9575611948f5ee353b6 --- lix/libstore/platform/linux.cc | 36 +++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/lix/libstore/platform/linux.cc b/lix/libstore/platform/linux.cc index 51f4394d3..bd6f06c42 100644 --- a/lix/libstore/platform/linux.cc +++ b/lix/libstore/platform/linux.cc @@ -32,6 +32,8 @@ #include #include #include +#include +#include #if __linux__ #include @@ -111,8 +113,10 @@ static Pid inClone(CloneStack & stack, int flags, InvocableR auto fn) * the current process. the child behaves much like a thread as a result and the * callback must not make changes to process memory that we cannot undo from the * parent, otherwise we may leak memory or fully trash the parent address space. + * + * \return pid and the result of the callback function (if the child has exited) */ -static auto inVFork(int flags, auto fn) +static auto asVFork(int flags, auto fn) -> std::pair>> { std::optional> result; @@ -128,14 +132,40 @@ static auto inVFork(int flags, auto fn) return 0; }); - if (int status = child.wait(); !statusOk(status)) { - throw Error("failed to run vfork child: %s", statusToString(status)); + while (true) { + int status; + auto result = waitpid(child.get(), &status, WNOHANG); + if (result == child.get()) { + child.release(); // it's gone, don't wait for it again + if (!statusOk(status)) { + throw Error("failed to run vfork child: %s", statusToString(status)); + } + break; + } else if (result == 0) { + break; // still running, so no exceptions thrown by callback + } else if (errno != EINTR) { + throw SysError("cannot get exit status of PID %d", child.get()); + } } // synchronize with vfork child. if the compiler doesn't treat syscalls // as optimization barriers for stack variables we would end up with an // incorrect result value, and barriers are cheap compared to syscalls. std::atomic_thread_fence(std::memory_order::acquire); + return {std::move(child), std::move(result)}; +} + +/** + * runs a callback in a vforked child process that shares its address space with + * the current process. the child behaves much like a thread as a result and the + * callback must not make changes to process memory that we cannot undo from the + * parent, otherwise we may leak memory or fully trash the parent address space. + * + * throws an exception if the child exec's or otherwise doesn't return a result. + */ +static auto inVFork(int flags, auto fn) +{ + auto [pid, result] = asVFork(flags, fn); if (result) { return std::move(result->value());