What changed, and why it matters
This patch fixes a crash in Core Lightning's lightningd daemon that could occur during splice-related RPC operations. The root cause was that splice command state could be consumed by external failures (like a peer rejecting the splice) while callbacks still held a stale pointer to it. The fix stops storing the splice command pointer inside callback data structures and instead looks it up fresh from the active list when needed, avoiding use-after-free-style crashes.
Apply the patch. Operators running nodes that expose splice RPCs should upgrade. The crash is triggered by external failure conditions, so it is not directly exploitable by an unauthenticated attacker, but a malicious peer may be able to influence splice rejection timing.
Security signals we found
Use of stale pointer to freed command object in async callbacks
Daemon crash (DoS) under specific RPC/splice error conditions
Memory management fix involving tal_steal removal and dynamic lookup
Null-deref protection added for error message formatting path
Evidence from the diff
The commit modifies lightningd/channel_control.c to remove the struct splice_command *cc field from struct send_splice_info. Previously, this pointer was captured at splice initiation and reused in asynchronous callbacks (handle_tx_broadcast, check_utxo_block). If the splice command was consumed/removed by an external failure path before the callback ran, the callback would dereference a freed/stale command object, causing a lightningd crash. The patch instead calls splice_command_for_chan() inside each callback to dynamically retrieve the current splice command for the channel, and adds null checks for info->final_tx and info->final_tx->wtx to avoid crashes when formatting error messages.
Changed components
lightningd/channel_control.csplice RPC handlingbitcoin transaction broadcast callbacksInspect captured patch +25 / −19
diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c
index 627e4360..917f8af2 100644
--- a/lightningd/channel_control.c
+++ b/lightningd/channel_control.c
@@ -482,7 +482,6 @@ static void handle_splice_lookup_tx(struct lightningd *ld,
/* Extra splice data we want to store for bitcoin send tx interface */
struct send_splice_info
{
- struct splice_command *cc;
struct channel *channel;
const struct bitcoin_tx *final_tx;
u32 output_index;
@@ -497,6 +496,7 @@ static void handle_tx_broadcast(struct send_splice_info *info)
struct bitcoin_txid txid;
u8 *tx_bytes;
int num_utxos;
+ struct splice_command *cc;
tx_bytes = linearize_tx(tmpctx, info->final_tx);
bitcoin_txid(info->final_tx, &txid);
@@ -508,15 +508,17 @@ static void handle_tx_broadcast(struct send_splice_info *info)
if (num_utxos)
wallet_transaction_add(ld->wallet, info->final_tx->wtx, 0, 0);
- if (info->cc) {
- response = json_stream_success(info->cc->cmd);
+ cc = splice_command_for_chan(ld, info->channel);
+
+ if (cc) {
+ response = json_stream_success(cc->cmd);
json_add_hex(response, "tx", tx_bytes, tal_bytelen(tx_bytes));
json_add_txid(response, "txid", &txid);
json_add_u32(response, "outnum", info->output_index);
json_add_psbt(response, "psbt", info->psbt);
- was_pending(command_success(info->cc->cmd, response));
+ was_pending(command_success(cc->cmd, response));
}
}
@@ -527,25 +529,32 @@ static void check_utxo_block(struct bitcoind *bitcoind UNUSED,
void *arg)
{
struct send_splice_info *info = arg;
+ struct lightningd *ld = info->channel->peer->ld;
+ struct splice_command *cc;
if(!txout) {
- if (info->cc)
- was_pending(command_fail(info->cc->cmd,
+ cc = splice_command_for_chan(ld, info->channel);
+ if (cc)
+ was_pending(command_fail(cc->cmd,
SPLICE_BROADCAST_FAIL,
- "Error broadcasting splice "
- "tx: %s. Unsent tx discarded "
- "%s.",
+ "Error broadcasting splice"
+ " %s. Unsent tx discarded"
+ " %s.",
info->err_msg,
- fmt_wally_tx(tmpctx,
- info->final_tx->wtx)));
+ info->final_tx && info->final_tx->wtx ?
+ fmt_wally_tx(tmpctx,
+ info->final_tx->wtx)
+ : "NULL"));
log_unusual(info->channel->log,
- "Error broadcasting splice "
- "tx: %s. Unsent tx discarded "
- "%s.",
+ "Error broadcasting splice"
+ " %s. Unsent tx discarded"
+ " %s.",
info->err_msg,
- fmt_wally_tx(tmpctx,
- info->final_tx->wtx));
+ info->final_tx && info->final_tx->wtx ?
+ fmt_wally_tx(tmpctx,
+ info->final_tx->wtx)
+ : "NULL");
}
else
handle_tx_broadcast(info);
@@ -558,8 +567,6 @@ static void send_splice_tx_done(struct bitcoind *bitcoind UNUSED,
bool success, const char *msg,
struct send_splice_info *info)
{
- /* A NULL value of `info->cc` means we got here without user intiation.
- * This means we are the ACCEPTER side of the splice */
struct lightningd *ld = info->channel->peer->ld;
struct bitcoin_outpoint outpoint;
@@ -593,7 +600,6 @@ static void send_splice_tx(struct channel *channel,
struct send_splice_info *info = tal(NULL, struct send_splice_info);
- info->cc = tal_steal(info, cc);
info->channel = channel;
info->final_tx = tal_steal(info, tx);
info->output_index = output_index;
Why this scored 51/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.