mirror of
https://github.com/meshtastic/firmware.git
synced 2026-10-30 20:09:18 -04:00
* Fail the native simulator job on a sanitizer fault, and fix the one it found The integration step backgrounded meshtasticd and ended in `wait "$PID" 2>/dev/null || true`, which discards the simulator's exit status. The coverage env is built with -fsanitize=address, so a fault there prints a full report and exits non-zero on the way out, after the Python harness has already printed its own success - and the job concluded green with an AddressSanitizer heap-use-after-free sitting in the log. Gate it: capture the simulator's output, read the status `wait` reports, and scan the output for a sanitizer report. The pattern moves to bin/lib/sanitizer.sh so bin/run-tests.sh and the new bin/check-sanitizer-log.sh share one matcher rather than drifting apart; the output is uploaded as an artifact, since a fault on the exit path prints after the last test has passed. Leak detection is off, as in bin/test-config-check.sh - exit_simulator exit()s with the node's globals still live. The fault it had been hiding: OSThread::run() points currentThread at the running thread for the whole of runOnce(), and exit_simulator calls exit(0) from AdminModule while ServerAPI is that thread. ServerAPI is declared `public StreamAPI, private OSThread`, so its Thread base frees ThreadName first and PhoneAPI::~PhoneAPI's own LOG_DEBUG then reads it back through currentThread. Clear the pointer in ~OSThread when it names the thread being destroyed, which covers any thread deleted from inside its own runOnce, not just this one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JjAvRt6xuztC3hKqWkW9Dw * Detach observers when an Observable is destroyed The new sanitizer gate turned the simulator job red on a second heap-use-after-free on the same exit path, this one in Observable::removeObserver. ~Observer removes itself from every observable it watches, but Observable has no destructor and dies with its watchers still pointing at it. exit_simulator exit()s from AdminModule, and static destruction takes the global xModem - whose packetReady is an Observable that PhoneAPI::handleStartConfig attached to - before the heap-allocated ServerAPI that is watching it. PhoneAPI::close() then calls unobserve(&xModem.packetReady) on the dead observable, and ~Observer would walk it again afterwards. Both halves of the contract, so neither side has to outlive the other: ~Observable drops itself from each watcher's list, and unobserve() checks membership before reading through the pointer it is given. Also take CodeRabbit's grep -m 4 in bin/check-sanitizer-log.sh: with more than four matching lines, head closing the pipe is a SIGPIPE that pipefail reports and set -e exits on, so the script gave 141 where it documents 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JjAvRt6xuztC3hKqWkW9Dw --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.7 KiB
C++
107 lines
3.7 KiB
C++
// Unit tests for OSThread::currentThread's lifetime
|
|
// (src/concurrency/OSThread.cpp).
|
|
//
|
|
// currentThread is the only attribution a log line carries:
|
|
// RedirectablePrint::log reads currentThread->ThreadName and prints it as the
|
|
// "[Tag] " prefix. OSThread::run() points it at the running thread for the
|
|
// whole of runOnce() and clears it afterwards, so during a runOnce() the
|
|
// pointer is a borrowed reference to an object that runOnce() is itself allowed
|
|
// to destroy - and the simulator does: exit_simulator calls exit(0) from
|
|
// AdminModule while ServerAPI is the running thread, which runs ServerAPI's
|
|
// destructor with currentThread still pointing at it.
|
|
//
|
|
// The regression guarded: a destroyed thread left reachable through
|
|
// currentThread. Bases are destroyed in reverse declaration order, so for
|
|
// `ServerAPI : public StreamAPI, private OSThread` the Thread base frees
|
|
// ThreadName first and PhoneAPI::~PhoneAPI's own LOG_DEBUG then reads it.
|
|
// AddressSanitizer caught that as a heap-use-after-free on the native
|
|
// simulator's exit path, in a CI run that still concluded green. The destructor
|
|
// has to drop the pointer along with the name it refers to; the log call after
|
|
// the delete below is the read that faulted, so a regression reproduces here
|
|
// under the ASan-linked coverage env and not only in the simulator.
|
|
#include "configuration.h"
|
|
|
|
#include "TestUtil.h"
|
|
#include "concurrency/OSThread.h"
|
|
#include <unity.h>
|
|
|
|
namespace
|
|
{
|
|
class ProbeThread : public concurrency::OSThread
|
|
{
|
|
public:
|
|
ProbeThread() : OSThread("ProbeThread") {}
|
|
const concurrency::OSThread *seenWhileRunning = nullptr;
|
|
void runNow() { run(); }
|
|
|
|
protected:
|
|
int32_t runOnce() override
|
|
{
|
|
seenWhileRunning = currentThread;
|
|
return 1000;
|
|
}
|
|
};
|
|
} // namespace
|
|
|
|
void setUp(void)
|
|
{
|
|
concurrency::OSThread::currentThread = nullptr;
|
|
}
|
|
void tearDown(void)
|
|
{
|
|
concurrency::OSThread::currentThread = nullptr;
|
|
}
|
|
|
|
// The mechanism the destructor has to undo: inside runOnce() the pointer is
|
|
// this thread, and run() clears it on the way out, so a thread that is not
|
|
// running is never the current one.
|
|
static void test_run_points_currentThread_at_the_running_thread()
|
|
{
|
|
ProbeThread probe;
|
|
|
|
probe.runNow();
|
|
TEST_ASSERT_EQUAL_PTR(&probe, probe.seenWhileRunning);
|
|
TEST_ASSERT_NULL(concurrency::OSThread::currentThread);
|
|
}
|
|
|
|
// Destroying the running thread - what exit_simulator does from inside
|
|
// ServerAPI's runOnce - must leave nothing behind to read. The LOG_DEBUG is
|
|
// that read: it goes through RedirectablePrint::log, which dereferences
|
|
// currentThread for the tag.
|
|
static void test_destroying_the_running_thread_clears_currentThread()
|
|
{
|
|
auto *probe = new ProbeThread();
|
|
concurrency::OSThread::currentThread = probe; // as run() has it, mid-runOnce()
|
|
|
|
delete probe;
|
|
|
|
TEST_ASSERT_NULL(concurrency::OSThread::currentThread);
|
|
LOG_DEBUG("logging after the running thread was destroyed");
|
|
}
|
|
|
|
// A thread destroyed while some *other* thread is running must not clear the
|
|
// pointer: that would drop the running thread's tag from every later log line
|
|
// in the same runOnce().
|
|
static void test_destroying_another_thread_leaves_currentThread_alone()
|
|
{
|
|
ProbeThread running;
|
|
auto *other = new ProbeThread();
|
|
concurrency::OSThread::currentThread = &running;
|
|
|
|
delete other;
|
|
|
|
TEST_ASSERT_EQUAL_PTR(&running, concurrency::OSThread::currentThread);
|
|
}
|
|
|
|
void setup()
|
|
{
|
|
initializeTestEnvironment();
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_run_points_currentThread_at_the_running_thread);
|
|
RUN_TEST(test_destroying_the_running_thread_clears_currentThread);
|
|
RUN_TEST(test_destroying_another_thread_leaves_currentThread_alone);
|
|
exit(UNITY_END());
|
|
}
|
|
|
|
void loop() {}
|