splice: multi channel stfu bugfix
What changed, and why it matters
This commit fixes a memory-handling bug in Core Lightning's 'splice' feature when stopping multiple payment channels at the same time. Previously, several related commands shared the same list of channel IDs and results. If that list was resized in memory, the other commands could end up pointing to freed/invalid memory, which can cause crashes or unpredictable behavior. The fix puts those shared lists into a single explicit shared object so every command sees the updated, valid pointer.
Treat as a stability/reliability fix and include in relevant release notes. While the commit does not frame this as a security vulnerability, the dangling-pointer behavior could be exploitable for denial of service or memory corruption under multi-channel splice operations. Users running splice-enabled nodes should upgrade. No immediate incident response is indicated absent further analysis or disclosure.
Security signals we found
Use-after-free / dangling-pointer risk from shared mutable arrays subject to reallocation
Memory corruption / crash potential during multi-channel splice STFU processing
Fix removes explicit `tal_free` of arrays now owned by command context
No explicit security framing or CVE reference in commit message
Evidence from the diff
In lightningd/channel_control.c, multiple splice_command objects created by json_stfu_channels shared the same channel_ids and results tal arrays. Functions such as tal_arr_remove and tal_arr_expand can reallocate the underlying array, invalidating the pointer held by sibling splice_command instances. The patch introduces a stfu_req_info container that owns both arrays and is referenced by each splice_command, ensuring all commands see the current array pointer after reallocation. The previous code also freed channel_ids and results explicitly in handle_confirmed_stfu; that is removed because the arrays are now parented to the command context.
Changed components
lightningd/channel_control.csplice_command structurejson_stfu_channels RPC handlerhandle_confirmed_stfu callbackInspect captured patch +37 / −37
diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c
index e8d64802..25454041 100644
--- a/lightningd/channel_control.c
+++ b/lightningd/channel_control.c
@@ -30,6 +30,13 @@ struct stfu_result
struct amount_msat available_funds;
};
+struct stfu_req_info {
+ /* The channel on which this stfu request was made */
+ struct channel_id **channel_ids;
+ /* For multi-channel stfu command: the pending result */
+ struct stfu_result **results;
+};
+
struct splice_command {
/* Inside struct lightningd splice_commands. */
struct list_node list;
@@ -37,13 +44,11 @@ struct splice_command {
struct command *cmd;
/* Channel being spliced. */
struct channel *channel;
- /* For multi-channel commands: remaining channels awaiting response.
- * Allocated on ld -- free when finished. */
- struct channel_id **channel_ids;
- /* For multi-channel stfu command: the pending result */
- struct stfu_result **results;
/* The user provided PSBT's version */
u32 user_psbt_ver;
+ /* For multi-channel commands: remaining channels awaiting response.
+ * Allocated on ld -- free when finished. */
+ struct stfu_req_info *stfu_req_info;
};
static u32 default_feerate(struct lightningd *ld, const struct channel *channel,
@@ -1529,17 +1534,16 @@ static void handle_confirmed_stfu(struct lightningd *ld,
return;
}
- log_info(channel->log, "lightningd got confirmed stfu from channeld,"
- " channel_id count: %zu", tal_count(cc->channel_ids));
-
- for (size_t i = 0; i < tal_count(cc->channel_ids); i++) {
- if (channel_id_eq(cc->channel_ids[i], &channel->cid)) {
- stfu_result = tal(cc->results, struct stfu_result);
+ for (size_t i = 0; i < tal_count(cc->stfu_req_info->channel_ids); i++) {
+ if (channel_id_eq(cc->stfu_req_info->channel_ids[i],
+ &channel->cid)) {
+ stfu_result = tal(cc->stfu_req_info->results, struct stfu_result);
stfu_result->channel_id = channel->cid;
stfu_result->available_funds = available_funds;
- tal_arr_expand(&cc->results, stfu_result);
- tal_arr_remove(&cc->channel_ids, i);
+ tal_arr_expand(&cc->stfu_req_info->results,
+ stfu_result);
+ tal_arr_remove(&cc->stfu_req_info->channel_ids, i);
log_info(channel->log, "lightningd found channel_id in command and removed it");
break;
@@ -1547,29 +1551,27 @@ static void handle_confirmed_stfu(struct lightningd *ld,
}
log_info(channel->log, "Finished processing confirmed stfu,"
- " channel_id count: %zu", tal_count(cc->channel_ids));
+ " channel_id count: %zu, results count: %zu",
+ tal_count(cc->stfu_req_info->channel_ids),
+ tal_count(cc->stfu_req_info->results));
/* Once we run out of pending stfu requests we return to user */
- if (tal_count(cc->channel_ids))
+ if (tal_count(cc->stfu_req_info->channel_ids))
return;
struct json_stream *response = json_stream_success(cc->cmd);
json_array_start(response, "channels");
- for (size_t i = 0; i < tal_count(cc->results); i++) {
+ for (size_t i = 0; i < tal_count(cc->stfu_req_info->results); i++) {
json_object_start(response, NULL);
json_add_channel_id(response, "channel_id",
- &cc->results[i]->channel_id);
+ &cc->stfu_req_info->results[i]->channel_id);
json_add_amount_msat(response, "available_msat",
- cc->results[i]->available_funds);
+ cc->stfu_req_info->results[i]->available_funds);
json_object_end(response);
}
json_array_end(response);
- /* channel_ids and results are free'd when the last stfu is finished */
- tal_free(cc->channel_ids);
- tal_free(cc->results);
-
was_pending(command_success(cc->cmd, response));
}
@@ -2355,8 +2357,7 @@ static struct command_result *json_splice_init(struct command *cmd,
cc->cmd = cmd;
cc->channel = channel;
- cc->channel_ids = NULL;
- cc->results = NULL;
+ cc->stfu_req_info = NULL;
cc->user_psbt_ver = initialpsbt->version;
if (initialpsbt->version != 2 && !psbt_set_version(initialpsbt, 2))
@@ -2407,8 +2408,7 @@ static struct command_result *json_splice_update(struct command *cmd,
cc->cmd = cmd;
cc->channel = channel;
- cc->channel_ids = NULL;
- cc->results = NULL;
+ cc->stfu_req_info = NULL;
cc->user_psbt_ver = psbt->version;
if (psbt->version != 2 && !psbt_set_version(psbt, 2))
@@ -2443,8 +2443,7 @@ static struct command_result *single_splice_signed(struct command *cmd,
cc->cmd = cmd;
cc->channel = channel;
- cc->channel_ids = NULL;
- cc->results = NULL;
+ cc->stfu_req_info = NULL;
cc->user_psbt_ver = psbt->version;
if (psbt->version != 2 && !psbt_set_version(psbt, 2))
@@ -2537,11 +2536,10 @@ static struct command_result *json_stfu_channels(struct command *cmd,
const jsmntok_t *params)
{
struct channel *channel, **channels;
- struct channel_id **channel_ids;
const jsmntok_t *channel_ids_tok, *channel_id_tok;
struct command_result *result;
struct splice_command *cc;
- struct stfu_result **stfu_result;
+ struct stfu_req_info *req;
size_t i;
if (!param_check(cmd, buffer, params,
@@ -2572,16 +2570,19 @@ static struct command_result *json_stfu_channels(struct command *cmd,
if (command_check_only(cmd))
return command_check_done(cmd);
+ req = tal(cmd, struct stfu_req_info);
+
/* Next we split into multiple `stfu` commands. The final command to
- * return will handle free'ing `stfu_result` and `channel_ids` */
- stfu_result = tal_arr(NULL, struct stfu_result*, 0);
- channel_ids = tal_arr(NULL, struct channel_id*, tal_count(channels));
+ * return finish the cmd response */
+ req->results = tal_arr(cmd, struct stfu_result*, 0);
+ req->channel_ids = tal_arr(cmd, struct channel_id*,
+ tal_count(channels));
for (i = 0; i < tal_count(channels); i++) {
channel = channels[i];
- channel_ids[i] = tal(channel_ids, struct channel_id);
- *channel_ids[i] = channel->cid;
+ req->channel_ids[i] = tal(req->channel_ids, struct channel_id);
+ *req->channel_ids[i] = channel->cid;
cc = tal(cmd, struct splice_command);
@@ -2590,8 +2591,7 @@ static struct command_result *json_stfu_channels(struct command *cmd,
cc->cmd = cmd;
cc->channel = channel;
- cc->channel_ids = channel_ids;
- cc->results = stfu_result;
+ cc->stfu_req_info = req;
subd_send_msg(channel->owner, take(towire_channeld_stfu(NULL)));
}
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.