fold/tests/unit/test_signal_isolation.cpp
trvon 86b2f14a43
feat(analysis): harden project-wide scans
- persist TU-owned graphs and guarded configuration evidence
- add closed-world production reachability classification
- calibrate detector precision and reviewed self-scan policy
- terminate poisoned workers instead of longjmp recovery
2026-07-30 18:18:04 -04:00

177 lines
5.7 KiB
C++

#include <sys/wait.h>
#include <unistd.h>
#include <catch2/catch_test_macros.hpp>
#include <csignal>
#include <cstdlib>
#include <mutex>
#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 <typename Fn>
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);
}