From 824f0c0ea7c3e729f33886774abdaabca581dc4d Mon Sep 17 00:00:00 2001 From: Antony Polukhin Date: Thu, 29 Feb 2024 20:47:13 +0300 Subject: [PATCH] Clarify the async-signal-safety guarantees in docs (refs #131) (#154) --- doc/Jamfile.v2 | 7 ++-- doc/stacktrace.qbk | 36 ++++++++++++++++--- include/boost/stacktrace/safe_dump_to.hpp | 18 +++++----- include/boost/stacktrace/stacktrace.hpp | 44 +++++++++++------------ 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/doc/Jamfile.v2 b/doc/Jamfile.v2 index c526e55..c170f63 100644 --- a/doc/Jamfile.v2 +++ b/doc/Jamfile.v2 @@ -22,6 +22,9 @@ doxygen autodoc SEARCH_INCLUDES=YES SHORT_NAMES=NO INCLUDE_PATH=../../../ + "ALIASES= \\ + \"asyncsafe=\\xmlonly\\endxmlonly Theoretically async signal safe \\xmlonly\\endxmlonly\" \\ + " "PREDEFINED=\"stl_type_info=std::type_info\" \\ \"BOOST_EXPLICIT_OPERATOR_BOOL_NOEXCEPT()=explicit operator bool() const noexcept;\" \\ \"BOOST_CONSTEXPR_EXPLICIT_OPERATOR_BOOL()=explicit constexpr operator bool() const noexcept;\" \\ @@ -39,9 +42,9 @@ boostbook standalone : stacktrace : - boost.root=http://www.boost.org/doc/libs/1_63_0 + boost.root=http\://www.boost.org/doc/libs/1_84_0 # boost.root=../../../.. - pdf:boost.url.prefix=http://www.boost.org/doc/libs/release/doc/html + pdf:boost.url.prefix=http\://www.boost.org/doc/libs/release/doc/html ; ############################################################################### diff --git a/doc/stacktrace.qbk b/doc/stacktrace.qbk index 9fa601c..b1a3d50 100644 --- a/doc/stacktrace.qbk +++ b/doc/stacktrace.qbk @@ -120,11 +120,15 @@ Previous run crashed: 9# 0x0000000000402209 ``` -[warning There's a temptation to write a signal handler that prints the stacktrace on `SIGSEGV` or abort. Unfortunately, there's no cross platform way to do that without a risk of deadlocking. Not all the platforms provide means for even getting stacktrace in async signal safe way. +[warning There's a temptation to write a signal handler that prints the stacktrace on `SIGSEGV` or abort. Unfortunately, +there's no cross platform way to do that without a risk of deadlocking. +Not all the platforms provide means for even getting stacktrace in async signal safe way. Signal handler is often invoked on a separate stack and trash is returned on attempt to get a trace! Generic recommendation is to *avoid signal handlers! Use* platform specific ways to store and decode *core files*. + +See [link stacktrace.theoretical_async_signal_safety "Theoretical async signal safety"] for more info. ] @@ -311,9 +315,9 @@ By default Boost.Stacktrace is a header-only library, but you may change that an In header only mode library could be tuned by macro. If one of the link macro from above is defined, you have to manually link with one of the libraries: [table:libconfig Config [[Macro name or default] [Library] [Effect] [Platforms] [Uses debug information [footnote This will provide more readable backtraces with *source code locations* if the binary is built with debug information.]] [Uses dynamic exports information [footnote This will provide readable function names in backtrace for functions that are exported by the binary. Compiling with `-rdynamic` flag, without `-fvisibility=hidden` or marking functions as exported produce a better stacktraces.]] ] - [[['default for MSVC, Intel on Windows, MinGW-w64] / *BOOST_STACKTRACE_USE_WINDBG*] [*boost_stacktrace_windbg*] [ Uses `dbgeng.h` to show debug info. May require linking with *ole32* and *dbgeng*. ] [MSVC, MinGW-w64, Intel on Windows] [yes] [no]] + [[['default for MSVC, Intel on Windows, MinGW-w64] / *BOOST_STACKTRACE_USE_WINDBG*] [*boost_stacktrace_windbg*] [ Uses `dbgeng.h` to show debug info, stores the implementation internals in a static varaible protected with mutex. May require linking with *ole32* and *dbgeng*. ] [MSVC, MinGW-w64, Intel on Windows] [yes] [no]] [[['default for other platforms]] [*boost_stacktrace_basic*] [Uses compiler intrinsics to collect stacktrace and if possible `::dladdr` to show information about the symbol. Requires linking with *libdl* library on POSIX platforms.] [Any compiler on POSIX or MinGW] [no] [yes]] - [[*BOOST_STACKTRACE_USE_WINDBG_CACHED*] [*boost_stacktrace_windbg_cached*] [ Uses `dbgeng.h` to show debug info and caches internals in TLS for better performance. Useful only for cases when traces are gathered very often. May require linking with *ole32* and *dbgeng*. ] [MSVC, Intel on Windows] [yes] [no]] + [[*BOOST_STACKTRACE_USE_WINDBG_CACHED*] [*boost_stacktrace_windbg_cached*] [ Uses `dbgeng.h` to show debug info and caches implementation internals in TLS for better performance. Useful only for cases when traces are gathered very often. May require linking with *ole32* and *dbgeng*. ] [MSVC, Intel on Windows] [yes] [no]] [[*BOOST_STACKTRACE_USE_BACKTRACE*] [*boost_stacktrace_backtrace*] [Requires linking with *libdl* on POSIX and *libbacktrace* libraries[footnote Some *libbacktrace* packages SEGFAULT if there's a concurrent work with the same `backtrace_state` instance. To avoid that issue the Boost.Stacktrace library uses `thread_local` states, unfortunately this may consume a lot of memory if you often create and destroy execution threads in your application. Define *BOOST_STACKTRACE_BACKTRACE_FORCE_STATIC* to force single instance, but make sure that [@https://github.com/boostorg/stacktrace/blob/develop/test/thread_safety_checking.cpp thread_safety_checking.cpp] works well in your setup. ]. *libbacktrace* is probably already installed in your system[footnote If you are using Clang with libstdc++ you could get into troubles of including ``, because on some platforms Clang does not search for headers in the GCC's include paths and any attempt to add GCC's include path leads to linker errors. To explicitly specify a path to the `` header you could define the *BOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE* to a full path to the header. For example on Ubuntu Xenial use the command line option *-DBOOST_STACKTRACE_BACKTRACE_INCLUDE_FILE=* while building with Clang. ], or built into your compiler. Otherwise (if you are a *MinGW*/*MinGW-w64* user for example) it can be downloaded [@https://github.com/ianlancetaylor/libbacktrace from here] or [@https://github.com/gcc-mirror/gcc/tree/master/libbacktrace from here]. ] [Any compiler on POSIX, or MinGW, or MinGW-w64] [yes] [yes]] @@ -359,7 +363,31 @@ There are multiple ways to deal with that issue if you distribute PDB files alon [endsect] -[section Acknowledgements] +[section Theoretical async signal safety] + +In theory, walking the stack without decoding should be async signal safe. + +In practice, it is not: + +* Looks like a page fault while dumping the trace on a containerized/virtualized + Windows system has a chance to deadlock. Page fault could happen easily + as we have to write the dump either to memory or to a file. +* On POSIX systems a deadlock could happen if a signal is received when throwing + an exception [@https://github.com/boostorg/stacktrace/issues/131 #131]. + Theoretically this could be worked around by bypassing the mutex locking + in C++-runtime at exception throw + ([@https://github.com/userver-framework/userver/blob/4246909c99506d3ab34bd130a5154b4acc8e87de/core/src/engine/task/exception_hacks.cpp#L241-L244 sample implementation] + in the 🐙 userver framework). +* `-fomit-frame-pointer` like flags add additional complexity to the stack + walking implementation, which may also negatively affect the signal safety. + +As a rule of thumb: do *not* capture stack traces in signal handlers unless you +are absolutely sure in your environment and inspected all of its source codes. + + +[endsect] + +[section Acknowledgments] In order of helping and advising: diff --git a/include/boost/stacktrace/safe_dump_to.hpp b/include/boost/stacktrace/safe_dump_to.hpp index b630905..89f0430 100644 --- a/include/boost/stacktrace/safe_dump_to.hpp +++ b/include/boost/stacktrace/safe_dump_to.hpp @@ -25,8 +25,10 @@ # pragma warning(disable:2196) // warning #2196: routine is both "inline" and "noinline" #endif -/// @file safe_dump_to.hpp This header contains low-level async-signal-safe functions for dumping call stacks. Dumps are binary serialized arrays of `void*`, -/// so you could read them by using 'od -tx8 -An stacktrace_dump_failename' Linux command or using boost::stacktrace::stacktrace::from_dump functions. +/// @file safe_dump_to.hpp \asyncsafe low-level +/// functions for dumping call stacks. Dumps are binary serialized arrays of `void*`, +/// so you could read them by using 'od -tx8 -An stacktrace_dump_failename' +/// Linux command or using boost::stacktrace::stacktrace::from_dump functions. namespace boost { namespace stacktrace { @@ -84,7 +86,7 @@ struct this_thread_frames { // struct is required to avoid warning about usage o /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. /// /// @returns Stored call sequence depth including terminating zero frame. To get the actually consumed bytes multiply this value by the sizeof(boost::stacktrace::frame::native_frame_ptr_t) /// @@ -99,7 +101,7 @@ BOOST_FORCEINLINE std::size_t safe_dump_to(void* memory, std::size_t size) noexc /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. /// /// @returns Stored call sequence depth including terminating zero frame. To get the actually consumed bytes multiply this value by the sizeof(boost::stacktrace::frame::native_frame_ptr_t) /// @@ -117,7 +119,7 @@ BOOST_FORCEINLINE std::size_t safe_dump_to(std::size_t skip, void* memory, std:: /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. /// /// @returns Stored call sequence depth including terminating zero frame. /// @@ -130,7 +132,7 @@ BOOST_FORCEINLINE std::size_t safe_dump_to(const char* file) noexcept { /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. /// /// @returns Stored call sequence depth including terminating zero frame. /// @@ -149,7 +151,7 @@ BOOST_FORCEINLINE std::size_t safe_dump_to(std::size_t skip, std::size_t max_dep /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. /// /// @returns Stored call sequence depth including terminating zero frame. /// @@ -160,7 +162,7 @@ BOOST_FORCEINLINE std::size_t safe_dump_to(platform_specific_descriptor fd) noex /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. /// /// @returns Stored call sequence depth including terminating zero frame. /// diff --git a/include/boost/stacktrace/stacktrace.hpp b/include/boost/stacktrace/stacktrace.hpp index 48269fe..6de3131 100644 --- a/include/boost/stacktrace/stacktrace.hpp +++ b/include/boost/stacktrace/stacktrace.hpp @@ -135,7 +135,7 @@ class basic_stacktrace { /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// - /// @b Async-Handler-Safety: Safe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. BOOST_FORCEINLINE basic_stacktrace() noexcept : impl_() { @@ -146,7 +146,7 @@ class basic_stacktrace { /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// - /// @b Async-Handler-Safety: Safe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. /// /// @param a Allocator that would be passed to underlying storage. BOOST_FORCEINLINE explicit basic_stacktrace(const allocator_type& a) noexcept @@ -159,7 +159,7 @@ class basic_stacktrace { /// /// @b Complexity: O(N) where N is call sequence length, O(1) if BOOST_STACKTRACE_USE_NOOP is defined. /// - /// @b Async-Handler-Safety: Safe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. /// /// @param skip How many top calls to skip and do not store in *this. /// @@ -177,14 +177,14 @@ class basic_stacktrace { /// @b Complexity: O(st.size()) /// - /// @b Async-Handler-Safety: Safe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. basic_stacktrace(const basic_stacktrace& st) : impl_(st.impl_) {} /// @b Complexity: O(st.size()) /// - /// @b Async-Handler-Safety: Safe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator construction, copying, Allocator::allocate and Allocator::deallocate are async signal safe. basic_stacktrace& operator=(const basic_stacktrace& st) { impl_ = st.impl_; return *this; @@ -193,21 +193,21 @@ class basic_stacktrace { #ifdef BOOST_STACKTRACE_DOXYGEN_INVOKED /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe if Allocator::deallocate is async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator::deallocate is async signal safe. ~basic_stacktrace() noexcept = default; #endif #if !defined(BOOST_NO_CXX11_RVALUE_REFERENCES) /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe if Allocator construction and copying are async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator construction and copying are async signal safe. basic_stacktrace(basic_stacktrace&& st) noexcept : impl_(std::move(st.impl_)) {} /// @b Complexity: O(st.size()) /// - /// @b Async-Handler-Safety: Safe if Allocator construction and copying are async signal safe. + /// @b Async-Handler-Safety: \asyncsafe if Allocator construction and copying are async signal safe. basic_stacktrace& operator=(basic_stacktrace&& st) #ifndef BOOST_NO_CXX11_HDR_TYPE_TRAITS noexcept(( std::is_nothrow_move_assignable< std::vector >::value )) @@ -224,7 +224,7 @@ class basic_stacktrace { /// /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. size_type size() const noexcept { return impl_.size(); } @@ -236,43 +236,43 @@ class basic_stacktrace { /// /// @b Complexity: O(1). /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_reference operator[](std::size_t frame_no) const noexcept { return impl_[frame_no]; } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_iterator begin() const noexcept { return impl_.begin(); } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_iterator cbegin() const noexcept { return impl_.begin(); } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_iterator end() const noexcept { return impl_.end(); } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_iterator cend() const noexcept { return impl_.end(); } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_reverse_iterator rbegin() const noexcept { return impl_.rbegin(); } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_reverse_iterator crbegin() const noexcept { return impl_.rbegin(); } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_reverse_iterator rend() const noexcept { return impl_.rend(); } /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. const_reverse_iterator crend() const noexcept { return impl_.rend(); } @@ -281,7 +281,7 @@ class basic_stacktrace { /// /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. constexpr explicit operator bool () const noexcept { return !empty(); } /// @brief Allows to check that stack trace failed. @@ -289,7 +289,7 @@ class basic_stacktrace { /// /// @b Complexity: O(1) /// - /// @b Async-Handler-Safety: Safe. + /// @b Async-Handler-Safety: \asyncsafe. bool empty() const noexcept { return !size(); } const std::vector& as_vector() const noexcept { @@ -398,7 +398,7 @@ class basic_stacktrace { /// /// @b Complexity: Amortized O(1); worst case O(size()) /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. template bool operator< (const basic_stacktrace& lhs, const basic_stacktrace& rhs) noexcept { return lhs.size() < rhs.size() || (lhs.size() == rhs.size() && lhs.as_vector() < rhs.as_vector()); @@ -408,7 +408,7 @@ bool operator< (const basic_stacktrace& lhs, const basic_stacktrace< /// /// @b Complexity: Amortized O(1); worst case O(size()) /// -/// @b Async-Handler-Safety: Safe. +/// @b Async-Handler-Safety: \asyncsafe. template bool operator==(const basic_stacktrace& lhs, const basic_stacktrace& rhs) noexcept { return lhs.as_vector() == rhs.as_vector();