Background
PR #882 (CI rescue) uncovered two related issues that were band-aided just enough to land. Both deserve a proper fix.
Issue 1 — Race in async socket destruction (score / threaded io_context)
unix_datagram_socket::~unix_datagram_socket now does:
if(m_socket.is_open()) {
boost::system::error_code ec;
m_socket.cancel(ec);
m_socket.close(ec);
}
while(m_context.poll() > 0) { }
This is correct in libossia's standalone tests (the dtor and the io_context runner are the same thread). In score, however, DeviceDocumentPlugin runs the io_context on a dedicated m_asioThread while devices/protocols are torn down on the main thread. cancel() queues the operation_aborted completion and poll() races the asio thread for dispatch. If the asio thread wins, the recv_op's stored completion handler is destroyed on the asio thread after ~unix_datagram_socket may have returned — i.e. the exact destruction-race pattern that caused the original SIGBUS in CI is still reachable, just less likely.
Same pattern applies (or will apply, if we replicate the fix) to:
src/ossia/network/sockets/tcp_socket.hpp (tcp_client, tcp_server)
src/ossia/network/sockets/udp_socket.hpp (udp_send_socket, udp_receive_socket)
src/ossia/network/sockets/websocket*.hpp
- the
tcp_*_client / unix_stream_*_server framing wrappers in src/ossia/network/sockets/*_framing.hpp
Proper-fix options
- Shared ownership for the socket. Make the receive lambdas capture a
weak_ptr/shared_ptr to the socket. The recv_op then keeps the socket alive until all in-flight handlers run, eliminating the dangling-this window entirely. This is the principled fix but is a bigger refactor.
- Synchronous post + future. In the dtor,
boost::asio::dispatch(make_strand(m_context), [&]{ cancel; close; }) with a std::promise/future for the wait, so cleanup runs on the asio thread regardless of caller thread. Smaller change, but the dtor now blocks on the io_context — needs to be safe against the dtor being called from the asio thread itself.
Issue 2 — Root-cause for the macOS arm64 Debug shared SIGBUS
.github/workflows/libossia.yml currently skips OSC_Unix*, OSC_TCP*, OSC_WS*, and OSCQuery* on the macos-15 arm64 (0, Debug) matrix cell:
EXCL=""
if [ \"$BUILD_TYPE\" = \"Debug\" ] && [ \"$STATIC\" = \"0\" ]; then
EXCL=\"-E ossia_target_(OSC_Unix|OSC_TCP|OSC_WS|OSCQuery)\"
fi
The crash:
EXC_BAD_ACCESS / SIGBUS with EXC_ARM_DA_ALIGN at 0x0000000000000002
- Frame: libc++'s
std::__1::__function::__value_func<void ()>::~__value_func() (function.h:402)
- Called from
boost::asio::detail::reactive_socket_recv*_op_base::do_perform tearing down the wrapped completion handler held by websocketpp / asio
- Only on macos-15 arm64, shared libossia, Debug. Static+Debug and both Release variants pass. Linux x86 (0, Debug) passes too.
- Capping boost at 1.87 did not fix it (so not a 1.88+ asio regression).
- Inlining the destructor cancel/poll in
unix_datagram_socket did not fix it either.
Working hypothesis: arm64e Pointer Authentication failure on the vtable pointer stored inside libc++'s std::function SBO storage when the function type is instantiated across the libossia.dylib boundary. The poisoned PAC pointer dereferences to NULL-region addresses (matching the 0x2 fault). Needs local arm64 macOS access to verify with lldb + -fdebug-prefix-map + maybe -fno-ptrauth-* toggles to isolate.
Follow-up tasks
Cross-ref: PR #882, commit messages for sockets/unix: cancel + drain pending recv ops in dtor to prevent UAF and ci/libossia: skip OSC_{Unix,TCP,WS}/OSCQuery on macOS arm64 shared+Debug.
Background
PR #882 (CI rescue) uncovered two related issues that were band-aided just enough to land. Both deserve a proper fix.
Issue 1 — Race in async socket destruction (score / threaded io_context)
unix_datagram_socket::~unix_datagram_socketnow does:This is correct in libossia's standalone tests (the dtor and the io_context runner are the same thread). In score, however,
DeviceDocumentPluginruns the io_context on a dedicatedm_asioThreadwhile devices/protocols are torn down on the main thread.cancel()queues theoperation_abortedcompletion andpoll()races the asio thread for dispatch. If the asio thread wins, the recv_op's stored completion handler is destroyed on the asio thread after~unix_datagram_socketmay have returned — i.e. the exact destruction-race pattern that caused the original SIGBUS in CI is still reachable, just less likely.Same pattern applies (or will apply, if we replicate the fix) to:
src/ossia/network/sockets/tcp_socket.hpp(tcp_client,tcp_server)src/ossia/network/sockets/udp_socket.hpp(udp_send_socket,udp_receive_socket)src/ossia/network/sockets/websocket*.hpptcp_*_client/unix_stream_*_serverframing wrappers insrc/ossia/network/sockets/*_framing.hppProper-fix options
weak_ptr/shared_ptrto the socket. The recv_op then keeps the socket alive until all in-flight handlers run, eliminating the dangling-thiswindow entirely. This is the principled fix but is a bigger refactor.boost::asio::dispatch(make_strand(m_context), [&]{ cancel; close; })with astd::promise/futurefor the wait, so cleanup runs on the asio thread regardless of caller thread. Smaller change, but the dtor now blocks on the io_context — needs to be safe against the dtor being called from the asio thread itself.Issue 2 — Root-cause for the macOS arm64 Debug shared SIGBUS
.github/workflows/libossia.ymlcurrently skipsOSC_Unix*,OSC_TCP*,OSC_WS*, andOSCQuery*on the macos-15 arm64 (0, Debug) matrix cell:The crash:
EXC_BAD_ACCESS/SIGBUSwithEXC_ARM_DA_ALIGN at 0x0000000000000002std::__1::__function::__value_func<void ()>::~__value_func()(function.h:402)boost::asio::detail::reactive_socket_recv*_op_base::do_performtearing down the wrapped completion handler held bywebsocketpp/ asiounix_datagram_socketdid not fix it either.Working hypothesis: arm64e Pointer Authentication failure on the vtable pointer stored inside libc++'s
std::functionSBO storage when the function type is instantiated across the libossia.dylib boundary. The poisoned PAC pointer dereferences to NULL-region addresses (matching the0x2fault). Needs local arm64 macOS access to verify with lldb +-fdebug-prefix-map+ maybe-fno-ptrauth-*toggles to isolate.Follow-up tasks
tcp_socket.hpp,udp_socket.hpp,unix_socket.hpp, websocket sockets, and the framing wrappers.github/workflows/libossia.ymland confirm the affected suites stay greenCross-ref: PR #882, commit messages for
sockets/unix: cancel + drain pending recv ops in dtor to prevent UAFandci/libossia: skip OSC_{Unix,TCP,WS}/OSCQuery on macOS arm64 shared+Debug.