lightningd: fix crash and busy-loop on invoice with huge expiry
What changed, and why it matters
This commit fixes a bug in Core Lightning's invoice creation command. If a user requested an invoice that expired extremely far in the future (more than about 136 years), the software would either crash the entire payment daemon or get stuck using 100% CPU. The fix simply rejects such large expiry values early, returning a normal error instead of crashing or looping.
Apply this patch promptly. It is a clean input-validation fix with no functional changes for normal expiry values. Review other u64 RPC parameters that are converted to nanoseconds or varint fields for similar overflow issues.
Security signals we found
Denial-of-service via daemon crash (SIGABRT) from oversized bolt11 x-field encoding
Denial-of-service via 100% CPU busy-loop from u64 nanosecond overflow in expiration timer
Input validation gap on RPC parameter leading to resource exhaustion
Regression test added for the boundary condition
Evidence from the diff
The invoice RPC accepted an unclamped expiry parameter as a u64. Values >= 2^60 caused bolt11_encode()’s push_varlen_field() to abort the daemon (SIGABRT/FATAL SIGNAL 6). Smaller but still huge values (past ~584k years) caused the invoice expiration timer’s nanosecond-grain u64 counter to overflow in install_expiration_timer(), making the timer read as already expired, trigger_expiration() find nothing expired, and re-arm in a busy-loop. The patch clamps expiry to < 2^32 seconds (~136 years) at parameter validation, returning JSONRPC2_INVALID_PARAMS. A regression test verifies the boundary and refusal behavior.
Changed components
lightningd/invoice.cinvoice RPC commandbolt11 encoding path (push_varlen_field/bolt11_encode)invoice expiration timer (install_expiration_timer/trigger_expiration)Inspect captured patch +35 / −0
### lightningd/invoice.c
@@ -1153,6 +1153,17 @@ static struct command_result *json_invoice(struct command *cmd,
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"dev-routes requires --developer");
+ /* Two hard limits on how far in the future an invoice can expire:
+ * push_varlen_field() can only encode up to 60 bits (larger values
+ * abort the daemon in bolt11_encode()), and the invoice expiration
+ * timer overflows its u64 nanosecond-based grain count far below
+ * that, leaving the expiry check looping forever. 2^32 seconds
+ * (~136 years) keeps a wide margin under both. */
+ if (*expiry >= (u64)1 << 32)
+ return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
+ "expiry must be below 2^32 seconds"
+ " (~136 years)");
+
if (strlen(info->label->s) > inv_max_label_len) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Label '%s' over %zu bytes", info->label->s, inv_max_label_len);
### tests/test_invoices.py
@@ -442,6 +442,30 @@ def test_invoice_expiry(node_factory, executor):
assert expiry >= start + 1 and expiry <= end + 1
+def test_invoice_expiry_too_large(node_factory):
+ """An expiry too large to be safe must be refused, not crash or wedge.
+
+ The `x` field is encoded by push_varlen_field(), which can only
+ express values of up to 60 bits and aborts the whole daemon for
+ anything larger. Long before that, the invoice expiration timer's
+ nanosecond-based counter overflows and the expiry check busy-loops
+ forever, so anything beyond 2^32 seconds (~136 years) is refused.
+ """
+ l1 = node_factory.get_node()
+
+ # The exact boundary still works: 2^32 - 1 is ~136 years of headroom.
+ ok = l1.rpc.invoice(amount_msat=1000, label='expiry-boundary-ok',
+ description='boundary', expiry=2**32 - 1)
+ assert ok['bolt11']
+
+ # One above the boundary: typed refusal, daemon stays alive.
+ with pytest.raises(RpcError, match='expiry must be below') as err:
+ l1.rpc.invoice(amount_msat=1000, label='expiry-too-large',
+ description='too large', expiry=2**32)
+ assert err.value.error['code'] == -32602
+ assert l1.rpc.getinfo()['id']
+
+
def test_waitinvoice(node_factory, executor):
"""Test waiting for one invoice will not return if another invoice is paid.
"""Why this scored 74/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.