static linking problem leading to SIGTRAPs: solved, but mysterious

Inspired by @pachde’s VS Code debugging info, and prior work on the subject from @Richie and others, I’ve been actually setting up my IDE for Rack development rather than using the printf-and-pray approach I’ve gotten used to on little personal plugins.

Debugging Rack itself was working fine. When I started to bring in plugins, though (building against Rack, not the SDK), things went south. I was getting a bunch of SIGTRAPs down in ntdll!RtlSetProcessPreferredUILanguages as soon as the plugin’s first basic_string was getting deallocated (which happens in different places on load-from-JSON and creation in the module browser, but there’s a sample call stack below if anyone’s curious). Things seemed to be going south when the ISO/GCC bits/new_allocator.h was handing over to msvcrt!free, so it felt like a library incompatibility.

-exec handle SIGTRAP nostop “worked”, in the sense that I could continue, but it’s obviously not ideal.

This happened in exactly the same way with both Fundamental and my own plugins; however (this is the big clue!) it didn’t happen with the Core plugins, which get linked into Rack directly. I couldn’t identify any compilation option difference between Rack and the external plugins, but as a first step I removed LDFLAGS += -static-libstdc++ in the ARCH_WIN section of plugin.mk and recompiled my plugin, and it worked! No SIGTRAPs, gdb working as expected.

But I still feel a bit weird about this.

First, I don’t have an explanation for why static linking was causing whatever incompatibility was leading to the SIGTRAPs–I’m compiling Rack and the plugins at the same time and they seem to be getting the same options, and I would expect fewer such issues with static linking anyway. Obviously I can change the makefile manually since I’m recompiling Rack anyway but it’s an uncomfortable feeling!

Second, as I was looking for some explanation for this, I found that the exact bugfix (plus a corresponding change for ARCH_LIN) was considered for Rack v2.6.4 here but reverted just before it came out. @Vortico and team might not have been considering it for the same reasons, of course, and I presume there’s a good reason it was reverted–still, all very weird.

I’m going to get back to sequencer design now :slight_smile: but am happy to help if anyone wants to dig deeper into this. (Rack 2.6.4, gcc 15.2.0, gdb 16.3, updated VS Code on Win10, by the way.)

Sample call stack for the SIGTRAPs I was getting for static linking:

Thread 1 received signal SIGTRAP, Trace/breakpoint trap.
0x00007ff88cb9a8d7 in ntdll!RtlSetProcessPreferredUILanguages () from C:\WINDOWS\SYSTEM32\ntdll.dll

ntdll.dll!ntdll!RtlSetProcessPreferredUILanguages (Unknown Source:0)
ntdll.dll!ntdll!RtlValidateHeap (Unknown Source:0)
ntdll.dll!ntdll!RtlSizeHeap (Unknown Source:0)
ntdll.dll!ntdll!RtlAllocateHeap (Unknown Source:0)
ntdll.dll!ntdll!RtlFreeHeap (Unknown Source:0)
msvcrt.dll!msvcrt!free (Unknown Source:0)
plugin.dll!std::__new_allocator<char>::deallocate(std::__new_allocator<char> * const this, char * __p, std::__new_allocator<char>::size_type __n) ({msys2}\mingw64\include\c++\15.2.0\bits\new_allocator.h:156)
plugin.dll!std::allocator_traits<std::allocator<char> >::deallocate(std::allocator_traits<std::allocator<char> >::allocator_type & __a, std::allocator_traits<std::allocator<char> >::pointer __p, std::allocator_traits<std::allocator<char> >::size_type __n) ({msys2}\mingw64\include\c++\15.2.0\bits\alloc_traits.h:649)
plugin.dll!std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_destroy(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > * const this, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::size_type __size) ({msys2}\mingw64\include\c++\15.2.0\bits\basic_string.h:305)
plugin.dll!std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > * const this) ({msys2}\mingw64\include\c++\15.2.0\bits\basic_string.h:299)
plugin.dll!std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > * const this) ({msys2}\mingw64\include\c++\15.2.0\bits\basic_string.h:896)
plugin.dll!Functor::Functor(Functor * const this, Functor * const this@entry) ({devel}\Rack\plugins\Functor\src\Functor.cpp:345)
plugin.dll!rack::createModel<Functor, FunctorWidget>(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)::TModel::createModule()(TModel * const this) ({devel}\Rack\include\helpers.hpp:27)
libRack.dll!rack::engine::Engine::fromJson(rack::engine::Engine * const this, json_t * rootJ) ({devel}\Rack\src\engine\Engine.cpp:1302)
libRack.dll!rack::patch::Manager::fromJson(rack::patch::Manager * const this, rack::patch::Manager * const this@entry, json_t * rootJ, json_t * rootJ@entry) ({devel}\Rack\src\patch.cpp:552)
libRack.dll!rack::patch::Manager::loadAutosave(rack::patch::Manager * const this, rack::patch::Manager * const this@entry) ({devel}\Rack\src\patch.cpp:381)
libRack.dll!rack::patch::Manager::launch(rack::patch::Manager * const this, std::string pathArg) ({devel}\Rack\src\patch.cpp:79)
main(int argc, char ** argv) ({devel}\Rack\adapters\standalone.cpp:260)

OK, couldn’t resist. Dynamic linking (plugin_dynamic, which is the one that works for me) gives the plugin libstdc++-6.dll, same as Rack. plugin_static, using the provided makefile, gives it libwinpthread-1.dll instead. So that’s presumably the direct cause of the difference in SIGTRAP behavior. Whether it bespeaks a deeper problem (including a potential issue with the first-order “solution”) I don’t know.

Unfortunately I don’t have any insight into your issue, but I have some as to why that change was reverted:

Very interesting! Thanks. It looks like the original issue (strings being deleted with a different implementation) was exactly the string behavior that was giving me SIGTRAPs, per @Vortico’s first comment.

EDIT: [Ugh, MSYS2 version management is not particularly convenient. When I get time,] I’ll test the downgrade to MINGW_W64 10.0.0 and see if it works with the original (static) linking method. Am I right that the current build environment instructions should reflect the limitation that’s now defined in the toolchain? I think anyone following those instructions would end up with the most recently released version, no?

yeah, in general it’s “brittle” to pass ownership of memory between plugins and their host. That’s why MS COM and the like have a solution for that.

1 Like

On that note, neat preview of the C-only ABI planned for Rack 3 in the issue discussion @phantombeta linked…

you can make a stable ABI that is c++ based if you want to. That’s what I did for AAX plugins. That’s what COM is. Seems to work fine… But, yeah, straight C is even easier…

1 Like