- 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
177 lines
5.7 KiB
C++
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);
|
|
}
|