From c537bf0f8b3b6a77e39df49fec78560b5e8e87b7 Mon Sep 17 00:00:00 2001 From: Rares Popa <2606875+rarepops@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:54:12 +0200 Subject: [PATCH] fix(foundation): resume spawn backoff after EINTR Signed-off-by: Rares Popa <2606875+rarepops@users.noreply.github.com> --- src/foundation/compat.c | 14 +++++++++++ src/foundation/compat.h | 3 +++ src/foundation/subprocess.c | 2 +- tests/test_subprocess.c | 48 +++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/foundation/compat.c b/src/foundation/compat.c index f0c4d3571..74c25bd8e 100644 --- a/src/foundation/compat.c +++ b/src/foundation/compat.c @@ -17,6 +17,20 @@ #include #endif +int cbm_nanosleep_full(const struct timespec *req) { +#ifdef _WIN32 + return cbm_nanosleep(req, NULL); +#else + struct timespec remaining = *req; + while (nanosleep(&remaining, &remaining) != 0) { + if (errno != EINTR) { + return -1; + } + } + return 0; +#endif +} + /* ── strndup (Windows lacks it) ───────────────────────────────── */ #ifdef _WIN32 diff --git a/src/foundation/compat.h b/src/foundation/compat.h index 055a45687..175d775e4 100644 --- a/src/foundation/compat.h +++ b/src/foundation/compat.h @@ -96,6 +96,9 @@ static inline int cbm_nanosleep(const struct timespec *req, struct timespec *rem #define cbm_nanosleep nanosleep #endif +/* Sleeps for the full requested duration even when POSIX signals interrupt it. */ +int cbm_nanosleep_full(const struct timespec *req); + /* ── gmtime_r (Windows lacks it) ─────────────────────────────── */ #ifdef _WIN32 static inline struct tm *cbm_gmtime_r(const time_t *timep, struct tm *result) { diff --git a/src/foundation/subprocess.c b/src/foundation/subprocess.c index 9174391f4..196165cea 100644 --- a/src/foundation/subprocess.c +++ b/src/foundation/subprocess.c @@ -970,7 +970,7 @@ static void cbm_spawn_backoff(int attempt) { int shift = attempt < CBM_SPAWN_BACKOFF_MAX_SHIFT ? attempt : CBM_SPAWN_BACKOFF_MAX_SHIFT; long ms = (long)CBM_SPAWN_BACKOFF_BASE_MS << shift; struct timespec delay = {ms / 1000L, (ms % 1000L) * 1000L * 1000L}; - (void)cbm_nanosleep(&delay, NULL); + (void)cbm_nanosleep_full(&delay); } /* fork() fails with EAGAIN under the same pressure posix_spawn does, and the diff --git a/tests/test_subprocess.c b/tests/test_subprocess.c index 30fd1537d..c0fb2224c 100644 --- a/tests/test_subprocess.c +++ b/tests/test_subprocess.c @@ -21,6 +21,7 @@ #ifndef _WIN32 #include #include +#include #include #endif @@ -192,6 +193,52 @@ TEST(subprocess_retries_transient_spawn_refusal) { #endif } +#ifndef _WIN32 +static volatile sig_atomic_t g_spawn_backoff_alarm_count = 0; + +static void spawn_backoff_alarm_handler(int signal_number) { + (void)signal_number; + g_spawn_backoff_alarm_count++; +} +#endif + +TEST(subprocess_spawn_backoff_resumes_after_eintr) { +#ifdef _WIN32 + SKIP_PLATFORM("POSIX signal interruption"); +#else + struct sigaction action = {0}; + struct sigaction previous_action = {0}; + action.sa_handler = spawn_backoff_alarm_handler; + (void)sigemptyset(&action.sa_mask); + bool handler_installed = sigaction(SIGALRM, &action, &previous_action) == 0; + + struct itimerval timer = { + .it_interval = {.tv_sec = 0, .tv_usec = 1000}, + .it_value = {.tv_sec = 0, .tv_usec = 1000}, + }; + g_spawn_backoff_alarm_count = 0; + bool timer_started = handler_installed && setitimer(ITIMER_REAL, &timer, NULL) == 0; + + uint64_t started_at = cbm_now_ms(); + cbm_subprocess_force_spawn_eagain_for_testing(3); + cbm_proc_result_t result = run_sh("exit 0", 0); + uint64_t elapsed_ms = cbm_now_ms() - started_at; + + struct itimerval disabled = {0}; + (void)setitimer(ITIMER_REAL, &disabled, NULL); + if (handler_installed) { + (void)sigaction(SIGALRM, &previous_action, NULL); + } + + ASSERT_TRUE(handler_installed); + ASSERT_TRUE(timer_started); + ASSERT_TRUE(g_spawn_backoff_alarm_count > 0); + ASSERT_TRUE(elapsed_ms >= 50); + ASSERT_EQ(result.outcome, CBM_PROC_CLEAN); + PASS(); +#endif +} + TEST(subprocess_gives_up_after_the_retry_budget) { #ifdef _WIN32 SKIP_PLATFORM("POSIX fork/EAGAIN path"); @@ -1033,6 +1080,7 @@ SUITE(subprocess) { RUN_TEST(subprocess_run_crash_is_crash); RUN_TEST(subprocess_run_hang_is_hang); RUN_TEST(subprocess_retries_transient_spawn_refusal); + RUN_TEST(subprocess_spawn_backoff_resumes_after_eintr); RUN_TEST(subprocess_gives_up_after_the_retry_budget); RUN_TEST(subprocess_run_spawn_failure); RUN_TEST(subprocess_run_null_bin_rejected);