From 1b6e2ef62cec9608d160ea25d99080f0e2964ee5 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 16 May 2024 13:30:14 -0400 Subject: On Windows, defend test.cpp against structured exceptions too. Since August 2023, we've seen occasional GitHub Windows build test runs terminate with 0xC00000FD: stack overflow. We've usually responded by bumping up the default coroutine stack size. On closer examination, it's always llleap_test.cpp that blows up that way -- and llleap_test.cpp doesn't appear to use coroutines at all. So apparently we've been consuming more address space for ALL viewer coroutines without actually addressing the problem. Reset the default coroutine stack size to where it was before we started bumping it up in response to these llleap_test.cpp stack overflow failures. Note that LLCoros already catches and reports Windows structured exceptions, underscoring that the observed stack overflow is not from within a coroutine. While at it, restore the Windows llleap_test.cpp data volume to match Posix. We think the problem that led to reducing that data volume was an APR bug, which we hope has been fixed. Equip test.cpp, the test driver program for all our TUT unit and integration tests, with a Windows structured exception handler. Try to treat a Windows structured exception as a test failure -- instead of silently terminating with 0xC00000FD. Moreover, when a structured exception occurs, output a stack trace so we can try to track it down. --- indra/llcommon/llcoros.cpp | 4 ++-- indra/llcommon/tests/llleap_test.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'indra/llcommon') diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index aa8eca7d90..a70e3d9ae7 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -123,7 +123,7 @@ LLCoros::LLCoros(): // Previously we used // boost::context::guarded_stack_allocator::default_stacksize(); // empirically this is insufficient. - mStackSize(1024*1024), + mStackSize(512*1024), // mCurrent does NOT own the current CoroData instance -- it simply // points to it. So initialize it with a no-op deleter. mCurrent{ [](CoroData*){} } @@ -155,7 +155,7 @@ void LLCoros::cleanupSingleton() // don't use llcoro::suspend() because that module depends // on this one // This will yield current(main) thread and will let active - // corutines run once + // coroutines run once boost::this_fiber::yield(); } printActiveCoroutines("after pumping"); diff --git a/indra/llcommon/tests/llleap_test.cpp b/indra/llcommon/tests/llleap_test.cpp index fa48bcdefd..3fb25b4cef 100644 --- a/indra/llcommon/tests/llleap_test.cpp +++ b/indra/llcommon/tests/llleap_test.cpp @@ -35,7 +35,7 @@ // causes Windows abdominal pain such that it later fails code-signing in some // mysterious way. Entirely suppressing these LLLeap tests pushes the failure // rate MUCH lower. Can we re-enable them with a smaller data size on Windows? -const size_t BUFFERED_LENGTH = 100*1024; +const size_t BUFFERED_LENGTH = 1023*1024; #else // not Windows const size_t BUFFERED_LENGTH = 1023*1024; // try wrangling just under a megabyte of data -- cgit v1.2.3 From 71d777ea126e7f02cb46c11bdb606094ca06f75c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 24 May 2024 17:34:04 -0400 Subject: Promote seh_catcher() et al. to llexception.{h,cpp} for general use. --- indra/llcommon/llcoros.cpp | 51 +------------------------ indra/llcommon/llexception.cpp | 50 ++++++++++++++++++++---- indra/llcommon/llexception.h | 86 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 126 insertions(+), 61 deletions(-) (limited to 'indra/llcommon') diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index a70e3d9ae7..f3943b7138 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -286,55 +286,6 @@ std::string LLCoros::launch(const std::string& prefix, const callable_t& callabl return name; } -namespace -{ - -#if LL_WINDOWS - -static const U32 STATUS_MSC_EXCEPTION = 0xE06D7363; // compiler specific - -U32 exception_filter(U32 code, struct _EXCEPTION_POINTERS *exception_infop) -{ - if (code == STATUS_MSC_EXCEPTION) - { - // C++ exception, go on - return EXCEPTION_CONTINUE_SEARCH; - } - else - { - // handle it - return EXCEPTION_EXECUTE_HANDLER; - } -} - -void sehandle(const LLCoros::callable_t& callable) -{ - __try - { - callable(); - } - __except (exception_filter(GetExceptionCode(), GetExceptionInformation())) - { - // convert to C++ styled exception - // Note: it might be better to use _se_set_translator - // if you want exception to inherit full callstack - char integer_string[512]; - sprintf(integer_string, "SEH, code: %lu\n", GetExceptionCode()); - throw std::exception(integer_string); - } -} - -#else // ! LL_WINDOWS - -inline void sehandle(const LLCoros::callable_t& callable) -{ - callable(); -} - -#endif // ! LL_WINDOWS - -} // anonymous namespace - // Top-level wrapper around caller's coroutine callable. // Normally we like to pass strings and such by const reference -- but in this // case, we WANT to copy both the name and the callable to our local stack! @@ -348,7 +299,7 @@ void LLCoros::toplevel(std::string name, callable_t callable) // run the code the caller actually wants in the coroutine try { - sehandle(callable); + seh_catcher(callable); } catch (const Stop& exc) { diff --git a/indra/llcommon/llexception.cpp b/indra/llcommon/llexception.cpp index c0154a569f..74b33f1e3b 100644 --- a/indra/llcommon/llexception.cpp +++ b/indra/llcommon/llexception.cpp @@ -15,7 +15,12 @@ #include "llexception.h" // STL headers // std headers +#include +#include #include +#if LL_WINDOWS +#include +#endif // LL_WINDOWS // external library headers #include #include @@ -29,7 +34,6 @@ // On Windows, header-only implementation causes macro collisions -- use // prebuilt library #define BOOST_STACKTRACE_LINK -#include #endif // LL_WINDOWS #include @@ -94,15 +98,34 @@ void annotate_exception_(boost::exception& exc) // For windows SEH exception handling we sometimes need a filter that will // separate C++ exceptions from C SEH exceptions -static const U32 STATUS_MSC_EXCEPTION = 0xE06D7363; // compiler specific +static constexpr U32 STATUS_MSC_EXCEPTION = 0xE06D7363; // compiler specific +static constexpr U32 STATUS_STACK_FULL = 0xC00000FD; -U32 msc_exception_filter(U32 code, struct _EXCEPTION_POINTERS *exception_infop) +U32 ll_seh_filter( + std::string& stacktrace, + std::function filter, + U32 code, + struct _EXCEPTION_POINTERS* exception_infop) { - const auto stack = to_string(boost::stacktrace::stacktrace()); - LL_WARNS() << "SEH Exception handled (that probably shouldn't be): Code " << code - << "\n Stack trace: \n" - << stack << LL_ENDL; + // By the time the handler gets control, the stack has been unwound, + // so report the stack trace now at filter() time. + // Even though stack overflow is a problem we would very much like to + // diagnose, calling another function when the stack is already blown only + // terminates us faster. + if (code == STATUS_STACK_FULL) + { + stacktrace = "(stack overflow, no traceback)"; + } + else + { + stacktrace = boost::stacktrace::stacktrace().to_string(); + } + + return filter(code, exception_infop); +} +U32 seh_filter(U32 code, struct _EXCEPTION_POINTERS*) +{ if (code == STATUS_MSC_EXCEPTION) { // C++ exception, go on @@ -110,9 +133,20 @@ U32 msc_exception_filter(U32 code, struct _EXCEPTION_POINTERS *exception_infop) } else { - // handle it + // This is a non-C++ exception, e.g. hardware check. return EXCEPTION_EXECUTE_HANDLER; } } +void seh_rethrow(U32 code, const std::string& stacktrace) +{ + std::ostringstream out; + out << "Windows exception 0x" << std::hex << code; + if (! stacktrace.empty()) + { + out << '\n' << stacktrace; + } + LLTHROW(Windows_SEH_exception(out.str())); +} + #endif //LL_WINDOWS diff --git a/indra/llcommon/llexception.h b/indra/llcommon/llexception.h index 68e609444e..3e50678b44 100644 --- a/indra/llcommon/llexception.h +++ b/indra/llcommon/llexception.h @@ -102,14 +102,94 @@ void crash_on_unhandled_exception_(const char*, int, const char*, const std::str log_unhandled_exception_(__FILE__, __LINE__, BOOST_CURRENT_FUNCTION, CONTEXT) void log_unhandled_exception_(const char*, int, const char*, const std::string&); +/***************************************************************************** +* Structured Exception Handling +*****************************************************************************/ +// this is used in platform-generic code -- define outside #if LL_WINDOWS +struct Windows_SEH_exception: public std::runtime_error +{ + Windows_SEH_exception(const std::string& what): std::runtime_error(what) {} +}; + +#if LL_WINDOWS //------------------------------------------------------------- + +#include + +// triadic variant specifies try(), filter(U32, struct _EXCEPTION_POINTERS*), +// handler(U32, const std::string& stacktrace) +// stacktrace may or may not be available +template +auto seh_catcher(TRYCODE&& trycode, FILTER&& filter, HANDLER&& handler) +{ + // don't try to construct a std::function at the moment of Structured Exception + std::function + filter_function(std::forward(filter)); + std::string stacktrace; + __try + { + return std::forward(trycode)(); + } + __except (ll_seh_filter( + stacktrace, + filter_function, + GetExceptionCode(), + GetExceptionInformation())) + { + return std::forward(handler)(GetExceptionCode(), stacktrace); + } +} + +// dyadic variant specifies try(), handler(U32, stacktrace), assumes default filter +template +auto seh_catcher(TRYCODE&& trycode, HANDLER&& handler) +{ + return seh_catcher( + std::forward(trycode), + seh_filter, + std::forward(handler)); +} -#if LL_WINDOWS +// monadic variant specifies try(), assumes default filter and handler +template +auto seh_catcher(TRYCODE&& trycode) +{ + return seh_catcher( + std::forward(trycode), + seh_filter, + seh_rethrow); +} // SEH exception filtering for use in __try __except // Separates C++ exceptions from C SEH exceptions // Todo: might be good idea to do some kind of seh_to_msc_wrapper(function, ARGS&&); -U32 msc_exception_filter(U32 code, struct _EXCEPTION_POINTERS *exception_infop); +U32 ll_seh_filter( + std::string& stacktrace, + std::function filter, + U32 code, + struct _EXCEPTION_POINTERS* exception_infop); +U32 seh_filter(U32 code, struct _EXCEPTION_POINTERS* exception_infop); +void seh_rethrow(U32 code, const std::string& stacktrace); + +#else // not LL_WINDOWS ----------------------------------------------------- + +template +auto seh_catcher(TRYCODE&& trycode, FILTER&&, HANDLER&&) +{ + return std::forward(trycode)(); +} + +template +auto seh_catcher(TRYCODE&& trycode, HANDLER&&) +{ + return std::forward(trycode)(); +} + +template +auto seh_catcher(TRYCODE&& trycode) +{ + return std::forward(trycode)(); +} -#endif //LL_WINDOWS +#endif // not LL_WINDOWS ----------------------------------------------------- #endif /* ! defined(LL_LLEXCEPTION_H) */ -- cgit v1.2.3 From 5ed8df22cd59680a685c4ada7daa5555bf59d4fe Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 28 May 2024 13:22:05 -0400 Subject: Fix up llexception.h's cross-platform SEH wrapper. Introduce AlwaysReturn specialization, which always discards any result of calling the specified callable with specified args. Derive new Windows_SEH_exception from LLException, not std::runtime_error. Put the various SEH functions in LL::seh nested namespace, e.g. LL::seh::catcher() as the primary API. Break out more levels of Windows SEH handler to work around the restrictions on functions containing __try/__except. The triadic catcher() overload now does little save declare a std::string stacktrace before forwarding the call to catcher_inner(), passing a reference to stacktrace along with the trycode, filter and handler functions. catcher_inner() accepts the stacktrace and the three function template arguments. It contains the __try/__except logic. It calls a new filter_() wrapper template, which calls fill_stacktrace() before forwarding the call to the caller's filter function. fill_stacktrace(), in the .cpp file, contains the logic to populate the stacktrace string -- unless the Structured Exception is stack overflow, in which case it puts an explanatory string instead. catcher_inner()'s __except clause passes not only the code, but also the stacktrace string, to the caller's handler function. It wraps the caller's handler function in always_return(), where rtype is the type returned by the trycode function. This allows a handler to return a value, while also supporting the void handler case, e.g. one that throws a C++ exception. (This is why we need AlwaysReturn: some trycode() functions are themselves void.) For the dyadic catcher() overload, introduce common_filter() containing the logic to distinguish a C++ exception from any other kind of Structured Exception. The fact that the stacktrace is captured before the filter function is called should permit capturing a stacktrace for a C++ exception as well as for most other Structured Exceptions. As before, the monadic catcher() overload supplies the rethrow() handler, in the .cpp file. Change existing calls from seh_catcher() to LL::seh::catcher(). --- indra/llcommon/always_return.h | 16 +++++++ indra/llcommon/llcoros.cpp | 2 +- indra/llcommon/llexception.cpp | 24 ++++------ indra/llcommon/llexception.h | 104 +++++++++++++++++++++++++---------------- 4 files changed, 88 insertions(+), 58 deletions(-) (limited to 'indra/llcommon') diff --git a/indra/llcommon/always_return.h b/indra/llcommon/always_return.h index a206471da5..b99eb49096 100644 --- a/indra/llcommon/always_return.h +++ b/indra/llcommon/always_return.h @@ -79,6 +79,22 @@ namespace LL DESIRED mDefault; }; + // specialize for AlwaysReturn + template <> + struct AlwaysReturn + { + public: + AlwaysReturn() {} + + // callable returns a type not convertible to DESIRED, return default + template + void operator()(CALLABLE&& callable, ARGS&&... args) + { + // discard whatever callable(args) returns + std::forward(callable)(std::forward(args)...); + } + }; + /** * always_return(some_function, some_args...) calls * some_function(some_args...). It is guaranteed to return a value of type diff --git a/indra/llcommon/llcoros.cpp b/indra/llcommon/llcoros.cpp index f3943b7138..7512bac88d 100644 --- a/indra/llcommon/llcoros.cpp +++ b/indra/llcommon/llcoros.cpp @@ -299,7 +299,7 @@ void LLCoros::toplevel(std::string name, callable_t callable) // run the code the caller actually wants in the coroutine try { - seh_catcher(callable); + LL::seh::catcher(callable); } catch (const Stop& exc) { diff --git a/indra/llcommon/llexception.cpp b/indra/llcommon/llexception.cpp index 74b33f1e3b..107fdc2b2d 100644 --- a/indra/llcommon/llexception.cpp +++ b/indra/llcommon/llexception.cpp @@ -101,44 +101,36 @@ void annotate_exception_(boost::exception& exc) static constexpr U32 STATUS_MSC_EXCEPTION = 0xE06D7363; // compiler specific static constexpr U32 STATUS_STACK_FULL = 0xC00000FD; -U32 ll_seh_filter( - std::string& stacktrace, - std::function filter, - U32 code, - struct _EXCEPTION_POINTERS* exception_infop) +void LL::seh::fill_stacktrace(std::string& stacktrace, U32 code) { - // By the time the handler gets control, the stack has been unwound, - // so report the stack trace now at filter() time. - // Even though stack overflow is a problem we would very much like to - // diagnose, calling another function when the stack is already blown only - // terminates us faster. + // Sadly, despite its diagnostic importance, trying to capture a + // stacktrace when the stack is already blown only terminates us faster. if (code == STATUS_STACK_FULL) { stacktrace = "(stack overflow, no traceback)"; } else { - stacktrace = boost::stacktrace::stacktrace().to_string(); + stacktrace = to_string(boost::stacktrace::stacktrace()); } - - return filter(code, exception_infop); } -U32 seh_filter(U32 code, struct _EXCEPTION_POINTERS*) +U32 LL::seh::common_filter(U32 code, struct _EXCEPTION_POINTERS*) { if (code == STATUS_MSC_EXCEPTION) { - // C++ exception, go on + // C++ exception, don't stop at this handler return EXCEPTION_CONTINUE_SEARCH; } else { // This is a non-C++ exception, e.g. hardware check. + // Pass control into the handler block. return EXCEPTION_EXECUTE_HANDLER; } } -void seh_rethrow(U32 code, const std::string& stacktrace) +void LL::seh::rethrow(U32 code, const std::string& stacktrace) { std::ostringstream out; out << "Windows exception 0x" << std::hex << code; diff --git a/indra/llcommon/llexception.h b/indra/llcommon/llexception.h index 3e50678b44..f58a553eb3 100644 --- a/indra/llcommon/llexception.h +++ b/indra/llcommon/llexception.h @@ -12,6 +12,7 @@ #if ! defined(LL_LLEXCEPTION_H) #define LL_LLEXCEPTION_H +#include "always_return.h" #include #include #include @@ -106,90 +107,111 @@ void log_unhandled_exception_(const char*, int, const char*, const std::string&) * Structured Exception Handling *****************************************************************************/ // this is used in platform-generic code -- define outside #if LL_WINDOWS -struct Windows_SEH_exception: public std::runtime_error +struct Windows_SEH_exception: public LLException { - Windows_SEH_exception(const std::string& what): std::runtime_error(what) {} + Windows_SEH_exception(const std::string& what): LLException(what) {} }; +namespace LL +{ +namespace seh +{ + #if LL_WINDOWS //------------------------------------------------------------- -#include +void fill_stacktrace(std::string& stacktrace, U32 code); + +// wrapper around caller's U32 filter(U32 code, struct _EXCEPTION_POINTERS*) +// filter function: capture a stacktrace, if possible, before forwarding the +// call to the caller's filter() function +template +U32 filter_(std::string& stacktrace, FILTER&& filter, + U32 code, struct _EXCEPTION_POINTERS* exptrs) +{ + // By the time the handler gets control, the stack has been unwound, + // so report the stack trace now at filter() time. + fill_stacktrace(stacktrace, code); + return std::forward(filter)(code, exptrs); +} -// triadic variant specifies try(), filter(U32, struct _EXCEPTION_POINTERS*), -// handler(U32, const std::string& stacktrace) -// stacktrace may or may not be available template -auto seh_catcher(TRYCODE&& trycode, FILTER&& filter, HANDLER&& handler) +auto catcher_inner(std::string& stacktrace, + TRYCODE&& trycode, FILTER&& filter, HANDLER&& handler) { - // don't try to construct a std::function at the moment of Structured Exception - std::function - filter_function(std::forward(filter)); - std::string stacktrace; __try { return std::forward(trycode)(); } - __except (ll_seh_filter( - stacktrace, - filter_function, - GetExceptionCode(), - GetExceptionInformation())) + __except (filter_(stacktrace, + std::forward(filter), + GetExceptionCode(), GetExceptionInformation())) { - return std::forward(handler)(GetExceptionCode(), stacktrace); + return always_return( + std::forward(handler), GetExceptionCode(), stacktrace); } } -// dyadic variant specifies try(), handler(U32, stacktrace), assumes default filter +// triadic variant specifies try(), filter(U32, struct _EXCEPTION_POINTERS*), +// handler(U32, const std::string& stacktrace) +// stacktrace may or may not be available +template +auto catcher(TRYCODE&& trycode, FILTER&& filter, HANDLER&& handler) +{ + // Construct and destroy this stacktrace string in the outer function + // because we can't do either in the function with __try/__except. + std::string stacktrace; + return catcher_inner(stacktrace, + std::forward(trycode), + std::forward(filter), + std::forward(handler)); +} + +// common_filter() handles the typical case in which we want our handler +// clause to handle only Structured Exceptions rather than explicitly-thrown +// C++ exceptions +U32 common_filter(U32 code, struct _EXCEPTION_POINTERS*); + +// dyadic variant specifies try(), handler(U32, stacktrace), assumes common_filter() template -auto seh_catcher(TRYCODE&& trycode, HANDLER&& handler) +auto catcher(TRYCODE&& trycode, HANDLER&& handler) { - return seh_catcher( - std::forward(trycode), - seh_filter, - std::forward(handler)); + return catcher(std::forward(trycode), + common_filter, + std::forward(handler)); } // monadic variant specifies try(), assumes default filter and handler template -auto seh_catcher(TRYCODE&& trycode) +auto catcher(TRYCODE&& trycode) { - return seh_catcher( - std::forward(trycode), - seh_filter, - seh_rethrow); + return catcher(std::forward(trycode), rethrow); } -// SEH exception filtering for use in __try __except -// Separates C++ exceptions from C SEH exceptions -// Todo: might be good idea to do some kind of seh_to_msc_wrapper(function, ARGS&&); -U32 ll_seh_filter( - std::string& stacktrace, - std::function filter, - U32 code, - struct _EXCEPTION_POINTERS* exception_infop); -U32 seh_filter(U32 code, struct _EXCEPTION_POINTERS* exception_infop); -void seh_rethrow(U32 code, const std::string& stacktrace); +[[noreturn]] void rethrow(U32 code, const std::string& stacktrace); #else // not LL_WINDOWS ----------------------------------------------------- template -auto seh_catcher(TRYCODE&& trycode, FILTER&&, HANDLER&&) +auto catcher(TRYCODE&& trycode, FILTER&&, HANDLER&&) { return std::forward(trycode)(); } template -auto seh_catcher(TRYCODE&& trycode, HANDLER&&) +auto catcher(TRYCODE&& trycode, HANDLER&&) { return std::forward(trycode)(); } template -auto seh_catcher(TRYCODE&& trycode) +auto catcher(TRYCODE&& trycode) { return std::forward(trycode)(); } #endif // not LL_WINDOWS ----------------------------------------------------- +} // namespace LL::seh +} // namespace LL + #endif /* ! defined(LL_LLEXCEPTION_H) */ -- cgit v1.2.3