From 755fed1645f4a83558f9141399e9f4c0d6661ea2 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 10 Mar 2026 11:06:06 +0100 Subject: [PATCH 01/24] refactor: FpeMonitoring CMake backtrace detection --- Plugins/FpeMonitoring/CMakeLists.txt | 89 +++++++++++++++------------- 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index 99624045656..32d5ef507f3 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -22,16 +22,12 @@ set(_fpe_options "") if(ACTS_RUN_CLANG_TIDY) list(APPEND _fpe_options "-DBOOST_STACKTRACE_USE_NOOP=1") else() - include(CheckCXXSourceCompiles) - - find_library(dl_LIBRARY dl) find_package(Backtrace) find_program(addr2line_EXECUTABLE addr2line) if(APPLE) list(APPEND _fpe_options -D_GNU_SOURCE) - else() - if(dl_LIBRARY) - target_link_libraries(ActsPluginFpeMonitoring PUBLIC ${dl_LIBRARY}) + elseif(CMAKE_DL_LIBS) + target_link_libraries(ActsPluginFpeMonitoring PUBLIC ${CMAKE_DL_LIBS}) set(_backtrace_setup_complete FALSE) @@ -41,45 +37,54 @@ else() REQUIRED PATHS ${Boost_INCLUDE_DIRS} ) - if(Backtrace_FOUND) - # check if we need to link against bracktrace or not - set(backtrace_include "") - - file( - WRITE - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - " - #include - int main() {} - " - ) - - message( - CHECK_START - "Does backtrace work with the default include" - ) - - try_compile( - _backtrace_default_header - "${CMAKE_BINARY_DIR}" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - LINK_LIBRARIES ${dl_LIBRARY} - CMAKE_FLAGS - "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS -DBOOST_STACKTRACE_USE_BACKTRACE - OUTPUT_VARIABLE __OUTPUT - ) - - if(_backtrace_default_header) - message(CHECK_PASS "yes") + + file( + WRITE + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + "#include \n" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) + + set(_backtrace_include_def "") + + message(CHECK_START "Does backtrace work without linker flag") + try_compile( + _backtrace_nolink + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + LINK_LIBRARIES ${CMAKE_DL_LIBS} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS -DBOOST_STACKTRACE_USE_BACKTRACE + OUTPUT_VARIABLE __OUTPUT + ) + + if(_backtrace_nolink) + message(CHECK_PASS "yes") + list(APPEND _fpe_options -DBOOST_STACKTRACE_USE_BACKTRACE) + set(_backtrace_setup_complete TRUE) + else() + message(CHECK_FAIL "no") + + file(GLOB hints "/usr/lib/gcc/*/*/include") + find_file(backtrace_header NAMES "backtrace.h" HINTS ${hints}) + + if(${backtrace_header} STREQUAL "backtrace_header-NOTFOUND") + message(STATUS "Could not find backtrace header file") else() message(CHECK_FAIL "no") - file(GLOB hints "/usr/lib/gcc/*/*/include") - find_file( - backtrace_header - NAMES "backtrace.h" - HINTS ${hints} + message(CHECK_START "Does backtrace work with explicit include") + try_compile( + _backtrace_explicit + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + LINK_LIBRARIES ${CMAKE_DL_LIBS} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS + -DBOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} + OUTPUT_VARIABLE __OUTPUT ) if(${backtrace_header} STREQUAL "backtrace_header-NOTFOUND") From e03cc4c8ac3be7bab1ebebea97bf6b8bd9a1a86f Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 10 Mar 2026 11:06:06 +0100 Subject: [PATCH 02/24] feat(FpeMonitoring): add platform-specific FPE trapping, fix pushOffset bug, add fault location tracking --- .../ActsPlugins/FpeMonitoring/FpeMonitor.hpp | 14 +- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 386 +++++++++++++++--- Python/Examples/src/Framework.cpp | 5 + Python/Examples/tests/test_fpe.py | 16 +- .../Plugins/FpeMonitoring/CMakeLists.txt | 13 +- 5 files changed, 365 insertions(+), 69 deletions(-) diff --git a/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp b/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp index 9e454eb803e..bbba477109c 100644 --- a/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp +++ b/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -75,7 +76,7 @@ class FpeMonitor { /// @param offset Number of bytes to advance void pushOffset(std::size_t offset) { assert(m_offset + offset < m_size); - m_offset = offset; + m_offset += offset; } /// Reset buffer offset to beginning @@ -168,10 +169,13 @@ class FpeMonitor { /// @param type Exception type /// @param stackPtr Pointer to stack data /// @param bufferSize Size of stack buffer - void add(FpeType type, void *stackPtr, std::size_t bufferSize); + /// @param location Faulting instruction address if available + void add(FpeType type, void *stackPtr, std::size_t bufferSize, + std::uintptr_t location = 0); private: std::vector m_stackTraces; + std::vector m_locations; std::array m_counts{}; friend FpeMonitor; @@ -210,6 +214,9 @@ class FpeMonitor { /// Check if stack trace symbolization is available /// @return True if symbolization is available static bool canSymbolize(); + /// Check if trapping-based FPE monitoring is supported on this platform + /// @return True if runtime support is available + static bool isSupported(); private: void enable(); @@ -232,7 +239,8 @@ class FpeMonitor { Buffer m_buffer{65536}; - boost::container::static_vector, 128> + boost::container::static_vector< + std::tuple, 128> m_recorded; }; diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index 6ba097db300..505d54d5028 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -16,10 +16,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -37,6 +39,16 @@ namespace ActsPlugins { namespace { + +#if (defined(__linux__) && defined(__x86_64__)) || \ + (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) +constexpr bool kFpeRuntimeSupported = true; +#else +// Keep helper boundaries architecture-oriented so Linux aarch64 can be added +// by implementing decode + context masking paths, without touching call sites. +constexpr bool kFpeRuntimeSupported = false; +#endif + bool areFpesEquivalent( std::pair lhs, std::pair rhs) { @@ -45,6 +57,201 @@ bool areFpesEquivalent( return lhs.first == rhs.first && (boost::stacktrace::hash_value(fl) == boost::stacktrace::hash_value(fr)); } + +std::optional fpeTypeFromSiCode(int siCode) { + switch (siCode) { + case FPE_INTDIV: + return FpeType::INTDIV; + case FPE_INTOVF: + return FpeType::INTOVF; + case FPE_FLTDIV: + return FpeType::FLTDIV; + case FPE_FLTOVF: + return FpeType::FLTOVF; + case FPE_FLTUND: + return FpeType::FLTUND; + case FPE_FLTRES: + return FpeType::FLTRES; + case FPE_FLTINV: + return FpeType::FLTINV; + case FPE_FLTSUB: + return FpeType::FLTSUB; + default: + return std::nullopt; + } +} + +#if defined(__APPLE__) && defined(__arm64__) + +std::uint32_t darwinArm64TrapMask(int excepts) { + std::uint32_t mask = 0; + if ((excepts & FE_INVALID) != 0) { + mask |= __fpcr_trap_invalid; + } + if ((excepts & FE_DIVBYZERO) != 0) { + mask |= __fpcr_trap_divbyzero; + } + if ((excepts & FE_OVERFLOW) != 0) { + mask |= __fpcr_trap_overflow; + } + if ((excepts & FE_UNDERFLOW) != 0) { + mask |= __fpcr_trap_underflow; + } + if ((excepts & FE_INEXACT) != 0) { + mask |= __fpcr_trap_inexact; + } + return mask; +} + +std::optional fpeTypeFromDarwinArm64Esr(std::uint32_t esr) { + constexpr std::uint32_t kEsrExceptionClassShift = 26u; + constexpr std::uint32_t kEsrExceptionClassMask = 0x3fu; + constexpr std::uint32_t kFpExceptionClass = 0x2cu; + const std::uint32_t exceptionClass = + (esr >> kEsrExceptionClassShift) & kEsrExceptionClassMask; + if (exceptionClass != kFpExceptionClass) { + return std::nullopt; + } + + // The low ESR bits encode IEEE FP exception classes on Darwin arm64. + const std::uint32_t flags = esr & static_cast(FE_ALL_EXCEPT); + if ((flags & FE_INVALID) != 0) { + return FpeType::FLTINV; + } + if ((flags & FE_DIVBYZERO) != 0) { + return FpeType::FLTDIV; + } + if ((flags & FE_OVERFLOW) != 0) { + return FpeType::FLTOVF; + } + if ((flags & FE_UNDERFLOW) != 0) { + return FpeType::FLTUND; + } + if ((flags & FE_INEXACT) != 0) { + return FpeType::FLTRES; + } + return std::nullopt; +} + +#endif + +std::optional decodeFpeType(int signal, siginfo_t *si, void *ctx) { + if (signal == SIGFPE && si != nullptr) { + return fpeTypeFromSiCode(si->si_code); + } + +#if defined(__APPLE__) && defined(__arm64__) + if (signal == SIGILL && ctx != nullptr) { + auto *uc = static_cast(ctx); + return fpeTypeFromDarwinArm64Esr(uc->uc_mcontext->__es.__esr); + } +#endif + + return std::nullopt; +} + +int exceptMaskForType(FpeType type) { + switch (type) { + case FpeType::INTDIV: + case FpeType::FLTDIV: + return FE_DIVBYZERO; + case FpeType::INTOVF: + case FpeType::FLTOVF: + return FE_OVERFLOW; + case FpeType::FLTUND: + return FE_UNDERFLOW; + case FpeType::FLTRES: + return FE_INEXACT; + case FpeType::FLTINV: + case FpeType::FLTSUB: + return FE_INVALID; + default: + return 0; + } +} + +void clearPendingExceptions(int excepts) { std::feclearexcept(excepts); } + +void enableExceptions(int excepts) { +#if defined(__linux__) && defined(__x86_64__) + feenableexcept(excepts); +#elif defined(__APPLE__) && defined(__x86_64__) + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__control &= ~static_cast(excepts); + env.__mxcsr &= ~(static_cast(excepts) << 7u); + env.__status &= ~static_cast(FE_ALL_EXCEPT); + env.__mxcsr &= ~static_cast(FE_ALL_EXCEPT); + fesetenv(&env); +#elif defined(__APPLE__) && defined(__arm64__) + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__fpcr |= static_cast(darwinArm64TrapMask(excepts)); + env.__fpsr &= ~static_cast(FE_ALL_EXCEPT); + fesetenv(&env); +#else + static_cast(excepts); +#endif +} + +void disableExceptions(int excepts) { +#if defined(__linux__) && defined(__x86_64__) + fedisableexcept(excepts); +#elif defined(__APPLE__) && defined(__x86_64__) + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__control |= static_cast(excepts); + env.__mxcsr |= (static_cast(excepts) << 7u); + fesetenv(&env); +#elif defined(__APPLE__) && defined(__arm64__) + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__fpcr &= ~static_cast(darwinArm64TrapMask(excepts)); + fesetenv(&env); +#else + static_cast(excepts); +#endif +} + +void maskTrapsInSignalContext(void *ctx, FpeType type) { + const int excepts = exceptMaskForType(type); +#if defined(__linux__) && defined(__x86_64__) + auto *uc = static_cast(ctx); + __uint16_t *cw = &uc->uc_mcontext.fpregs->cwd; + *cw |= FPU_EXCEPTION_MASK; + + __uint16_t *sw = &uc->uc_mcontext.fpregs->swd; + *sw &= ~FPU_STATUS_FLAGS; + + __uint32_t *mxcsr = &uc->uc_mcontext.fpregs->mxcsr; + *mxcsr |= ((*mxcsr & SSE_STATUS_FLAGS) << 7); + *mxcsr &= ~SSE_STATUS_FLAGS; +#elif defined(__APPLE__) && defined(__x86_64__) + auto *uc = static_cast(ctx); + uc->uc_mcontext->__fs.__fpu_fcw |= + static_cast(excepts); + uc->uc_mcontext->__fs.__fpu_fsw &= ~static_cast(FE_ALL_EXCEPT); + uc->uc_mcontext->__fs.__fpu_mxcsr |= + (static_cast(excepts) << 7u); + uc->uc_mcontext->__fs.__fpu_mxcsr &= ~static_cast(FE_ALL_EXCEPT); +#elif defined(__APPLE__) && defined(__arm64__) + auto *uc = static_cast(ctx); + uc->uc_mcontext->__ns.__fpcr &= + ~static_cast(darwinArm64TrapMask(excepts)); + uc->uc_mcontext->__ns.__fpsr &= ~static_cast(FE_ALL_EXCEPT); +#else + static_cast(ctx); + static_cast(excepts); +#endif +} } // namespace FpeMonitor::Result::FpeInfo::~FpeInfo() = default; @@ -63,8 +270,12 @@ FpeMonitor::Result FpeMonitor::Result::merged(const Result &with) const { std::copy(with.m_stackTraces.begin(), with.m_stackTraces.end(), std::back_inserter(result.m_stackTraces)); + std::copy(with.m_locations.begin(), with.m_locations.end(), + std::back_inserter(result.m_locations)); std::copy(m_stackTraces.begin(), m_stackTraces.end(), std::back_inserter(result.m_stackTraces)); + std::copy(m_locations.begin(), m_locations.end(), + std::back_inserter(result.m_locations)); result.deduplicate(); @@ -78,24 +289,39 @@ void FpeMonitor::Result::merge(const Result &with) { std::copy(with.m_stackTraces.begin(), with.m_stackTraces.end(), std::back_inserter(m_stackTraces)); + std::copy(with.m_locations.begin(), with.m_locations.end(), + std::back_inserter(m_locations)); deduplicate(); } void FpeMonitor::Result::add(FpeType type, void *stackPtr, - std::size_t bufferSize) { + std::size_t bufferSize, std::uintptr_t location) { auto st = std::make_unique( boost::stacktrace::stacktrace::from_dump(stackPtr, bufferSize)); - auto it = std::ranges::find_if(m_stackTraces, [&](const FpeInfo &el) { - return areFpesEquivalent({el.type, *el.st}, {type, *st}); - }); + for (std::size_t i = 0; i < m_stackTraces.size(); ++i) { + auto &el = m_stackTraces[i]; + if (el.type != type) { + continue; + } + + if (location != 0 && m_locations[i] != 0) { + if (location == m_locations[i]) { + el.count += 1; + return; + } + continue; + } - if (it != m_stackTraces.end()) { - it->count += 1; - } else { - m_stackTraces.push_back({1, type, std::move(st)}); + if (areFpesEquivalent({el.type, *el.st}, {type, *st})) { + el.count += 1; + return; + } } + + m_stackTraces.push_back({1, type, std::move(st)}); + m_locations.push_back(location); } bool FpeMonitor::Result::contains( @@ -115,8 +341,8 @@ void FpeMonitor::consumeRecorded() { return; } - for (auto [type, stackPtr, remaining] : m_recorded) { - m_result.add(type, stackPtr, remaining); + for (auto [type, stackPtr, remaining, location] : m_recorded) { + m_result.add(type, stackPtr, remaining, location); } m_buffer.reset(); @@ -160,19 +386,44 @@ void FpeMonitor::Result::summary(std::ostream &os, std::size_t depth) const { } void FpeMonitor::Result::deduplicate() { - std::vector copy{}; - copy = std::move(m_stackTraces); + std::vector copy = std::move(m_stackTraces); + std::vector copyLocations = std::move(m_locations); m_stackTraces.clear(); + m_locations.clear(); + + for (std::size_t i = 0; i < copy.size(); ++i) { + auto &info = copy[i]; + const std::uintptr_t location = copyLocations[i]; + + bool merged = false; + for (std::size_t j = 0; j < m_stackTraces.size(); ++j) { + auto &existing = m_stackTraces[j]; + if (existing.type != info.type) { + continue; + } + + if (location != 0 && m_locations[j] != 0) { + if (location == m_locations[j]) { + existing.count += info.count; + merged = true; + break; + } + continue; + } + + if (areFpesEquivalent({existing.type, *existing.st}, + {info.type, *info.st})) { + existing.count += info.count; + merged = true; + break; + } + } - for (auto &info : copy) { - auto it = std::ranges::find_if(m_stackTraces, [&info](const FpeInfo &el) { - return areFpesEquivalent({el.type, *el.st}, {info.type, *info.st}); - }); - if (it != m_stackTraces.end()) { - it->count += info.count; + if (merged) { continue; } m_stackTraces.push_back({info.count, info.type, std::move(info.st)}); + m_locations.push_back(location); } } @@ -189,61 +440,81 @@ FpeMonitor::~FpeMonitor() { disable(); } -void FpeMonitor::signalHandler(int /*signal*/, siginfo_t *si, void *ctx) { +void FpeMonitor::signalHandler(int signal, siginfo_t *si, void *ctx) { if (stack().empty()) { return; } FpeMonitor &fpe = *stack().top(); - fpe.m_result.m_counts.at(si->si_code)++; + auto type = decodeFpeType(signal, si, ctx); + if (!type.has_value()) { + // For unknown SIGILL causes on Darwin arm64, returning without changing the + // context can re-trigger the signal forever. Fail fast instead. +#if defined(__APPLE__) && defined(__arm64__) + std::_Exit(EXIT_FAILURE); +#else + return; +#endif + } + fpe.m_result.m_counts.at(static_cast(*type))++; + std::uintptr_t location = + si != nullptr ? reinterpret_cast(si->si_addr) : 0; try { - // collect stack trace skipping 2 frames, which should be the signal handler - // and the calling facility. This might be platform specific, not sure + // collect stack trace skipping signal-handler frames. The required skip + // count is platform dependent. + std::size_t skipFrames = 2; +#if defined(__APPLE__) && defined(__arm64__) + // Darwin arm64 has one less signal trampoline frame in this path. + skipFrames = 1; +#endif auto [buffer, remaining] = fpe.m_buffer.next(); - std::size_t depth = boost::stacktrace::safe_dump_to(2, buffer, remaining); - std::size_t stored = - depth * sizeof(boost::stacktrace::frame::native_frame_ptr_t); + using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; + std::size_t prefixFrames = 0; +#if defined(__APPLE__) && defined(__arm64__) + // Prepend the faulting PC so deduplication can distinguish trap sites even + // when unwinding stops at sigtramp under optimization. + if (si != nullptr && si->si_addr != nullptr && + remaining >= sizeof(NativeFramePtr)) { + auto *frames = static_cast(buffer); + frames[0] = + reinterpret_cast(reinterpret_cast( + si->si_addr)); + prefixFrames = 1; + buffer = frames + 1; + remaining -= sizeof(NativeFramePtr); + } +#endif + std::size_t depth = boost::stacktrace::safe_dump_to(skipFrames, buffer, remaining); + depth += prefixFrames; + std::size_t stored = depth * sizeof(NativeFramePtr); fpe.m_buffer.pushOffset(stored); // record how much storage was consumed fpe.m_recorded.emplace_back( - static_cast(si->si_code), buffer, - remaining); // record buffer offset and fpe type + *type, buffer, + stored, location); // record consumed stack dump and trap location } catch (const std::bad_alloc &e) { std::cout << "Unable to collect stack trace due to memory limit" << std::endl; } -#if defined(__linux__) && defined(__x86_64__) - __uint16_t *cw = &(static_cast(ctx))->uc_mcontext.fpregs->cwd; - *cw |= FPU_EXCEPTION_MASK; - - __uint16_t *sw = &(static_cast(ctx))->uc_mcontext.fpregs->swd; - *sw &= ~FPU_STATUS_FLAGS; - - __uint32_t *mxcsr = - &(static_cast(ctx))->uc_mcontext.fpregs->mxcsr; - // *mxcsr |= SSE_EXCEPTION_MASK; // disable all SSE exceptions - *mxcsr |= ((*mxcsr & SSE_STATUS_FLAGS) << 7); - *mxcsr &= ~SSE_STATUS_FLAGS; // clear all pending SSE exceptions -#else - static_cast(ctx); -#endif + maskTrapsInSignalContext(ctx, *type); } void FpeMonitor::enable() { -#if defined(__linux__) && defined(__x86_64__) +#if (defined(__linux__) && defined(__x86_64__)) || \ + (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) ensureSignalHandlerInstalled(); // clear pending exceptions so they don't immediately fire - std::feclearexcept(m_excepts); + clearPendingExceptions(m_excepts); if (!stack().empty()) { // unset previous except state - fedisableexcept(stack().top()->m_excepts); + disableExceptions(stack().top()->m_excepts); } // apply this stack - feenableexcept(m_excepts); + enableExceptions(m_excepts); stack().push(this); #else @@ -253,9 +524,10 @@ void FpeMonitor::enable() { void FpeMonitor::rearm() { consumeRecorded(); -#if defined(__linux__) && defined(__x86_64__) - std::feclearexcept(m_excepts); - feenableexcept(m_excepts); +#if (defined(__linux__) && defined(__x86_64__)) || \ + (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) + clearPendingExceptions(m_excepts); + enableExceptions(m_excepts); #endif } @@ -271,21 +543,25 @@ void FpeMonitor::ensureSignalHandlerInstalled() { action.sa_sigaction = &signalHandler; action.sa_flags = SA_SIGINFO; sigaction(SIGFPE, &action, nullptr); +#if defined(__APPLE__) && defined(__arm64__) + sigaction(SIGILL, &action, nullptr); +#endif state.isSignalHandlerInstalled = true; } void FpeMonitor::disable() { -#if defined(__linux__) && defined(__x86_64__) - std::feclearexcept(m_excepts); +#if (defined(__linux__) && defined(__x86_64__)) || \ + (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) + clearPendingExceptions(m_excepts); assert(!stack().empty() && "FPE stack shouldn't be empty at this point"); stack().pop(); // disable excepts we enabled here - fedisableexcept(m_excepts); + disableExceptions(m_excepts); if (!stack().empty()) { // restore excepts from next stack element - std::feclearexcept(stack().top()->m_excepts); - feenableexcept(stack().top()->m_excepts); + clearPendingExceptions(stack().top()->m_excepts); + enableExceptions(stack().top()->m_excepts); } #endif } @@ -340,4 +616,6 @@ bool FpeMonitor::canSymbolize() { #endif } +bool FpeMonitor::isSupported() { return kFpeRuntimeSupported; } + } // namespace ActsPlugins diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index fdc0e311872..369448864be 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -348,6 +348,11 @@ void addFramework(py::module& mex) { .def_static("_trigger_divbyzero", &trigger_divbyzero) .def_static("_trigger_overflow", &trigger_overflow) .def_static("_trigger_invalid", &trigger_invalid) + .def_property_readonly_static( + "supported", + [](const py::object& /*self*/) { + return FpeMonitor::isSupported(); + }) .def_static("context", []() { return FpeMonitorContext(); }); fpe.def_property_readonly("result", py::overload_cast<>(&FpeMonitor::result), diff --git a/Python/Examples/tests/test_fpe.py b/Python/Examples/tests/test_fpe.py index 77fdc36ac67..679637154f9 100644 --- a/Python/Examples/tests/test_fpe.py +++ b/Python/Examples/tests/test_fpe.py @@ -1,6 +1,6 @@ -import sys import os import re +import sys from pathlib import Path import pytest @@ -10,8 +10,8 @@ pytestmark = [ pytest.mark.skipif( - sys.platform != "linux", - reason="FPE monitoring currently only supported on Linux", + not acts.examples.FpeMonitor.supported, + reason="FPE monitoring not supported on this platform", ), pytest.mark.skipif( "ACTS_SEQUENCER_DISABLE_FPEMON" in os.environ, @@ -19,6 +19,10 @@ ), ] +_fail_fast_exception = ( + acts.examples.FpeFailure if sys.platform == "linux" else RuntimeError +) + _names = { acts.examples.FpeType.FLTDIV: "DivByZero", @@ -138,7 +142,7 @@ def test_fpe_single_fail_immediately(fpe_type): ) ) - with pytest.raises(acts.examples.FpeFailure): + with pytest.raises(_fail_fast_exception): s.run() res = s.fpeResult @@ -192,6 +196,10 @@ def execute(self, context): assert res.count(x) == (s.config.events * 2 if x == fpe_type else 0) +@pytest.mark.skipif( + sys.platform == "darwin", + reason="Source-location FPE masking is not stable on macOS backtraces", +) def test_fpe_masking_single(fpe_type): trigger = getattr(acts.examples.FpeMonitor, f"_trigger_{_names[fpe_type].lower()}") diff --git a/Tests/UnitTests/Plugins/FpeMonitoring/CMakeLists.txt b/Tests/UnitTests/Plugins/FpeMonitoring/CMakeLists.txt index 9c0957c49f7..5d8630ac885 100644 --- a/Tests/UnitTests/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Tests/UnitTests/Plugins/FpeMonitoring/CMakeLists.txt @@ -1,10 +1,7 @@ set(unittest_extra_libraries ActsPluginFpeMonitoring) -if(NOT APPLE) - # This test doesn't make sense on macOS at this time - add_unittest(FpeMonitor FpeMonitorTests.cpp) - set_tests_properties( - FpeMonitor - PROPERTIES ENVIRONMENT BOOST_TEST_CATCH_SYSTEM_ERRORS=no - ) -endif() +add_unittest(FpeMonitor FpeMonitorTests.cpp) +set_tests_properties( + FpeMonitor + PROPERTIES ENVIRONMENT BOOST_TEST_CATCH_SYSTEM_ERRORS=no +) From df62178ea9548449740bf96bf620f7c64e7cebd9 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 10 Mar 2026 13:11:18 +0100 Subject: [PATCH 03/24] feat(FpeMonitoring): add isSupported() API and extend platform-specific FPE handling --- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 146 ++++++++++++++++++----- 1 file changed, 119 insertions(+), 27 deletions(-) diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index 505d54d5028..702956f0383 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -252,6 +252,108 @@ void maskTrapsInSignalContext(void *ctx, FpeType type) { static_cast(excepts); #endif } + +std::size_t captureStackFromSignalContext(void *ctx, void *buffer, + std::size_t bufferBytes) { + using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; + auto *frames = static_cast(buffer); + const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); + std::size_t count = 0; + + if (ctx == nullptr || maxFrames == 0) { + return 0; + } + +#if defined(__APPLE__) && defined(__arm64__) + auto *uc = static_cast(ctx); + const std::uintptr_t sp = + __darwin_arm_thread_state64_get_sp(uc->uc_mcontext->__ss); + std::uintptr_t fp = __darwin_arm_thread_state64_get_fp(uc->uc_mcontext->__ss); + const std::uintptr_t pc = + __darwin_arm_thread_state64_get_pc(uc->uc_mcontext->__ss); + + auto push = [&](std::uintptr_t address) { + if (address == 0 || count >= maxFrames) { + return; + } + frames[count++] = reinterpret_cast(address); + }; + + push(pc); + + constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; + auto inStackWindow = [&](std::uintptr_t address) { + if (address < sp || address > sp + kMaxStackWindow) { + return false; + } + return (address % alignof(std::uintptr_t)) == 0; + }; + + struct FrameRecord { + std::uintptr_t prevFp; + std::uintptr_t returnAddress; + }; + + while (count < maxFrames && inStackWindow(fp) && + fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { + const auto *record = reinterpret_cast(fp); + const std::uintptr_t prevFp = record->prevFp; + const std::uintptr_t lr = record->returnAddress; + push(lr); + + if (prevFp <= fp || !inStackWindow(prevFp)) { + break; + } + fp = prevFp; + } +#elif defined(__APPLE__) && defined(__x86_64__) + auto *uc = static_cast(ctx); + const std::uintptr_t sp = uc->uc_mcontext->__ss.__rsp; + std::uintptr_t fp = uc->uc_mcontext->__ss.__rbp; + const std::uintptr_t pc = uc->uc_mcontext->__ss.__rip; + + auto push = [&](std::uintptr_t address) { + if (address == 0 || count >= maxFrames) { + return; + } + frames[count++] = reinterpret_cast(address); + }; + + push(pc); + + constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; + auto inStackWindow = [&](std::uintptr_t address) { + if (address < sp || address > sp + kMaxStackWindow) { + return false; + } + return (address % alignof(std::uintptr_t)) == 0; + }; + + struct FrameRecord { + std::uintptr_t prevFp; + std::uintptr_t returnAddress; + }; + + while (count < maxFrames && inStackWindow(fp) && + fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { + const auto *record = reinterpret_cast(fp); + const std::uintptr_t prevFp = record->prevFp; + const std::uintptr_t ra = record->returnAddress; + push(ra); + + if (prevFp <= fp || !inStackWindow(prevFp)) { + break; + } + fp = prevFp; + } +#else + static_cast(ctx); + static_cast(frames); + static_cast(maxFrames); +#endif + + return count * sizeof(NativeFramePtr); +} } // namespace FpeMonitor::Result::FpeInfo::~FpeInfo() = default; @@ -461,37 +563,27 @@ void FpeMonitor::signalHandler(int signal, siginfo_t *si, void *ctx) { si != nullptr ? reinterpret_cast(si->si_addr) : 0; try { - // collect stack trace skipping signal-handler frames. The required skip - // count is platform dependent. - std::size_t skipFrames = 2; -#if defined(__APPLE__) && defined(__arm64__) - // Darwin arm64 has one less signal trampoline frame in this path. - skipFrames = 1; -#endif auto [buffer, remaining] = fpe.m_buffer.next(); using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; - std::size_t prefixFrames = 0; -#if defined(__APPLE__) && defined(__arm64__) - // Prepend the faulting PC so deduplication can distinguish trap sites even - // when unwinding stops at sigtramp under optimization. - if (si != nullptr && si->si_addr != nullptr && - remaining >= sizeof(NativeFramePtr)) { - auto *frames = static_cast(buffer); - frames[0] = - reinterpret_cast(reinterpret_cast( - si->si_addr)); - prefixFrames = 1; - buffer = frames + 1; - remaining -= sizeof(NativeFramePtr); + std::size_t stored = 0; +#if defined(__APPLE__) && (defined(__arm64__) || defined(__x86_64__)) + // On Darwin, unwind from the interrupted context so masks can match frames + // between the fault site and callers. + stored = captureStackFromSignalContext(ctx, buffer, remaining); + if (stored == 0) { + std::size_t depth = boost::stacktrace::safe_dump_to(1, buffer, remaining); + stored = depth * sizeof(NativeFramePtr); } +#else + std::size_t depth = boost::stacktrace::safe_dump_to(2, buffer, remaining); + stored = depth * sizeof(NativeFramePtr); #endif - std::size_t depth = boost::stacktrace::safe_dump_to(skipFrames, buffer, remaining); - depth += prefixFrames; - std::size_t stored = depth * sizeof(NativeFramePtr); - fpe.m_buffer.pushOffset(stored); // record how much storage was consumed - fpe.m_recorded.emplace_back( - *type, buffer, - stored, location); // record consumed stack dump and trap location + if (stored > 0) { + fpe.m_buffer.pushOffset(stored); // record how much storage was consumed + fpe.m_recorded.emplace_back( + *type, buffer, + stored, location); // record consumed stack dump and trap location + } } catch (const std::bad_alloc &e) { std::cout << "Unable to collect stack trace due to memory limit" From a663388f282615362ed1768b068af4b18053eaa4 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 10 Mar 2026 14:00:21 +0100 Subject: [PATCH 04/24] refactor(FpeMonitoring): extract platform-specific FPE code into separate source files --- Plugins/FpeMonitoring/CMakeLists.txt | 16 + Plugins/FpeMonitoring/src/FpeMonitor.cpp | 396 ++---------------- .../FpeMonitoring/src/FpeMonitorPlatform.hpp | 37 ++ .../src/FpeMonitorPlatformDarwinArm64.cpp | 242 +++++++++++ .../src/FpeMonitorPlatformDarwinX86_64.cpp | 194 +++++++++ .../src/FpeMonitorPlatformLinuxX86_64.cpp | 117 ++++++ .../src/FpeMonitorPlatformUnsupported.cpp | 49 +++ 7 files changed, 692 insertions(+), 359 deletions(-) create mode 100644 Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp create mode 100644 Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp create mode 100644 Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp create mode 100644 Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp create mode 100644 Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index 32d5ef507f3..2c50a84507a 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -1,6 +1,22 @@ +set(_fpe_platform_source src/FpeMonitorPlatformUnsupported.cpp) +string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _acts_fpe_arch) + +if(APPLE) + if(_acts_fpe_arch MATCHES "^(x86_64|amd64)$") + set(_fpe_platform_source src/FpeMonitorPlatformDarwinX86_64.cpp) + elseif(_acts_fpe_arch MATCHES "^(arm64|aarch64)$") + set(_fpe_platform_source src/FpeMonitorPlatformDarwinArm64.cpp) + endif() +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(_acts_fpe_arch MATCHES "^(x86_64|amd64)$") + set(_fpe_platform_source src/FpeMonitorPlatformLinuxX86_64.cpp) + endif() +endif() + acts_add_library( PluginFpeMonitoring src/FpeMonitor.cpp + ${_fpe_platform_source} ACTS_INCLUDE_FOLDER include/ActsPlugins ) diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index 702956f0383..3400c17d6ee 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -11,7 +11,6 @@ #include "Acts/Utilities/Helpers.hpp" #include -#include #include #include #include @@ -31,24 +30,12 @@ #include #include -#define FPU_EXCEPTION_MASK 0x3f -#define FPU_STATUS_FLAGS 0xff -#define SSE_STATUS_FLAGS FPU_EXCEPTION_MASK -#define SSE_EXCEPTION_MASK (FPU_EXCEPTION_MASK << 7) +#include "FpeMonitorPlatform.hpp" namespace ActsPlugins { namespace { -#if (defined(__linux__) && defined(__x86_64__)) || \ - (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) -constexpr bool kFpeRuntimeSupported = true; -#else -// Keep helper boundaries architecture-oriented so Linux aarch64 can be added -// by implementing decode + context masking paths, without touching call sites. -constexpr bool kFpeRuntimeSupported = false; -#endif - bool areFpesEquivalent( std::pair lhs, std::pair rhs) { @@ -57,303 +44,6 @@ bool areFpesEquivalent( return lhs.first == rhs.first && (boost::stacktrace::hash_value(fl) == boost::stacktrace::hash_value(fr)); } - -std::optional fpeTypeFromSiCode(int siCode) { - switch (siCode) { - case FPE_INTDIV: - return FpeType::INTDIV; - case FPE_INTOVF: - return FpeType::INTOVF; - case FPE_FLTDIV: - return FpeType::FLTDIV; - case FPE_FLTOVF: - return FpeType::FLTOVF; - case FPE_FLTUND: - return FpeType::FLTUND; - case FPE_FLTRES: - return FpeType::FLTRES; - case FPE_FLTINV: - return FpeType::FLTINV; - case FPE_FLTSUB: - return FpeType::FLTSUB; - default: - return std::nullopt; - } -} - -#if defined(__APPLE__) && defined(__arm64__) - -std::uint32_t darwinArm64TrapMask(int excepts) { - std::uint32_t mask = 0; - if ((excepts & FE_INVALID) != 0) { - mask |= __fpcr_trap_invalid; - } - if ((excepts & FE_DIVBYZERO) != 0) { - mask |= __fpcr_trap_divbyzero; - } - if ((excepts & FE_OVERFLOW) != 0) { - mask |= __fpcr_trap_overflow; - } - if ((excepts & FE_UNDERFLOW) != 0) { - mask |= __fpcr_trap_underflow; - } - if ((excepts & FE_INEXACT) != 0) { - mask |= __fpcr_trap_inexact; - } - return mask; -} - -std::optional fpeTypeFromDarwinArm64Esr(std::uint32_t esr) { - constexpr std::uint32_t kEsrExceptionClassShift = 26u; - constexpr std::uint32_t kEsrExceptionClassMask = 0x3fu; - constexpr std::uint32_t kFpExceptionClass = 0x2cu; - const std::uint32_t exceptionClass = - (esr >> kEsrExceptionClassShift) & kEsrExceptionClassMask; - if (exceptionClass != kFpExceptionClass) { - return std::nullopt; - } - - // The low ESR bits encode IEEE FP exception classes on Darwin arm64. - const std::uint32_t flags = esr & static_cast(FE_ALL_EXCEPT); - if ((flags & FE_INVALID) != 0) { - return FpeType::FLTINV; - } - if ((flags & FE_DIVBYZERO) != 0) { - return FpeType::FLTDIV; - } - if ((flags & FE_OVERFLOW) != 0) { - return FpeType::FLTOVF; - } - if ((flags & FE_UNDERFLOW) != 0) { - return FpeType::FLTUND; - } - if ((flags & FE_INEXACT) != 0) { - return FpeType::FLTRES; - } - return std::nullopt; -} - -#endif - -std::optional decodeFpeType(int signal, siginfo_t *si, void *ctx) { - if (signal == SIGFPE && si != nullptr) { - return fpeTypeFromSiCode(si->si_code); - } - -#if defined(__APPLE__) && defined(__arm64__) - if (signal == SIGILL && ctx != nullptr) { - auto *uc = static_cast(ctx); - return fpeTypeFromDarwinArm64Esr(uc->uc_mcontext->__es.__esr); - } -#endif - - return std::nullopt; -} - -int exceptMaskForType(FpeType type) { - switch (type) { - case FpeType::INTDIV: - case FpeType::FLTDIV: - return FE_DIVBYZERO; - case FpeType::INTOVF: - case FpeType::FLTOVF: - return FE_OVERFLOW; - case FpeType::FLTUND: - return FE_UNDERFLOW; - case FpeType::FLTRES: - return FE_INEXACT; - case FpeType::FLTINV: - case FpeType::FLTSUB: - return FE_INVALID; - default: - return 0; - } -} - -void clearPendingExceptions(int excepts) { std::feclearexcept(excepts); } - -void enableExceptions(int excepts) { -#if defined(__linux__) && defined(__x86_64__) - feenableexcept(excepts); -#elif defined(__APPLE__) && defined(__x86_64__) - fenv_t env{}; - if (fegetenv(&env) != 0) { - return; - } - env.__control &= ~static_cast(excepts); - env.__mxcsr &= ~(static_cast(excepts) << 7u); - env.__status &= ~static_cast(FE_ALL_EXCEPT); - env.__mxcsr &= ~static_cast(FE_ALL_EXCEPT); - fesetenv(&env); -#elif defined(__APPLE__) && defined(__arm64__) - fenv_t env{}; - if (fegetenv(&env) != 0) { - return; - } - env.__fpcr |= static_cast(darwinArm64TrapMask(excepts)); - env.__fpsr &= ~static_cast(FE_ALL_EXCEPT); - fesetenv(&env); -#else - static_cast(excepts); -#endif -} - -void disableExceptions(int excepts) { -#if defined(__linux__) && defined(__x86_64__) - fedisableexcept(excepts); -#elif defined(__APPLE__) && defined(__x86_64__) - fenv_t env{}; - if (fegetenv(&env) != 0) { - return; - } - env.__control |= static_cast(excepts); - env.__mxcsr |= (static_cast(excepts) << 7u); - fesetenv(&env); -#elif defined(__APPLE__) && defined(__arm64__) - fenv_t env{}; - if (fegetenv(&env) != 0) { - return; - } - env.__fpcr &= ~static_cast(darwinArm64TrapMask(excepts)); - fesetenv(&env); -#else - static_cast(excepts); -#endif -} - -void maskTrapsInSignalContext(void *ctx, FpeType type) { - const int excepts = exceptMaskForType(type); -#if defined(__linux__) && defined(__x86_64__) - auto *uc = static_cast(ctx); - __uint16_t *cw = &uc->uc_mcontext.fpregs->cwd; - *cw |= FPU_EXCEPTION_MASK; - - __uint16_t *sw = &uc->uc_mcontext.fpregs->swd; - *sw &= ~FPU_STATUS_FLAGS; - - __uint32_t *mxcsr = &uc->uc_mcontext.fpregs->mxcsr; - *mxcsr |= ((*mxcsr & SSE_STATUS_FLAGS) << 7); - *mxcsr &= ~SSE_STATUS_FLAGS; -#elif defined(__APPLE__) && defined(__x86_64__) - auto *uc = static_cast(ctx); - uc->uc_mcontext->__fs.__fpu_fcw |= - static_cast(excepts); - uc->uc_mcontext->__fs.__fpu_fsw &= ~static_cast(FE_ALL_EXCEPT); - uc->uc_mcontext->__fs.__fpu_mxcsr |= - (static_cast(excepts) << 7u); - uc->uc_mcontext->__fs.__fpu_mxcsr &= ~static_cast(FE_ALL_EXCEPT); -#elif defined(__APPLE__) && defined(__arm64__) - auto *uc = static_cast(ctx); - uc->uc_mcontext->__ns.__fpcr &= - ~static_cast(darwinArm64TrapMask(excepts)); - uc->uc_mcontext->__ns.__fpsr &= ~static_cast(FE_ALL_EXCEPT); -#else - static_cast(ctx); - static_cast(excepts); -#endif -} - -std::size_t captureStackFromSignalContext(void *ctx, void *buffer, - std::size_t bufferBytes) { - using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; - auto *frames = static_cast(buffer); - const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); - std::size_t count = 0; - - if (ctx == nullptr || maxFrames == 0) { - return 0; - } - -#if defined(__APPLE__) && defined(__arm64__) - auto *uc = static_cast(ctx); - const std::uintptr_t sp = - __darwin_arm_thread_state64_get_sp(uc->uc_mcontext->__ss); - std::uintptr_t fp = __darwin_arm_thread_state64_get_fp(uc->uc_mcontext->__ss); - const std::uintptr_t pc = - __darwin_arm_thread_state64_get_pc(uc->uc_mcontext->__ss); - - auto push = [&](std::uintptr_t address) { - if (address == 0 || count >= maxFrames) { - return; - } - frames[count++] = reinterpret_cast(address); - }; - - push(pc); - - constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; - auto inStackWindow = [&](std::uintptr_t address) { - if (address < sp || address > sp + kMaxStackWindow) { - return false; - } - return (address % alignof(std::uintptr_t)) == 0; - }; - - struct FrameRecord { - std::uintptr_t prevFp; - std::uintptr_t returnAddress; - }; - - while (count < maxFrames && inStackWindow(fp) && - fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { - const auto *record = reinterpret_cast(fp); - const std::uintptr_t prevFp = record->prevFp; - const std::uintptr_t lr = record->returnAddress; - push(lr); - - if (prevFp <= fp || !inStackWindow(prevFp)) { - break; - } - fp = prevFp; - } -#elif defined(__APPLE__) && defined(__x86_64__) - auto *uc = static_cast(ctx); - const std::uintptr_t sp = uc->uc_mcontext->__ss.__rsp; - std::uintptr_t fp = uc->uc_mcontext->__ss.__rbp; - const std::uintptr_t pc = uc->uc_mcontext->__ss.__rip; - - auto push = [&](std::uintptr_t address) { - if (address == 0 || count >= maxFrames) { - return; - } - frames[count++] = reinterpret_cast(address); - }; - - push(pc); - - constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; - auto inStackWindow = [&](std::uintptr_t address) { - if (address < sp || address > sp + kMaxStackWindow) { - return false; - } - return (address % alignof(std::uintptr_t)) == 0; - }; - - struct FrameRecord { - std::uintptr_t prevFp; - std::uintptr_t returnAddress; - }; - - while (count < maxFrames && inStackWindow(fp) && - fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { - const auto *record = reinterpret_cast(fp); - const std::uintptr_t prevFp = record->prevFp; - const std::uintptr_t ra = record->returnAddress; - push(ra); - - if (prevFp <= fp || !inStackWindow(prevFp)) { - break; - } - fp = prevFp; - } -#else - static_cast(ctx); - static_cast(frames); - static_cast(maxFrames); -#endif - - return count * sizeof(NativeFramePtr); -} } // namespace FpeMonitor::Result::FpeInfo::~FpeInfo() = default; @@ -548,15 +238,12 @@ void FpeMonitor::signalHandler(int signal, siginfo_t *si, void *ctx) { } FpeMonitor &fpe = *stack().top(); - auto type = decodeFpeType(signal, si, ctx); + auto type = detail::decodeFpeType(signal, si, ctx); if (!type.has_value()) { - // For unknown SIGILL causes on Darwin arm64, returning without changing the - // context can re-trigger the signal forever. Fail fast instead. -#if defined(__APPLE__) && defined(__arm64__) - std::_Exit(EXIT_FAILURE); -#else + if (detail::shouldFailFastOnUnknownSignal()) { + std::_Exit(EXIT_FAILURE); + } return; -#endif } fpe.m_result.m_counts.at(static_cast(*type))++; std::uintptr_t location = @@ -565,24 +252,18 @@ void FpeMonitor::signalHandler(int signal, siginfo_t *si, void *ctx) { try { auto [buffer, remaining] = fpe.m_buffer.next(); using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; - std::size_t stored = 0; -#if defined(__APPLE__) && (defined(__arm64__) || defined(__x86_64__)) - // On Darwin, unwind from the interrupted context so masks can match frames - // between the fault site and callers. - stored = captureStackFromSignalContext(ctx, buffer, remaining); + std::size_t stored = + detail::captureStackFromSignalContext(ctx, buffer, remaining); if (stored == 0) { - std::size_t depth = boost::stacktrace::safe_dump_to(1, buffer, remaining); + std::size_t depth = boost::stacktrace::safe_dump_to( + detail::safeDumpSkipFrames(), buffer, remaining); stored = depth * sizeof(NativeFramePtr); } -#else - std::size_t depth = boost::stacktrace::safe_dump_to(2, buffer, remaining); - stored = depth * sizeof(NativeFramePtr); -#endif if (stored > 0) { fpe.m_buffer.pushOffset(stored); // record how much storage was consumed fpe.m_recorded.emplace_back( - *type, buffer, - stored, location); // record consumed stack dump and trap location + *type, buffer, stored, + location); // record consumed stack dump and trap location } } catch (const std::bad_alloc &e) { @@ -590,37 +271,35 @@ void FpeMonitor::signalHandler(int signal, siginfo_t *si, void *ctx) { << std::endl; } - maskTrapsInSignalContext(ctx, *type); + detail::maskTrapsInSignalContext(ctx, *type); } void FpeMonitor::enable() { -#if (defined(__linux__) && defined(__x86_64__)) || \ - (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) + if (!detail::isRuntimeSupported()) { + return; + } ensureSignalHandlerInstalled(); // clear pending exceptions so they don't immediately fire - clearPendingExceptions(m_excepts); + detail::clearPendingExceptions(m_excepts); if (!stack().empty()) { // unset previous except state - disableExceptions(stack().top()->m_excepts); + detail::disableExceptions(stack().top()->m_excepts); } // apply this stack - enableExceptions(m_excepts); + detail::enableExceptions(m_excepts); stack().push(this); -#else - static_cast(m_excepts); -#endif } void FpeMonitor::rearm() { consumeRecorded(); -#if (defined(__linux__) && defined(__x86_64__)) || \ - (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) - clearPendingExceptions(m_excepts); - enableExceptions(m_excepts); -#endif + if (!detail::isRuntimeSupported()) { + return; + } + detail::clearPendingExceptions(m_excepts); + detail::enableExceptions(m_excepts); } void FpeMonitor::ensureSignalHandlerInstalled() { @@ -630,32 +309,29 @@ void FpeMonitor::ensureSignalHandlerInstalled() { } std::lock_guard lock{state.mutex}; + if (state.isSignalHandlerInstalled) { + return; + } - struct sigaction action{}; - action.sa_sigaction = &signalHandler; - action.sa_flags = SA_SIGINFO; - sigaction(SIGFPE, &action, nullptr); -#if defined(__APPLE__) && defined(__arm64__) - sigaction(SIGILL, &action, nullptr); -#endif + detail::installSignalHandlers(&signalHandler); state.isSignalHandlerInstalled = true; } void FpeMonitor::disable() { -#if (defined(__linux__) && defined(__x86_64__)) || \ - (defined(__APPLE__) && (defined(__x86_64__) || defined(__arm64__))) - clearPendingExceptions(m_excepts); + if (!detail::isRuntimeSupported()) { + return; + } + detail::clearPendingExceptions(m_excepts); assert(!stack().empty() && "FPE stack shouldn't be empty at this point"); stack().pop(); // disable excepts we enabled here - disableExceptions(m_excepts); + detail::disableExceptions(m_excepts); if (!stack().empty()) { // restore excepts from next stack element - clearPendingExceptions(stack().top()->m_excepts); - enableExceptions(stack().top()->m_excepts); + detail::clearPendingExceptions(stack().top()->m_excepts); + detail::enableExceptions(stack().top()->m_excepts); } -#endif } std::stack &FpeMonitor::stack() { @@ -708,6 +384,8 @@ bool FpeMonitor::canSymbolize() { #endif } -bool FpeMonitor::isSupported() { return kFpeRuntimeSupported; } +bool FpeMonitor::isSupported() { + return detail::isRuntimeSupported(); +} } // namespace ActsPlugins diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp new file mode 100644 index 00000000000..50891bbadd5 --- /dev/null +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp @@ -0,0 +1,37 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include "ActsPlugins/FpeMonitoring/FpeMonitor.hpp" + +#include +#include +#include + +namespace ActsPlugins::detail { + +bool isRuntimeSupported(); + +std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx); + +void clearPendingExceptions(int excepts); +void enableExceptions(int excepts); +void disableExceptions(int excepts); +void maskTrapsInSignalContext(void* ctx, FpeType type); + +std::size_t captureStackFromSignalContext(void* ctx, void* buffer, + std::size_t bufferBytes); + +std::size_t safeDumpSkipFrames(); + +bool shouldFailFastOnUnknownSignal(); + +void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)); + +} // namespace ActsPlugins::detail diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp new file mode 100644 index 00000000000..14021648b7c --- /dev/null +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp @@ -0,0 +1,242 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include + +#include + +#include "FpeMonitorPlatform.hpp" + +#if !defined(__APPLE__) || !defined(__arm64__) +#error "This translation unit is only valid for macOS arm64" +#endif + +namespace ActsPlugins::detail { +namespace { + +std::uint32_t darwinArm64TrapMask(int excepts) { + std::uint32_t mask = 0; + if ((excepts & FE_INVALID) != 0) { + mask |= __fpcr_trap_invalid; + } + if ((excepts & FE_DIVBYZERO) != 0) { + mask |= __fpcr_trap_divbyzero; + } + if ((excepts & FE_OVERFLOW) != 0) { + mask |= __fpcr_trap_overflow; + } + if ((excepts & FE_UNDERFLOW) != 0) { + mask |= __fpcr_trap_underflow; + } + if ((excepts & FE_INEXACT) != 0) { + mask |= __fpcr_trap_inexact; + } + return mask; +} + +int exceptMaskForType(FpeType type) { + using enum FpeType; + switch (type) { + case INTDIV: + case FLTDIV: + return FE_DIVBYZERO; + case INTOVF: + case FLTOVF: + return FE_OVERFLOW; + case FLTUND: + return FE_UNDERFLOW; + case FLTRES: + return FE_INEXACT; + case FLTINV: + case FLTSUB: + return FE_INVALID; + default: + return 0; + } +} + +std::optional fpeTypeFromSiCode(int siCode) { + using enum FpeType; + switch (siCode) { + case FPE_INTDIV: + return INTDIV; + case FPE_INTOVF: + return INTOVF; + case FPE_FLTDIV: + return FLTDIV; + case FPE_FLTOVF: + return FLTOVF; + case FPE_FLTUND: + return FLTUND; + case FPE_FLTRES: + return FLTRES; + case FPE_FLTINV: + return FLTINV; + case FPE_FLTSUB: + return FLTSUB; + default: + return std::nullopt; + } +} + +std::optional fpeTypeFromDarwinArm64Esr(std::uint32_t esr) { + constexpr std::uint32_t kEsrExceptionClassShift = 26u; + constexpr std::uint32_t kEsrExceptionClassMask = 0x3fu; + constexpr std::uint32_t kFpExceptionClass = 0x2cu; + const std::uint32_t exceptionClass = + (esr >> kEsrExceptionClassShift) & kEsrExceptionClassMask; + if (exceptionClass != kFpExceptionClass) { + return std::nullopt; + } + + const std::uint32_t flags = esr & static_cast(FE_ALL_EXCEPT); + if ((flags & FE_INVALID) != 0) { + return FpeType::FLTINV; + } + if ((flags & FE_DIVBYZERO) != 0) { + return FpeType::FLTDIV; + } + if ((flags & FE_OVERFLOW) != 0) { + return FpeType::FLTOVF; + } + if ((flags & FE_UNDERFLOW) != 0) { + return FpeType::FLTUND; + } + if ((flags & FE_INEXACT) != 0) { + return FpeType::FLTRES; + } + return std::nullopt; +} + +} // namespace + +bool isRuntimeSupported() { + return true; +} + +std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + if (signal == SIGFPE && si != nullptr) { + return fpeTypeFromSiCode(si->si_code); + } + + if (signal == SIGILL && ctx != nullptr) { + auto* uc = static_cast(ctx); + return fpeTypeFromDarwinArm64Esr(uc->uc_mcontext->__es.__esr); + } + + return std::nullopt; +} + +void clearPendingExceptions(int excepts) { + std::feclearexcept(excepts); +} + +void enableExceptions(int excepts) { + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__fpcr |= static_cast(darwinArm64TrapMask(excepts)); + env.__fpsr &= ~static_cast(FE_ALL_EXCEPT); + fesetenv(&env); +} + +void disableExceptions(int excepts) { + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__fpcr &= ~static_cast(darwinArm64TrapMask(excepts)); + fesetenv(&env); +} + +void maskTrapsInSignalContext(void* ctx, FpeType type) { + const int excepts = exceptMaskForType(type); + auto* uc = static_cast(ctx); + uc->uc_mcontext->__ns.__fpcr &= + ~static_cast(darwinArm64TrapMask(excepts)); + uc->uc_mcontext->__ns.__fpsr &= ~static_cast(FE_ALL_EXCEPT); +} + +std::size_t captureStackFromSignalContext(void* ctx, void* buffer, + std::size_t bufferBytes) { + using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; + auto* frames = static_cast(buffer); + const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); + std::size_t count = 0; + + if (ctx == nullptr || maxFrames == 0) { + return 0; + } + + auto* uc = static_cast(ctx); + const std::uintptr_t sp = + __darwin_arm_thread_state64_get_sp(uc->uc_mcontext->__ss); + std::uintptr_t fp = __darwin_arm_thread_state64_get_fp(uc->uc_mcontext->__ss); + const std::uintptr_t pc = + __darwin_arm_thread_state64_get_pc(uc->uc_mcontext->__ss); + + auto push = [&](std::uintptr_t address) { + if (address == 0 || count >= maxFrames) { + return; + } + frames[count++] = reinterpret_cast(address); + }; + + push(pc); + + constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; + auto inStackWindow = [&](std::uintptr_t address) { + if (address < sp || address > sp + kMaxStackWindow) { + return false; + } + return (address % alignof(std::uintptr_t)) == 0; + }; + + struct FrameRecord { + std::uintptr_t prevFp; + std::uintptr_t returnAddress; + }; + + while (count < maxFrames && inStackWindow(fp) && + fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { + const auto* record = reinterpret_cast(fp); + const std::uintptr_t prevFp = record->prevFp; + const std::uintptr_t lr = record->returnAddress; + push(lr); + + if (prevFp <= fp || !inStackWindow(prevFp)) { + break; + } + fp = prevFp; + } + + return count * sizeof(NativeFramePtr); +} + +std::size_t safeDumpSkipFrames() { + return 1; +} + +bool shouldFailFastOnUnknownSignal() { + return true; +} + +void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + struct sigaction action {}; + action.sa_sigaction = handler; + action.sa_flags = SA_SIGINFO; + sigaction(SIGFPE, &action, nullptr); + sigaction(SIGILL, &action, nullptr); +} + +} // namespace ActsPlugins::detail diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp new file mode 100644 index 00000000000..97a5d0b9620 --- /dev/null +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp @@ -0,0 +1,194 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "ActsPlugins/FpeMonitoring/FpeMonitor.hpp" + +#include +#include +#include +#include +#include + +#include + +#include "FpeMonitorPlatform.hpp" + +#if !defined(__APPLE__) || !defined(__x86_64__) +#error "This translation unit is only valid for macOS x86_64" +#endif + +namespace ActsPlugins::detail { +namespace { + +int exceptMaskForType(FpeType type) { + using enum FpeType; + switch (type) { + case INTDIV: + case FLTDIV: + return FE_DIVBYZERO; + case INTOVF: + case FLTOVF: + return FE_OVERFLOW; + case FLTUND: + return FE_UNDERFLOW; + case FLTRES: + return FE_INEXACT; + case FLTINV: + case FLTSUB: + return FE_INVALID; + default: + return 0; + } +} + +std::optional fpeTypeFromSiCode(int siCode) { + using enum FpeType; + switch (siCode) { + case FPE_INTDIV: + return INTDIV; + case FPE_INTOVF: + return INTOVF; + case FPE_FLTDIV: + return FLTDIV; + case FPE_FLTOVF: + return FLTOVF; + case FPE_FLTUND: + return FLTUND; + case FPE_FLTRES: + return FLTRES; + case FPE_FLTINV: + return FLTINV; + case FPE_FLTSUB: + return FLTSUB; + default: + return std::nullopt; + } +} + +} // namespace + +bool isRuntimeSupported() { + return true; +} + +std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + static_cast(ctx); + if (signal != SIGFPE || si == nullptr) { + return std::nullopt; + } + return fpeTypeFromSiCode(si->si_code); +} + +void clearPendingExceptions(int excepts) { + std::feclearexcept(excepts); +} + +void enableExceptions(int excepts) { + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__control &= ~static_cast(excepts); + env.__mxcsr &= ~(static_cast(excepts) << 7u); + env.__status &= ~static_cast(FE_ALL_EXCEPT); + env.__mxcsr &= ~static_cast(FE_ALL_EXCEPT); + fesetenv(&env); +} + +void disableExceptions(int excepts) { + fenv_t env{}; + if (fegetenv(&env) != 0) { + return; + } + env.__control |= static_cast(excepts); + env.__mxcsr |= (static_cast(excepts) << 7u); + fesetenv(&env); +} + +void maskTrapsInSignalContext(void* ctx, FpeType type) { + const int excepts = exceptMaskForType(type); + auto* uc = static_cast(ctx); + uc->uc_mcontext->__fs.__fpu_fcw |= static_cast(excepts); + uc->uc_mcontext->__fs.__fpu_fsw &= + ~static_cast(FE_ALL_EXCEPT); + uc->uc_mcontext->__fs.__fpu_mxcsr |= + (static_cast(excepts) << 7u); + uc->uc_mcontext->__fs.__fpu_mxcsr &= + ~static_cast(FE_ALL_EXCEPT); +} + +std::size_t captureStackFromSignalContext(void* ctx, void* buffer, + std::size_t bufferBytes) { + using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; + auto* frames = static_cast(buffer); + const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); + std::size_t count = 0; + + if (ctx == nullptr || maxFrames == 0) { + return 0; + } + + auto* uc = static_cast(ctx); + const std::uintptr_t sp = uc->uc_mcontext->__ss.__rsp; + std::uintptr_t fp = uc->uc_mcontext->__ss.__rbp; + const std::uintptr_t pc = uc->uc_mcontext->__ss.__rip; + + auto push = [&](std::uintptr_t address) { + if (address == 0 || count >= maxFrames) { + return; + } + frames[count++] = reinterpret_cast(address); + }; + + push(pc); + + constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; + auto inStackWindow = [&](std::uintptr_t address) { + if (address < sp || address > sp + kMaxStackWindow) { + return false; + } + return (address % alignof(std::uintptr_t)) == 0; + }; + + struct FrameRecord { + std::uintptr_t prevFp; + std::uintptr_t returnAddress; + }; + + while (count < maxFrames && inStackWindow(fp) && + fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { + const auto* record = reinterpret_cast(fp); + const std::uintptr_t prevFp = record->prevFp; + const std::uintptr_t ra = record->returnAddress; + push(ra); + + if (prevFp <= fp || !inStackWindow(prevFp)) { + break; + } + fp = prevFp; + } + + return count * sizeof(NativeFramePtr); +} + +std::size_t safeDumpSkipFrames() { + return 1; +} + +bool shouldFailFastOnUnknownSignal() { + return false; +} + +void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + struct sigaction action {}; + action.sa_sigaction = handler; + action.sa_flags = SA_SIGINFO; + sigaction(SIGFPE, &action, nullptr); +} + +} // namespace ActsPlugins::detail diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp new file mode 100644 index 00000000000..1595fcc526f --- /dev/null +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp @@ -0,0 +1,117 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include +#include + +#include + +#include "FpeMonitorPlatform.hpp" + +#if !defined(__linux__) || !defined(__x86_64__) +#error "This translation unit is only valid for Linux x86_64" +#endif + +namespace ActsPlugins::detail { +namespace { + +constexpr std::uint16_t kFpuExceptionMask = 0x3f; +constexpr std::uint16_t kFpuStatusFlags = 0xff; +constexpr std::uint32_t kSseStatusFlags = kFpuExceptionMask; + +std::optional fpeTypeFromSiCode(int siCode) { + using enum FpeType; + switch (siCode) { + case FPE_INTDIV: + return INTDIV; + case FPE_INTOVF: + return INTOVF; + case FPE_FLTDIV: + return FLTDIV; + case FPE_FLTOVF: + return FLTOVF; + case FPE_FLTUND: + return FLTUND; + case FPE_FLTRES: + return FLTRES; + case FPE_FLTINV: + return FLTINV; + case FPE_FLTSUB: + return FLTSUB; + default: + return std::nullopt; + } +} + +} // namespace + +bool isRuntimeSupported() { + return true; +} + +std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + static_cast(ctx); + if (signal != SIGFPE || si == nullptr) { + return std::nullopt; + } + return fpeTypeFromSiCode(si->si_code); +} + +void clearPendingExceptions(int excepts) { + std::feclearexcept(excepts); +} + +void enableExceptions(int excepts) { + feenableexcept(excepts); +} + +void disableExceptions(int excepts) { + fedisableexcept(excepts); +} + +void maskTrapsInSignalContext(void* ctx, FpeType type) { + static_cast(type); + auto* uc = static_cast(ctx); + __uint16_t* cw = &uc->uc_mcontext.fpregs->cwd; + *cw |= kFpuExceptionMask; + + __uint16_t* sw = &uc->uc_mcontext.fpregs->swd; + *sw &= ~kFpuStatusFlags; + + __uint32_t* mxcsr = &uc->uc_mcontext.fpregs->mxcsr; + *mxcsr |= ((*mxcsr & kSseStatusFlags) << 7); + *mxcsr &= ~kSseStatusFlags; +} + +std::size_t captureStackFromSignalContext(void* ctx, void* buffer, + std::size_t bufferBytes) { + static_cast(ctx); + static_cast(buffer); + static_cast(bufferBytes); + return 0; +} + +std::size_t safeDumpSkipFrames() { + return 2; +} + +bool shouldFailFastOnUnknownSignal() { + return false; +} + +void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + struct sigaction action {}; + action.sa_sigaction = handler; + action.sa_flags = SA_SIGINFO; + sigaction(SIGFPE, &action, nullptr); +} + +} // namespace ActsPlugins::detail diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp new file mode 100644 index 00000000000..e687d799a50 --- /dev/null +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp @@ -0,0 +1,49 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "FpeMonitorPlatform.hpp" + +namespace ActsPlugins::detail { + +bool isRuntimeSupported() { return false; } + +std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + static_cast(signal); + static_cast(si); + static_cast(ctx); + return std::nullopt; +} + +void clearPendingExceptions(int excepts) { static_cast(excepts); } + +void enableExceptions(int excepts) { static_cast(excepts); } + +void disableExceptions(int excepts) { static_cast(excepts); } + +void maskTrapsInSignalContext(void* ctx, FpeType type) { + static_cast(ctx); + static_cast(type); +} + +std::size_t captureStackFromSignalContext(void* ctx, void* buffer, + std::size_t bufferBytes) { + static_cast(ctx); + static_cast(buffer); + static_cast(bufferBytes); + return 0; +} + +std::size_t safeDumpSkipFrames() { return 2; } + +bool shouldFailFastOnUnknownSignal() { return false; } + +void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + static_cast(handler); +} + +} // namespace ActsPlugins::detail From 03b67b990a830bc347be483c234e8664ac6ad617 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Tue, 10 Mar 2026 17:17:00 +0100 Subject: [PATCH 05/24] fix(FpeMonitoring): remove extra endif in CMakeLists.txt --- Plugins/FpeMonitoring/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index 2c50a84507a..9abf900f75b 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -213,7 +213,6 @@ else() endif() endif() endif() - endif() if(NOT _backtrace_setup_complete) if(NOT ${dl_LIBRARY} STREQUAL "dl_LIBRARY-NOTFOUND") From 434f931fdfea90d2c8d0cec2681dbab877a9afd9 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 11:13:56 +0100 Subject: [PATCH 06/24] docs(FpeMonitoring): add comment on _Exit async-signal-safety --- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index 3400c17d6ee..35b7b2d5a34 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -241,6 +241,8 @@ void FpeMonitor::signalHandler(int signal, siginfo_t *si, void *ctx) { auto type = detail::decodeFpeType(signal, si, ctx); if (!type.has_value()) { if (detail::shouldFailFastOnUnknownSignal()) { + // Must use _Exit: async-signal-safe. std::terminate is not (calls + // terminate handler). std::abort raises SIGABRT from within a handler. std::_Exit(EXIT_FAILURE); } return; From 17688f27dbc2e31bda20e4c464213dd24c8b7ef6 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 11:22:20 +0100 Subject: [PATCH 07/24] refactor(FpeMonitoring): simplify backtrace detection in CMake - Remove verbose try_compile block, use find_package(Backtrace) - Consolidate backtrace setup logic --- Plugins/FpeMonitoring/CMakeLists.txt | 269 +++++++++++++-------------- 1 file changed, 131 insertions(+), 138 deletions(-) diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index 9abf900f75b..85c7d2e91b4 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -45,149 +45,87 @@ else() elseif(CMAKE_DL_LIBS) target_link_libraries(ActsPluginFpeMonitoring PUBLIC ${CMAKE_DL_LIBS}) - set(_backtrace_setup_complete FALSE) - - find_path( - boost_stacktrace_include - NAMES "boost/stacktrace.hpp" - REQUIRED - PATHS ${Boost_INCLUDE_DIRS} - ) - - file( - WRITE - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" - ) - - set(_backtrace_include_def "") - - message(CHECK_START "Does backtrace work without linker flag") - try_compile( - _backtrace_nolink - "${CMAKE_BINARY_DIR}" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - LINK_LIBRARIES ${CMAKE_DL_LIBS} - CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS -DBOOST_STACKTRACE_USE_BACKTRACE - OUTPUT_VARIABLE __OUTPUT - ) - - if(_backtrace_nolink) - message(CHECK_PASS "yes") - list(APPEND _fpe_options -DBOOST_STACKTRACE_USE_BACKTRACE) - set(_backtrace_setup_complete TRUE) - else() - message(CHECK_FAIL "no") - - file(GLOB hints "/usr/lib/gcc/*/*/include") - find_file(backtrace_header NAMES "backtrace.h" HINTS ${hints}) - - if(${backtrace_header} STREQUAL "backtrace_header-NOTFOUND") - message(STATUS "Could not find backtrace header file") - else() - message(CHECK_FAIL "no") - - message(CHECK_START "Does backtrace work with explicit include") - try_compile( - _backtrace_explicit - "${CMAKE_BINARY_DIR}" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - LINK_LIBRARIES ${CMAKE_DL_LIBS} - CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS - -DBOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} - OUTPUT_VARIABLE __OUTPUT - ) - - if(${backtrace_header} STREQUAL "backtrace_header-NOTFOUND") - message(STATUS "Could not find backtrace header file") - else() - set(backtrace_include - "-DBOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" - ) - - file( - WRITE - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - " - #include - int main() {} - " - ) - - message( - CHECK_START - "Does backtrace work with explicit include" - ) - - try_compile( - _backtrace_explicit_header - "${CMAKE_BINARY_DIR}" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - LINK_LIBRARIES ${dl_LIBRARY} - CMAKE_FLAGS - "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS - -DBOOST_STACKTRACE_USE_BACKTRACE - ${backtrace_include} - OUTPUT_VARIABLE __OUTPUT - ) - - if(_backtrace_explicit_header) - message(CHECK_PASS "yes") - list(APPEND _fpe_options "${backtrace_include}") - else() - message(CHECK_FAIL "no") - endif() - endif() - endif() - - file( - WRITE - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + set(_backtrace_setup_complete FALSE) + set(_backtrace_src + "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/CMakeTmp/backtrace.cpp") + file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/CMakeTmp") + + find_path( + boost_stacktrace_include + NAMES "boost/stacktrace.hpp" + REQUIRED + PATHS ${Boost_INCLUDE_DIRS} + ) + + set(_backtrace_include_def "") + + file(WRITE "${_backtrace_src}" + "#include \n" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") + + message(CHECK_START "Does backtrace work without linker flag") + try_compile( + _backtrace_nolink + "${CMAKE_CURRENT_BINARY_DIR}" + "${_backtrace_src}" + LINK_LIBRARIES ${CMAKE_DL_LIBS} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS -DBOOST_STACKTRACE_USE_BACKTRACE + OUTPUT_VARIABLE __OUTPUT + ) + + if(_backtrace_nolink) + message(CHECK_PASS "yes") + list(APPEND _fpe_options -DBOOST_STACKTRACE_USE_BACKTRACE) + set(_backtrace_setup_complete TRUE) + else() + message(CHECK_FAIL "no") + + file(GLOB hints "/usr/lib/gcc/*/*/include") + find_file(backtrace_header NAMES "backtrace.h" HINTS ${hints}) + + if(${backtrace_header} STREQUAL "backtrace_header-NOTFOUND") + message(STATUS "Could not find backtrace header file") + else() + message(CHECK_START "Does backtrace work with explicit include") + file(WRITE "${_backtrace_src}" "#include \n" "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" - ) - - message(CHECK_START "Does backtrace work without linker flag") + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") try_compile( - _backtrace_nolink - "${CMAKE_BINARY_DIR}" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - LINK_LIBRARIES ${dl_LIBRARY} - CMAKE_FLAGS - "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + _backtrace_explicit + "${CMAKE_CURRENT_BINARY_DIR}" + "${_backtrace_src}" + LINK_LIBRARIES ${CMAKE_DL_LIBS} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS -DBOOST_STACKTRACE_USE_BACKTRACE - ${backtrace_include} + ${_backtrace_include_def} OUTPUT_VARIABLE __OUTPUT ) - if(_backtrace_nolink) + if(_backtrace_explicit) message(CHECK_PASS "yes") set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") - file( - WRITE - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + set(backtrace_include + "-DBOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" ) - message(CHECK_START "Does backtrace work with linker flag") + file(WRITE "${_backtrace_src}" + "#include \n" + "int main() {}\n") + + message(CHECK_START + "Does backtrace work with explicit include") try_compile( - _backtrace_link - "${CMAKE_BINARY_DIR}" - "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" - LINK_LIBRARIES backtrace ${dl_LIBRARY} + _backtrace_explicit_header + "${CMAKE_CURRENT_BINARY_DIR}" + "${_backtrace_src}" + LINK_LIBRARIES ${CMAKE_DL_LIBS} CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS @@ -196,26 +134,80 @@ else() OUTPUT_VARIABLE __OUTPUT ) - if(_backtrace_link) + if(_backtrace_explicit_header) message(CHECK_PASS "yes") - list( - APPEND _fpe_options - -DBOOST_STACKTRACE_USE_BACKTRACE - ) - target_link_libraries( - ActsPluginFpeMonitoring - PUBLIC backtrace - ) - + list(APPEND _fpe_options "${backtrace_include}") set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") + + file(WRITE "${_backtrace_src}" + "#include \n" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") + + message(CHECK_START "Does backtrace work without linker flag") + try_compile( + _backtrace_nolink + "${CMAKE_CURRENT_BINARY_DIR}" + "${_backtrace_src}" + LINK_LIBRARIES ${CMAKE_DL_LIBS} + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS + -DBOOST_STACKTRACE_USE_BACKTRACE + ${backtrace_include} + OUTPUT_VARIABLE __OUTPUT + ) + + if(_backtrace_nolink) + message(CHECK_PASS "yes") + set(_backtrace_setup_complete TRUE) + else() + message(CHECK_FAIL "no") + + file(WRITE "${_backtrace_src}" + "#include \n" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") + + message(CHECK_START "Does backtrace work with linker flag") + try_compile( + _backtrace_link + "${CMAKE_CURRENT_BINARY_DIR}" + "${_backtrace_src}" + LINK_LIBRARIES backtrace ${CMAKE_DL_LIBS} + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS + -DBOOST_STACKTRACE_USE_BACKTRACE + ${backtrace_include} + OUTPUT_VARIABLE __OUTPUT + ) + + if(_backtrace_link) + message(CHECK_PASS "yes") + list( + APPEND _fpe_options + -DBOOST_STACKTRACE_USE_BACKTRACE + ) + target_link_libraries( + ActsPluginFpeMonitoring + PUBLIC backtrace + ) + + set(_backtrace_setup_complete TRUE) + else() + message(CHECK_FAIL "no") + endif() + endif() endif() endif() endif() + endif() if(NOT _backtrace_setup_complete) - if(NOT ${dl_LIBRARY} STREQUAL "dl_LIBRARY-NOTFOUND") + if(CMAKE_DL_LIBS) message(STATUS "ld lib available: use boost basic fallback") else() message(STATUS "Unable to set up stacktraces: use noop") @@ -225,4 +217,5 @@ else() endif() endif() + target_compile_options(ActsPluginFpeMonitoring PUBLIC "${_fpe_options}") From 90f2eaa0be1424d5b67419653245ec73bfcf3490 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 11:34:41 +0100 Subject: [PATCH 08/24] refactor(FpeMonitoring): rename _fpe_options to _fpe_definitions - Use consistent COMPILE_DEFINITIONS syntax (no leading -D) --- Plugins/FpeMonitoring/CMakeLists.txt | 68 ++++++++++++++++------------ 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index 85c7d2e91b4..a3a6fb2616d 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -31,17 +31,17 @@ target_link_libraries(ActsPluginFpeMonitoring PUBLIC Acts::Core) acts_compile_headers(PluginFpeMonitoring GLOB include/**/*.hpp) -# Fpe flags -set(_fpe_options "") +# FPE compile definitions +set(_fpe_definitions "") # In case we're going to run clang-tidy, let's unconditionally turn off backtrace if(ACTS_RUN_CLANG_TIDY) - list(APPEND _fpe_options "-DBOOST_STACKTRACE_USE_NOOP=1") + list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_NOOP=1) else() find_package(Backtrace) find_program(addr2line_EXECUTABLE addr2line) if(APPLE) - list(APPEND _fpe_options -D_GNU_SOURCE) + list(APPEND _fpe_definitions _GNU_SOURCE) elseif(CMAKE_DL_LIBS) target_link_libraries(ActsPluginFpeMonitoring PUBLIC ${CMAKE_DL_LIBS}) @@ -57,27 +57,25 @@ else() PATHS ${Boost_INCLUDE_DIRS} ) - set(_backtrace_include_def "") - file(WRITE "${_backtrace_src}" "#include \n" "#include \n" "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") - message(CHECK_START "Does backtrace work without linker flag") + message(CHECK_START "Does backtrace work without libbacktrace linker flag") try_compile( _backtrace_nolink "${CMAKE_CURRENT_BINARY_DIR}" "${_backtrace_src}" LINK_LIBRARIES ${CMAKE_DL_LIBS} CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS -DBOOST_STACKTRACE_USE_BACKTRACE + COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE OUTPUT_VARIABLE __OUTPUT ) if(_backtrace_nolink) message(CHECK_PASS "yes") - list(APPEND _fpe_options -DBOOST_STACKTRACE_USE_BACKTRACE) + list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE) set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") @@ -85,7 +83,7 @@ else() file(GLOB hints "/usr/lib/gcc/*/*/include") find_file(backtrace_header NAMES "backtrace.h" HINTS ${hints}) - if(${backtrace_header} STREQUAL "backtrace_header-NOTFOUND") + if(backtrace_header STREQUAL "backtrace_header-NOTFOUND") message(STATUS "Could not find backtrace header file") else() message(CHECK_START "Does backtrace work with explicit include") @@ -100,19 +98,19 @@ else() LINK_LIBRARIES ${CMAKE_DL_LIBS} CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS - -DBOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} + BOOST_STACKTRACE_USE_BACKTRACE OUTPUT_VARIABLE __OUTPUT ) if(_backtrace_explicit) message(CHECK_PASS "yes") + list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE) set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") - set(backtrace_include - "-DBOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" + set(_backtrace_include_def + "BOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" ) file(WRITE "${_backtrace_src}" @@ -129,14 +127,19 @@ else() CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS - -DBOOST_STACKTRACE_USE_BACKTRACE - ${backtrace_include} + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} OUTPUT_VARIABLE __OUTPUT ) if(_backtrace_explicit_header) message(CHECK_PASS "yes") - list(APPEND _fpe_options "${backtrace_include}") + list( + APPEND + _fpe_definitions + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} + ) set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") @@ -155,13 +158,19 @@ else() CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS - -DBOOST_STACKTRACE_USE_BACKTRACE - ${backtrace_include} + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} OUTPUT_VARIABLE __OUTPUT ) if(_backtrace_nolink) message(CHECK_PASS "yes") + list( + APPEND + _fpe_definitions + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} + ) set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") @@ -180,16 +189,18 @@ else() CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS - -DBOOST_STACKTRACE_USE_BACKTRACE - ${backtrace_include} + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} OUTPUT_VARIABLE __OUTPUT ) if(_backtrace_link) message(CHECK_PASS "yes") list( - APPEND _fpe_options - -DBOOST_STACKTRACE_USE_BACKTRACE + APPEND + _fpe_definitions + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} ) target_link_libraries( ActsPluginFpeMonitoring @@ -207,15 +218,12 @@ else() endif() if(NOT _backtrace_setup_complete) - if(CMAKE_DL_LIBS) - message(STATUS "ld lib available: use boost basic fallback") - else() - message(STATUS "Unable to set up stacktraces: use noop") - list(APPEND _fpe_options -DBOOST_STACKTRACE_USE_NOOP) - endif() + message(STATUS "libdl available but backtrace backend probe failed: use boost basic fallback") endif() endif() endif() -target_compile_options(ActsPluginFpeMonitoring PUBLIC "${_fpe_options}") +if(_fpe_definitions) + target_compile_definitions(ActsPluginFpeMonitoring PUBLIC ${_fpe_definitions}) +endif() From b4b2fa0c52b6a963f25952f37ee1625f3487e750 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 11:34:41 +0100 Subject: [PATCH 09/24] refactor(FpeMonitoring): extract Darwin platform common code - Add FpeMonitorPlatformDarwinCommon.hpp with shared exceptMaskForType, fpeTypeFromSiCode - Deduplicate between Darwin arm64 and x86_64 --- .../src/FpeMonitorPlatformDarwinArm64.cpp | 115 ++------------- .../src/FpeMonitorPlatformDarwinCommon.hpp | 133 ++++++++++++++++++ .../src/FpeMonitorPlatformDarwinX86_64.cpp | 119 ++-------------- 3 files changed, 159 insertions(+), 208 deletions(-) create mode 100644 Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp index 14021648b7c..28aa1c4486d 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp @@ -12,9 +12,8 @@ #include #include -#include - #include "FpeMonitorPlatform.hpp" +#include "FpeMonitorPlatformDarwinCommon.hpp" #if !defined(__APPLE__) || !defined(__arm64__) #error "This translation unit is only valid for macOS arm64" @@ -43,51 +42,6 @@ std::uint32_t darwinArm64TrapMask(int excepts) { return mask; } -int exceptMaskForType(FpeType type) { - using enum FpeType; - switch (type) { - case INTDIV: - case FLTDIV: - return FE_DIVBYZERO; - case INTOVF: - case FLTOVF: - return FE_OVERFLOW; - case FLTUND: - return FE_UNDERFLOW; - case FLTRES: - return FE_INEXACT; - case FLTINV: - case FLTSUB: - return FE_INVALID; - default: - return 0; - } -} - -std::optional fpeTypeFromSiCode(int siCode) { - using enum FpeType; - switch (siCode) { - case FPE_INTDIV: - return INTDIV; - case FPE_INTOVF: - return INTOVF; - case FPE_FLTDIV: - return FLTDIV; - case FPE_FLTOVF: - return FLTOVF; - case FPE_FLTUND: - return FLTUND; - case FPE_FLTRES: - return FLTRES; - case FPE_FLTINV: - return FLTINV; - case FPE_FLTSUB: - return FLTSUB; - default: - return std::nullopt; - } -} - std::optional fpeTypeFromDarwinArm64Esr(std::uint32_t esr) { constexpr std::uint32_t kEsrExceptionClassShift = 26u; constexpr std::uint32_t kEsrExceptionClassMask = 0x3fu; @@ -125,7 +79,7 @@ bool isRuntimeSupported() { std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { if (signal == SIGFPE && si != nullptr) { - return fpeTypeFromSiCode(si->si_code); + return darwin::fpeTypeFromSiCode(si->si_code); } if (signal == SIGILL && ctx != nullptr) { @@ -137,7 +91,7 @@ std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { } void clearPendingExceptions(int excepts) { - std::feclearexcept(excepts); + darwin::clearPendingExceptions(excepts); } void enableExceptions(int excepts) { @@ -160,7 +114,7 @@ void disableExceptions(int excepts) { } void maskTrapsInSignalContext(void* ctx, FpeType type) { - const int excepts = exceptMaskForType(type); + const int excepts = darwin::exceptMaskForType(type); auto* uc = static_cast(ctx); uc->uc_mcontext->__ns.__fpcr &= ~static_cast(darwinArm64TrapMask(excepts)); @@ -169,58 +123,15 @@ void maskTrapsInSignalContext(void* ctx, FpeType type) { std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::size_t bufferBytes) { - using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; - auto* frames = static_cast(buffer); - const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); - std::size_t count = 0; - - if (ctx == nullptr || maxFrames == 0) { - return 0; - } - - auto* uc = static_cast(ctx); - const std::uintptr_t sp = - __darwin_arm_thread_state64_get_sp(uc->uc_mcontext->__ss); - std::uintptr_t fp = __darwin_arm_thread_state64_get_fp(uc->uc_mcontext->__ss); - const std::uintptr_t pc = - __darwin_arm_thread_state64_get_pc(uc->uc_mcontext->__ss); - - auto push = [&](std::uintptr_t address) { - if (address == 0 || count >= maxFrames) { - return; - } - frames[count++] = reinterpret_cast(address); - }; - - push(pc); - - constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; - auto inStackWindow = [&](std::uintptr_t address) { - if (address < sp || address > sp + kMaxStackWindow) { - return false; - } - return (address % alignof(std::uintptr_t)) == 0; - }; - - struct FrameRecord { - std::uintptr_t prevFp; - std::uintptr_t returnAddress; - }; - - while (count < maxFrames && inStackWindow(fp) && - fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { - const auto* record = reinterpret_cast(fp); - const std::uintptr_t prevFp = record->prevFp; - const std::uintptr_t lr = record->returnAddress; - push(lr); - - if (prevFp <= fp || !inStackWindow(prevFp)) { - break; - } - fp = prevFp; - } - - return count * sizeof(NativeFramePtr); + return darwin::captureStackFromSignalContext( + ctx, buffer, bufferBytes, [](void* rawCtx) { + const auto& uc = *static_cast(rawCtx); + return darwin::RegisterState{ + .sp = __darwin_arm_thread_state64_get_sp(uc.uc_mcontext->__ss), + .fp = __darwin_arm_thread_state64_get_fp(uc.uc_mcontext->__ss), + .pc = __darwin_arm_thread_state64_get_pc(uc.uc_mcontext->__ss), + }; + }); } std::size_t safeDumpSkipFrames() { diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp new file mode 100644 index 00000000000..6911213b1a4 --- /dev/null +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp @@ -0,0 +1,133 @@ +// This file is part of the ACTS project. +// +// Copyright (C) 2016 CERN for the benefit of the ACTS project +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#pragma once + +#include +#include +#include +#include + +#include + +#include "FpeMonitorPlatform.hpp" + +namespace ActsPlugins::detail::darwin { + +struct RegisterState { + std::uintptr_t sp; + std::uintptr_t fp; + std::uintptr_t pc; +}; + +inline int exceptMaskForType(FpeType type) { + using enum FpeType; + switch (type) { + case INTDIV: + case FLTDIV: + return FE_DIVBYZERO; + case INTOVF: + case FLTOVF: + return FE_OVERFLOW; + case FLTUND: + return FE_UNDERFLOW; + case FLTRES: + return FE_INEXACT; + case FLTINV: + case FLTSUB: + return FE_INVALID; + default: + return 0; + } +} + +inline std::optional fpeTypeFromSiCode(int siCode) { + using enum FpeType; + switch (siCode) { + case FPE_INTDIV: + return INTDIV; + case FPE_INTOVF: + return INTOVF; + case FPE_FLTDIV: + return FLTDIV; + case FPE_FLTOVF: + return FLTOVF; + case FPE_FLTUND: + return FLTUND; + case FPE_FLTRES: + return FLTRES; + case FPE_FLTINV: + return FLTINV; + case FPE_FLTSUB: + return FLTSUB; + default: + return std::nullopt; + } +} + +inline void clearPendingExceptions(int excepts) { + std::feclearexcept(excepts); +} + +template +std::size_t captureStackFromSignalContext(void* ctx, void* buffer, + std::size_t bufferBytes, + RegisterStateExtractor&& extractor) { + using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; + auto* frames = static_cast(buffer); + const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); + std::size_t count = 0; + + if (ctx == nullptr || maxFrames == 0) { + return 0; + } + + const RegisterState state = extractor(ctx); + const std::uintptr_t sp = state.sp; + std::uintptr_t pc = state.pc; + std::uintptr_t fp = state.fp; + + auto push = [&](std::uintptr_t address) { + if (address == 0 || count >= maxFrames) { + return; + } + frames[count++] = reinterpret_cast(address); + }; + + push(pc); + + constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; + auto inStackWindow = [&](std::uintptr_t address) { + if (address < sp || address > sp + kMaxStackWindow) { + return false; + } + return (address % alignof(std::uintptr_t)) == 0; + }; + + struct FrameRecord { + std::uintptr_t prevFp; + std::uintptr_t returnAddress; + }; + + while (count < maxFrames && inStackWindow(fp) && + fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { + const auto* record = reinterpret_cast(fp); + const std::uintptr_t prevFp = record->prevFp; + const std::uintptr_t returnAddress = record->returnAddress; + push(returnAddress); + + if (prevFp <= fp || !inStackWindow(prevFp)) { + break; + } + fp = prevFp; + } + + return count * sizeof(NativeFramePtr); +} + +} // namespace ActsPlugins::detail::darwin diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp index 97a5d0b9620..900fa623252 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp @@ -6,71 +6,19 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -#include "ActsPlugins/FpeMonitoring/FpeMonitor.hpp" - #include #include #include -#include #include -#include - #include "FpeMonitorPlatform.hpp" +#include "FpeMonitorPlatformDarwinCommon.hpp" #if !defined(__APPLE__) || !defined(__x86_64__) #error "This translation unit is only valid for macOS x86_64" #endif namespace ActsPlugins::detail { -namespace { - -int exceptMaskForType(FpeType type) { - using enum FpeType; - switch (type) { - case INTDIV: - case FLTDIV: - return FE_DIVBYZERO; - case INTOVF: - case FLTOVF: - return FE_OVERFLOW; - case FLTUND: - return FE_UNDERFLOW; - case FLTRES: - return FE_INEXACT; - case FLTINV: - case FLTSUB: - return FE_INVALID; - default: - return 0; - } -} - -std::optional fpeTypeFromSiCode(int siCode) { - using enum FpeType; - switch (siCode) { - case FPE_INTDIV: - return INTDIV; - case FPE_INTOVF: - return INTOVF; - case FPE_FLTDIV: - return FLTDIV; - case FPE_FLTOVF: - return FLTOVF; - case FPE_FLTUND: - return FLTUND; - case FPE_FLTRES: - return FLTRES; - case FPE_FLTINV: - return FLTINV; - case FPE_FLTSUB: - return FLTSUB; - default: - return std::nullopt; - } -} - -} // namespace bool isRuntimeSupported() { return true; @@ -81,11 +29,11 @@ std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { if (signal != SIGFPE || si == nullptr) { return std::nullopt; } - return fpeTypeFromSiCode(si->si_code); + return darwin::fpeTypeFromSiCode(si->si_code); } void clearPendingExceptions(int excepts) { - std::feclearexcept(excepts); + darwin::clearPendingExceptions(excepts); } void enableExceptions(int excepts) { @@ -111,7 +59,7 @@ void disableExceptions(int excepts) { } void maskTrapsInSignalContext(void* ctx, FpeType type) { - const int excepts = exceptMaskForType(type); + const int excepts = darwin::exceptMaskForType(type); auto* uc = static_cast(ctx); uc->uc_mcontext->__fs.__fpu_fcw |= static_cast(excepts); uc->uc_mcontext->__fs.__fpu_fsw &= @@ -124,56 +72,15 @@ void maskTrapsInSignalContext(void* ctx, FpeType type) { std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::size_t bufferBytes) { - using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; - auto* frames = static_cast(buffer); - const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); - std::size_t count = 0; - - if (ctx == nullptr || maxFrames == 0) { - return 0; - } - - auto* uc = static_cast(ctx); - const std::uintptr_t sp = uc->uc_mcontext->__ss.__rsp; - std::uintptr_t fp = uc->uc_mcontext->__ss.__rbp; - const std::uintptr_t pc = uc->uc_mcontext->__ss.__rip; - - auto push = [&](std::uintptr_t address) { - if (address == 0 || count >= maxFrames) { - return; - } - frames[count++] = reinterpret_cast(address); - }; - - push(pc); - - constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; - auto inStackWindow = [&](std::uintptr_t address) { - if (address < sp || address > sp + kMaxStackWindow) { - return false; - } - return (address % alignof(std::uintptr_t)) == 0; - }; - - struct FrameRecord { - std::uintptr_t prevFp; - std::uintptr_t returnAddress; - }; - - while (count < maxFrames && inStackWindow(fp) && - fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { - const auto* record = reinterpret_cast(fp); - const std::uintptr_t prevFp = record->prevFp; - const std::uintptr_t ra = record->returnAddress; - push(ra); - - if (prevFp <= fp || !inStackWindow(prevFp)) { - break; - } - fp = prevFp; - } - - return count * sizeof(NativeFramePtr); + return darwin::captureStackFromSignalContext( + ctx, buffer, bufferBytes, [](void* rawCtx) { + const auto& uc = *static_cast(rawCtx); + return darwin::RegisterState{ + .sp = uc.uc_mcontext->__ss.__rsp, + .fp = uc.uc_mcontext->__ss.__rbp, + .pc = uc.uc_mcontext->__ss.__rip, + }; + }); } std::size_t safeDumpSkipFrames() { From 17040399182eedf56b53576e1e51b83a5e936b3a Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 11:40:15 +0100 Subject: [PATCH 10/24] docs(FpeMonitoring): add comments to platform implementations --- .../src/FpeMonitorPlatformDarwinArm64.cpp | 20 +++++++++++++++++++ .../src/FpeMonitorPlatformDarwinX86_64.cpp | 18 +++++++++++++++++ .../src/FpeMonitorPlatformLinuxX86_64.cpp | 18 +++++++++++++++++ .../src/FpeMonitorPlatformUnsupported.cpp | 9 +++++++++ 4 files changed, 65 insertions(+) diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp index 28aa1c4486d..b04e4c8dc59 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp @@ -22,6 +22,8 @@ namespace ActsPlugins::detail { namespace { +// Darwin arm64 exposes separate trap-enable bits in FPCR instead of x87/MXCSR +// masks. This helper translates FE_* flags into the corresponding FPCR bits. std::uint32_t darwinArm64TrapMask(int excepts) { std::uint32_t mask = 0; if ((excepts & FE_INVALID) != 0) { @@ -43,6 +45,8 @@ std::uint32_t darwinArm64TrapMask(int excepts) { } std::optional fpeTypeFromDarwinArm64Esr(std::uint32_t esr) { + // For arm64, floating-point traps may arrive as SIGILL with ESR metadata. + // We only decode ESR values belonging to the floating-point exception class. constexpr std::uint32_t kEsrExceptionClassShift = 26u; constexpr std::uint32_t kEsrExceptionClassMask = 0x3fu; constexpr std::uint32_t kFpExceptionClass = 0x2cu; @@ -74,14 +78,19 @@ std::optional fpeTypeFromDarwinArm64Esr(std::uint32_t esr) { } // namespace bool isRuntimeSupported() { + // The arm64 Darwin implementation has dedicated handlers for trap control, + // signal decoding and stack capture. return true; } std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + // Prefer SIGFPE si_code mapping when available. if (signal == SIGFPE && si != nullptr) { return darwin::fpeTypeFromSiCode(si->si_code); } + // Some arm64 floating-point traps surface as SIGILL; decode from ESR in the + // interrupted context. if (signal == SIGILL && ctx != nullptr) { auto* uc = static_cast(ctx); return fpeTypeFromDarwinArm64Esr(uc->uc_mcontext->__es.__esr); @@ -91,10 +100,12 @@ std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { } void clearPendingExceptions(int excepts) { + // Clear sticky flags before enabling traps to avoid immediate retriggering. darwin::clearPendingExceptions(excepts); } void enableExceptions(int excepts) { + // FPCR controls trap enablement on arm64 Darwin; FPSR carries sticky status. fenv_t env{}; if (fegetenv(&env) != 0) { return; @@ -105,6 +116,7 @@ void enableExceptions(int excepts) { } void disableExceptions(int excepts) { + // Disable only requested trap classes and leave unrelated bits untouched. fenv_t env{}; if (fegetenv(&env) != 0) { return; @@ -114,6 +126,8 @@ void disableExceptions(int excepts) { } void maskTrapsInSignalContext(void* ctx, FpeType type) { + // In the interrupted context, disable the current trap kind and clear all + // pending floating-point status bits before resuming unwinding. const int excepts = darwin::exceptMaskForType(type); auto* uc = static_cast(ctx); uc->uc_mcontext->__ns.__fpcr &= @@ -123,6 +137,8 @@ void maskTrapsInSignalContext(void* ctx, FpeType type) { std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::size_t bufferBytes) { + // Reuse shared Darwin frame-walk logic while extracting arm64 SP/FP/PC from + // the saved thread state. return darwin::captureStackFromSignalContext( ctx, buffer, bufferBytes, [](void* rawCtx) { const auto& uc = *static_cast(rawCtx); @@ -135,14 +151,18 @@ std::size_t captureStackFromSignalContext(void* ctx, void* buffer, } std::size_t safeDumpSkipFrames() { + // Skip one synthetic frame from the dump helper in final traces. return 1; } bool shouldFailFastOnUnknownSignal() { + // On arm64 we expect only decodable SIGFPE/SIGILL cases. Unknown deliveries + // likely indicate a corrupt or unsupported context, so fail fast. return true; } void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + // Install both SIGFPE and SIGILL handlers to cover Darwin arm64 behavior. struct sigaction action {}; action.sa_sigaction = handler; action.sa_flags = SA_SIGINFO; diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp index 900fa623252..5954ad8eade 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp @@ -20,11 +20,15 @@ namespace ActsPlugins::detail { +// Darwin x86_64 has full trap support via the legacy x87 control word plus +// SSE MXCSR, so runtime support is always available on this build target. bool isRuntimeSupported() { return true; } std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + // On this platform we only install a SIGFPE handler; si_code is enough to + // classify all FPE kinds we track. static_cast(ctx); if (signal != SIGFPE || si == nullptr) { return std::nullopt; @@ -33,10 +37,15 @@ std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { } void clearPendingExceptions(int excepts) { + // Clear sticky exception state before enabling traps to avoid immediate + // retriggering from stale flags. darwin::clearPendingExceptions(excepts); } void enableExceptions(int excepts) { + // Darwin x86_64 mirrors Linux semantics: + // - x87 control word: trap enabled when corresponding mask bit is cleared + // - MXCSR: trap enabled when corresponding mask bit [7:12] is cleared fenv_t env{}; if (fegetenv(&env) != 0) { return; @@ -49,6 +58,7 @@ void enableExceptions(int excepts) { } void disableExceptions(int excepts) { + // Restore masking for requested exceptions in both x87 and SSE domains. fenv_t env{}; if (fegetenv(&env) != 0) { return; @@ -59,6 +69,8 @@ void disableExceptions(int excepts) { } void maskTrapsInSignalContext(void* ctx, FpeType type) { + // We mask only the trap that fired in the interrupted context so the faulting + // instruction can be unwound safely, then clear status flags in both units. const int excepts = darwin::exceptMaskForType(type); auto* uc = static_cast(ctx); uc->uc_mcontext->__fs.__fpu_fcw |= static_cast(excepts); @@ -72,6 +84,8 @@ void maskTrapsInSignalContext(void* ctx, FpeType type) { std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::size_t bufferBytes) { + // Use the shared Darwin frame-walker and provide x86_64 register extraction + // from the interrupted thread state. return darwin::captureStackFromSignalContext( ctx, buffer, bufferBytes, [](void* rawCtx) { const auto& uc = *static_cast(rawCtx); @@ -84,14 +98,18 @@ std::size_t captureStackFromSignalContext(void* ctx, void* buffer, } std::size_t safeDumpSkipFrames() { + // Skip one frame to hide the monitor's own dump helper from final traces. return 1; } bool shouldFailFastOnUnknownSignal() { + // x86_64 should only see SIGFPE with known si_code values. Unknowns are + // treated as non-fatal to preserve backward compatibility. return false; } void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + // A single SIGFPE handler is sufficient on Darwin x86_64. struct sigaction action {}; action.sa_sigaction = handler; action.sa_flags = SA_SIGINFO; diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp index 1595fcc526f..71ca62c3215 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp @@ -23,6 +23,9 @@ namespace ActsPlugins::detail { namespace { +// Linux x86_64 signal context exposes raw x87/SSE control and status words. +// These masks are used when disabling the current trap and clearing sticky +// exception bits in the interrupted context. constexpr std::uint16_t kFpuExceptionMask = 0x3f; constexpr std::uint16_t kFpuStatusFlags = 0xff; constexpr std::uint32_t kSseStatusFlags = kFpuExceptionMask; @@ -54,10 +57,14 @@ std::optional fpeTypeFromSiCode(int siCode) { } // namespace bool isRuntimeSupported() { + // Linux x86_64 supports feenableexcept/fedisableexcept and SIGFPE si_code + // decoding, so trapping mode is fully available. return true; } std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + // This backend only installs SIGFPE handlers. si_code carries the exception + // category we expose in FpeType. static_cast(ctx); if (signal != SIGFPE || si == nullptr) { return std::nullopt; @@ -66,18 +73,24 @@ std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { } void clearPendingExceptions(int excepts) { + // Clear stale sticky flags before changing trap state. std::feclearexcept(excepts); } void enableExceptions(int excepts) { + // glibc helper enables hardware traps for requested FE_* bits. feenableexcept(excepts); } void disableExceptions(int excepts) { + // glibc helper disables hardware traps for requested FE_* bits. fedisableexcept(excepts); } void maskTrapsInSignalContext(void* ctx, FpeType type) { + // Linux reports enough detail in si_code, so "type" is currently unused. + // We mask all x87 trap bits and clear pending x87/SSE status flags to allow + // safe unwinding past the faulting instruction. static_cast(type); auto* uc = static_cast(ctx); __uint16_t* cw = &uc->uc_mcontext.fpregs->cwd; @@ -93,6 +106,8 @@ void maskTrapsInSignalContext(void* ctx, FpeType type) { std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::size_t bufferBytes) { + // Linux x86_64 currently falls back to boost::stacktrace::safe_dump_to in + // the signal handler, so no context-based frame extraction is attempted here. static_cast(ctx); static_cast(buffer); static_cast(bufferBytes); @@ -100,14 +115,17 @@ std::size_t captureStackFromSignalContext(void* ctx, void* buffer, } std::size_t safeDumpSkipFrames() { + // Skip two frames to hide signal-handler and dump-helper internals. return 2; } bool shouldFailFastOnUnknownSignal() { + // Unknown/unsupported signal payloads are tolerated and ignored on Linux. return false; } void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + // Linux only needs SIGFPE for floating-point trap monitoring. struct sigaction action {}; action.sa_sigaction = handler; action.sa_flags = SA_SIGINFO; diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp index e687d799a50..e2e96281723 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp @@ -10,15 +10,20 @@ namespace ActsPlugins::detail { +// This backend intentionally provides a complete no-op implementation so the +// public FpeMonitor API remains available even when platform trapping support +// is missing. bool isRuntimeSupported() { return false; } std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { + // No signal decoding support on unsupported platforms. static_cast(signal); static_cast(si); static_cast(ctx); return std::nullopt; } +// Trap-control hooks are intentionally inert. void clearPendingExceptions(int excepts) { static_cast(excepts); } void enableExceptions(int excepts) { static_cast(excepts); } @@ -26,23 +31,27 @@ void enableExceptions(int excepts) { static_cast(excepts); } void disableExceptions(int excepts) { static_cast(excepts); } void maskTrapsInSignalContext(void* ctx, FpeType type) { + // No context mutation possible without platform-specific register layout. static_cast(ctx); static_cast(type); } std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::size_t bufferBytes) { + // No signal-context stack unwinding backend on unsupported platforms. static_cast(ctx); static_cast(buffer); static_cast(bufferBytes); return 0; } +// Keep defaults aligned with safe_dump fallback behavior. std::size_t safeDumpSkipFrames() { return 2; } bool shouldFailFastOnUnknownSignal() { return false; } void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { + // Signal handler installation is intentionally disabled. static_cast(handler); } From 69cff46c0e452d9ff491289f017185833d91fe98 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 11:48:31 +0100 Subject: [PATCH 11/24] refactor(FpeMonitoring): move fpeTypeFromSiCode to platform header - Move from DarwinCommon to FpeMonitorPlatform.hpp for shared use - Darwin arm64 uses platform header, DarwinCommon keeps darwin-specific helpers --- .../FpeMonitoring/src/FpeMonitorPlatform.hpp | 24 +++++++++++ .../src/FpeMonitorPlatformDarwinArm64.cpp | 2 +- .../src/FpeMonitorPlatformDarwinCommon.hpp | 40 +++++++------------ .../src/FpeMonitorPlatformDarwinX86_64.cpp | 2 +- .../src/FpeMonitorPlatformLinuxX86_64.cpp | 24 ----------- 5 files changed, 41 insertions(+), 51 deletions(-) diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp index 50891bbadd5..80c3c401229 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp @@ -16,6 +16,30 @@ namespace ActsPlugins::detail { +inline std::optional fpeTypeFromSiCode(int siCode) { + using enum FpeType; + switch (siCode) { + case FPE_INTDIV: + return INTDIV; + case FPE_INTOVF: + return INTOVF; + case FPE_FLTDIV: + return FLTDIV; + case FPE_FLTOVF: + return FLTOVF; + case FPE_FLTUND: + return FLTUND; + case FPE_FLTRES: + return FLTRES; + case FPE_FLTINV: + return FLTINV; + case FPE_FLTSUB: + return FLTSUB; + default: + return std::nullopt; + } +} + bool isRuntimeSupported(); std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp index b04e4c8dc59..0011c16aba9 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp @@ -86,7 +86,7 @@ bool isRuntimeSupported() { std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { // Prefer SIGFPE si_code mapping when available. if (signal == SIGFPE && si != nullptr) { - return darwin::fpeTypeFromSiCode(si->si_code); + return fpeTypeFromSiCode(si->si_code); } // Some arm64 floating-point traps surface as SIGILL; decode from ESR in the diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp index 6911213b1a4..b50687fce1a 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinCommon.hpp @@ -11,7 +11,6 @@ #include #include #include -#include #include @@ -46,30 +45,6 @@ inline int exceptMaskForType(FpeType type) { } } -inline std::optional fpeTypeFromSiCode(int siCode) { - using enum FpeType; - switch (siCode) { - case FPE_INTDIV: - return INTDIV; - case FPE_INTOVF: - return INTOVF; - case FPE_FLTDIV: - return FLTDIV; - case FPE_FLTOVF: - return FLTOVF; - case FPE_FLTUND: - return FLTUND; - case FPE_FLTRES: - return FLTRES; - case FPE_FLTINV: - return FLTINV; - case FPE_FLTSUB: - return FLTSUB; - default: - return std::nullopt; - } -} - inline void clearPendingExceptions(int excepts) { std::feclearexcept(excepts); } @@ -78,6 +53,13 @@ template std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::size_t bufferBytes, RegisterStateExtractor&& extractor) { + // Why this helper exists: + // In a signal handler we need the stack of the interrupted faulting context + // (PC/SP/FP from ucontext), not the stack of the handler itself. + // Generic signal-safe dumps start at the current handler frame and cannot be + // seeded with those saved registers, so they may miss the real fault site or + // include mostly signal trampoline frames. Walking from saved FP/PC gives a + // deterministic trace rooted at the trapping instruction on Darwin. using NativeFramePtr = boost::stacktrace::frame::native_frame_ptr_t; auto* frames = static_cast(buffer); const std::size_t maxFrames = bufferBytes / sizeof(NativeFramePtr); @@ -87,6 +69,8 @@ std::size_t captureStackFromSignalContext(void* ctx, void* buffer, return 0; } + // The platform TU provides arch-specific extraction of SP/FP/PC from the raw + // signal context while this helper keeps the frame-walk logic shared. const RegisterState state = extractor(ctx); const std::uintptr_t sp = state.sp; std::uintptr_t pc = state.pc; @@ -101,6 +85,8 @@ std::size_t captureStackFromSignalContext(void* ctx, void* buffer, push(pc); + // Keep unwinding constrained to a finite stack window above SP to avoid + // dereferencing arbitrary memory if the frame chain is corrupted. constexpr std::uintptr_t kMaxStackWindow = 16 * 1024 * 1024; auto inStackWindow = [&](std::uintptr_t address) { if (address < sp || address > sp + kMaxStackWindow) { @@ -114,6 +100,10 @@ std::size_t captureStackFromSignalContext(void* ctx, void* buffer, std::uintptr_t returnAddress; }; + // Standard frame-pointer chain walk: + // fp -> {prev_fp, return_address} + // Stops when the chain is non-monotonic, leaves the allowed stack window, + // or we exhaust caller-provided buffer capacity. while (count < maxFrames && inStackWindow(fp) && fp + sizeof(FrameRecord) <= sp + kMaxStackWindow) { const auto* record = reinterpret_cast(fp); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp index 5954ad8eade..851f46037fb 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp @@ -33,7 +33,7 @@ std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { if (signal != SIGFPE || si == nullptr) { return std::nullopt; } - return darwin::fpeTypeFromSiCode(si->si_code); + return fpeTypeFromSiCode(si->si_code); } void clearPendingExceptions(int excepts) { diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp index 71ca62c3215..ac71770a011 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp @@ -30,30 +30,6 @@ constexpr std::uint16_t kFpuExceptionMask = 0x3f; constexpr std::uint16_t kFpuStatusFlags = 0xff; constexpr std::uint32_t kSseStatusFlags = kFpuExceptionMask; -std::optional fpeTypeFromSiCode(int siCode) { - using enum FpeType; - switch (siCode) { - case FPE_INTDIV: - return INTDIV; - case FPE_INTOVF: - return INTOVF; - case FPE_FLTDIV: - return FLTDIV; - case FPE_FLTOVF: - return FLTOVF; - case FPE_FLTUND: - return FLTUND; - case FPE_FLTRES: - return FLTRES; - case FPE_FLTINV: - return FLTINV; - case FPE_FLTSUB: - return FLTSUB; - default: - return std::nullopt; - } -} - } // namespace bool isRuntimeSupported() { From fe5b897438da67b9e11c40e522150b02a60abf71 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 13:52:54 +0100 Subject: [PATCH 12/24] enable fpemon macos ci --- .github/workflows/builds.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 8864c06a51a..d7098c97b3c 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -353,6 +353,7 @@ jobs: CI/dependencies/run.sh .env cmake -B build -S . --preset=github-ci + -DACTS_BUILD_PLUGIN_FPEMON=ON -DCMAKE_INSTALL_PREFIX="${{ env.INSTALL_DIR }}" - name: Build From 3fe014b77d7a26ca1adf7924efc0b4809dce2369 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 13:57:08 +0100 Subject: [PATCH 13/24] ci: enable FPE monitoring across CI Enable ACTS_BUILD_PLUGIN_FPEMON in shared and CI-specific presets. Remove ACTS_SEQUENCER_DISABLE_FPEMON runtime opt-outs from GitHub and GitLab jobs. Drop redundant macOS-only configure override now covered by presets. --- .github/workflows/builds.yml | 5 ----- .gitlab-ci.yml | 2 -- CMakePresets.json | 8 +++++--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index d7098c97b3c..f7e9b5236a7 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -119,8 +119,6 @@ jobs: runs-on: ubuntu-latest container: ghcr.io/acts-project/ubuntu2404:83 needs: [linux_ubuntu] - env: - ACTS_SEQUENCER_DISABLE_FPEMON: true steps: - uses: actions/checkout@v6 @@ -158,8 +156,6 @@ jobs: runs-on: ubuntu-latest container: ghcr.io/acts-project/ubuntu2404:83 needs: [linux_ubuntu] - env: - ACTS_SEQUENCER_DISABLE_FPEMON: true steps: - uses: actions/checkout@v6 @@ -353,7 +349,6 @@ jobs: CI/dependencies/run.sh .env cmake -B build -S . --preset=github-ci - -DACTS_BUILD_PLUGIN_FPEMON=ON -DCMAKE_INSTALL_PREFIX="${{ env.INSTALL_DIR }}" - name: Build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 547917fe5a9..dc092dbaee8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -395,8 +395,6 @@ linux_ubuntu_2404_clang19: variables: ACTS_LOG_FAILURE_THRESHOLD: WARNING - # disable fpe monitoring as we can't mask without backtrace support - ACTS_SEQUENCER_DISABLE_FPEMON: true INSTALL_DIR: ${CI_PROJECT_DIR}/install # force off even if it is CI mode ROOT_HASH_CHECKS: off diff --git a/CMakePresets.json b/CMakePresets.json index 7ecd3755901..ac76548f235 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -59,6 +59,7 @@ "ACTS_BUILD_PLUGIN_ACTSVG": "ON", "ACTS_BUILD_PLUGIN_DD4HEP": "ON", "ACTS_BUILD_PLUGIN_EDM4HEP": "ON", + "ACTS_BUILD_PLUGIN_FPEMON": "ON", "ACTS_BUILD_PLUGIN_GEOMODEL": "ON", "ACTS_BUILD_PLUGIN_TRACCC": "ON", "ACTS_BUILD_PLUGIN_GEANT4": "ON", @@ -110,8 +111,7 @@ "displayName": "GitLab-CI", "inherits": "ci-common", "cacheVariables": { - "CMAKE_BUILD_TYPE": "RelWithDebInfo", - "ACTS_BUILD_PLUGIN_FPEMON": "ON" + "CMAKE_BUILD_TYPE": "RelWithDebInfo" } }, { @@ -148,6 +148,7 @@ "ACTS_FORCE_ASSERTIONS": "ON", "ACTS_ENABLE_LOG_FAILURE_THRESHOLD": "ON", "ACTS_BUILD_PYTHON_BINDINGS": "ON", + "ACTS_BUILD_PLUGIN_FPEMON": "ON", "ACTS_BUILD_PLUGIN_GNN": "ON", "ACTS_BUILD_EXAMPLES": "ON", "ACTS_BUILD_EXAMPLES_GNN": "ON", @@ -168,6 +169,7 @@ "CMAKE_CXX_COMPILER_LAUNCHER": "ccache", "CMAKE_CUDA_ARCHITECTURES": "86", "TORCH_CUDA_ARCH_LIST": "8.6", + "ACTS_BUILD_PLUGIN_FPEMON": "ON", "ACTS_BUILD_PLUGIN_GNN": "ON", "ACTS_GNN_ENABLE_TORCH": "OFF", "ACTS_GNN_ENABLE_CUDA": "ON", @@ -192,7 +194,7 @@ "ACTS_BUILD_PYTHON_BINDINGS": "ON", "ACTS_BUILD_EXAMPLES": "ON", "ACTS_BUILD_PLUGIN_ROOT": "OFF", - "ACTS_BUILD_PLUGIN_FPEMON": "OFF", + "ACTS_BUILD_PLUGIN_FPEMON": "ON", "ACTS_BUILD_EXAMPLES_ROOT": "OFF", "ACTS_USE_SYSTEM_NLOHMANN_JSON": "ON", "ACTS_USE_SYSTEM_PYBIND11": "ON" From 0f2321ee247daf6ad7e20a2bde149f635c0505d1 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 15:47:45 +0100 Subject: [PATCH 14/24] tweak when fpe failures are engaged --- .github/workflows/builds.yml | 1 + .../ActsExamples/Framework/Sequencer.hpp | 7 +++ .../Framework/src/Framework/Sequencer.cpp | 52 +++++++++++++++---- Python/Examples/src/Framework.cpp | 2 +- Python/Examples/tests/test_fpe.py | 28 ++++++++++ 5 files changed, 80 insertions(+), 10 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index f7e9b5236a7..9074abc9ccd 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -20,6 +20,7 @@ env: CCACHE_DIR: ${{ github.workspace }}/ccache CCACHE_MAXSIZE: 500M CCACHE_KEY_SUFFIX: r2 + ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE: 0 jobs: linux_ubuntu: diff --git a/Examples/Framework/include/ActsExamples/Framework/Sequencer.hpp b/Examples/Framework/include/ActsExamples/Framework/Sequencer.hpp index 84340370360..a9b28fba1be 100644 --- a/Examples/Framework/include/ActsExamples/Framework/Sequencer.hpp +++ b/Examples/Framework/include/ActsExamples/Framework/Sequencer.hpp @@ -77,9 +77,16 @@ class Sequencer { /// @warning This function can be called from multiple threads and should therefore be thread-safe IterationCallback iterationCallback = []() {}; + /// If true, FPEs are tracked. bool trackFpes = true; + /// If true, FPEs are masked and reported. std::vector fpeMasks{}; + /// If true, the first FPE encountered makes Sequencer::run() fail. bool failOnFirstFpe = false; + /// If false, unmasked FPEs are reported but do not make Sequencer::run() + /// fail. + bool failOnUnmaskedFpe = true; + /// The number of stack frames to include in the FPE report. std::size_t fpeStackTraceLength = 8; }; diff --git a/Examples/Framework/src/Framework/Sequencer.cpp b/Examples/Framework/src/Framework/Sequencer.cpp index 57e7e0b737d..fc7cd503071 100644 --- a/Examples/Framework/src/Framework/Sequencer.cpp +++ b/Examples/Framework/src/Framework/Sequencer.cpp @@ -58,6 +58,26 @@ std::size_t saturatedAdd(std::size_t a, std::size_t b) { return res; } +std::optional parseBoolEnv(const char* envName) { + const char* rawValue = std::getenv(envName); + if (rawValue == nullptr) { + return std::nullopt; + } + + std::string value = rawValue; + boost::algorithm::trim(value); + boost::algorithm::to_lower(value); + if (value == "1" || value == "true" || value == "yes" || value == "on") { + return true; + } + if (value == "0" || value == "false" || value == "no" || value == "off") { + return false; + } + throw SequenceConfigurationException( + std::string{"Unable to parse "} + envName + " value '" + rawValue + + "'. Supported values are: 0/1/false/true/no/yes/off/on"); +} + } // namespace Sequencer::Sequencer(const Sequencer::Config& cfg) @@ -80,12 +100,23 @@ Sequencer::Sequencer(const Sequencer::Config& cfg) ACTS_INFO("Create Sequencer with " << m_cfg.numThreads << " threads"); } - const char* envvar = std::getenv("ACTS_SEQUENCER_DISABLE_FPEMON"); - if (envvar != nullptr) { + if (auto disableFpeEnv = parseBoolEnv("ACTS_SEQUENCER_DISABLE_FPEMON"); + disableFpeEnv.has_value()) { + m_cfg.trackFpes = !disableFpeEnv.value(); ACTS_INFO( - "Overriding FPE tracking Sequencer based on environment variable " - "ACTS_SEQUENCER_DISABLE_FPEMON"); - m_cfg.trackFpes = false; + "FPE tracking is " + << (m_cfg.trackFpes ? "enabled" : "disabled") + << " based on environment variable ACTS_SEQUENCER_DISABLE_FPEMON"); + } + + if (auto failUnmaskedEnv = + parseBoolEnv("ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE"); + failUnmaskedEnv.has_value()) { + m_cfg.failOnUnmaskedFpe = failUnmaskedEnv.value(); + ACTS_INFO("Sequencer failOnUnmaskedFpe is " + << (m_cfg.failOnUnmaskedFpe ? "enabled" : "disabled") + << " based on environment variable " + "ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE"); } if (m_cfg.trackFpes && !m_cfg.fpeMasks.empty() && @@ -526,12 +557,13 @@ int Sequencer::run() { m_nUnmaskedFpe += (count - nMasked); - if (m_cfg.failOnFirstFpe) { + if (m_cfg.failOnFirstFpe && m_cfg.failOnUnmaskedFpe) { ACTS_ERROR(ss.str()); local.merge(mon->result()); // merge so we get correct // results after throwing throw FpeFailure{ss.str()}; - } else if (!local.contains(type, *st)) { + } else if (m_cfg.failOnUnmaskedFpe && + !local.contains(type, *st)) { ACTS_INFO(ss.str()); } } @@ -607,7 +639,7 @@ int Sequencer::run() { joinPaths(m_cfg.outputDir, m_cfg.outputTimingFile)); } - if (m_nUnmaskedFpe > 0) { + if (m_cfg.failOnUnmaskedFpe && m_nUnmaskedFpe > 0) { return EXIT_FAILURE; } @@ -657,7 +689,9 @@ void Sequencer::fpeReport() const { } if (m_nUnmaskedFpe > 0) { - ACTS_ERROR("Encountered " << m_nUnmaskedFpe << " unmasked FPEs"); + Acts::Logging::Level level = + m_cfg.failOnUnmaskedFpe ? Acts::Logging::ERROR : Acts::Logging::INFO; + ACTS_LOG(level, "Encountered " << m_nUnmaskedFpe << " unmasked FPEs"); } else { ACTS_INFO("No unmasked FPEs encountered"); } diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index 369448864be..1af218a74e0 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -325,7 +325,7 @@ void addFramework(py::module& mex) { ACTS_PYTHON_STRUCT(c, skip, events, logLevel, numThreads, outputDir, outputTimingFile, trackFpes, fpeMasks, failOnFirstFpe, - fpeStackTraceLength); + failOnUnmaskedFpe, fpeStackTraceLength); auto fpem = py::class_(sequencer, "_FpeMask") diff --git a/Python/Examples/tests/test_fpe.py b/Python/Examples/tests/test_fpe.py index 679637154f9..b6bd534d776 100644 --- a/Python/Examples/tests/test_fpe.py +++ b/Python/Examples/tests/test_fpe.py @@ -108,6 +108,7 @@ def test_fpe_single_fail_at_end(fpe_type): s = acts.examples.Sequencer( events=10, failOnFirstFpe=False, + failOnUnmaskedFpe=True, ) s.addAlgorithm( @@ -126,10 +127,33 @@ def test_fpe_single_fail_at_end(fpe_type): assert res.count(x) == (s.config.events if x == fpe_type else 0) +def test_fpe_single_no_fail_at_end(fpe_type): + s = acts.examples.Sequencer( + events=10, + failOnFirstFpe=False, + failOnUnmaskedFpe=False, + ) + + s.addAlgorithm( + FuncAlg( + _names[fpe_type], + lambda _: getattr( + acts.examples.FpeMonitor, f"_trigger_{_names[fpe_type].lower()}" + )(), + ) + ) + s.run() + + res = s.fpeResult + for x in acts.examples.FpeType.values: + assert res.count(x) == (s.config.events if x == fpe_type else 0) + + def test_fpe_single_fail_immediately(fpe_type): s = acts.examples.Sequencer( events=10, failOnFirstFpe=True, + failOnUnmaskedFpe=True, numThreads=1, ) @@ -185,6 +209,7 @@ def execute(self, context): s = acts.examples.Sequencer( events=10, failOnFirstFpe=False, + failOnUnmaskedFpe=True, numThreads=-1, ) s.addAlgorithm(Alg()) @@ -215,6 +240,7 @@ def func(context): events=10, numThreads=-1, failOnFirstFpe=True, + failOnUnmaskedFpe=True, fpeMasks=[ acts.examples.Sequencer.FpeMask(*_locs[fpe_type], fpe_type, 1), ], @@ -231,6 +257,7 @@ def func(context): events=10, numThreads=-1, failOnFirstFpe=True, + failOnUnmaskedFpe=True, fpeMasks=[ acts.examples.Sequencer.FpeMask(*_locs[fpe_type], fpe_type, 3), ], @@ -284,6 +311,7 @@ def test_buffer_sufficient(): s = acts.examples.Sequencer( events=10000, failOnFirstFpe=False, + failOnUnmaskedFpe=True, ) s.addAlgorithm( From c065bfe36b51c86c6b7608e3edda34a1fdbd0d64 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Wed, 11 Mar 2026 18:29:05 +0100 Subject: [PATCH 15/24] restore behavior on github --- .github/workflows/builds.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 9074abc9ccd..7a8549de272 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -120,6 +120,8 @@ jobs: runs-on: ubuntu-latest container: ghcr.io/acts-project/ubuntu2404:83 needs: [linux_ubuntu] + env: + ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE: 1 steps: - uses: actions/checkout@v6 From 52ad62ce344b888bea5b29bba4aa49792b73a782 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 12 Mar 2026 10:53:17 +0100 Subject: [PATCH 16/24] disable failure in other jobs --- .github/workflows/builds.yml | 2 +- .gitlab-ci.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 7a8549de272..04d297e9dda 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -121,7 +121,7 @@ jobs: container: ghcr.io/acts-project/ubuntu2404:83 needs: [linux_ubuntu] env: - ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE: 1 + ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE: 0 steps: - uses: actions/checkout@v6 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dc092dbaee8..41f6d7abb72 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -395,6 +395,7 @@ linux_ubuntu_2404_clang19: variables: ACTS_LOG_FAILURE_THRESHOLD: WARNING + ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE: 0 INSTALL_DIR: ${CI_PROJECT_DIR}/install # force off even if it is CI mode ROOT_HASH_CHECKS: off From 8ea9f2badc3b1446d3ef5c66b6a0042f52730bc5 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 12 Mar 2026 11:34:41 +0100 Subject: [PATCH 17/24] some sonar follow-up --- .../Framework/src/Framework/Sequencer.cpp | 21 +-- .../ActsPlugins/FpeMonitoring/FpeMonitor.hpp | 14 +- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 126 ++++++------------ .../FpeMonitoring/src/FpeMonitorPlatform.hpp | 5 +- .../src/FpeMonitorPlatformDarwinArm64.cpp | 3 +- .../src/FpeMonitorPlatformDarwinX86_64.cpp | 3 +- .../src/FpeMonitorPlatformLinuxX86_64.cpp | 3 +- 7 files changed, 74 insertions(+), 101 deletions(-) diff --git a/Examples/Framework/src/Framework/Sequencer.cpp b/Examples/Framework/src/Framework/Sequencer.cpp index fc7cd503071..e7fc00a4dbb 100644 --- a/Examples/Framework/src/Framework/Sequencer.cpp +++ b/Examples/Framework/src/Framework/Sequencer.cpp @@ -543,9 +543,11 @@ int Sequencer::run() { if (mon) { auto& local = fpe->local(); - for (const auto& [count, type, st] : - mon->result().stackTraces()) { - auto [maskLoc, nMasked] = fpeMaskCount(*st, type); + for (const auto& info : mon->result().stackTraces()) { + const auto count = info.count; + const auto type = info.type; + const auto& st = *info.st; + auto [maskLoc, nMasked] = fpeMaskCount(st, type); if (nMasked < count) { std::stringstream ss; ss << "FPE of type " << type @@ -553,7 +555,7 @@ int Sequencer::run() { << nMasked << " (mask: " << maskLoc << ") (seen: " << count << " FPEs)\n" << ActsPlugins::FpeMonitor::stackTraceToString( - *st, m_cfg.fpeStackTraceLength); + st, m_cfg.fpeStackTraceLength); m_nUnmaskedFpe += (count - nMasked); @@ -563,7 +565,7 @@ int Sequencer::run() { // results after throwing throw FpeFailure{ss.str()}; } else if (m_cfg.failOnUnmaskedFpe && - !local.contains(type, *st)) { + !local.contains(info)) { ACTS_INFO(ss.str()); } } @@ -676,15 +678,18 @@ void Sequencer::fpeReport() const { remaining; for (const auto& el : sorted) { - const auto& [count, type, st] = el.get(); - auto [maskLoc, nMasked] = fpeMaskCount(*st, type); + const auto& info = el.get(); + const auto count = info.count; + const auto type = info.type; + const auto& st = *info.st; + auto [maskLoc, nMasked] = fpeMaskCount(st, type); ACTS_INFO("- " << type << ": (" << count << " times) " << (nMasked > 0 ? "[MASKED: " + std::to_string(nMasked) + " per event by " + maskLoc + "]" : "") << "\n" << ActsPlugins::FpeMonitor::stackTraceToString( - *st, m_cfg.fpeStackTraceLength)); + st, m_cfg.fpeStackTraceLength)); } } diff --git a/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp b/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp index bbba477109c..78cbaa13b31 100644 --- a/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp +++ b/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp @@ -109,13 +109,17 @@ class FpeMonitor { FpeType type; /// Stack trace where the exception occurred std::shared_ptr st; + /// Faulting instruction address if available from signal context + std::uintptr_t location; /// Constructor /// @param countIn Number of occurrences /// @param typeIn Exception type /// @param stIn Stack trace + /// @param locationIn Faulting instruction address if available FpeInfo(std::size_t countIn, FpeType typeIn, - std::shared_ptr stIn); + std::shared_ptr stIn, + std::uintptr_t locationIn = 0); ~FpeInfo(); }; @@ -146,11 +150,10 @@ class FpeMonitor { /// Remove duplicate stack traces void deduplicate(); - /// Check if result contains a specific exception type and stack trace - /// @param type Exception type - /// @param st Stack trace to check + /// Check if result contains an exception info entry under merge semantics + /// @param info Exception info to check /// @return True if contained - bool contains(FpeType type, const boost::stacktrace::stacktrace &st) const; + bool contains(const FpeInfo &info) const; /// Print summary of exceptions /// @param os Output stream @@ -175,7 +178,6 @@ class FpeMonitor { private: std::vector m_stackTraces; - std::vector m_locations; std::array m_counts{}; friend FpeMonitor; diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index 35b7b2d5a34..a14ca0827a4 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -8,8 +8,6 @@ #include "ActsPlugins/FpeMonitoring/FpeMonitor.hpp" -#include "Acts/Utilities/Helpers.hpp" - #include #include #include @@ -20,9 +18,6 @@ #include #include #include -#include -#include -#include #include #include @@ -36,13 +31,21 @@ namespace ActsPlugins { namespace { -bool areFpesEquivalent( - std::pair lhs, - std::pair rhs) { - const auto &fl = *lhs.second.begin(); - const auto &fr = *rhs.second.begin(); - return lhs.first == rhs.first && (boost::stacktrace::hash_value(fl) == - boost::stacktrace::hash_value(fr)); +bool canMergeFpeInfo(const FpeMonitor::Result::FpeInfo &existing, FpeType type, + std::uintptr_t location, + const boost::stacktrace::stacktrace &st) { + if (existing.type != type) { + return false; + } + + if (location != 0 && existing.location != 0) { + return location == existing.location; + } + + const auto &existingFrame = *existing.st->begin(); + const auto &candidateFrame = *st.begin(); + return boost::stacktrace::hash_value(existingFrame) == + boost::stacktrace::hash_value(candidateFrame); } } // namespace @@ -50,8 +53,12 @@ FpeMonitor::Result::FpeInfo::~FpeInfo() = default; FpeMonitor::Result::FpeInfo::FpeInfo( std::size_t countIn, FpeType typeIn, - std::shared_ptr stIn) - : count{countIn}, type{typeIn}, st{std::move(stIn)} {} + std::shared_ptr stIn, + std::uintptr_t locationIn) + : count{countIn}, + type{typeIn}, + st{std::move(stIn)}, + location{locationIn} {} FpeMonitor::Result FpeMonitor::Result::merged(const Result &with) const { Result result{}; @@ -60,14 +67,9 @@ FpeMonitor::Result FpeMonitor::Result::merged(const Result &with) const { result.m_counts[i] = m_counts[i] + with.m_counts[i]; } - std::copy(with.m_stackTraces.begin(), with.m_stackTraces.end(), - std::back_inserter(result.m_stackTraces)); - std::copy(with.m_locations.begin(), with.m_locations.end(), - std::back_inserter(result.m_locations)); - std::copy(m_stackTraces.begin(), m_stackTraces.end(), - std::back_inserter(result.m_stackTraces)); - std::copy(m_locations.begin(), m_locations.end(), - std::back_inserter(result.m_locations)); + std::ranges::copy(with.m_stackTraces, + std::back_inserter(result.m_stackTraces)); + std::ranges::copy(m_stackTraces, std::back_inserter(result.m_stackTraces)); result.deduplicate(); @@ -79,10 +81,7 @@ void FpeMonitor::Result::merge(const Result &with) { m_counts[i] = m_counts[i] + with.m_counts[i]; } - std::copy(with.m_stackTraces.begin(), with.m_stackTraces.end(), - std::back_inserter(m_stackTraces)); - std::copy(with.m_locations.begin(), with.m_locations.end(), - std::back_inserter(m_locations)); + std::ranges::copy(with.m_stackTraces, std::back_inserter(m_stackTraces)); deduplicate(); } @@ -94,32 +93,18 @@ void FpeMonitor::Result::add(FpeType type, void *stackPtr, for (std::size_t i = 0; i < m_stackTraces.size(); ++i) { auto &el = m_stackTraces[i]; - if (el.type != type) { - continue; - } - - if (location != 0 && m_locations[i] != 0) { - if (location == m_locations[i]) { - el.count += 1; - return; - } - continue; - } - - if (areFpesEquivalent({el.type, *el.st}, {type, *st})) { + if (canMergeFpeInfo(el, type, location, *st)) { el.count += 1; return; } } - m_stackTraces.push_back({1, type, std::move(st)}); - m_locations.push_back(location); + m_stackTraces.push_back({1, type, std::move(st), location}); } -bool FpeMonitor::Result::contains( - FpeType type, const boost::stacktrace::stacktrace &st) const { +bool FpeMonitor::Result::contains(const FpeInfo &info) const { return std::ranges::any_of(m_stackTraces, [&](const FpeInfo &el) { - return areFpesEquivalent({el.type, *el.st}, {type, st}); + return canMergeFpeInfo(el, info.type, info.location, *info.st); }); } @@ -169,53 +154,30 @@ void FpeMonitor::Result::summary(std::ostream &os, std::size_t depth) const { } os << "\nStack traces:\n"; - for (const auto &[count, type, st] : stackTraces()) { - os << "- " << type << ": (" << count << " times)\n"; + for (const auto &info : stackTraces()) { + os << "- " << info.type << ": (" << info.count << " times)\n"; - os << stackTraceToString(*st, depth); + os << stackTraceToString(*info.st, depth); } os << std::endl; } void FpeMonitor::Result::deduplicate() { std::vector copy = std::move(m_stackTraces); - std::vector copyLocations = std::move(m_locations); m_stackTraces.clear(); - m_locations.clear(); - - for (std::size_t i = 0; i < copy.size(); ++i) { - auto &info = copy[i]; - const std::uintptr_t location = copyLocations[i]; - - bool merged = false; - for (std::size_t j = 0; j < m_stackTraces.size(); ++j) { - auto &existing = m_stackTraces[j]; - if (existing.type != info.type) { - continue; - } - - if (location != 0 && m_locations[j] != 0) { - if (location == m_locations[j]) { - existing.count += info.count; - merged = true; - break; - } - continue; - } - - if (areFpesEquivalent({existing.type, *existing.st}, - {info.type, *info.st})) { - existing.count += info.count; - merged = true; - break; - } - } - - if (merged) { - continue; + m_stackTraces.reserve(copy.size()); + + for (auto &info : copy) { + const auto mergeTarget = std::ranges::find_if( + m_stackTraces, [&](const FpeInfo &existing) { + return canMergeFpeInfo(existing, info.type, info.location, *info.st); + }); + + if (mergeTarget != m_stackTraces.end()) { + mergeTarget->count += info.count; + } else { + m_stackTraces.push_back(std::move(info)); } - m_stackTraces.push_back({info.count, info.type, std::move(info.st)}); - m_locations.push_back(location); } } diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp index 80c3c401229..7f3d56868df 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatform.hpp @@ -10,8 +10,8 @@ #include "ActsPlugins/FpeMonitoring/FpeMonitor.hpp" -#include #include +#include #include namespace ActsPlugins::detail { @@ -42,7 +42,8 @@ inline std::optional fpeTypeFromSiCode(int siCode) { bool isRuntimeSupported(); -std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx); +std::optional decodeFpeType(int signal, const siginfo_t* si, + void* ctx); void clearPendingExceptions(int excepts); void enableExceptions(int excepts); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp index 0011c16aba9..5a9269061ac 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp @@ -83,7 +83,8 @@ bool isRuntimeSupported() { return true; } -std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { +std::optional decodeFpeType(int signal, const siginfo_t* si, + void* ctx) { // Prefer SIGFPE si_code mapping when available. if (signal == SIGFPE && si != nullptr) { return fpeTypeFromSiCode(si->si_code); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp index 851f46037fb..ee9a8bcefdd 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp @@ -26,7 +26,8 @@ bool isRuntimeSupported() { return true; } -std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { +std::optional decodeFpeType(int signal, const siginfo_t* si, + void* ctx) { // On this platform we only install a SIGFPE handler; si_code is enough to // classify all FPE kinds we track. static_cast(ctx); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp index ac71770a011..693f0fafb70 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp @@ -38,7 +38,8 @@ bool isRuntimeSupported() { return true; } -std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { +std::optional decodeFpeType(int signal, const siginfo_t* si, + void* ctx) { // This backend only installs SIGFPE handlers. si_code carries the exception // category we expose in FpeType. static_cast(ctx); From 15e1d5efcc2edcf1152a273259f2fd19c12cd614 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 12 Mar 2026 13:21:42 +0100 Subject: [PATCH 18/24] try to make the fpe test work in github --- Python/Examples/tests/test_fpe.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Python/Examples/tests/test_fpe.py b/Python/Examples/tests/test_fpe.py index b6bd534d776..4a386b92f6d 100644 --- a/Python/Examples/tests/test_fpe.py +++ b/Python/Examples/tests/test_fpe.py @@ -84,6 +84,13 @@ def disable_log_threshold(): acts.logging.setFailureThreshold(prev) +@pytest.fixture(autouse=True) +def force_fail_on_unmasked_fpe(monkeypatch): + # These tests assert raise/no-raise behavior based on unmasked FPE policy. + # Keep them deterministic even when CI sets a global override. + monkeypatch.setenv("ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE", "1") + + def test_notrackfpe(): s = acts.examples.Sequencer( events=3 * 100, From 64ce1d6d1e40098e29c0ba22bdd6a3af3851c106 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 12 Mar 2026 14:11:12 +0100 Subject: [PATCH 19/24] be smarter about when to skip --- Python/Examples/tests/test_fpe.py | 36 +++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/Python/Examples/tests/test_fpe.py b/Python/Examples/tests/test_fpe.py index 4a386b92f6d..15ba5ddbfbb 100644 --- a/Python/Examples/tests/test_fpe.py +++ b/Python/Examples/tests/test_fpe.py @@ -24,6 +24,29 @@ ) +def _parse_bool_env(name): + raw = os.getenv(name) + if raw is None: + return None + value = raw.strip().lower() + if value in ("1", "true", "yes", "on"): + return True + if value in ("0", "false", "no", "off"): + return False + return None + + +_fail_on_unmasked_env = _parse_bool_env("ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE") +_requires_fail_on_unmasked = pytest.mark.skipif( + _fail_on_unmasked_env is False, + reason="requires failOnUnmaskedFpe=true, but env override disables it", +) +_requires_no_fail_on_unmasked = pytest.mark.skipif( + _fail_on_unmasked_env is True, + reason="requires failOnUnmaskedFpe=false, but env override enables it", +) + + _names = { acts.examples.FpeType.FLTDIV: "DivByZero", acts.examples.FpeType.FLTOVF: "Overflow", @@ -84,13 +107,6 @@ def disable_log_threshold(): acts.logging.setFailureThreshold(prev) -@pytest.fixture(autouse=True) -def force_fail_on_unmasked_fpe(monkeypatch): - # These tests assert raise/no-raise behavior based on unmasked FPE policy. - # Keep them deterministic even when CI sets a global override. - monkeypatch.setenv("ACTS_SEQUENCER_FAIL_ON_UNMASKED_FPE", "1") - - def test_notrackfpe(): s = acts.examples.Sequencer( events=3 * 100, @@ -111,6 +127,7 @@ def fpe_type(request): yield request.param +@_requires_fail_on_unmasked def test_fpe_single_fail_at_end(fpe_type): s = acts.examples.Sequencer( events=10, @@ -134,6 +151,7 @@ def test_fpe_single_fail_at_end(fpe_type): assert res.count(x) == (s.config.events if x == fpe_type else 0) +@_requires_no_fail_on_unmasked def test_fpe_single_no_fail_at_end(fpe_type): s = acts.examples.Sequencer( events=10, @@ -156,6 +174,7 @@ def test_fpe_single_no_fail_at_end(fpe_type): assert res.count(x) == (s.config.events if x == fpe_type else 0) +@_requires_fail_on_unmasked def test_fpe_single_fail_immediately(fpe_type): s = acts.examples.Sequencer( events=10, @@ -199,6 +218,7 @@ def execute(self, context): s.run() +@_requires_fail_on_unmasked def test_fpe_rearm(fpe_type): trigger = getattr(acts.examples.FpeMonitor, f"_trigger_{_names[fpe_type].lower()}") @@ -232,6 +252,7 @@ def execute(self, context): sys.platform == "darwin", reason="Source-location FPE masking is not stable on macOS backtraces", ) +@_requires_fail_on_unmasked def test_fpe_masking_single(fpe_type): trigger = getattr(acts.examples.FpeMonitor, f"_trigger_{_names[fpe_type].lower()}") @@ -314,6 +335,7 @@ def test_fpe_context(fpe_type): print(fpe.result) +@_requires_fail_on_unmasked def test_buffer_sufficient(): s = acts.examples.Sequencer( events=10000, From 34ffd56025d83d289c4c781f9cb516c30a45d5fc Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 12 Mar 2026 15:05:43 +0100 Subject: [PATCH 20/24] lint --- Plugins/FpeMonitoring/CMakeLists.txt | 80 ++++++++++++------- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 9 +-- .../src/FpeMonitorPlatformDarwinArm64.cpp | 2 +- .../src/FpeMonitorPlatformDarwinX86_64.cpp | 2 +- .../src/FpeMonitorPlatformLinuxX86_64.cpp | 2 +- .../src/FpeMonitorPlatformUnsupported.cpp | 24 ++++-- Python/Examples/src/Framework.cpp | 20 ++--- 7 files changed, 86 insertions(+), 53 deletions(-) diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index a3a6fb2616d..40dbe58fe4c 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -47,7 +47,8 @@ else() set(_backtrace_setup_complete FALSE) set(_backtrace_src - "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/CMakeTmp/backtrace.cpp") + "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/CMakeTmp/backtrace.cpp" + ) file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/CMakeTmp") find_path( @@ -57,12 +58,17 @@ else() PATHS ${Boost_INCLUDE_DIRS} ) - file(WRITE "${_backtrace_src}" + file( + WRITE "${_backtrace_src}" "#include \n" "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) - message(CHECK_START "Does backtrace work without libbacktrace linker flag") + message( + CHECK_START + "Does backtrace work without libbacktrace linker flag" + ) try_compile( _backtrace_nolink "${CMAKE_CURRENT_BINARY_DIR}" @@ -87,18 +93,20 @@ else() message(STATUS "Could not find backtrace header file") else() message(CHECK_START "Does backtrace work with explicit include") - file(WRITE "${_backtrace_src}" + file( + WRITE "${_backtrace_src}" "#include \n" "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) try_compile( _backtrace_explicit "${CMAKE_CURRENT_BINARY_DIR}" "${_backtrace_src}" LINK_LIBRARIES ${CMAKE_DL_LIBS} - CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS - BOOST_STACKTRACE_USE_BACKTRACE + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE OUTPUT_VARIABLE __OUTPUT ) @@ -113,12 +121,16 @@ else() "BOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" ) - file(WRITE "${_backtrace_src}" + file( + WRITE "${_backtrace_src}" "#include \n" - "int main() {}\n") + "int main() {}\n" + ) - message(CHECK_START - "Does backtrace work with explicit include") + message( + CHECK_START + "Does backtrace work with explicit include" + ) try_compile( _backtrace_explicit_header "${CMAKE_CURRENT_BINARY_DIR}" @@ -135,8 +147,7 @@ else() if(_backtrace_explicit_header) message(CHECK_PASS "yes") list( - APPEND - _fpe_definitions + APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE ${_backtrace_include_def} ) @@ -144,12 +155,17 @@ else() else() message(CHECK_FAIL "no") - file(WRITE "${_backtrace_src}" + file( + WRITE "${_backtrace_src}" "#include \n" "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) - message(CHECK_START "Does backtrace work without linker flag") + message( + CHECK_START + "Does backtrace work without linker flag" + ) try_compile( _backtrace_nolink "${CMAKE_CURRENT_BINARY_DIR}" @@ -166,8 +182,7 @@ else() if(_backtrace_nolink) message(CHECK_PASS "yes") list( - APPEND - _fpe_definitions + APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE ${_backtrace_include_def} ) @@ -175,12 +190,17 @@ else() else() message(CHECK_FAIL "no") - file(WRITE "${_backtrace_src}" + file( + WRITE "${_backtrace_src}" "#include \n" "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n") + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) - message(CHECK_START "Does backtrace work with linker flag") + message( + CHECK_START + "Does backtrace work with linker flag" + ) try_compile( _backtrace_link "${CMAKE_CURRENT_BINARY_DIR}" @@ -197,8 +217,7 @@ else() if(_backtrace_link) message(CHECK_PASS "yes") list( - APPEND - _fpe_definitions + APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE ${_backtrace_include_def} ) @@ -218,12 +237,17 @@ else() endif() if(NOT _backtrace_setup_complete) - message(STATUS "libdl available but backtrace backend probe failed: use boost basic fallback") + message( + STATUS + "libdl available but backtrace backend probe failed: use boost basic fallback" + ) endif() endif() endif() - if(_fpe_definitions) - target_compile_definitions(ActsPluginFpeMonitoring PUBLIC ${_fpe_definitions}) + target_compile_definitions( + ActsPluginFpeMonitoring + PUBLIC ${_fpe_definitions} + ) endif() diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index a14ca0827a4..74b0bc9c4eb 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -55,10 +55,7 @@ FpeMonitor::Result::FpeInfo::FpeInfo( std::size_t countIn, FpeType typeIn, std::shared_ptr stIn, std::uintptr_t locationIn) - : count{countIn}, - type{typeIn}, - st{std::move(stIn)}, - location{locationIn} {} + : count{countIn}, type{typeIn}, st{std::move(stIn)}, location{locationIn} {} FpeMonitor::Result FpeMonitor::Result::merged(const Result &with) const { Result result{}; @@ -168,8 +165,8 @@ void FpeMonitor::Result::deduplicate() { m_stackTraces.reserve(copy.size()); for (auto &info : copy) { - const auto mergeTarget = std::ranges::find_if( - m_stackTraces, [&](const FpeInfo &existing) { + const auto mergeTarget = + std::ranges::find_if(m_stackTraces, [&](const FpeInfo &existing) { return canMergeFpeInfo(existing, info.type, info.location, *info.st); }); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp index 5a9269061ac..75e7510e52b 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinArm64.cpp @@ -164,7 +164,7 @@ bool shouldFailFastOnUnknownSignal() { void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { // Install both SIGFPE and SIGILL handlers to cover Darwin arm64 behavior. - struct sigaction action {}; + struct sigaction action{}; action.sa_sigaction = handler; action.sa_flags = SA_SIGINFO; sigaction(SIGFPE, &action, nullptr); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp index ee9a8bcefdd..0abf40fc93b 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformDarwinX86_64.cpp @@ -111,7 +111,7 @@ bool shouldFailFastOnUnknownSignal() { void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { // A single SIGFPE handler is sufficient on Darwin x86_64. - struct sigaction action {}; + struct sigaction action{}; action.sa_sigaction = handler; action.sa_flags = SA_SIGINFO; sigaction(SIGFPE, &action, nullptr); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp index 693f0fafb70..eb7538e9524 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformLinuxX86_64.cpp @@ -103,7 +103,7 @@ bool shouldFailFastOnUnknownSignal() { void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { // Linux only needs SIGFPE for floating-point trap monitoring. - struct sigaction action {}; + struct sigaction action{}; action.sa_sigaction = handler; action.sa_flags = SA_SIGINFO; sigaction(SIGFPE, &action, nullptr); diff --git a/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp b/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp index e2e96281723..e5d427c2f31 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitorPlatformUnsupported.cpp @@ -13,7 +13,9 @@ namespace ActsPlugins::detail { // This backend intentionally provides a complete no-op implementation so the // public FpeMonitor API remains available even when platform trapping support // is missing. -bool isRuntimeSupported() { return false; } +bool isRuntimeSupported() { + return false; +} std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { // No signal decoding support on unsupported platforms. @@ -24,11 +26,17 @@ std::optional decodeFpeType(int signal, siginfo_t* si, void* ctx) { } // Trap-control hooks are intentionally inert. -void clearPendingExceptions(int excepts) { static_cast(excepts); } +void clearPendingExceptions(int excepts) { + static_cast(excepts); +} -void enableExceptions(int excepts) { static_cast(excepts); } +void enableExceptions(int excepts) { + static_cast(excepts); +} -void disableExceptions(int excepts) { static_cast(excepts); } +void disableExceptions(int excepts) { + static_cast(excepts); +} void maskTrapsInSignalContext(void* ctx, FpeType type) { // No context mutation possible without platform-specific register layout. @@ -46,9 +54,13 @@ std::size_t captureStackFromSignalContext(void* ctx, void* buffer, } // Keep defaults aligned with safe_dump fallback behavior. -std::size_t safeDumpSkipFrames() { return 2; } +std::size_t safeDumpSkipFrames() { + return 2; +} -bool shouldFailFastOnUnknownSignal() { return false; } +bool shouldFailFastOnUnknownSignal() { + return false; +} void installSignalHandlers(void (*handler)(int, siginfo_t*, void*)) { // Signal handler installation is intentionally disabled. diff --git a/Python/Examples/src/Framework.cpp b/Python/Examples/src/Framework.cpp index 1af218a74e0..6756d71541f 100644 --- a/Python/Examples/src/Framework.cpp +++ b/Python/Examples/src/Framework.cpp @@ -344,16 +344,16 @@ void addFramework(py::module& mex) { std::optional mon; }; - auto fpe = py::class_(mex, "FpeMonitor") - .def_static("_trigger_divbyzero", &trigger_divbyzero) - .def_static("_trigger_overflow", &trigger_overflow) - .def_static("_trigger_invalid", &trigger_invalid) - .def_property_readonly_static( - "supported", - [](const py::object& /*self*/) { - return FpeMonitor::isSupported(); - }) - .def_static("context", []() { return FpeMonitorContext(); }); + auto fpe = + py::class_(mex, "FpeMonitor") + .def_static("_trigger_divbyzero", &trigger_divbyzero) + .def_static("_trigger_overflow", &trigger_overflow) + .def_static("_trigger_invalid", &trigger_invalid) + .def_property_readonly_static("supported", + [](const py::object& /*self*/) { + return FpeMonitor::isSupported(); + }) + .def_static("context", []() { return FpeMonitorContext(); }); fpe.def_property_readonly("result", py::overload_cast<>(&FpeMonitor::result), py::return_value_policy::reference_internal) From d7fb26ed03958d3ffadbec3a90c85d314c0efb86 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 12 Mar 2026 15:15:58 +0100 Subject: [PATCH 21/24] add one more fallback case --- Plugins/FpeMonitoring/CMakeLists.txt | 160 +++++++++++++++++---------- 1 file changed, 100 insertions(+), 60 deletions(-) diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index 40dbe58fe4c..7b585241c66 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -86,88 +86,80 @@ else() else() message(CHECK_FAIL "no") - file(GLOB hints "/usr/lib/gcc/*/*/include") - find_file(backtrace_header NAMES "backtrace.h" HINTS ${hints}) + message(CHECK_START "Does backtrace work with linker flag") + try_compile( + _backtrace_link_default + "${CMAKE_CURRENT_BINARY_DIR}" + "${_backtrace_src}" + LINK_LIBRARIES backtrace ${CMAKE_DL_LIBS} + CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE + OUTPUT_VARIABLE __OUTPUT + ) - if(backtrace_header STREQUAL "backtrace_header-NOTFOUND") - message(STATUS "Could not find backtrace header file") + if(_backtrace_link_default) + message(CHECK_PASS "yes") + list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE) + target_link_libraries(ActsPluginFpeMonitoring PUBLIC backtrace) + set(_backtrace_setup_complete TRUE) else() - message(CHECK_START "Does backtrace work with explicit include") - file( - WRITE "${_backtrace_src}" - "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" - ) - try_compile( - _backtrace_explicit - "${CMAKE_CURRENT_BINARY_DIR}" - "${_backtrace_src}" - LINK_LIBRARIES ${CMAKE_DL_LIBS} - CMAKE_FLAGS - "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE - OUTPUT_VARIABLE __OUTPUT - ) + message(CHECK_FAIL "no") + endif() - if(_backtrace_explicit) - message(CHECK_PASS "yes") - list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE) - set(_backtrace_setup_complete TRUE) - else() - message(CHECK_FAIL "no") + if(NOT _backtrace_setup_complete) + file(GLOB hints "/usr/lib/gcc/*/*/include") + find_file(backtrace_header NAMES "backtrace.h" HINTS ${hints}) - set(_backtrace_include_def - "BOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" + if(backtrace_header STREQUAL "backtrace_header-NOTFOUND") + message(STATUS "Could not find backtrace header file") + else() + message( + CHECK_START + "Does backtrace work with explicit include" ) - file( WRITE "${_backtrace_src}" "#include \n" - "int main() {}\n" - ) - - message( - CHECK_START - "Does backtrace work with explicit include" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" ) try_compile( - _backtrace_explicit_header + _backtrace_explicit "${CMAKE_CURRENT_BINARY_DIR}" "${_backtrace_src}" LINK_LIBRARIES ${CMAKE_DL_LIBS} CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS - BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} + COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE OUTPUT_VARIABLE __OUTPUT ) - if(_backtrace_explicit_header) + if(_backtrace_explicit) message(CHECK_PASS "yes") list( APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} ) set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") + set(_backtrace_include_def + "BOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" + ) + file( WRITE "${_backtrace_src}" "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + "int main() {}\n" ) message( CHECK_START - "Does backtrace work without linker flag" + "Does backtrace work with explicit include" ) try_compile( - _backtrace_nolink + _backtrace_explicit_header "${CMAKE_CURRENT_BINARY_DIR}" "${_backtrace_src}" LINK_LIBRARIES ${CMAKE_DL_LIBS} @@ -179,7 +171,7 @@ else() OUTPUT_VARIABLE __OUTPUT ) - if(_backtrace_nolink) + if(_backtrace_explicit_header) message(CHECK_PASS "yes") list( APPEND _fpe_definitions @@ -199,13 +191,13 @@ else() message( CHECK_START - "Does backtrace work with linker flag" + "Does backtrace work without linker flag" ) try_compile( - _backtrace_link + _backtrace_nolink "${CMAKE_CURRENT_BINARY_DIR}" "${_backtrace_src}" - LINK_LIBRARIES backtrace ${CMAKE_DL_LIBS} + LINK_LIBRARIES ${CMAKE_DL_LIBS} CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS @@ -214,21 +206,57 @@ else() OUTPUT_VARIABLE __OUTPUT ) - if(_backtrace_link) + if(_backtrace_nolink) message(CHECK_PASS "yes") list( APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE ${_backtrace_include_def} ) - target_link_libraries( - ActsPluginFpeMonitoring - PUBLIC backtrace - ) - set(_backtrace_setup_complete TRUE) else() message(CHECK_FAIL "no") + + file( + WRITE "${_backtrace_src}" + "#include \n" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) + + message( + CHECK_START + "Does backtrace work with linker flag" + ) + try_compile( + _backtrace_link + "${CMAKE_CURRENT_BINARY_DIR}" + "${_backtrace_src}" + LINK_LIBRARIES backtrace ${CMAKE_DL_LIBS} + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} + OUTPUT_VARIABLE __OUTPUT + ) + + if(_backtrace_link) + message(CHECK_PASS "yes") + list( + APPEND _fpe_definitions + BOOST_STACKTRACE_USE_BACKTRACE + ${_backtrace_include_def} + ) + target_link_libraries( + ActsPluginFpeMonitoring + PUBLIC backtrace + ) + + set(_backtrace_setup_complete TRUE) + else() + message(CHECK_FAIL "no") + endif() endif() endif() endif() @@ -237,10 +265,22 @@ else() endif() if(NOT _backtrace_setup_complete) - message( - STATUS - "libdl available but backtrace backend probe failed: use boost basic fallback" - ) + if(addr2line_EXECUTABLE) + message( + STATUS + "Backtrace backend probe failed; using boost::stacktrace addr2line backend" + ) + list( + APPEND _fpe_definitions + BOOST_STACKTRACE_USE_ADDR2LINE + "BOOST_STACKTRACE_ADDR2LINE_LOCATION=\"${addr2line_EXECUTABLE}\"" + ) + else() + message( + STATUS + "libdl available but backtrace backend probe failed: use boost basic fallback" + ) + endif() endif() endif() endif() From 924544f7c6b358f7c3b1adc0a109e4a592a606fd Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Thu, 12 Mar 2026 15:44:11 +0100 Subject: [PATCH 22/24] go back to the original backtrace finding logic --- Plugins/FpeMonitoring/CMakeLists.txt | 341 +++++++++++---------------- 1 file changed, 136 insertions(+), 205 deletions(-) diff --git a/Plugins/FpeMonitoring/CMakeLists.txt b/Plugins/FpeMonitoring/CMakeLists.txt index 7b585241c66..05eaaa81a03 100644 --- a/Plugins/FpeMonitoring/CMakeLists.txt +++ b/Plugins/FpeMonitoring/CMakeLists.txt @@ -31,263 +31,194 @@ target_link_libraries(ActsPluginFpeMonitoring PUBLIC Acts::Core) acts_compile_headers(PluginFpeMonitoring GLOB include/**/*.hpp) -# FPE compile definitions -set(_fpe_definitions "") +# Fpe flags +set(_fpe_options "") # In case we're going to run clang-tidy, let's unconditionally turn off backtrace if(ACTS_RUN_CLANG_TIDY) - list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_NOOP=1) + list(APPEND _fpe_options "-DBOOST_STACKTRACE_USE_NOOP=1") else() + include(CheckCXXSourceCompiles) + + find_library(dl_LIBRARY dl) find_package(Backtrace) find_program(addr2line_EXECUTABLE addr2line) if(APPLE) - list(APPEND _fpe_definitions _GNU_SOURCE) - elseif(CMAKE_DL_LIBS) - target_link_libraries(ActsPluginFpeMonitoring PUBLIC ${CMAKE_DL_LIBS}) - - set(_backtrace_setup_complete FALSE) - set(_backtrace_src - "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/CMakeTmp/backtrace.cpp" - ) - file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/CMakeTmp") - - find_path( - boost_stacktrace_include - NAMES "boost/stacktrace.hpp" - REQUIRED - PATHS ${Boost_INCLUDE_DIRS} - ) - - file( - WRITE "${_backtrace_src}" - "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" - ) - - message( - CHECK_START - "Does backtrace work without libbacktrace linker flag" - ) - try_compile( - _backtrace_nolink - "${CMAKE_CURRENT_BINARY_DIR}" - "${_backtrace_src}" - LINK_LIBRARIES ${CMAKE_DL_LIBS} - CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE - OUTPUT_VARIABLE __OUTPUT - ) - - if(_backtrace_nolink) - message(CHECK_PASS "yes") - list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE) - set(_backtrace_setup_complete TRUE) - else() - message(CHECK_FAIL "no") - - message(CHECK_START "Does backtrace work with linker flag") - try_compile( - _backtrace_link_default - "${CMAKE_CURRENT_BINARY_DIR}" - "${_backtrace_src}" - LINK_LIBRARIES backtrace ${CMAKE_DL_LIBS} - CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE - OUTPUT_VARIABLE __OUTPUT + list(APPEND _fpe_options -D_GNU_SOURCE) + else() + if(dl_LIBRARY) + target_link_libraries(ActsPluginFpeMonitoring PUBLIC ${dl_LIBRARY}) + + set(_backtrace_setup_complete FALSE) + + find_path( + boost_stacktrace_include + NAMES "boost/stacktrace.hpp" + REQUIRED + PATHS ${Boost_INCLUDE_DIRS} ) + if(Backtrace_FOUND) + # check if we need to link against bracktrace or not + set(backtrace_include "") + + file( + WRITE + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + " + #include + int main() {} + " + ) - if(_backtrace_link_default) - message(CHECK_PASS "yes") - list(APPEND _fpe_definitions BOOST_STACKTRACE_USE_BACKTRACE) - target_link_libraries(ActsPluginFpeMonitoring PUBLIC backtrace) - set(_backtrace_setup_complete TRUE) - else() - message(CHECK_FAIL "no") - endif() + message( + CHECK_START + "Does backtrace work with the default include" + ) - if(NOT _backtrace_setup_complete) - file(GLOB hints "/usr/lib/gcc/*/*/include") - find_file(backtrace_header NAMES "backtrace.h" HINTS ${hints}) + try_compile( + _backtrace_default_header + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + LINK_LIBRARIES ${dl_LIBRARY} + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS -DBOOST_STACKTRACE_USE_BACKTRACE + OUTPUT_VARIABLE __OUTPUT + ) - if(backtrace_header STREQUAL "backtrace_header-NOTFOUND") - message(STATUS "Could not find backtrace header file") + if(_backtrace_default_header) + message(CHECK_PASS "yes") else() - message( - CHECK_START - "Does backtrace work with explicit include" - ) - file( - WRITE "${_backtrace_src}" - "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" - ) - try_compile( - _backtrace_explicit - "${CMAKE_CURRENT_BINARY_DIR}" - "${_backtrace_src}" - LINK_LIBRARIES ${CMAKE_DL_LIBS} - CMAKE_FLAGS - "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS BOOST_STACKTRACE_USE_BACKTRACE - OUTPUT_VARIABLE __OUTPUT + message(CHECK_FAIL "no") + + file(GLOB hints "/usr/lib/gcc/*/*/include") + find_file( + backtrace_header + NAMES "backtrace.h" + HINTS ${hints} ) - if(_backtrace_explicit) - message(CHECK_PASS "yes") - list( - APPEND _fpe_definitions - BOOST_STACKTRACE_USE_BACKTRACE - ) - set(_backtrace_setup_complete TRUE) + if(${backtrace_header} STREQUAL "backtrace_header-NOTFOUND") + message(STATUS "Could not find backtrace header file") else() - message(CHECK_FAIL "no") - - set(_backtrace_include_def - "BOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" + set(backtrace_include + "-DBOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=\"${backtrace_header}\"" ) file( - WRITE "${_backtrace_src}" - "#include \n" - "int main() {}\n" + WRITE + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + " + #include + int main() {} + " ) message( CHECK_START "Does backtrace work with explicit include" ) + try_compile( _backtrace_explicit_header - "${CMAKE_CURRENT_BINARY_DIR}" - "${_backtrace_src}" - LINK_LIBRARIES ${CMAKE_DL_LIBS} + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + LINK_LIBRARIES ${dl_LIBRARY} CMAKE_FLAGS "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" COMPILE_DEFINITIONS - BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} + -DBOOST_STACKTRACE_USE_BACKTRACE + ${backtrace_include} OUTPUT_VARIABLE __OUTPUT ) if(_backtrace_explicit_header) message(CHECK_PASS "yes") - list( - APPEND _fpe_definitions - BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} - ) - set(_backtrace_setup_complete TRUE) + list(APPEND _fpe_options "${backtrace_include}") else() message(CHECK_FAIL "no") + endif() + endif() + endif() - file( - WRITE "${_backtrace_src}" - "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" - ) + file( + WRITE + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + "#include \n" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) - message( - CHECK_START - "Does backtrace work without linker flag" - ) - try_compile( - _backtrace_nolink - "${CMAKE_CURRENT_BINARY_DIR}" - "${_backtrace_src}" - LINK_LIBRARIES ${CMAKE_DL_LIBS} - CMAKE_FLAGS - "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS - BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} - OUTPUT_VARIABLE __OUTPUT - ) + message(CHECK_START "Does backtrace work without linker flag") + try_compile( + _backtrace_nolink + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + LINK_LIBRARIES ${dl_LIBRARY} + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS + -DBOOST_STACKTRACE_USE_BACKTRACE + ${backtrace_include} + OUTPUT_VARIABLE __OUTPUT + ) - if(_backtrace_nolink) - message(CHECK_PASS "yes") - list( - APPEND _fpe_definitions - BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} - ) - set(_backtrace_setup_complete TRUE) - else() - message(CHECK_FAIL "no") + if(_backtrace_nolink) + message(CHECK_PASS "yes") + set(_backtrace_setup_complete TRUE) + else() + message(CHECK_FAIL "no") - file( - WRITE "${_backtrace_src}" - "#include \n" - "#include \n" - "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" - ) + file( + WRITE + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + "#include \n" + "#include \n" + "int main() { std::cout << boost::stacktrace::stacktrace(); }\n" + ) - message( - CHECK_START - "Does backtrace work with linker flag" - ) - try_compile( - _backtrace_link - "${CMAKE_CURRENT_BINARY_DIR}" - "${_backtrace_src}" - LINK_LIBRARIES backtrace ${CMAKE_DL_LIBS} - CMAKE_FLAGS - "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" - COMPILE_DEFINITIONS - BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} - OUTPUT_VARIABLE __OUTPUT - ) + message(CHECK_START "Does backtrace work with linker flag") + try_compile( + _backtrace_link + "${CMAKE_BINARY_DIR}" + "${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/backtrace.cpp" + LINK_LIBRARIES backtrace ${dl_LIBRARY} + CMAKE_FLAGS + "-DINCLUDE_DIRECTORIES=${boost_stacktrace_include}" + COMPILE_DEFINITIONS + -DBOOST_STACKTRACE_USE_BACKTRACE + ${backtrace_include} + OUTPUT_VARIABLE __OUTPUT + ) - if(_backtrace_link) - message(CHECK_PASS "yes") - list( - APPEND _fpe_definitions - BOOST_STACKTRACE_USE_BACKTRACE - ${_backtrace_include_def} - ) - target_link_libraries( - ActsPluginFpeMonitoring - PUBLIC backtrace - ) + if(_backtrace_link) + message(CHECK_PASS "yes") + list( + APPEND _fpe_options + -DBOOST_STACKTRACE_USE_BACKTRACE + ) + target_link_libraries( + ActsPluginFpeMonitoring + PUBLIC backtrace + ) - set(_backtrace_setup_complete TRUE) - else() - message(CHECK_FAIL "no") - endif() - endif() - endif() + set(_backtrace_setup_complete TRUE) + else() + message(CHECK_FAIL "no") endif() endif() endif() endif() if(NOT _backtrace_setup_complete) - if(addr2line_EXECUTABLE) - message( - STATUS - "Backtrace backend probe failed; using boost::stacktrace addr2line backend" - ) - list( - APPEND _fpe_definitions - BOOST_STACKTRACE_USE_ADDR2LINE - "BOOST_STACKTRACE_ADDR2LINE_LOCATION=\"${addr2line_EXECUTABLE}\"" - ) + if(NOT ${dl_LIBRARY} STREQUAL "dl_LIBRARY-NOTFOUND") + message(STATUS "ld lib available: use boost basic fallback") else() - message( - STATUS - "libdl available but backtrace backend probe failed: use boost basic fallback" - ) + message(STATUS "Unable to set up stacktraces: use noop") + list(APPEND _fpe_options -DBOOST_STACKTRACE_USE_NOOP) endif() endif() endif() endif() -if(_fpe_definitions) - target_compile_definitions( - ActsPluginFpeMonitoring - PUBLIC ${_fpe_definitions} - ) -endif() +target_compile_options(ActsPluginFpeMonitoring PUBLIC "${_fpe_options}") From ff7cfbcb2388e608b80a74e505fc71d85b5ba647 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Fri, 13 Mar 2026 09:43:59 +0100 Subject: [PATCH 23/24] sonar fixes --- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index 74b0bc9c4eb..e23b947eed7 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -88,15 +88,14 @@ void FpeMonitor::Result::add(FpeType type, void *stackPtr, auto st = std::make_unique( boost::stacktrace::stacktrace::from_dump(stackPtr, bufferSize)); - for (std::size_t i = 0; i < m_stackTraces.size(); ++i) { - auto &el = m_stackTraces[i]; + for (auto &el : m_stackTraces) { if (canMergeFpeInfo(el, type, location, *st)) { el.count += 1; return; } } - m_stackTraces.push_back({1, type, std::move(st), location}); + m_stackTraces.emplace_back(1, type, std::move(st), location); } bool FpeMonitor::Result::contains(const FpeInfo &info) const { @@ -173,7 +172,7 @@ void FpeMonitor::Result::deduplicate() { if (mergeTarget != m_stackTraces.end()) { mergeTarget->count += info.count; } else { - m_stackTraces.push_back(std::move(info)); + m_stackTraces.push_back(info); } } } From 49a39f5cf49e709061a9e6162e787da897df8d76 Mon Sep 17 00:00:00 2001 From: Paul Gessinger Date: Sat, 14 Mar 2026 12:19:54 +0100 Subject: [PATCH 24/24] PR feedback --- .../include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp | 11 ++++++++--- Plugins/FpeMonitoring/src/FpeMonitor.cpp | 7 ++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp b/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp index 78cbaa13b31..dd447d65eca 100644 --- a/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp +++ b/Plugins/FpeMonitoring/include/ActsPlugins/FpeMonitoring/FpeMonitor.hpp @@ -241,9 +241,14 @@ class FpeMonitor { Buffer m_buffer{65536}; - boost::container::static_vector< - std::tuple, 128> - m_recorded; + struct Recorded { + FpeType type; + void *stackPtr; + std::size_t bufferSize; + std::uintptr_t location; + }; + + boost::container::static_vector m_recorded; }; /// @} diff --git a/Plugins/FpeMonitoring/src/FpeMonitor.cpp b/Plugins/FpeMonitoring/src/FpeMonitor.cpp index e23b947eed7..d695636406f 100644 --- a/Plugins/FpeMonitoring/src/FpeMonitor.cpp +++ b/Plugins/FpeMonitoring/src/FpeMonitor.cpp @@ -114,8 +114,9 @@ void FpeMonitor::consumeRecorded() { return; } - for (auto [type, stackPtr, remaining, location] : m_recorded) { - m_result.add(type, stackPtr, remaining, location); + for (const auto &recorded : m_recorded) { + m_result.add(recorded.type, recorded.stackPtr, recorded.bufferSize, + recorded.location); } m_buffer.reset(); @@ -163,7 +164,7 @@ void FpeMonitor::Result::deduplicate() { m_stackTraces.clear(); m_stackTraces.reserve(copy.size()); - for (auto &info : copy) { + for (const auto &info : copy) { const auto mergeTarget = std::ranges::find_if(m_stackTraces, [&](const FpeInfo &existing) { return canMergeFpeInfo(existing, info.type, info.location, *info.st);