Skip to content

Socket destruction: race-free cleanup in score's threaded asio model + macOS arm64 Debug shared SIGBUS RCA #883

Description

@jcelerier

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

  1. 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.
  2. 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

  • Reproduce locally on an Apple Silicon Mac (macos-15, Xcode 26) — both shared+Debug and static+Debug, to confirm the asymmetry
  • Decide between socket-ownership refactor (option 1) and per-socket synchronous cleanup (option 2). Apply consistently across tcp_socket.hpp, udp_socket.hpp, unix_socket.hpp, websocket sockets, and the framing wrappers
  • Once the socket-ownership fix lands, drop the macOS test-skip in .github/workflows/libossia.yml and confirm the affected suites stay green
  • Investigate the OSCTest 7-minute runtime on the same matrix cell (suggests an unrelated wait/timeout that's also worth pursuing)

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions