From 59ec7998feba9da97882f4f374becec470d9dc9e Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 27 Oct 2026 01:35:32 -0500 Subject: [PATCH] Reset the prefault helper's state in forked children fork() copies only the calling thread, so a child forked while the allocator's prefault helper held scr_sa_pf_lock waited on that lock forever at its first fault-in request, and a child that released the range the helper was faulting in waited forever for the helper to finish it. Native builds' emit workers hung this way, with the parent waiting in waitpid. The helper's first start now registers pthread_atfork handlers. The prepare handler takes the lock across the fork and the parent handler releases it. The child handler resets the ring, the busy range, the waiter state and the heap polling state, reinitializes the lock and both conditions, and marks the helper not started, so the child's first request starts a helper of its own. check_fork_while_prefaulting in scr_alloc.test.c forks while the helper faults in 64 MiB, over 64 rounds; each child releases that range, faults in a range of its own and must exit within five seconds. ThreadSanitizer builds skip it. --- packages/runtime/src/scr_alloc.c | 36 ++++++++++++++++ packages/runtime/src/scr_alloc.test.c | 62 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/packages/runtime/src/scr_alloc.c b/packages/runtime/src/scr_alloc.c index 087c926f7..ac80df6e2 100644 --- a/packages/runtime/src/scr_alloc.c +++ b/packages/runtime/src/scr_alloc.c @@ -368,6 +368,34 @@ static bool scr_sa_pf_on(void) { return s > 0; } +/* fork() copies only the calling thread, so a child has no helper. The + * prepare handler takes the lock, so no other thread holds it across the + * fork. The child handler gives the child fresh helper state: an empty ring, + * no busy range or waiters, new conditions (the parent's can still record + * the helper as a waiter), and the heap watermarks cleared. Its first + * request then starts a helper of its own. */ +static bool scr_sa_pf_atfork; + +static void scr_sa_pf_fork_prepare(void) { pthread_mutex_lock(&scr_sa_pf_lock); } + +static void scr_sa_pf_fork_parent(void) { pthread_mutex_unlock(&scr_sa_pf_lock); } + +static void scr_sa_pf_fork_child(void) { + (void)pthread_mutex_init(&scr_sa_pf_lock, NULL); + (void)pthread_cond_init(&scr_sa_pf_cond, NULL); + (void)pthread_cond_init(&scr_sa_pf_done, NULL); + scr_sa_pf_head = scr_sa_pf_tail = 0; + scr_sa_pf_busy = (ScrSaPfRange){NULL, NULL}; + scr_sa_pf_forgetting = 0; + scr_sa_pf_waiting = false; + scr_sa_pf_started = false; +#ifdef SCR_SA_PF_HEAP + scr_sa_pf_heap_mode = 0; + scr_sa_pf_brk0 = scr_sa_pf_brk = NULL; + scr_sa_pf_heap_big = false; +#endif +} + /* Asks the helper to fault in [lo, hi) (page aligned). Dropped when the * ring is full: the owner then faults those pages itself. */ static void scr_sa_prefault(char *lo, char *hi) { @@ -375,6 +403,14 @@ static void scr_sa_prefault(char *lo, char *hi) { pthread_mutex_lock(&scr_sa_pf_lock); if (!scr_sa_pf_started) { scr_sa_pf_started = true; + /* Once per process: a child inherits the parent's handlers. */ + if (!scr_sa_pf_atfork && + pthread_atfork(scr_sa_pf_fork_prepare, scr_sa_pf_fork_parent, scr_sa_pf_fork_child) != 0) { + pthread_mutex_unlock(&scr_sa_pf_lock); + atomic_store_explicit(&scr_sa_pf_state, -1, memory_order_relaxed); + return; + } + scr_sa_pf_atfork = true; #ifdef SCR_SA_PF_HEAP /* Nobody waits on the condition yet: give it the monotonic clock for * the helper's polls (no polls without it). */ diff --git a/packages/runtime/src/scr_alloc.test.c b/packages/runtime/src/scr_alloc.test.c index dfdd9bf5c..bf9fc7e64 100644 --- a/packages/runtime/src/scr_alloc.test.c +++ b/packages/runtime/src/scr_alloc.test.c @@ -1023,6 +1023,65 @@ static void check_discard(void) { } #endif +/* ThreadSanitizer stops a child that starts a thread after a fork in a + * threaded program, so its build skips the fork check. */ +#define FORK_CHECK 1 +#if defined(__SANITIZE_THREAD__) +#undef FORK_CHECK +#elif defined(__has_feature) +#if __has_feature(thread_sanitizer) +#undef FORK_CHECK +#endif +#endif + +#if defined(__linux__) && SCR_SMALL_ALLOC && defined(FORK_CHECK) +#include +#include +#include +#include +#include +#define FORK_ROUNDS 64 +#define FORK_SPAN ((size_t)64 << 20) +#define FORK_OWN ((size_t)1 << 20) + +/* A child forked while the prefault helper faults in a range has no helper. + * It releases that range (which waits for a busy range to finish), then + * requests a fault-in of its own and releases it, and must exit within five + * seconds. */ +static void check_fork_while_prefaulting(void) { + for (unsigned round = 0; round < FORK_ROUNDS; round++) { + char *lo = mmap(NULL, FORK_SPAN, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + assert(lo != MAP_FAILED); + scr_mem_prefault(lo, lo + FORK_SPAN); + pid_t pid = fork(); + assert(pid >= 0); + if (pid == 0) { + scr_mem_release_ahead(lo, lo + FORK_SPAN); + char *own = mmap(NULL, FORK_OWN, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (own == MAP_FAILED) _exit(2); + scr_mem_prefault(own, own + FORK_OWN); + scr_mem_release_ahead(own, own + FORK_OWN); + _exit(0); + } + int status = 0; + pid_t done = 0; + for (unsigned ms = 0; ms < 5000 && done == 0; ms++) { + done = waitpid(pid, &status, WNOHANG); + if (done == 0) nanosleep(&(struct timespec){0, 1000000L}, NULL); + } + if (done == 0) { + kill(pid, SIGKILL); + (void)waitpid(pid, &status, 0); + fprintf(stderr, "the child forked in round %u did not exit\n", round); + abort(); + } + assert(done == pid && WIFEXITED(status) && WEXITSTATUS(status) == 0); + scr_mem_release_ahead(lo, lo + FORK_SPAN); + assert(munmap(lo, FORK_SPAN) == 0); + } +} +#endif + #if defined(__linux__) && SCR_SMALL_ALLOC /* The program-lifetime region hands out zeroed, 8-byte aligned blocks below * 32 GiB, in order from the calling thread's chunk; a block larger than a @@ -1087,6 +1146,9 @@ int main(void) { check_system_pointers(); check_churn(); check_free_order(); +#if defined(__linux__) && SCR_SMALL_ALLOC && defined(FORK_CHECK) + check_fork_while_prefaulting(); +#endif #if SCR_SMALL_ALLOC && defined(__linux__) && defined(__GLIBC__) check_heap_huge_pages(); #endif