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
123 changes: 109 additions & 14 deletions syncd/Asan.cpp
Original file line number Diff line number Diff line change
@@ -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 <csignal>
#include <sanitizer/lsan_interface.h>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <thread>
#include <unistd.h>

/* 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;
}
57 changes: 57 additions & 0 deletions syncd/Asan.h
Original file line number Diff line number Diff line change
@@ -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 <csignal>
#include <cstddef>

// 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);
43 changes: 43 additions & 0 deletions syncd/AsanCtor.cpp
Original file line number Diff line number Diff line change
@@ -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 <cstdlib>
#include <sanitizer/lsan_interface.h>
#include <unistd.h>

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);
}
}
2 changes: 1 addition & 1 deletion syncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions tests/aspell.en.pws
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ AIED
API
APIs
ASAN
Asan
ASIC
ASICs
ATTR
Expand Down Expand Up @@ -59,6 +60,7 @@ LLC
LLR
LOGLEVEL
LOOPBACK
LSan
MACsec
MCAST
MTU
Expand Down Expand Up @@ -88,7 +90,9 @@ SCs
SDK
SGs
SHA
SIGTERM
SONiC
SPDX
SRC
STP
SaiAttr
Expand Down Expand Up @@ -131,6 +135,7 @@ ASICs
asicSet
asicview
AsicView
asm
async
attr
ATTR
Expand Down Expand Up @@ -164,6 +169,8 @@ cpp
cpu
CreateObject
CRM
csignal
cstdlib
currentObj
currentObject
currentView
Expand Down Expand Up @@ -267,6 +274,7 @@ KEYs
KEYs
lck
lgtm
libc
librediscommon
libsairedis
libswsscommon
Expand All @@ -279,12 +287,14 @@ LOOPBACK
lua
macsec
MACsec
malloc
MCAST
md
mdio
MDIO
Mellanox
memcpy
memset
metadata
mlnx
mpls
Expand Down Expand Up @@ -395,6 +405,7 @@ shmem
sleeptime
soAll
SONiC
SPDX
splitted
src
SRC
Expand All @@ -412,6 +423,7 @@ struct
structs
structure
subport
suppressions
sw
swid
Switch
Expand Down
4 changes: 3 additions & 1 deletion unittest/syncd/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading