What changed, and why it matters
This commit fixes a typo in several Core Lightning plugins where the wrong printf format string was used. The buggy '%*.s' tells the program to treat a string pointer as a width and then read an unlimited number of bytes from another pointer, which can read past the end of a buffer and crash or leak memory. The fix changes it to '%.*s', which correctly limits how many bytes are read from the string. The commit also adds a build-time check to stop the typo from being reintroduced.
Apply the patch and ensure the new check-bad-sprintf Makefile rule runs in CI. Review any other custom printf wrappers for similar specifier typos. No additional CVE coordination is indicated by the supplied materials, but the issue is a genuine memory-safety bug.
Security signals we found
Heap-buffer-overflow in printf_common triggered by plugin logging
Incorrect printf format specifier '%*.s' used in place of '%.*s'
Unbounded read from JSON token buffers during plugin_log/plugin_err calls
AddressSanitizer crash trace included in commit message
Build-time lint rule added to detect the typo pattern
Evidence from the diff
Multiple plugins used the printf-style format specifier ‘%.s’ instead of ‘%.s’. In standard printf semantics, ‘%.s’ takes a precision (length) and a string pointer, printing at most that many characters. ‘%.s’ takes a field width and no precision, then reads the string with no length limit while using the supplied value as a minimum field width. Because the code passed json_tok_full_len() as the first argument, that length was interpreted as width rather than precision, and the string pointer was read without bound. This caused heap-buffer-overflow reads when logging JSON tokens, as demonstrated by the AddressSanitizer trace in the commit message (plugin_logv -> json_out_addv -> printf_common). The patch corrects the specifier in funder.c, multifundchannel.c, openchannel.c, and txprepare.c, and adds a Makefile lint rule (check-bad-sprintf) that greps for ‘%*.s’ to prevent recurrence.
Changed components
plugins/funder.cplugins/spender/multifundchannel.cplugins/spender/openchannel.cplugins/txprepare.cMakefile (check-bad-sprintf lint rule)Inspect captured patch +17 / −14
diff --git a/Makefile b/Makefile
index 3cee618..32293fd 100644
--- a/Makefile
+++ b/Makefile
@@ -585,6 +585,9 @@ check-tmpctx:
check-discouraged-functions:
@if git grep -E "[^a-z_/](fgets|fputs|gets|scanf|sprintf)\(" -- "*.c" "*.h" ":(exclude)ccan/" ":(exclude)contrib/"; then exit 1; fi
+check-bad-sprintf:
+ @if git grep -n "%[*]\.s"; then exit 1; fi
+
# Don't access amount_msat and amount_sat members directly without a good reason
# since it risks overflow.
check-amount-access:
@@ -609,7 +612,7 @@ check-doc-examples: update-doc-examples
git diff --exit-code HEAD
# For those without working cppcheck
-check-source-no-cppcheck: check-makefile check-source-bolt check-whitespace check-spelling check-python check-includes check-shellcheck check-setup_locale check-tmpctx check-discouraged-functions check-amount-access
+check-source-no-cppcheck: check-makefile check-source-bolt check-whitespace check-spelling check-python check-includes check-shellcheck check-setup_locale check-tmpctx check-discouraged-functions check-amount-access check-bad-sprintf
check-source: check-source-no-cppcheck
diff --git a/plugins/funder.c b/plugins/funder.c
index 15ab3f1..2843e59 100644
--- a/plugins/funder.c
+++ b/plugins/funder.c
@@ -80,7 +80,7 @@ unreserve_done(struct command *aux_cmd,
struct pending_open *open)
{
plugin_log(open->p, LOG_DBG,
- "`unreserveinputs` for channel %s completed. %*.s",
+ "`unreserveinputs` for channel %s completed. %.*s",
fmt_channel_id(tmpctx, &open->channel_id),
json_tok_full_len(result),
json_tok_full(buf, result));
@@ -159,7 +159,7 @@ datastore_del_success(struct command *cmd,
{
/* Cool we deleted some stuff */
plugin_log(cmd->plugin, LOG_DBG,
- "`datastore` del succeeded: %*.s",
+ "`datastore` del succeeded: %.*s",
json_tok_full_len(result),
json_tok_full(buf, result));
@@ -175,7 +175,7 @@ datastore_add_fail(struct command *cmd,
{
/* Oops, something's broken */
plugin_log(cmd->plugin, LOG_BROKEN,
- "%s failed: %*.s",
+ "%s failed: %.*s",
method, json_tok_full_len(error),
json_tok_full(buf, error));
@@ -197,7 +197,7 @@ datastore_add_success(struct command *cmd,
if (err)
plugin_err(cmd->plugin,
- "`datastore` payload did not scan. %s: %*.s",
+ "`datastore` payload did not scan. %s: %.*s",
err, json_tok_full_len(result),
json_tok_full(buf, result));
@@ -266,7 +266,7 @@ signpsbt_done(struct command *cmd,
if (err)
plugin_err(cmd->plugin,
- "`signpsbt` payload did not scan %s: %*.s",
+ "`signpsbt` payload did not scan %s: %.*s",
err, json_tok_full_len(result),
json_tok_full(buf, result));
@@ -594,7 +594,7 @@ listfunds_success(struct command *cmd,
outputs_tok = json_get_member(buf, result, "outputs");
if (!outputs_tok)
plugin_err(cmd->plugin,
- "`listfunds` payload has no outputs token: %*.s",
+ "`listfunds` payload has no outputs token: %.*s",
json_tok_full_len(result),
json_tok_full(buf, result));
@@ -624,7 +624,7 @@ listfunds_success(struct command *cmd,
JSON_SCAN(json_to_number, &utxo->out.n));
if (err)
plugin_err(cmd->plugin,
- "`listfunds` payload did not scan. %s: %*.s",
+ "`listfunds` payload did not scan. %s: %.*s",
err, json_tok_full_len(result),
json_tok_full(buf, result));
@@ -923,7 +923,7 @@ datastore_list_fail(struct command *cmd,
/* Oops, something's broken */
plugin_log(cmd->plugin, LOG_BROKEN,
- "`datastore` list failed: %*.s",
+ "`datastore` list failed: %.*s",
json_tok_full_len(error),
json_tok_full(buf, error));
@@ -965,7 +965,7 @@ datastore_list_success(struct command *cmd,
if (err)
plugin_err(cmd->plugin,
"`listdatastore` payload did"
- " not scan. %s: %*.s",
+ " not scan. %s: %.*s",
err, json_tok_full_len(result),
json_tok_full(buf, result));
diff --git a/plugins/spender/multifundchannel.c b/plugins/spender/multifundchannel.c
index af42573..c722386 100644
--- a/plugins/spender/multifundchannel.c
+++ b/plugins/spender/multifundchannel.c
@@ -1450,7 +1450,7 @@ after_getfeerate(struct command *cmd,
JSON_SCAN(json_to_number, &feerate));
if (err)
mfc_fail(mfc, JSONRPC2_INVALID_PARAMS,
- "Unable to parse feerate %s: %*.s",
+ "Unable to parse feerate %s: %.*s",
err, json_tok_full_len(result),
json_tok_full(buf, result));
diff --git a/plugins/spender/openchannel.c b/plugins/spender/openchannel.c
index 85d15b0..7d21916 100644
--- a/plugins/spender/openchannel.c
+++ b/plugins/spender/openchannel.c
@@ -554,7 +554,7 @@ static struct command_result *json_peer_sigs(struct command *cmd,
JSON_SCAN_TAL(cmd, json_to_psbt, &psbt));
if (err)
plugin_err(cmd->plugin,
- "`openchannel_peer_sigs` did not scan: %s. %*.s",
+ "`openchannel_peer_sigs` did not scan: %s. %.*s",
err, json_tok_full_len(params),
json_tok_full(buf, params));
diff --git a/plugins/txprepare.c b/plugins/txprepare.c
index d1be784..5d97b7e 100644
--- a/plugins/txprepare.c
+++ b/plugins/txprepare.c
@@ -522,7 +522,7 @@ static struct command_result *listfunds_done(struct command *cmd,
txp->output_total = AMOUNT_SAT(0);
if (!outputs_tok)
plugin_err(cmd->plugin,
- "`listfunds` payload has no outputs token: %*.s",
+ "`listfunds` payload has no outputs token: %.*s",
json_tok_full_len(result),
json_tok_full(buf, result));
@@ -547,7 +547,7 @@ static struct command_result *listfunds_done(struct command *cmd,
JSON_SCAN(json_to_number, &prev_out.n));
if (err)
plugin_err(cmd->plugin,
- "`listfunds` payload did not scan. %s: %*.s",
+ "`listfunds` payload did not scan. %s: %.*s",
err, json_tok_full_len(result),
json_tok_full(buf, result));
Why this scored 68/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.