diff --git a/cfgmgr/Makefile.am b/cfgmgr/Makefile.am index 0f71ad7b0bb..0e7478ba3d6 100644 --- a/cfgmgr/Makefile.am +++ b/cfgmgr/Makefile.am @@ -126,20 +126,21 @@ stpmgrd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -vlanmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -teammgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -portmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -intfmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -buffermgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -vrfmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -nbrmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -vxlanmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -sflowmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -natmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -coppmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -tunnelmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -macsecmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -fabricmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp -stpmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp +ASAN_SOURCES = $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp +vlanmgrd_SOURCES += $(ASAN_SOURCES) +teammgrd_SOURCES += $(ASAN_SOURCES) +portmgrd_SOURCES += $(ASAN_SOURCES) +intfmgrd_SOURCES += $(ASAN_SOURCES) +buffermgrd_SOURCES += $(ASAN_SOURCES) +vrfmgrd_SOURCES += $(ASAN_SOURCES) +nbrmgrd_SOURCES += $(ASAN_SOURCES) +vxlanmgrd_SOURCES += $(ASAN_SOURCES) +sflowmgrd_SOURCES += $(ASAN_SOURCES) +natmgrd_SOURCES += $(ASAN_SOURCES) +coppmgrd_SOURCES += $(ASAN_SOURCES) +tunnelmgrd_SOURCES += $(ASAN_SOURCES) +macsecmgrd_SOURCES += $(ASAN_SOURCES) +fabricmgrd_SOURCES += $(ASAN_SOURCES) +stpmgrd_SOURCES += $(ASAN_SOURCES) endif diff --git a/fdbsyncd/Makefile.am b/fdbsyncd/Makefile.am index 93271f4e788..2d7c4360810 100644 --- a/fdbsyncd/Makefile.am +++ b/fdbsyncd/Makefile.am @@ -19,6 +19,6 @@ fdbsyncd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -fdbsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +fdbsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/fpmsyncd/Makefile.am b/fpmsyncd/Makefile.am index b61ebdfa930..29e32df2e92 100644 --- a/fpmsyncd/Makefile.am +++ b/fpmsyncd/Makefile.am @@ -20,6 +20,6 @@ fpmsyncd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -fpmsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +fpmsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/gearsyncd/Makefile.am b/gearsyncd/Makefile.am index 1a1d9983a25..94df61286ad 100644 --- a/gearsyncd/Makefile.am +++ b/gearsyncd/Makefile.am @@ -19,6 +19,6 @@ gearsyncd_LDADD += -lgcovpreload endif if ASAN_ENABLED -gearsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +gearsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/lib/asan.cpp b/lib/asan.cpp index 1f7d074e680..24eccead2af 100644 --- a/lib/asan.cpp +++ b/lib/asan.cpp @@ -1,26 +1,92 @@ +/* + * ASAN support helpers: + * 1. Install a SIGTERM handler that runs an injected LSan leak check. + * 2. When /etc/sonic/inject_asan_test_leak_enabled exists, inject a known test + * leak used to verify the ASAN/LSan path is working as expected. + * + * ENABLE_ASAN=y daemon builds also link asan_ctor.cpp, whose constructor calls + * swss_asan_init_impl() before main(). Unit tests leave asan_ctor.cpp out and + * call swss_asan_init_impl() / swss_asan_sigterm_handler_impl() with test- + * double functions for the dependencies. + */ + +#include "asan.h" + #include -#include -#include +#include +#include +#include +#include #include -extern "C" { - const char* __lsan_default_suppressions() { - return "leak:__static_initialization_and_destruction_0\n"; +/* ASAN test-leak injection + * + * When ASAN is enabled and /etc/sonic/inject_asan_test_leak_enabled exists, + * allocate a block and deliberately never free it so LSAN has a known leak to + * report on process exit or in the SIGTERM handler. This is useful for + * verifying that the ASAN build, configuration, and SIGTERM handlers are + * working as expected. + * + * The memory block has to still look unreachable when the leak check runs, + * which is difficult. LSan scans thread stacks conservatively, and at -O2 ASAN + * moves the injector's locals into a "fake stack" frame that lives on the heap + * for the lifetime of the thread. Overwriting the real stack never reaches + * those copies, so a leak injected on the main thread stays reachable and is + * silently dropped by __lsan_do_leak_check() in the SIGTERM handler. + * + * Injecting from a short-lived helper thread sidesteps that: once the thread is + * joined, both its stack and its ASAN fake stack are gone, so no stale pointer + * survives for LSan to trip over. This works at every optimization level and + * needs no ASAN_OPTIONS tuning. + * + * The intentional leak is injected at startup so it is present when the leak + * check runs in the SIGTERM handler. Do not call the LSan leak-check callback + * here: that terminates the process when leaks are present; call it only from + * the SIGTERM handler. + */ + +// Set by swss_asan_init_impl(); invoked from the SIGTERM handler. +static SwssLsanLeakCheckFn g_lsan_leak_check = nullptr; + +__attribute__((noinline)) +void swss_asan_inject_test_leak(SwssMallocFn malloc_fn) +{ + void *probe = malloc_fn(SWSS_ASAN_TEST_LEAK_SIZE); + if (!probe) + { + SWSS_LOG_ERROR("failed to allocate %zu bytes for the ASAN test leak, no leak injected", + SWSS_ASAN_TEST_LEAK_SIZE); + return; } + + std::memset(probe, 0xCD, SWSS_ASAN_TEST_LEAK_SIZE); + + // Feed the pointer to an opaque asm that also reads memory, so -O2 cannot + // drop the malloc and memset as dead stores. Nothing stores the pointer, so + // the block remains unreachable. + asm volatile("" : : "r"(probe) : "memory"); } -static void swss_asan_sigterm_handler(int signo) +void swss_asan_sigterm_handler_impl(int signo, + SwssLsanLeakCheckFn leak_check_fn, + SwssSigactionFn sigaction_fn, + SwssExitFn exit_fn, + SwssRaiseFn raise_fn) { SWSS_LOG_ENTER(); - __lsan_do_leak_check(); + if (leak_check_fn) + { + leak_check_fn(); + } struct sigaction sigact; - if (sigaction(SIGTERM, NULL, &sigact)) + if (sigaction_fn(SIGTERM, NULL, &sigact)) { SWSS_LOG_ERROR("failed to get current SIGTERM action handler"); - _exit(EXIT_FAILURE); + exit_fn(EXIT_FAILURE); + return; } // Check the currently set signal handler. @@ -32,26 +98,52 @@ static void swss_asan_sigterm_handler(int signo) sigemptyset(&sigact.sa_mask); sigact.sa_flags = 0; sigact.sa_handler = SIG_DFL; - if (sigaction(SIGTERM, &sigact, NULL)) + if (sigaction_fn(SIGTERM, &sigact, NULL)) { SWSS_LOG_ERROR("failed to setup SIGTERM action handler"); - _exit(EXIT_FAILURE); + exit_fn(EXIT_FAILURE); + return; } - raise(signo); + raise_fn(signo); } } -__attribute__((constructor)) -static void swss_asan_init() +void swss_asan_sigterm_handler(int signo) +{ + swss_asan_sigterm_handler_impl(signo, g_lsan_leak_check, ::sigaction, ::_exit, ::raise); +} + +bool swss_asan_init_impl(SwssSigactionFn sigaction_fn, + SwssAccessFn access_fn, + SwssMallocFn malloc_fn, + SwssLsanLeakCheckFn leak_check_fn) { SWSS_LOG_ENTER(); + g_lsan_leak_check = leak_check_fn; + struct sigaction sigact = {}; sigact.sa_handler = swss_asan_sigterm_handler; - if (sigaction(SIGTERM, &sigact, NULL)) + if (sigaction_fn(SIGTERM, &sigact, nullptr)) { SWSS_LOG_ERROR("failed to setup SIGTERM action handler"); - exit(EXIT_FAILURE); + return false; } + + if (access_fn("/etc/sonic/inject_asan_test_leak_enabled", F_OK) == 0) + { + try + { + // See comment above swss_asan_inject_test_leak() for why this must + // run in a separate thread. + std::thread(swss_asan_inject_test_leak, malloc_fn).join(); + } + catch (const std::exception& e) + { + SWSS_LOG_ERROR("failed to inject ASAN test leak: %s", e.what()); + } + } + + return true; } diff --git a/lib/asan.h b/lib/asan.h new file mode 100644 index 00000000000..fdc8e742652 --- /dev/null +++ b/lib/asan.h @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES + * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Testable ASAN helpers. Production daemons enable them via the constructor in + * asan_ctor.cpp; unit tests call swss_asan_init_impl() / + * swss_asan_sigterm_handler_impl() with injected dependencies and leave + * asan_ctor.cpp out of the link. + */ + +#pragma once + +#include +#include + +// Unique size to identify the intentional ASAN test-leak allocation in reports. +static constexpr size_t SWSS_ASAN_TEST_LEAK_SIZE = 9861842; + +// Function pointers for passing real implementations or test doubles. +// sigaction() from signal.h +using SwssSigactionFn = int (*)(int, const struct sigaction *, struct sigaction *); +// access() from unistd.h +using SwssAccessFn = int (*)(const char *, int); +// malloc() from stdlib.h +using SwssMallocFn = void *(*)(size_t); +// __lsan_do_leak_check() from sanitizer/lsan_interface.h +using SwssLsanLeakCheckFn = void (*)(void); +// _exit() from unistd.h +using SwssExitFn = void (*)(int); +// raise() from signal.h +using SwssRaiseFn = int (*)(int); + +// SIGTERM handler installed by swss_asan_init_impl(). Thin wrapper around +// swss_asan_sigterm_handler_impl() that passes g_lsan_leak_check and the real +// libc entry points. Exposed so tests can verify the handler pointer that was +// passed to sigaction. +void swss_asan_sigterm_handler(int signo); + +// Testable SIGTERM-handler body. Production wrapper passes g_lsan_leak_check, +// ::sigaction, ::_exit, and ::raise; unit tests inject doubles. +void swss_asan_sigterm_handler_impl(int signo, + SwssLsanLeakCheckFn leak_check_fn, + SwssSigactionFn sigaction_fn, + SwssExitFn exit_fn, + SwssRaiseFn raise_fn); + +// Allocate (and never free) the intentional test leak via malloc_fn. +void swss_asan_inject_test_leak(SwssMallocFn malloc_fn); + +// Set up custom machinery for ASAN builds. +// - Installs a SIGTERM handler for checking for leaks. +// - When /etc/sonic/inject_asan_test_leak_enabled exists, injects a known +// test leak via malloc_fn. +// - Returns false if signal-handler installation fails; true otherwise (including +// when leak injection is skipped or malloc_fn returns nullptr). +bool swss_asan_init_impl(SwssSigactionFn sigaction_fn, + SwssAccessFn access_fn, + SwssMallocFn malloc_fn, + SwssLsanLeakCheckFn leak_check_fn); diff --git a/lib/asan_ctor.cpp b/lib/asan_ctor.cpp new file mode 100644 index 00000000000..b1d26b84d19 --- /dev/null +++ b/lib/asan_ctor.cpp @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES + * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * ASAN process bootstrap. Adds code for ASAN builds, including: + * - a constructor that wires our custom swss_asan_init_impl() into the process + * initialization that occurs before main(). + * - default LSan suppressions + * + * Include this file only in ASAN builds where you want that functionality + * enabled (ENABLE_ASAN=y daemon targets). To run unit tests against the + * implementation in asan.cpp in a non-ASAN build without installing a SIGTERM + * handler, injecting a test leak, or pulling in sanitizer symbols, leave this + * file out of the build and call swss_asan_init_impl() with test doubles for + * the dependencies. + */ + +#include "asan.h" + +#include + +#include +#include + +// Configure default LSan suppressions. +extern "C" { + const char* __lsan_default_suppressions() { + return "leak:__static_initialization_and_destruction_0\n"; + } +} + +// Wire swss_asan_init_impl() into the process initialization that occurs +// before main(). +__attribute__((constructor)) +static void swss_asan_init() +{ + if (!swss_asan_init_impl(::sigaction, ::access, std::malloc, __lsan_do_leak_check)) + { + exit(EXIT_FAILURE); + } +} diff --git a/mclagsyncd/Makefile.am b/mclagsyncd/Makefile.am index eb4fc20d0c4..dfefe10e735 100644 --- a/mclagsyncd/Makefile.am +++ b/mclagsyncd/Makefile.am @@ -19,6 +19,6 @@ mclagsyncd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -mclagsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +mclagsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/natsyncd/Makefile.am b/natsyncd/Makefile.am index 562d452c418..2aef40a640a 100644 --- a/natsyncd/Makefile.am +++ b/natsyncd/Makefile.am @@ -19,6 +19,6 @@ natsyncd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -natsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +natsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/neighsyncd/Makefile.am b/neighsyncd/Makefile.am index 1f34e9e92ff..46286ba43bb 100644 --- a/neighsyncd/Makefile.am +++ b/neighsyncd/Makefile.am @@ -19,6 +19,6 @@ neighsyncd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -neighsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +neighsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/orchagent/Makefile.am b/orchagent/Makefile.am index f535971c547..f20200008d0 100644 --- a/orchagent/Makefile.am +++ b/orchagent/Makefile.am @@ -193,8 +193,8 @@ orchagent_restart_check_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -orchagent_SOURCES += $(top_srcdir)/lib/asan.cpp -routeresync_SOURCES += $(top_srcdir)/lib/asan.cpp -orchagent_restart_check_SOURCES += $(top_srcdir)/lib/asan.cpp +orchagent_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp +routeresync_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp +orchagent_restart_check_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/portsyncd/Makefile.am b/portsyncd/Makefile.am index b65e3b4a4ff..51570b32317 100644 --- a/portsyncd/Makefile.am +++ b/portsyncd/Makefile.am @@ -19,6 +19,6 @@ portsyncd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -portsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +portsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/swssconfig/Makefile.am b/swssconfig/Makefile.am index 70d4090fcad..9e9a02c7588 100644 --- a/swssconfig/Makefile.am +++ b/swssconfig/Makefile.am @@ -26,8 +26,8 @@ swssplayer_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -swssconfig_SOURCES += $(top_srcdir)/lib/asan.cpp -swssplayer_SOURCES += $(top_srcdir)/lib/asan.cpp +swssconfig_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp +swssplayer_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif swssconfig_SOURCES += $(top_srcdir)/lib/orch_zmq_config.cpp diff --git a/teamsyncd/Makefile.am b/teamsyncd/Makefile.am index 594ea1ba944..7a7a93e5da7 100644 --- a/teamsyncd/Makefile.am +++ b/teamsyncd/Makefile.am @@ -19,6 +19,6 @@ teamsyncd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -teamsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp +teamsyncd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif diff --git a/tests/mock_tests/Makefile.am b/tests/mock_tests/Makefile.am index 157416d96c6..c083fc91891 100644 --- a/tests/mock_tests/Makefile.am +++ b/tests/mock_tests/Makefile.am @@ -105,6 +105,8 @@ tests_SOURCES = aclorch_ut.cpp \ icmporch_ut.cpp \ mock_sai_capability_wrap.cpp \ notifications_ut.cpp \ + asan_ut.cpp \ + $(top_srcdir)/lib/asan.cpp \ $(top_srcdir)/warmrestart/warmRestartHelper.cpp \ $(top_srcdir)/lib/gearboxutils.cpp \ $(top_srcdir)/lib/subintf.cpp \ diff --git a/tests/mock_tests/asan_ut.cpp b/tests/mock_tests/asan_ut.cpp new file mode 100644 index 00000000000..4c3ce1ad97e --- /dev/null +++ b/tests/mock_tests/asan_ut.cpp @@ -0,0 +1,351 @@ +#include "asan.h" + +#include + +#include + +#include +#include +#include + +namespace +{ + +struct AsanSigactionState +{ + int calls = 0; + // Count of calls to sigaction with act != nullptr. + int set_calls = 0; + // Count of calls to sigaction with oldact != nullptr. + int query_calls = 0; + + int last_sig = 0; + // Last non-null act passed to sigaction (init install or handler restore). + struct sigaction last_set = {}; + // Value returned via oldact on a query (act == nullptr). + struct sigaction oldact = {}; + bool oldact_set = false; + // Per-call return codes. Missing entries (including when empty) succeed with 0. + std::vector rcs; +}; + +struct AsanTestState +{ + AsanSigactionState sigaction; + + int access_calls = 0; + int access_rc = -1; + std::string access_path; + + int malloc_calls = 0; + size_t malloc_size = 0; + // When true, mock_malloc returns nullptr. Otherwise it returns storage.data(). + bool malloc_fail = false; + std::vector storage; + + int leak_check_calls = 0; + + int exit_calls = 0; + int exit_status = -1; + + int raise_calls = 0; + int raise_signo = -1; +}; + +AsanTestState *g_state = nullptr; + +int mock_sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) +{ + EXPECT_NE(g_state, nullptr); + auto& sa = g_state->sigaction; + sa.calls++; + sa.last_sig = sig; + + const size_t call_index = static_cast(sa.calls - 1); + const int rc = (call_index < sa.rcs.size()) ? sa.rcs[call_index] : 0; + + if (act) + { + sa.last_set = *act; + sa.set_calls++; + } + if (oldact) + { + sa.query_calls++; + if (sa.oldact_set) + { + *oldact = sa.oldact; + } + else + { + std::memset(oldact, 0, sizeof(*oldact)); + } + } + return rc; +} + +int mock_access(const char *path, int mode) +{ + EXPECT_NE(g_state, nullptr); + EXPECT_EQ(mode, F_OK); + g_state->access_calls++; + g_state->access_path = path ? path : ""; + return g_state->access_rc; +} + +void *mock_malloc(size_t size) +{ + EXPECT_NE(g_state, nullptr); + g_state->malloc_calls++; + g_state->malloc_size = size; + if (g_state->malloc_fail) + { + return nullptr; + } + g_state->storage.assign(size, 0); + return g_state->storage.data(); +} + +void mock_leak_check(void) +{ + EXPECT_NE(g_state, nullptr); + g_state->leak_check_calls++; +} + +void mock_exit(int status) +{ + EXPECT_NE(g_state, nullptr); + g_state->exit_calls++; + g_state->exit_status = status; +} + +int mock_raise(int signo) +{ + EXPECT_NE(g_state, nullptr); + g_state->raise_calls++; + g_state->raise_signo = signo; + return 0; +} + +void invoke_handler_impl() +{ + swss_asan_sigterm_handler_impl(SIGTERM, mock_leak_check, mock_sigaction, mock_exit, mock_raise); +} + +} // namespace + +class AsanInitTest : public ::testing::Test +{ +protected: + void SetUp() override + { + state_ = {}; + g_state = &state_; + } + + void TearDown() override + { + g_state = nullptr; + } + + AsanTestState state_; +}; + +TEST_F(AsanInitTest, InstallsSigtermHandler) +{ + state_.access_rc = -1; + + ASSERT_TRUE(swss_asan_init_impl(mock_sigaction, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.sigaction.calls, 1); + EXPECT_EQ(state_.sigaction.last_sig, SIGTERM); + EXPECT_EQ(state_.sigaction.last_set.sa_handler, swss_asan_sigterm_handler); + EXPECT_EQ(state_.access_calls, 1); + EXPECT_EQ(state_.access_path, "/etc/sonic/inject_asan_test_leak_enabled"); + EXPECT_EQ(state_.malloc_calls, 0); +} + +TEST_F(AsanInitTest, SigactionFailureReturnsFalse) +{ + state_.sigaction.rcs = {-1}; + + EXPECT_FALSE(swss_asan_init_impl(mock_sigaction, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.sigaction.calls, 1); + EXPECT_EQ(state_.access_calls, 0); + EXPECT_EQ(state_.malloc_calls, 0); +} + +TEST_F(AsanInitTest, SkipsLeakInjectionWhenFlagFileMissing) +{ + state_.access_rc = -1; + + ASSERT_TRUE(swss_asan_init_impl(mock_sigaction, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.malloc_calls, 0); +} + +TEST_F(AsanInitTest, InjectsLeakWhenFlagFilePresent) +{ + state_.access_rc = 0; + + ASSERT_TRUE(swss_asan_init_impl(mock_sigaction, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.malloc_calls, 1); + EXPECT_EQ(state_.malloc_size, SWSS_ASAN_TEST_LEAK_SIZE); + ASSERT_EQ(state_.storage.size(), SWSS_ASAN_TEST_LEAK_SIZE); + EXPECT_EQ(state_.storage.front(), static_cast(0xCD)); + EXPECT_EQ(state_.storage.back(), static_cast(0xCD)); + EXPECT_EQ(state_.storage[state_.storage.size() / 2], static_cast(0xCD)); +} + +TEST_F(AsanInitTest, MallocFailureStillReturnsTrue) +{ + state_.access_rc = 0; + state_.malloc_fail = true; + + // Injection failure is logged; init itself still succeeds so the daemon + // keeps running with the SIGTERM handler installed. + ASSERT_TRUE(swss_asan_init_impl(mock_sigaction, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.malloc_calls, 1); + EXPECT_EQ(state_.malloc_size, SWSS_ASAN_TEST_LEAK_SIZE); + EXPECT_TRUE(state_.storage.empty()); +} + +TEST(AsanInjectTest, FillsAllocationViaInjectedMalloc) +{ + AsanTestState state; + g_state = &state; + + swss_asan_inject_test_leak(mock_malloc); + + EXPECT_EQ(state.malloc_calls, 1); + EXPECT_EQ(state.malloc_size, SWSS_ASAN_TEST_LEAK_SIZE); + ASSERT_EQ(state.storage.size(), SWSS_ASAN_TEST_LEAK_SIZE); + EXPECT_EQ(state.storage.front(), static_cast(0xCD)); + EXPECT_EQ(state.storage.back(), static_cast(0xCD)); + + g_state = nullptr; +} + +TEST(AsanInjectTest, NullMallocIsANoOp) +{ + AsanTestState state; + state.malloc_fail = true; + g_state = &state; + + swss_asan_inject_test_leak(mock_malloc); + + EXPECT_EQ(state.malloc_calls, 1); + EXPECT_TRUE(state.storage.empty()); + + g_state = nullptr; +} + +class AsanSigtermHandlerTest : public ::testing::Test +{ +protected: + void SetUp() override + { + state_ = {}; + g_state = &state_; + } + + void TearDown() override + { + g_state = nullptr; + } + + AsanTestState state_; +}; + +TEST_F(AsanSigtermHandlerTest, RunsLeakCheckWhenProvided) +{ + state_.sigaction.oldact.sa_handler = SIG_DFL; + state_.sigaction.oldact_set = true; + + invoke_handler_impl(); + + EXPECT_EQ(state_.leak_check_calls, 1); + EXPECT_EQ(state_.sigaction.query_calls, 1); + EXPECT_EQ(state_.sigaction.set_calls, 0); + EXPECT_EQ(state_.raise_calls, 0); + EXPECT_EQ(state_.exit_calls, 0); +} + +TEST_F(AsanSigtermHandlerTest, NullLeakCheckDoesNotCrash) +{ + state_.sigaction.oldact.sa_handler = SIG_DFL; + state_.sigaction.oldact_set = true; + + swss_asan_sigterm_handler_impl(SIGTERM, nullptr, mock_sigaction, mock_exit, mock_raise); + + EXPECT_EQ(state_.leak_check_calls, 0); + EXPECT_EQ(state_.sigaction.query_calls, 1); + EXPECT_EQ(state_.raise_calls, 0); + EXPECT_EQ(state_.exit_calls, 0); +} + +TEST_F(AsanSigtermHandlerTest, OwnHandlerRestoresDefaultAndRaises) +{ + state_.sigaction.oldact.sa_handler = swss_asan_sigterm_handler; + state_.sigaction.oldact_set = true; + + invoke_handler_impl(); + + EXPECT_EQ(state_.leak_check_calls, 1); + EXPECT_EQ(state_.sigaction.query_calls, 1); + EXPECT_EQ(state_.sigaction.set_calls, 1); + EXPECT_EQ(state_.sigaction.last_set.sa_handler, SIG_DFL); + EXPECT_EQ(state_.raise_calls, 1); + EXPECT_EQ(state_.raise_signo, SIGTERM); + EXPECT_EQ(state_.exit_calls, 0); +} + +TEST_F(AsanSigtermHandlerTest, AppHandlerOnlyRunsLeakCheck) +{ + // A non-ASAN handler means the application installed its own; after the + // leak check the ASAN handler must not restore SIG_DFL or re-raise. + state_.sigaction.oldact.sa_handler = SIG_IGN; + state_.sigaction.oldact_set = true; + + invoke_handler_impl(); + + EXPECT_EQ(state_.leak_check_calls, 1); + EXPECT_EQ(state_.sigaction.query_calls, 1); + EXPECT_EQ(state_.sigaction.set_calls, 0); + EXPECT_EQ(state_.raise_calls, 0); + EXPECT_EQ(state_.exit_calls, 0); +} + +TEST_F(AsanSigtermHandlerTest, QuerySigactionFailureExits) +{ + state_.sigaction.rcs = {-1}; + + invoke_handler_impl(); + + EXPECT_EQ(state_.leak_check_calls, 1); + EXPECT_EQ(state_.sigaction.query_calls, 1); + EXPECT_EQ(state_.sigaction.set_calls, 0); + EXPECT_EQ(state_.raise_calls, 0); + EXPECT_EQ(state_.exit_calls, 1); + EXPECT_EQ(state_.exit_status, EXIT_FAILURE); +} + +TEST_F(AsanSigtermHandlerTest, RestoreDefaultSigactionFailureExits) +{ + state_.sigaction.oldact.sa_handler = swss_asan_sigterm_handler; + state_.sigaction.oldact_set = true; + // First call (query) succeeds; second call (set SIG_DFL) fails. + state_.sigaction.rcs = {0, -1}; + + invoke_handler_impl(); + + EXPECT_EQ(state_.leak_check_calls, 1); + EXPECT_EQ(state_.sigaction.query_calls, 1); + EXPECT_EQ(state_.sigaction.set_calls, 1); + EXPECT_EQ(state_.raise_calls, 0); + EXPECT_EQ(state_.exit_calls, 1); + EXPECT_EQ(state_.exit_status, EXIT_FAILURE); +} diff --git a/tlm_teamd/Makefile.am b/tlm_teamd/Makefile.am index 4548ea06ba3..5d7006153d4 100644 --- a/tlm_teamd/Makefile.am +++ b/tlm_teamd/Makefile.am @@ -19,6 +19,6 @@ tlm_teamd_SOURCES += ../gcovpreload/gcovpreload.cpp endif if ASAN_ENABLED -tlm_teamd_SOURCES += $(top_srcdir)/lib/asan.cpp +tlm_teamd_SOURCES += $(top_srcdir)/lib/asan.cpp $(top_srcdir)/lib/asan_ctor.cpp endif