#include #include #include #include #include #include #include "fold/cli/signal_isolation.hpp" #include "fold/core/assert.hpp" // Re-install at the start of each test: Catch2's test harness installs its // own SIGSEGV/SIGABRT handlers in main() to print stack traces on test // failure, which would clobber ours if we only installed once during static // initialization. `installSignalIsolation()` re-issues `sigaction(...)` for // each fatal signal on every call. namespace { template int runInChild(Fn&& fn) { const pid_t child = ::fork(); if (child == 0) { fold::cli::installSignalIsolation(); fn(); ::_Exit(0); } if (child < 0) return -1; int status = 0; if (::waitpid(child, &status, 0) != child) return -1; return status; } void requireSignal(int status, int signal) { REQUIRE(status >= 0); REQUIRE(WIFSIGNALED(status)); REQUIRE(WTERMSIG(status) == signal); } } // namespace TEST_CASE("signal_isolation: normal completion returns 0", "[signal_isolation]") { fold::cli::installSignalIsolation(); int counter = 0; int sig = fold::cli::runWithSignalIsolation([&] { counter = 7; }); REQUIRE(sig == 0); REQUIRE(counter == 7); } TEST_CASE("signal_isolation: abort terminates the isolated process", "[signal_isolation]") { const int status = runInChild([] { fold::cli::runWithSignalIsolation([] { std::abort(); }); }); requireSignal(status, SIGABRT); } TEST_CASE("signal_isolation: null-pointer deref terminates the isolated process", "[signal_isolation]") { const int status = runInChild([] { fold::cli::runWithSignalIsolation([] { // Suppress the obvious-null warning the optimizer would emit. volatile int* p = nullptr; *p = 1; }); }); REQUIRE(status >= 0); REQUIRE(WIFSIGNALED(status)); REQUIRE((WTERMSIG(status) == SIGSEGV || WTERMSIG(status) == SIGBUS)); } TEST_CASE("signal_isolation: Fold hard assertions are caught as SIGABRT", "[signal_isolation][assert]") { const int status = runInChild([] { fold::cli::runWithSignalIsolation( [] { FOLD_ASSERT(false, "intentional hard assertion failure"); }); }); requireSignal(status, SIGABRT); } TEST_CASE("signal_isolation: Fold precondition assertions are caught as SIGABRT", "[signal_isolation][assert]") { const int status = runInChild([] { fold::cli::runWithSignalIsolation( [] { FOLD_PRECONDITION(false, "intentional precondition failure"); }); }); requireSignal(status, SIGABRT); } TEST_CASE("signal_isolation: Fold debug checks are enabled in test builds", "[signal_isolation][assert][dcheck]") { #if !defined(FOLD_ENABLE_DCHECK) #error "build-tests=true must define FOLD_ENABLE_DCHECK for this test target" #endif const int status = runInChild([] { fold::cli::runWithSignalIsolation( [] { FOLD_DCHECK(false, "intentional debug-check failure"); }); }); requireSignal(status, SIGABRT); } TEST_CASE("signal_isolation: a terminated child does not poison the parent", "[signal_isolation][process-boundary]") { requireSignal(runInChild([] { fold::cli::runWithSignalIsolation([] { std::abort(); }); }), SIGABRT); fold::cli::installSignalIsolation(); int counter = 0; REQUIRE(fold::cli::runWithSignalIsolation([&] { counter = 42; }) == 0); REQUIRE(counter == 42); } TEST_CASE("signal_isolation: timeout terminates a child instead of abandoning held locks", "[signal_isolation][watchdog][process-boundary]") { const pid_t child = ::fork(); REQUIRE(child >= 0); if (child == 0) { fold::cli::installSignalIsolation(); std::mutex mutex; const int sig = fold::cli::runWithTimeoutIsolation( [&] { std::lock_guard lock(mutex); volatile int value = 0; while (true) value = value + 1; }, 1); // Returning after SIGALRM means the process has abandoned the locked // guard and is unsafe to reuse. The fixed implementation never reaches // either exit below. if (sig != SIGALRM) ::_Exit(90); ::_Exit(mutex.try_lock() ? 91 : 92); } int status = 0; REQUIRE(::waitpid(child, &status, 0) == child); REQUIRE(WIFSIGNALED(status)); REQUIRE(WTERMSIG(status) == SIGALRM); } TEST_CASE("signal_isolation: watchdog terminates an infinite-loop child", "[signal_isolation][watchdog][process-boundary]") { const int status = runInChild([] { // 1-second budget; the loop runs forever without it. fold::cli::runWithTimeoutIsolation( [] { volatile int i = 0; while (true) i = i + 1; }, 1); }); requireSignal(status, SIGALRM); } TEST_CASE("signal_isolation: watchdog disarms on normal completion", "[signal_isolation][watchdog]") { fold::cli::installSignalIsolation(); int counter = 0; int sig = fold::cli::runWithTimeoutIsolation([&] { counter = 99; }, 60); REQUIRE(sig == 0); REQUIRE(counter == 99); // The next isolation call must not see a stale alarm fire on it. int sig2 = fold::cli::runWithTimeoutIsolation([&] { counter = 100; }, 60); REQUIRE(sig2 == 0); REQUIRE(counter == 100); } TEST_CASE("signal_isolation: timeout=0 falls back to plain isolation", "[signal_isolation][watchdog]") { fold::cli::installSignalIsolation(); int counter = 0; int sig = fold::cli::runWithTimeoutIsolation([&] { counter = 7; }, 0); REQUIRE(sig == 0); REQUIRE(counter == 7); }