From cb95d735d1280de8951e522d19dcd321baf87262 Mon Sep 17 00:00:00 2001 From: Storm Knight Date: Sat, 28 Mar 2026 10:53:32 -0400 Subject: [PATCH] lightningd: wait for all plugins to gracefully shutdown When multiple plugins are shutting down and one exits before others, io_loop() can return early (e.g. when num_fds drops to 0), causing remaining plugins to be killed immediately instead of being given their full 30-second grace period. Fix by wrapping io_loop() in a while loop that continues as long as plugins are still alive, only breaking when the timer expires. Fixes #7697 Changelog-Fixed: Plugins: now wait for all plugins to gracefully shutdown instead of killing remaining plugins when one exits first. ([#7697]) --- CHANGELOG.md | 1 + lightningd/plugin.c | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c413d26d6005..24bc647f2e2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ Note: You should always set `allow-deprecated-apis=false` to test for changes. - renepay: fixes a race condition that leads to **BROKEN** plugin-cln-renepay: Unable to parse sendpay_failure ([#8798]) - fuzz: fix build with newer clang. ([#8717]) - Plugins: `bkpr_listbalances` no longer crashes if we lost our db, then do emergencyrecover and close a channel. ([#8890]) + - Plugins: now wait for all plugins to gracefully shutdown instead of killing remaining plugins when one exits first. ([#7697]) - lightningd: possible crash when peers disconnected if there was more than one plugin servicing the `peer_connected` hook. ([#8889]) - JSON-RPC: `decode` is now more informative with malformed strings (won't claim everything is a malformed rune!). ([#8814]) - reckless search now returns partial matches instead of requiring exact plugin names. ([#8762]) diff --git a/lightningd/plugin.c b/lightningd/plugin.c index 447eb59d7cde..35bda53c92b1 100644 --- a/lightningd/plugin.c +++ b/lightningd/plugin.c @@ -2602,8 +2602,15 @@ void shutdown_plugins(struct lightningd *ld) timers_init(timer, time_mono()); new_reltimer(timer, timer, time_from_sec(30), NULL, NULL); - void *ret = io_loop(timer, &expired); - assert(ret == NULL || ret == destroy_plugin); + /* Keep looping while plugins are still shutting down. + * io_loop can return early when one plugin exits (e.g. + * num_fds drops to 0) but others may still need time. */ + while (!list_empty(&ld->plugins->plugins)) { + void *ret = io_loop(timer, &expired); + /* Timer expired, stop waiting. */ + if (expired) + break; + } /* Report and free remaining plugins. */ while (!list_empty(&ld->plugins->plugins)) {