Skip to content

lightningd crashes (SIGSEGV) on any setconfig after a dynamically restarted plugin drops an option #9385

Description

@ksedgwic

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:

  1. A configvar for plugin option X exists in memory: from a config
    file line read at boot, or from an earlier setconfig X.

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

  3. 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).

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