From f832d75dd99272510e2578a4f1f82510924ceb63 Mon Sep 17 00:00:00 2001 From: Judson Wilson Date: Wed, 6 May 2026 00:51:44 +0300 Subject: [PATCH 1/2] Add optional ASAN test-leak injection gated by a config file If /etc/sonic/inject_asan_test_leak_enabled exists, swss daemons inject a known leak, so that when sent SIGTERM the leak detector will emit an ASAN report. This can be used to verify that the ASAN build and the leak check in the signal handler are working as intended. This matches a similar addition being made in sonic-sairedis. How to verify it: 1. Create /etc/sonic/inject_asan_test_leak_enabled. 2. Restart the daemon once if necessary (e.g. `config reload`) so it starts with leak injection enabled. 3. Run `config reload` or reboot to send SIGTERM to the daemon. 4. After it exits, daemons that are working correctly will produce ASAN report files in /var/log/asan/. Signed-off-by: Judson Wilson --- cfgmgr/Makefile.am | 31 +++--- fdbsyncd/Makefile.am | 2 +- fpmsyncd/Makefile.am | 2 +- gearsyncd/Makefile.am | 2 +- lib/asan.cpp | 102 +++++++++++++++++--- lib/asan.h | 45 +++++++++ lib/asan_ctor.cpp | 42 ++++++++ mclagsyncd/Makefile.am | 2 +- natsyncd/Makefile.am | 2 +- neighsyncd/Makefile.am | 2 +- orchagent/Makefile.am | 6 +- portsyncd/Makefile.am | 2 +- swssconfig/Makefile.am | 4 +- teamsyncd/Makefile.am | 2 +- tests/mock_tests/Makefile.am | 2 + tests/mock_tests/asan_ut.cpp | 182 +++++++++++++++++++++++++++++++++++ tlm_teamd/Makefile.am | 2 +- 17 files changed, 392 insertions(+), 40 deletions(-) create mode 100644 lib/asan.h create mode 100644 lib/asan_ctor.cpp create mode 100644 tests/mock_tests/asan_ut.cpp 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..5bfe9a50bd7 100644 --- a/lib/asan.cpp +++ b/lib/asan.cpp @@ -1,20 +1,80 @@ +/* + * 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() 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(int signo) { SWSS_LOG_ENTER(); - __lsan_do_leak_check(); + if (g_lsan_leak_check) + { + g_lsan_leak_check(); + } struct sigaction sigact; if (sigaction(SIGTERM, NULL, &sigact)) @@ -42,16 +102,36 @@ static void swss_asan_sigterm_handler(int signo) } } -__attribute__((constructor)) -static void swss_asan_init() +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..e0e86cdfd4d --- /dev/null +++ b/lib/asan.h @@ -0,0 +1,45 @@ +/* + * 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() 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); + +// SIGTERM handler installed by swss_asan_init_impl(). Exposed so tests can +// verify the handler pointer that was passed to sigaction. +void swss_asan_sigterm_handler(int signo); + +// 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..a52995cc6b6 --- /dev/null +++ b/tests/mock_tests/asan_ut.cpp @@ -0,0 +1,182 @@ +#include "asan.h" + +#include + +#include + +#include +#include +#include + +namespace +{ + +struct AsanTestState +{ + int sigaction_calls = 0; + int sigaction_rc = 0; + int last_sig = 0; + struct sigaction installed = {}; + + 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; +}; + +AsanTestState *g_state = nullptr; + +int mock_sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) +{ + EXPECT_NE(g_state, nullptr); + g_state->sigaction_calls++; + g_state->last_sig = sig; + if (act) + { + g_state->installed = *act; + } + (void)oldact; + return g_state->sigaction_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) +{ +} + +} // 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_.last_sig, SIGTERM); + EXPECT_EQ(state_.installed.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_rc = -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; +} 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 From 4e83557678ab21fedae2df363a057dd5c7ad0152 Mon Sep 17 00:00:00 2001 From: Judson Wilson Date: Thu, 6 Aug 2026 05:41:52 +0300 Subject: [PATCH 2/2] More unit tests Signed-off-by: Judson Wilson --- lib/asan.cpp | 30 ++++-- lib/asan.h | 23 +++- tests/mock_tests/asan_ut.cpp | 197 ++++++++++++++++++++++++++++++++--- 3 files changed, 223 insertions(+), 27 deletions(-) diff --git a/lib/asan.cpp b/lib/asan.cpp index 5bfe9a50bd7..24eccead2af 100644 --- a/lib/asan.cpp +++ b/lib/asan.cpp @@ -6,7 +6,8 @@ * * 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() with test-double functions for the dependencies. + * call swss_asan_init_impl() / swss_asan_sigterm_handler_impl() with test- + * double functions for the dependencies. */ #include "asan.h" @@ -67,20 +68,25 @@ void swss_asan_inject_test_leak(SwssMallocFn malloc_fn) asm volatile("" : : "r"(probe) : "memory"); } -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(); - if (g_lsan_leak_check) + if (leak_check_fn) { - g_lsan_leak_check(); + 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. @@ -92,16 +98,22 @@ 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); } } +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, diff --git a/lib/asan.h b/lib/asan.h index e0e86cdfd4d..fdc8e742652 100644 --- a/lib/asan.h +++ b/lib/asan.h @@ -4,8 +4,9 @@ * 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() with injected - * dependencies and leave asan_ctor.cpp out of the link. + * 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 @@ -25,11 +26,25 @@ using SwssAccessFn = int (*)(const char *, int); 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(). Exposed so tests can -// verify the handler pointer that was passed to sigaction. +// 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); diff --git a/tests/mock_tests/asan_ut.cpp b/tests/mock_tests/asan_ut.cpp index a52995cc6b6..4c3ce1ad97e 100644 --- a/tests/mock_tests/asan_ut.cpp +++ b/tests/mock_tests/asan_ut.cpp @@ -11,12 +11,27 @@ namespace { -struct AsanTestState +struct AsanSigactionState { - int sigaction_calls = 0; - int sigaction_rc = 0; + 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; - struct sigaction installed = {}; + // 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; @@ -27,6 +42,14 @@ struct AsanTestState // 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; @@ -34,14 +57,31 @@ AsanTestState *g_state = nullptr; int mock_sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) { EXPECT_NE(g_state, nullptr); - g_state->sigaction_calls++; - g_state->last_sig = sig; + 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) { - g_state->installed = *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)); + } } - (void)oldact; - return g_state->sigaction_rc; + return rc; } int mock_access(const char *path, int mode) @@ -68,6 +108,28 @@ void *mock_malloc(size_t size) 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 @@ -95,9 +157,9 @@ TEST_F(AsanInitTest, InstallsSigtermHandler) ASSERT_TRUE(swss_asan_init_impl(mock_sigaction, mock_access, mock_malloc, mock_leak_check)); - EXPECT_EQ(state_.sigaction_calls, 1); - EXPECT_EQ(state_.last_sig, SIGTERM); - EXPECT_EQ(state_.installed.sa_handler, swss_asan_sigterm_handler); + 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); @@ -105,11 +167,11 @@ TEST_F(AsanInitTest, InstallsSigtermHandler) TEST_F(AsanInitTest, SigactionFailureReturnsFalse) { - state_.sigaction_rc = -1; + 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_.sigaction.calls, 1); EXPECT_EQ(state_.access_calls, 0); EXPECT_EQ(state_.malloc_calls, 0); } @@ -180,3 +242,110 @@ TEST(AsanInjectTest, NullMallocIsANoOp) 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); +}