libplugin: correctly mark that plugin_notification_end STEALS the stream.
What changed, and why it matters
This is a small cleanup in Core Lightning's plugin helper library. It changes how memory ownership is documented for plugin notification messages so the code's automatic memory tracking matches what actually happens. The change itself is defensive and unlikely to be directly exploitable, but it fixes a mismatch that could hide real memory-management bugs in plugins.
Treat as a low-risk correctness fix. Review any custom plugins that call plugin_notification_start/plugin_notification_end to ensure they do not manually free the json_stream after plugin_notification_end(), since the stream is now explicitly stolen. No urgent patching required unless static analysis or runtime memory issues are observed.
Security signals we found
Memory ownership annotation corrected from TAKES to STEALS
Function signature generalized to accept any tal context
Potential for use-after-free or double-free if callers relied on incorrect ownership semantics
No explicit security disclosure or CVE referenced in commit
Evidence from the diff
The commit updates plugin_notification_start() to accept any tal context (const tal_t ctx) rather than only struct plugin , and changes the plugin_notification_end() annotation from TAKES to STEALS. In Core Lightning’s tal-based type system, TAKES means the function consumes a reference, while STEALS means the object is reparented under a new owner. The previous TAKES annotation was incorrect because jsonrpc_finish_and_send() ultimately reparents/steals the json_stream, not merely consumes a reference. The mismatch between annotation and behavior could lead to incorrect static analysis, double-free, or use-after-free assumptions in plugin code that sends custom notifications.
Changed components
plugins/libplugin.cplugins/libplugin.hplugins/test/run-route-calc.cplugins/test/run-route-overlong.cInspect captured patch +9 / −9
diff --git a/plugins/libplugin.c b/plugins/libplugin.c
index b70fe94d..896564f7 100644
--- a/plugins/libplugin.c
+++ b/plugins/libplugin.c
@@ -1859,10 +1859,10 @@ void plugin_gossmap_logcb(struct plugin *plugin,
va_end(ap);
}
-struct json_stream *plugin_notification_start(struct plugin *plugin,
+struct json_stream *plugin_notification_start(const tal_t *ctx,
const char *method)
{
- struct json_stream *js = new_json_stream(plugin, NULL, NULL);
+ struct json_stream *js = new_json_stream(ctx, NULL, NULL);
json_object_start(js, NULL);
json_add_string(js, "jsonrpc", "2.0");
@@ -1873,7 +1873,7 @@ struct json_stream *plugin_notification_start(struct plugin *plugin,
}
void plugin_notification_end(struct plugin *plugin,
- struct json_stream *stream)
+ struct json_stream *stream STEALS)
{
json_object_end(stream);
jsonrpc_finish_and_send(plugin, stream);
diff --git a/plugins/libplugin.h b/plugins/libplugin.h
index 09a29a97..54b4f883 100644
--- a/plugins/libplugin.h
+++ b/plugins/libplugin.h
@@ -534,10 +534,10 @@ void plugin_notify_end(struct command *cmd, struct json_stream *js);
/* Send a notification for a custom notification topic. These are sent
* to lightningd and distributed to subscribing plugins. */
-struct json_stream *plugin_notification_start(struct plugin *plugins,
+struct json_stream *plugin_notification_start(const tal_t *ctx,
const char *method);
void plugin_notification_end(struct plugin *plugin,
- struct json_stream *stream TAKES);
+ struct json_stream *stream STEALS);
/* Convenience wrapper for notify "message" */
void plugin_notify_message(struct command *cmd,
diff --git a/plugins/test/run-route-calc.c b/plugins/test/run-route-calc.c
index a0bdda82..3898c3f4 100644
--- a/plugins/test/run-route-calc.c
+++ b/plugins/test/run-route-calc.c
@@ -299,10 +299,10 @@ void plugin_log(struct plugin *p UNNEEDED, enum log_level l UNNEEDED, const char
{ fprintf(stderr, "plugin_log called!\n"); abort(); }
/* Generated stub for plugin_notification_end */
void plugin_notification_end(struct plugin *plugin UNNEEDED,
- struct json_stream *stream TAKES UNNEEDED)
+ struct json_stream *stream STEALS UNNEEDED)
{ fprintf(stderr, "plugin_notification_end called!\n"); abort(); }
/* Generated stub for plugin_notification_start */
-struct json_stream *plugin_notification_start(struct plugin *plugins UNNEEDED,
+struct json_stream *plugin_notification_start(const tal_t *ctx UNNEEDED,
const char *method UNNEEDED)
{ fprintf(stderr, "plugin_notification_start called!\n"); abort(); }
/* Generated stub for plugin_notify_message */
diff --git a/plugins/test/run-route-overlong.c b/plugins/test/run-route-overlong.c
index da6f0a60..3683c0dd 100644
--- a/plugins/test/run-route-overlong.c
+++ b/plugins/test/run-route-overlong.c
@@ -296,10 +296,10 @@ void plugin_log(struct plugin *p UNNEEDED, enum log_level l UNNEEDED, const char
{ fprintf(stderr, "plugin_log called!\n"); abort(); }
/* Generated stub for plugin_notification_end */
void plugin_notification_end(struct plugin *plugin UNNEEDED,
- struct json_stream *stream TAKES UNNEEDED)
+ struct json_stream *stream STEALS UNNEEDED)
{ fprintf(stderr, "plugin_notification_end called!\n"); abort(); }
/* Generated stub for plugin_notification_start */
-struct json_stream *plugin_notification_start(struct plugin *plugins UNNEEDED,
+struct json_stream *plugin_notification_start(const tal_t *ctx UNNEEDED,
const char *method UNNEEDED)
{ fprintf(stderr, "plugin_notification_start called!\n"); abort(); }
/* Generated stub for plugin_notify_message */
Why this scored 24/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.