Skip to content

lightningd: don't ask the Bitcoin backend anything once we're shutting down - #9720

Open
nepet wants to merge 1 commit into
ElementsProject:masterfrom
nepet:shutdown-bcli-segfault
Open

nepet wants to merge 1 commit into
ElementsProject:masterfrom
nepet:shutdown-bcli-segfault

Conversation

@nepet

@nepet nepet commented Nov 8, 2026

Copy link
Copy Markdown
Member

Checklist

Before submitting the PR, ensure the following tasks are completed. If an item is not applicable to your PR, please mark it as checked:

  • The changelog has been updated in the relevant commit(s) according to the guidelines.
  • Tests have been added or modified to reflect the changes.
  • Documentation has been reviewed and updated as needed.
  • Related issues have been listed and linked, including any that this PR closes.
  • Important All PRs must consider how to reverse any persistent changes for tools/lightning-downgrade

Fixes #9719.

At shutdown, the backend plugin (bcli) can exit and be freed while bitcoind->pluginsmap still points at it. #8687 stopped gossipd's get_txout() from using it at that point. A channel that fails during shutdown uses it too: drop_to_chain() broadcasts the commitment tx through bitcoin_plugin_send(), and lightningd crashes in plugin_send() (backtrace in #9719).

With this PR, bitcoin_plugin_send() sends nothing once LD_STATE_SHUTDOWN is set, and estimatefees, which called plugin_request_send() directly, goes through it too. Callers get no callback. That's fine at shutdown: stop_topology() already drops the answers to topology's requests. A close we don't broadcast is broadcast on restart by resend_closing_transactions(), because the channel is already AWAITING_UNILATERAL in the db. The check #8687 added in get_txout() stays, and now only saves starting the request.

One behaviour change: a plugin that uses the backend from its shutdown handler, over an RPC connection it already has open, gets no answer and is killed after 30 seconds. Today that works only if bcli hasn't exited yet; otherwise it reads freed memory.

The new test makes a peer that forgot the channel reconnect while lightningd waits for a plugin that ignores shutdown. With this PR, lightningd fails the channel, doesn't broadcast, exits cleanly, and broadcasts the close after the restart. On master it aborts in plugin_send().

It doesn't stop connectd from reconnecting during shutdown, which is how a channeld gets started after subd_shutdown_nonglobals(). See #9719.

Andezion
Andezion previously approved these changes Nov 9, 2026

@Andezion Andezion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test_shutdown_peer_reconnect makes shutdown take about 30s because of dont_shutdown. Could the test plugin take a shorter delay?

Comment thread lightningd/bitcoind.c
"sendrawtransaction", "getutxout",
"estimatefees"};

static void bitcoin_destructor(struct plugin *p)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe in addition to the state check, make bitcoin_destructor() remove the plugin s entries from pluginsmap during shutdown (or set them to NULL). It can reach the map through p->plugins->ld->topology->bitcoind. Then the map can never point at freed memory, what do you think??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e201d08, the destructor now drops the plugin's entries. It also protects bitcoin_plugin_error(), which reads the map with no shutdown check.

Comment thread tests/test_plugin.py
'test_libplugin: failed to self-terminate in time, killing.'])


def test_shutdown_peer_reconnect(node_factory, bitcoind, executor):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@unittest.skipIf(VALGRIND, "Under valgrind, restarting l2 takes longer than the 30 second plugin shutdown wait")

Otherwise need to add a configurable plugin shutdown timeout...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: skipped under valgrind

@nepet

nepet commented Nov 13, 2026

Copy link
Copy Markdown
Member Author

test_shutdown_peer_reconnect makes shutdown take about 30s because of dont_shutdown. Could the test plugin take a shorter delay?

The test now SIGKILLs the stuck plugin once the broadcast is skipped, so shutdown doesn't wait out the 30s (about 8s total).

…g down

bitcoind->pluginsmap keeps a pointer to the backend plugin for each of
its methods.  When the plugin exits during shutdown, bitcoin_destructor()
lets it go, but the map still points at the freed plugin, so anything
that asks the backend after that uses freed memory.

0b8239f ("lightningd: fix access to bcli plugin once it's freed.")
stopped gossipd's get_txout from doing that.  A channel that fails
during shutdown does it too, through drop_to_chain():

    lightningd: FATAL SIGNAL 11 (version v26.06.9)
    plugin_send <- plugin_request_send <- bitcoin_plugin_send
      <- bitcoind_sendrawtx_ <- broadcast_tx_ <- sign_and_send_last
      <- drop_to_chain <- channel_fail_perm <- channel_fail_permanent
      <- channel_errmsg <- channel_control_errmsg <- handle_peer_error
      <- sd_msg_read <- ... <- io_loop <- shutdown_plugins <- main

There, connectd reconnected to the peer while shutdown_plugins() waited
for the plugins to exit, lightningd started channeld for the channel,
and the peer answered the reestablish with an error.  So lightningd
failed the channel and broadcast its commitment tx through the freed
bcli.

Check in bitcoin_plugin_send() instead, so no caller can get there, and
send estimatefees through it too.  Not broadcasting the close is safe:
the channel is AWAITING_UNILATERAL in the db, and
resend_closing_transactions() broadcasts it on restart.

Also make bitcoin_destructor() remove the plugin from pluginsmap, so the
map never points at a freed plugin (bitcoin_plugin_error() reads it too).

Fixes: ElementsProject#9719
Changelog-Fixed: lightningd: no longer crashes when a channel is force-closed while plugins shut down (e.g. a peer reconnects and sends an error); the close is broadcast on restart.
@nepet
nepet force-pushed the shutdown-bcli-segfault branch from e201d08 to b6f1bcd Compare November 13, 2026 13:12

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lightningd crashes at shutdown when a channel fails after bcli has exited

3 participants