Skip to content

inspector: abort (no message) when the main thread posts to an exiting Worker's connectToMainThread() session #67079

Open

Description

@nick-inkeep

Version

v24.21.0, v24.22.1. src/inspector/main_thread_interface.cc is unchanged through v26.12.1 and main (ec7a28f).

Platform

Linux x64 (Ubuntu 24.04), official nodejs.org binaries

Subsystem

inspector, worker

What steps will reproduce the bug?

A Worker opens new inspector.Session().connectToMainThread() and exits while the main thread still has something to post back to that session: a protocol reply or event, or the delete it sends when the main-thread side of the session is destroyed after session.disconnect().

test/parallel/test-inspector-connect-to-main-thread.js is the minimal form: connect, disconnect, exit.

How often does it reproduce? Is there a required condition?

Rarely. It needs a narrow native interleaving, and there is no deterministic JS reproduction. Seen twice in CI, in a suite that repeatedly connects, pauses and disconnects Worker sessions to the main thread. Node's CI has seen the test above crash with exit code -6 and no output (#54804, closed as a duplicate of #40611). With no native stack there, I can't confirm it is this bug.

Required condition: the main thread posts to the Worker's handle, while the Worker's request queue is empty, after the Worker's MainThreadInterface use count reaches zero and before ~MainThreadInterface resets the handle.

What is the expected behavior? Why is that the expected behavior?

No crash. Posting to a thread that is going away is meant to be tolerated: MainThreadHandle::Post returns false once the handle is reset (main_thread_interface.cc#L317-L323), and ~AnotherThreadObjectReference posts unconditionally, noting "Disappearing thread may cause a memory leak" (#L101-L104). The test above says "Do not crash".

What do you see instead?

SIGABRT, with no Node error message. Main thread, v24.21.0 (gdb on the core):

#2  abort ()
#3  node::inspector::MainThreadInterface::Post(...) [clone .cold]
#4  node::inspector::(anonymous namespace)::ThreadSafeDelegate::SendMessageToFrontend(...)
#5  node::inspector::(anonymous namespace)::ChannelImpl::sendResponse(...)
...
#8  v8_inspector::protocol::Debugger::DomainDispatcherImpl::pause(...)
...
#14 node::inspector::MainThreadInterface::DispatchMessages() [clone .part.0]

The Worker's thread at the same moment:

#2  uv_mutex_lock
#3  node::inspector::MainThreadInterface::~MainThreadInterface()
#4  std::_Sp_counted_base<...>::_M_release_last_use_cold()
#5  node::inspector::NodeInspectorClient::~NodeInspectorClient()
...
#7  node::Environment::~Environment()
#9  node::FreeEnvironment(node::Environment*)
#10 node::worker::Worker::Run()

On v24.22.1 the Worker's thread is the same, and the main thread's post comes from the session's deletion instead: MainThreadInterface::RemoveObject → ~SameThreadInspectorSession → ~ChannelImpl → ~ThreadSafeDelegate → MainThreadInterface::Post [clone .cold] → abort.

Additional information

Cause, at ec7a28f:

  • MainThreadHandle::Post holds block_lock_ and forwards only while main_thread_ is set (L317-L323). main_thread_ is cleared only by ~MainThreadInterface (L202-L205), which runs after the interface's only long-lived owner, NodeInspectorClient::interface_ (inspector_agent.cc#L840), has dropped the use count to zero.
  • In that window, MainThreadInterface::Post calls shared_from_this() when its queue is empty (L213). Node builds with -fno-exceptions (common.gypi#L605), so the expired shared_from_this() aborts instead of throwing std::bad_weak_ptr. In the official v24.21.0 linux-x64 binary, Post's cold block (0x7812c0: call abort@plt) is reached only from that call's two bad_weak_ptr branches, and the captured return address is 0x7812c5. The Worker above is waiting in Reset() for block_lock_, which the main thread holds.
  • disconnect() returns once the delete is queued on the main thread (the delegate is "destroyed asynchronously", inspector: fix session.disconnect crash #46942), and nothing tells the Worker when the main thread is done with the session. A Worker that exits right after disconnect(), as the test does, can hit this.

Every cross-thread post to a Worker's interface goes through these two functions, so a fix there covers the reply, event and delete paths. Fix idea: reset the handle while the interface is still owned, for example in ~NodeInspectorClient before interface_ is released, so a racing post takes the existing return false path. Using weak_from_this() at L213 would also remove the abort.

Disclosure: this report was drafted and posted by an AI coding agent (Claude Code) at the direction of @nick-inkeep, who decided to file it. Before posting, a second, independent agent re-checked every linked source line at ec7a28f, the duplicate searches, both stacks (symbol-mapped against the official v24.21.0 and v24.22.1 linux-x64 binaries) and the disassembly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions