pytest: test for crashing with HTLC added tlvs.
What changed, and why it matters
This commit adds a test that demonstrates a crash bug in Core Lightning when two HTLC (payment) messages with custom TLV fields are processed together. The crash occurs because internal memory-management code incorrectly treats a list of added HTLCs as if each entry were independently allocated, when only the first one is. The test is marked as expected to fail for now, meaning the underlying crash has not been fixed in this commit.
Treat this as a known unpatched crash/DOS bug. Locate and fix the marshalling code so it no longer treats non-head elements of the htlc_added array as independent tal objects. Run the new test to confirm the crash is resolved and remove the xfail marker once fixed. Consider whether the crash could be exploited more severely than denial of service.
Security signals we found
Denial-of-service vector: remote peer can trigger node crash by sending batched HTLCs with custom TLVs
Memory-management bug in TLV marshalling (tal object/array confusion)
Test is xfail: vulnerability is reproduced but not patched in this commit
Reported by external contributor grubles
Evidence from the diff
The new pytest test_htlc_tlv_crash exercises a memory-management bug in the HTLC-added TLV marshalling path. The comment in the test states that the marshalling code treats an array of htlc_added structures as if they were all tal-allocated objects, but only the head of the array is a tal object. When more than one HTLC with added TLVs is present, this mismatch causes a crash (likely a use-after-free or double-free). The test sets up a three-node line graph, installs a plugin that adds a custom TLV, forces slow commitment timing to encourage batching, sends two simultaneous payments, and asserts behavior. It is decorated with @pytest.mark.xfail(strict=True), so the commit only introduces the regression test, not the fix.
Changed components
Core Lightning HTLC-added TLV marshalling codePayment/HTLC handling pathPlugin TLV processing (htlc_accepted-customtlv.py)Inspect captured patch +34 / −0
diff --git a/tests/test_pay.py b/tests/test_pay.py
index 62420c23..1d3b9353 100644
--- a/tests/test_pay.py
+++ b/tests/test_pay.py
@@ -7014,3 +7014,37 @@ def test_sendonion_sendpay(node_factory, bitcoind):
invoice = only_one(l3.rpc.listinvoices("inv")["invoices"])
# the receive amount should be exact
assert invoice["amount_received_msat"] == Millisatoshi(total_amount)
+
+
+@pytest.mark.xfail(strict=True)
+def test_htlc_tlv_crash(node_factory):
+ """Marshalling code treated an array of htlc_added as if they were tal objects, but only the head is a tal object so if we have more than one, BOOM!"""
+ plugin = os.path.join(os.path.dirname(__file__), 'plugins/htlc_accepted-customtlv.py')
+ # To crash, we need TWO added htlcs at once. Try to force batching!
+ l1, l2, l3 = node_factory.line_graph(3, opts=[{},
+ {'commit-time': 10000, 'plugin': plugin},
+ {'plugin': plugin}],
+ wait_for_announce=True)
+
+ single_tlv = "fe00010001012a" # represents type: 65537, lenght: 1, value: 42
+ l2.rpc.setcustomtlvs(tlvs=single_tlv)
+
+ route = [{'amount_msat': 101,
+ 'id': l2.info['id'],
+ 'delay': 16,
+ 'channel': first_scid(l1, l2),
+ },
+ {'amount_msat': 100,
+ 'id': l3.info['id'],
+ 'delay': 10,
+ 'channel': first_scid(l2, l3)
+ }]
+
+ # Amount must be nonzero!
+ inv1 = l3.rpc.invoice(100, "inv1", "inv1")
+ inv2 = l3.rpc.invoice(100, "inv2", "inv2")
+ l1.rpc.sendpay(route, inv1['payment_hash'], payment_secret=inv1['payment_secret'])
+ l1.rpc.sendpay(route, inv2['payment_hash'], payment_secret=inv2['payment_secret'])
+
+ l1.rpc.waitsendpay(inv1['payment_hash'], TIMEOUT)
+ l1.rpc.waitsendpay(inv2['payment_hash'], TIMEOUT)
Why this scored 64/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.