Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions cfgmgr/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion fdbsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion fpmsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion gearsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

124 changes: 108 additions & 16 deletions lib/asan.cpp
Original file line number Diff line number Diff line change
@@ -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 <unistd.h>
#include <signal.h>
#include <sanitizer/lsan_interface.h>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <thread>

#include <logger.h>

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.
Expand All @@ -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;
}
60 changes: 60 additions & 0 deletions lib/asan.h
Original file line number Diff line number Diff line change
@@ -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 <cstddef>
#include <signal.h>

// 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);
42 changes: 42 additions & 0 deletions lib/asan_ctor.cpp
Original file line number Diff line number Diff line change
@@ -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 <unistd.h>

#include <cstdlib>
#include <sanitizer/lsan_interface.h>

// 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);
}
}
2 changes: 1 addition & 1 deletion mclagsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion natsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion neighsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

6 changes: 3 additions & 3 deletions orchagent/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion portsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

4 changes: 2 additions & 2 deletions swssconfig/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion teamsyncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 2 additions & 0 deletions tests/mock_tests/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
Loading