diff --git a/syncd/Asan.cpp b/syncd/Asan.cpp index 3bc84ff646..39c121f5bf 100644 --- a/syncd/Asan.cpp +++ b/syncd/Asan.cpp @@ -1,32 +1,127 @@ +/* + * 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 syncd builds also link AsanCtor.cpp, whose constructor calls + * asan_init_impl() before main(). Unit tests leave AsanCtor.cpp out and call + * asan_init_impl() / asan_sigterm_handler_impl() with test-double functions for + * the dependencies. + */ + +#include "Asan.h" + #include "swss/logger.h" -#include -#include +#include +#include +#include +#include +#include + +/* 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. syncd is + * long-running and is stopped via SIGTERM, so that is the path that matters. + * + * 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 asan_init_impl(); invoked from the SIGTERM handler. +static AsanLsanLeakCheckFn g_lsan_leak_check = nullptr; + +__attribute__((noinline)) +void asan_inject_test_leak(AsanMallocFn malloc_fn) +{ + SWSS_LOG_ENTER(); -extern "C" { - const char* __lsan_default_suppressions() { - // SWSS_LOG_ENTER(); // disabled - return "leak:__static_initialization_and_destruction_0\n"; + void *probe = malloc_fn(ASAN_TEST_LEAK_SIZE); + if (!probe) + { + SWSS_LOG_ERROR("failed to allocate %zu bytes for the ASAN test leak, no leak injected", + ASAN_TEST_LEAK_SIZE); + return; } + + std::memset(probe, 0xCD, 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 sigterm_handler(int signo) +void asan_sigterm_handler_impl(int signo, + AsanLsanLeakCheckFn leak_check_fn, + AsanSignalFn signal_fn, + AsanRaiseFn raise_fn) { SWSS_LOG_ENTER(); - __lsan_do_leak_check(); - signal(signo, SIG_DFL); - raise(signo); + if (leak_check_fn) + { + leak_check_fn(); + } + + signal_fn(signo, SIG_DFL); + raise_fn(signo); +} + +void asan_sigterm_handler(int signo) +{ + SWSS_LOG_ENTER(); + + asan_sigterm_handler_impl(signo, g_lsan_leak_check, ::signal, ::raise); } -__attribute__((constructor)) -static void asan_init() +bool asan_init_impl(AsanSignalFn signal_fn, + AsanAccessFn access_fn, + AsanMallocFn malloc_fn, + AsanLsanLeakCheckFn leak_check_fn) { SWSS_LOG_ENTER(); - if (signal(SIGTERM, sigterm_handler) == SIG_ERR) + g_lsan_leak_check = leak_check_fn; + + if (signal_fn(SIGTERM, asan_sigterm_handler) == SIG_ERR) { SWSS_LOG_ERROR("failed to setup SIGTERM action"); - exit(1); + return false; } + + if (access_fn("/etc/sonic/inject_asan_test_leak_enabled", F_OK) == 0) + { + try + { + // See comment above asan_inject_test_leak() for why this must run + // in a separate thread. + std::thread(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/syncd/Asan.h b/syncd/Asan.h new file mode 100644 index 0000000000..399e344b8c --- /dev/null +++ b/syncd/Asan.h @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES + * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Testable ASAN helpers for syncd. Production builds enable them via the + * constructor in AsanCtor.cpp; unit tests call asan_init_impl() / + * asan_sigterm_handler_impl() with injected dependencies and leave + * AsanCtor.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 ASAN_TEST_LEAK_SIZE = 9861842; + +// Function pointers for real implementations or test doubles. +// signal() from csignal +using AsanSignalFn = sighandler_t (*)(int, sighandler_t); +// access() from unistd.h +using AsanAccessFn = int (*)(const char *, int); +// malloc() from cstdlib +using AsanMallocFn = void *(*)(size_t); +// __lsan_do_leak_check() from sanitizer/lsan_interface.h +using AsanLsanLeakCheckFn = void (*)(void); +// raise() from csignal +using AsanRaiseFn = int (*)(int); + +// SIGTERM handler installed by asan_init_impl(). Thin wrapper around +// 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 signal(). +void asan_sigterm_handler(int signo); + +// Testable SIGTERM-handler body. Production wrapper passes g_lsan_leak_check, +// ::signal, and ::raise; unit tests inject doubles. +void asan_sigterm_handler_impl(int signo, + AsanLsanLeakCheckFn leak_check_fn, + AsanSignalFn signal_fn, + AsanRaiseFn raise_fn); + +// Allocate (and never free) the intentional test leak via malloc_fn. +void asan_inject_test_leak(AsanMallocFn malloc_fn); + +// Set up ASAN helpers: +// - Installs a SIGTERM handler that runs leak_check_fn. +// - 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 asan_init_impl(AsanSignalFn signal_fn, + AsanAccessFn access_fn, + AsanMallocFn malloc_fn, + AsanLsanLeakCheckFn leak_check_fn); diff --git a/syncd/AsanCtor.cpp b/syncd/AsanCtor.cpp new file mode 100644 index 0000000000..1b25f1ca9b --- /dev/null +++ b/syncd/AsanCtor.cpp @@ -0,0 +1,43 @@ +/* + * 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 asan_init_impl() into process initialization + * before main(). + * - default LSan suppressions + * + * Include this file only in ASAN builds where you want that functionality + * enabled (ENABLE_ASAN=y syncd). 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 asan_init_impl() with test doubles for the + * dependencies. + */ + +#include "Asan.h" + +#include "swss/logger.h" + +#include +#include +#include + +extern "C" { + const char* __lsan_default_suppressions() { + // SWSS_LOG_ENTER(); // disabled + return "leak:__static_initialization_and_destruction_0\n"; + } +} + +__attribute__((constructor)) +static void asan_init() +{ + SWSS_LOG_ENTER(); + + if (!asan_init_impl(::signal, ::access, std::malloc, __lsan_do_leak_check)) + { + exit(EXIT_FAILURE); + } +} diff --git a/syncd/Makefile.am b/syncd/Makefile.am index c5bb32f3e8..d71e71f43b 100644 --- a/syncd/Makefile.am +++ b/syncd/Makefile.am @@ -68,7 +68,7 @@ libSyncd_a_CXXFLAGS = $(DBGFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS_COMMON) $(CODE_COVER syncd_SOURCES = main.cpp if ASAN_ENABLED -syncd_SOURCES += Asan.cpp +syncd_SOURCES += Asan.cpp AsanCtor.cpp libSyncd_a_CXXFLAGS += -DASAN_ENABLED endif syncd_CPPFLAGS = $(CODE_COVERAGE_CPPFLAGS) diff --git a/tests/aspell.en.pws b/tests/aspell.en.pws index 5eb0162a8d..c32fead4ea 100644 --- a/tests/aspell.en.pws +++ b/tests/aspell.en.pws @@ -7,6 +7,7 @@ AIED API APIs ASAN +Asan ASIC ASICs ATTR @@ -59,6 +60,7 @@ LLC LLR LOGLEVEL LOOPBACK +LSan MACsec MCAST MTU @@ -88,7 +90,9 @@ SCs SDK SGs SHA +SIGTERM SONiC +SPDX SRC STP SaiAttr @@ -131,6 +135,7 @@ ASICs asicSet asicview AsicView +asm async attr ATTR @@ -164,6 +169,8 @@ cpp cpu CreateObject CRM +csignal +cstdlib currentObj currentObject currentView @@ -267,6 +274,7 @@ KEYs KEYs lck lgtm +libc librediscommon libsairedis libswsscommon @@ -279,12 +287,14 @@ LOOPBACK lua macsec MACsec +malloc MCAST md mdio MDIO Mellanox memcpy +memset metadata mlnx mpls @@ -395,6 +405,7 @@ shmem sleeptime soAll SONiC +SPDX splitted src SRC @@ -412,6 +423,7 @@ struct structs structure subport +suppressions sw swid Switch diff --git a/unittest/syncd/Makefile.am b/unittest/syncd/Makefile.am index 49cec2fc36..586ff168e0 100644 --- a/unittest/syncd/Makefile.am +++ b/unittest/syncd/Makefile.am @@ -26,7 +26,9 @@ tests_SOURCES = main.cpp \ TestWorkaround.cpp \ TestSyncd.cpp \ TestVendorSai.cpp \ - TestFlowDump.cpp + TestFlowDump.cpp \ + TestAsan.cpp \ + $(top_srcdir)/syncd/Asan.cpp tests_CXXFLAGS = $(DBGFLAGS) $(AM_CXXFLAGS) $(CXXFLAGS_COMMON) -fno-access-control tests_LDFLAGS = -Wl,-rpath,$(top_srcdir)/lib/.libs -Wl,-rpath,$(top_srcdir)/meta/.libs diff --git a/unittest/syncd/TestAsan.cpp b/unittest/syncd/TestAsan.cpp new file mode 100644 index 0000000000..446f7e0521 --- /dev/null +++ b/unittest/syncd/TestAsan.cpp @@ -0,0 +1,250 @@ +#include "Asan.h" + +#include +#include +#include +#include + +namespace +{ + +struct AsanSignalState +{ + // When true, mock_signal returns SIG_ERR. + bool fail = false; + + int calls = 0; + int last_sig = 0; + // Last handler passed to signal() (init install or handler restore). + sighandler_t last_set = SIG_DFL; +}; + +struct AsanTestState +{ + AsanSignalState signal; + + 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 raise_calls = 0; + int raise_signo = -1; +}; + +AsanTestState *g_state = nullptr; + +sighandler_t mock_signal(int sig, sighandler_t handler) +{ + // SWSS_LOG_ENTER(); // disabled + EXPECT_NE(g_state, nullptr); + auto& sigst = g_state->signal; + sigst.calls++; + sigst.last_sig = sig; + sigst.last_set = handler; + return sigst.fail ? SIG_ERR : SIG_DFL; +} + +int mock_access(const char *path, int mode) +{ + // SWSS_LOG_ENTER(); // disabled + 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) +{ + // SWSS_LOG_ENTER(); // disabled + 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) +{ + // SWSS_LOG_ENTER(); // disabled + EXPECT_NE(g_state, nullptr); + g_state->leak_check_calls++; +} + +int mock_raise(int signo) +{ + // SWSS_LOG_ENTER(); // disabled + EXPECT_NE(g_state, nullptr); + g_state->raise_calls++; + g_state->raise_signo = signo; + return 0; +} + +void invoke_handler_impl() +{ + // SWSS_LOG_ENTER(); // disabled + asan_sigterm_handler_impl(SIGTERM, mock_leak_check, mock_signal, 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(asan_init_impl(mock_signal, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.signal.calls, 1); + EXPECT_EQ(state_.signal.last_sig, SIGTERM); + EXPECT_EQ(state_.signal.last_set, 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, SignalFailureReturnsFalse) +{ + state_.signal.fail = true; + + EXPECT_FALSE(asan_init_impl(mock_signal, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.signal.calls, 1); + EXPECT_EQ(state_.access_calls, 0); + EXPECT_EQ(state_.malloc_calls, 0); +} + +TEST_F(AsanInitTest, SkipsLeakInjectionWhenFlagFileMissing) +{ + state_.access_rc = -1; + + ASSERT_TRUE(asan_init_impl(mock_signal, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.malloc_calls, 0); +} + +TEST_F(AsanInitTest, InjectsLeakWhenFlagFilePresent) +{ + state_.access_rc = 0; + + ASSERT_TRUE(asan_init_impl(mock_signal, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.malloc_calls, 1); + EXPECT_EQ(state_.malloc_size, ASAN_TEST_LEAK_SIZE); + ASSERT_EQ(state_.storage.size(), 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 syncd keeps + // running with the SIGTERM handler installed. + ASSERT_TRUE(asan_init_impl(mock_signal, mock_access, mock_malloc, mock_leak_check)); + + EXPECT_EQ(state_.malloc_calls, 1); + EXPECT_EQ(state_.malloc_size, ASAN_TEST_LEAK_SIZE); + EXPECT_TRUE(state_.storage.empty()); +} + +TEST(AsanInjectTest, FillsAllocationViaInjectedMalloc) +{ + AsanTestState state; + g_state = &state; + + asan_inject_test_leak(mock_malloc); + + EXPECT_EQ(state.malloc_calls, 1); + EXPECT_EQ(state.malloc_size, ASAN_TEST_LEAK_SIZE); + ASSERT_EQ(state.storage.size(), 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; + + 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, RunsLeakCheckRestoresDefaultAndRaises) +{ + invoke_handler_impl(); + + EXPECT_EQ(state_.leak_check_calls, 1); + EXPECT_EQ(state_.signal.calls, 1); + EXPECT_EQ(state_.signal.last_sig, SIGTERM); + EXPECT_EQ(state_.signal.last_set, SIG_DFL); + EXPECT_EQ(state_.raise_calls, 1); + EXPECT_EQ(state_.raise_signo, SIGTERM); +} + +TEST_F(AsanSigtermHandlerTest, NullLeakCheckDoesNotCrash) +{ + asan_sigterm_handler_impl(SIGTERM, nullptr, mock_signal, mock_raise); + + EXPECT_EQ(state_.leak_check_calls, 0); + EXPECT_EQ(state_.signal.calls, 1); + EXPECT_EQ(state_.signal.last_set, SIG_DFL); + EXPECT_EQ(state_.raise_calls, 1); + EXPECT_EQ(state_.raise_signo, SIGTERM); +}