Issue and Steps to Reproduce
lightningd segfaults on any setconfig once a configvar refers to a
plugin option that is no longer registered. This arises in normal
operation when a plugin is dynamically restarted (plugin stop +
plugin start) as a newer build that dropped one of its options --
i.e. a routine plugin upgrade.
Sequence:
-
A configvar for plugin option X exists in memory: from a config
file line read at boot, or from an earlier setconfig X.
-
The plugin is stopped. destroy_plugin_opt() unregisters the
option (opt_unregister) but only removes configvars created via
plugin start parameters; the configvar naming X remains.
(Restarting the plugin as a build without X, or editing X out of
the config file, does not help: files are only read at boot and
the stale entry lives in the running daemon.)
-
Any subsequent setconfig -- any option at all, builtin or plugin,
persistent or transient -- runs configvar_finalize_overrides()
over ALL configvars. For the stale one, opt_find_long() returns
NULL, and the next line dereferences it:
opts[i] = opt_find_long(cvs[i]->optvar, NULL);
if (opts[i]->type & OPT_MULTI) /* <- NULL deref */
Backtrace, reproduced on master (116fd7c):
FATAL SIGNAL 11
common/configvar.c:112 (configvar_finalize_overrides)
lightningd/configs.c:337 (configvar_updated)
lightningd/configs.c:530 (configvar_save)
lightningd/configs.c:600 (setconfig_success)
lightningd/configs.c:804 (json_setconfig)
First observed on a production node running a v26.04.1-based build:
there the setconfig that crashed was itself a plugin option, so the
trace ran via plugin_setconfig_done() instead of json_setconfig(),
reaching the same dereference.
Minimal reproduction against master, using a stock test fixture
plugin (this is the pytest in the fix PR):
l1.rpc.plugin_start('tests/plugins/dynamic_option.py')
l1.rpc.setconfig(config='test-dynamic-config', val='soothing')
l1.rpc.plugin_stop('tests/plugins/dynamic_option.py')
l1.rpc.setconfig(config='min-capacity-sat', val=100000) # SIGSEGV
Fix: PR #9386 NULL-guards the lookup (stale configvars override
nothing). A deeper question for maintainers: should configvars be
dropped when their owning plugin option is destroyed?
destroy_plugin_opt() already does this for CONFIGVAR_PLUGIN_START
entries but leaves file- and setconfig-sourced ones behind. The
guard is correct either way and is the safe backportable piece.
Related: e9fee87 (setconfig: fix crash on dynamic multi-value
plugin options) fixed a different crash in the same area.
getinfo output
Observed on a production node running a v26.04.1-based build;
reproduced on current master (116fd7c).
Issue and Steps to Reproduce
lightningd segfaults on any setconfig once a configvar refers to a
plugin option that is no longer registered. This arises in normal
operation when a plugin is dynamically restarted (plugin stop +
plugin start) as a newer build that dropped one of its options --
i.e. a routine plugin upgrade.
Sequence:
A configvar for plugin option X exists in memory: from a config
file line read at boot, or from an earlier setconfig X.
The plugin is stopped. destroy_plugin_opt() unregisters the
option (opt_unregister) but only removes configvars created via
plugin start parameters; the configvar naming X remains.
(Restarting the plugin as a build without X, or editing X out of
the config file, does not help: files are only read at boot and
the stale entry lives in the running daemon.)
Any subsequent setconfig -- any option at all, builtin or plugin,
persistent or transient -- runs configvar_finalize_overrides()
over ALL configvars. For the stale one, opt_find_long() returns
NULL, and the next line dereferences it:
opts[i] = opt_find_long(cvs[i]->optvar, NULL);
if (opts[i]->type & OPT_MULTI) /* <- NULL deref */
Backtrace, reproduced on master (116fd7c):
FATAL SIGNAL 11
common/configvar.c:112 (configvar_finalize_overrides)
lightningd/configs.c:337 (configvar_updated)
lightningd/configs.c:530 (configvar_save)
lightningd/configs.c:600 (setconfig_success)
lightningd/configs.c:804 (json_setconfig)
First observed on a production node running a v26.04.1-based build:
there the setconfig that crashed was itself a plugin option, so the
trace ran via plugin_setconfig_done() instead of json_setconfig(),
reaching the same dereference.
Minimal reproduction against master, using a stock test fixture
plugin (this is the pytest in the fix PR):
l1.rpc.plugin_start('tests/plugins/dynamic_option.py')
l1.rpc.setconfig(config='test-dynamic-config', val='soothing')
l1.rpc.plugin_stop('tests/plugins/dynamic_option.py')
l1.rpc.setconfig(config='min-capacity-sat', val=100000) # SIGSEGV
Fix: PR #9386 NULL-guards the lookup (stale configvars override
nothing). A deeper question for maintainers: should configvars be
dropped when their owning plugin option is destroyed?
destroy_plugin_opt() already does this for CONFIGVAR_PLUGIN_START
entries but leaves file- and setconfig-sourced ones behind. The
guard is correct either way and is the safe backportable piece.
Related: e9fee87 (setconfig: fix crash on dynamic multi-value
plugin options) fixed a different crash in the same area.
getinfo output
Observed on a production node running a v26.04.1-based build;
reproduced on current master (116fd7c).