libplugin: don't wait for clean_tmpctx() to free requests as we process them.
What changed, and why it matters
This is a bug-fix patch for Core Lightning's plugin library. It changes when plugin-to-JSON-RPC request objects are freed so that an internal plugin called xpay can immediately send follow-up commands instead of waiting until the next event loop wakeup. The commit describes it as a functional bug that was hidden until xpay stopped monitoring every command, not as a security vulnerability.
Treat as a normal bug fix. Review whether the lifetime change affects any other plugins that rely on deferred tmpctx cleanup, and run xpay regression tests. No immediate security response is indicated by the supplied materials.
Security signals we found
Memory lifetime change for RPC request/response objects
Functional correctness fix for plugin command chaining (xpay)
No explicit security claim in commit message or diff
No input validation, bounds, or cryptographic changes visible
No incident or exploit references supplied
Evidence from the diff
The patch modifies plugins/libplugin.c so that handle_rpc_reply() receives a dedicated temporary allocation context (working_ctx) instead of the global tmpctx. The caller now creates working_ctx before its read loop and explicitly frees it after processing all pending RPC replies. Previously, out_req objects were stolen onto tmpctx and only freed later when clean_tmpctx() ran. The commit message says xpay relies on the request destructor to trigger additional RPC requests, and the old deferral meant those requests were not submitted until the next wakeup. The patch makes the lifetime explicit and immediate.
Changed components
plugins/libplugin.cCore Lightning plugin frameworkxpay plugin command dispatchInspect captured patch +10 / −3
diff --git a/plugins/libplugin.c b/plugins/libplugin.c
index 8dfba2ce..22d49954 100644
--- a/plugins/libplugin.c
+++ b/plugins/libplugin.c
@@ -1018,7 +1018,8 @@ static void destroy_cmd_mark_freed(struct command *cmd, bool *cmd_freed)
*cmd_freed = true;
}
-static void handle_rpc_reply(struct plugin *plugin, const char *buf, const jsmntok_t *toks)
+static void handle_rpc_reply(const tal_t *working_ctx,
+ struct plugin *plugin, const char *buf, const jsmntok_t *toks)
{
const jsmntok_t *idtok, *contenttok;
struct out_req *out;
@@ -1042,7 +1043,7 @@ static void handle_rpc_reply(struct plugin *plugin, const char *buf, const jsmnt
}
/* We want to free this if callback doesn't. */
- tal_steal(tmpctx, out);
+ tal_steal(working_ctx, out);
/* If they return complete, cmd should have been freed! */
cmd_freed = false;
@@ -1356,6 +1357,8 @@ static void rpc_conn_finished(struct io_conn *conn,
static struct io_plan *rpc_conn_read_response(struct io_conn *conn,
struct plugin *plugin)
{
+ const tal_t *working_ctx = tal(NULL, char);
+
/* Gather an parse any new bytes */
for (;;) {
const jsmntok_t *toks;
@@ -1371,10 +1374,14 @@ static struct io_plan *rpc_conn_read_response(struct io_conn *conn,
if (!toks)
break;
- handle_rpc_reply(plugin, buf, toks);
+ handle_rpc_reply(working_ctx, plugin, buf, toks);
jsonrpc_io_parse_done(plugin->jsonrpc_in);
}
+ /* Explicitly free any expired requests now; xpay uses this to
+ * fire more commands! */
+ tal_free(working_ctx);
+
/* Read more */
return jsonrpc_io_read(conn, plugin->jsonrpc_in,
rpc_conn_read_response,
Why this scored 29/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.