I use Visual Studio 2022 version 17.9.2, and in an exception handler of my application I started logging call stacks using new C++23 feature to_string( std::stacktrace::current() )
. In general it works fine, but if an exception happens during DLL loding (WIN32 API call LoadLibraryW
), then my program simply hangs.
It can be reproduced even without exceptions, simply if my DLL has a global object calling to_string
in its constructor:
struct A {
A() {
(void)to_string( std::stacktrace::current() );
}
} a_;
For example, on Windows 11 the program hangs with the stack as follows:
ntdll.dll!NtWaitForSingleObject()
KernelBase.dll!WaitForSingleObjectEx()
winnsi.dll!NsiRpcRegisterChangeNotificationEx()
winnsi.dll!NsiRpcRegisterChangeNotification()
IPHLPAPI.DLL!InternalRegisterChangeNotification()
IPHLPAPI.DLL!NotifyIpInterfaceChange()
cryptnet.dll!I_CryptNetGetConnectivity()
crypt32.dll!ChainGetConnectivity(void)
crypt32.dll!CChainCallContext::IsConnected(void)
crypt32.dll!CCertChainEngine::TriggerPreFetch()
crypt32.dll!CCertChainEngine::GetChainContext()
crypt32.dll!CertGetCertificateChain()
wintrust.dll!_WalkChain()
wintrust.dll!WintrustCertificateTrust()
wintrust.dll!I_VerifyTrust()
wintrust.dll!WinVerifyTrust()
dbgeng.dll!DbgWinVerifyTrust(struct HWND__ *,struct _GUID *,void *)
dbgeng.dll!VerifyFileSignature(unsigned short const *)
dbgeng.dll!ExtensionSecurityValidate()
dbgeng.dll!ExtensionInfo::Load(class DebugClient *,class TargetInfo *,unsigned short const *,bool)
dbgeng.dll!TargetInfo::AddSpecificExtensions(class DebugClient *)
dbgeng.dll!NotifyDebuggeeActivation(void)
dbgeng.dll!LiveUserTargetInfo::WaitForEvent(unsigned long,unsigned long,unsigned long,unsigned long *)
dbgeng.dll!WaitForAnyTarget(unsigned long,unsigned long,unsigned long,unsigned long,unsigned long *)
dbgeng.dll!RawWaitForEvent(class DebugClient *,unsigned long,unsigned long)
dbgeng.dll!DebugClient::WaitForEvent()
MyDLL.dll!`anonymous namespace'::dbg_eng_data::try_initialize() Line 113 C++
MyDLL.dll!__std_stacktrace_to_string(const void * const * const _Addresses, const unsigned __int64 _Size, void * const _Str, unsigned __int64(*)(unsigned __int64, void *, void *, unsigned __int64(*)(char *, unsigned __int64, void *) noexcept) _Fill) Line 306 C++
MyDLL.dll!std::to_string(const std::basic_stacktrace<std::allocator<std::stacktrace_entry>> &) Line 343 C++
MyDLL.dll!A::{ctor}() Line 265 C++
MyDLL.dll!`dynamic initializer for 'a_''() Line 267 C++
ucrtbase.dll!_initterm()
MyDLL.dll!dllmain_crt_process_attach(HINSTANCE__ * const instance, void * const reserved) Line 66 C++
...
Is it a known limitation of std::stacktrace
on Windows? And how one can use it safely (e.g. detect the moments when it cannot be called)?
This is less of an issue of when it is safe to call std::stacktrace::current in particular.
Most things are not allowed during DllMain, which is where global constructors of DLLs are run from.
Microsoft outlines the best practices but it short you should do almost nothing in a global constructor.
In this case getting the stack trace looks to involve calling WinVerifyTrust indirectly which can invoke creating threads which can lead to deadlocks when the loader lock is held as described in more detail in the linked page. But again this isn’t “stacktrace::current” has special rules, it’s “global constructors in DLLs have special rules”.