From 210caba58f0b88ac6ad892c728355095714bca28 Mon Sep 17 00:00:00 2001 From: got3nks Date: Tue, 22 Sep 2026 00:08:00 +0100 Subject: [PATCH] test(core): stop the double-free probe failing where the allocator absorbs it (#1525) The abort-backtrace suite forks a child, double frees for real and requires the child to die. glibc and macOS libmalloc oblige. FreeBSD's jemalloc takes the second free into its thread cache and says nothing unless libc was built --enable-debug, so the child ran to its own exit and the case failed on a property no aMule code owns. MALLOC_CONF is read at allocator init, long before the fork, so the test cannot ask for detection either. Give that exit its own code and accept it: where nothing died there is no report to assert on, and the trap and abort cases still cover the handler. Closes #1524. --- unittests/tests/FatalAbortBacktraceTest.cpp | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git unittests/tests/FatalAbortBacktraceTest.cpp unittests/tests/FatalAbortBacktraceTest.cpp index 5df94a97a9..3b11d3f92c 100644 --- unittests/tests/FatalAbortBacktraceTest.cpp +++ unittests/tests/FatalAbortBacktraceTest.cpp @@ -73,9 +73,14 @@ struct ChildResult std::string stderr_text; bool exited_on_signal = false; int signal_number = 0; + int exit_code = -1; // only meaningful when the child was not signalled bool timed_out = false; }; +// What a child exits with when its own double free went through unnoticed, so the parent can tell +// an allocator that absorbed the corruption from any other quiet exit. +const int kAllocatorAbsorbedIt = 70; + // Runs `body` in a forked child with its stderr on a pipe, and returns what it wrote plus how it // died. The alarm is the deadlock detector: a handler that allocates can block forever inside // malloc, and without a bound that would hang the whole suite instead of failing one case. @@ -128,6 +133,8 @@ ChildResult RunInChild(void (*body)(), unsigned timeout_seconds = 10) result.exited_on_signal = true; result.signal_number = WTERMSIG(status); result.timed_out = (result.signal_number == SIGALRM); + } else if (WIFEXITED(status)) { + result.exit_code = WEXITSTATUS(status); } return result; } @@ -161,7 +168,7 @@ void ChildDoubleFree() #endif free(p); free(Launder(p)); - _exit(0); // not reached while the allocator detects the double free + _exit(kAllocatorAbsorbedIt); // reached only where the allocator lets a double free through } // The trap path on its own, without depending on what an allocator decides to do. __builtin_trap() @@ -413,7 +420,18 @@ TEST(FatalAbortBacktrace, RealHeapCorruptionStillProducesABacktrace) const ChildResult r = RunInChild(ChildDoubleFree); ASSERT_FALSE(r.timed_out); // a handler that allocates deadlocks here - ASSERT_TRUE(r.exited_on_signal); + + // Whether a double free is noticed at all is the allocator's choice, not ours. glibc and + // macOS libmalloc both kill the child. FreeBSD's jemalloc takes the second free into its + // thread cache and says nothing unless libc was built --enable-debug, and the test cannot + // change that from inside the process: MALLOC_CONF is read at allocator init, long before + // the fork. Where nothing died there is no report to assert on, and the trap and abort + // cases below still cover the handler. amule-org/amule#1524. + if (!r.exited_on_signal) { + ASSERT_EQUALS(kAllocatorAbsorbedIt, r.exit_code); + return; + } + // Which signal carries the corruption is the allocator's business: glibc abort()s, macOS // libmalloc may trap for the very same double free. Asserting one of them is what made this // case fail intermittently on macOS. The report is the behaviour under test.