-
Notifications
You must be signed in to change notification settings - Fork 403
avoid panic when truncating payer_note in UTF-8 code point #3750
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
avoid panic when truncating payer_note in UTF-8 code point #3750
Conversation
I've assigned @wpaulino as a reviewer! |
06a6e0e
to
92f1bf4
Compare
92f1bf4
to
20c5ac3
Compare
🔔 1st Reminder Hey @wpaulino! This PR has been waiting for your review. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly questions to complete my understanding of this part.
20c5ac3
to
d6886c5
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #3750 +/- ##
==========================================
+ Coverage 89.13% 89.38% +0.24%
==========================================
Files 157 157
Lines 123851 125812 +1961
Branches 123851 125812 +1961
==========================================
+ Hits 110398 112459 +2061
+ Misses 10779 10713 -66
+ Partials 2674 2640 -34 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Can keep talking in open threads about background info.
// In fuzzing its relatively challenging for the fuzzer to find cases where we have issues | ||
// in a BOLT 12 object but also have a right-sized nonce. So instead we allow any size | ||
// nonce. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we simply make InvoiceRequestFields
accessible from the unverified InvoiceRequest
when fuzzing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kinda liked the idea of running through the same flow as we do in production, including the verfiy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... but most everything is being skipped. Like we aren't adding any nonce bytes or TLV records as input to the HMAC by returning early. We could at least do that and not change verify_metadata
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm, fair, I pushed an updated version that uses more common path by having a default nonce.
`String::truncate` takes a byte index but panics if we split in the middle of a UTF-8 codepoint. Sadly, in `InvoiceRequest::fields` we want to tuncate the payer note to a maximum of 512 bytes, which may be in the middle of a UTF-8 codepoint and can cause panic. Here we iterate over the bytes in the string until we find one not in the middle of a UTF-8 codepoint and then split the string there.
d6886c5
to
a4fd137
Compare
Squashed with a cleaned-up test: $ git diff-tree -U1 d6886c5b2 a4fd1371e
diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs
index 0b0a029b3..2e399738b 100644
--- a/lightning/src/offers/invoice_request.rs
+++ b/lightning/src/offers/invoice_request.rs
@@ -3031,11 +3031,11 @@ mod tests {
let s = String::from("❤️");
- for new_len in 0..(s.len() + 5) {
- if new_len >= s.len() {
- assert_eq!(s, string_truncate_safe(s.clone(), new_len));
- } else if (3..s.len()).contains(&new_len) {
- assert_eq!("❤", string_truncate_safe(s.clone(), new_len));
- } else {
- assert_eq!("", string_truncate_safe(s.clone(), new_len));
- }
- }
+ assert_eq!(s.len(), 6);
+ assert_eq!(s, string_truncate_safe(s.clone(), 7));
+ assert_eq!(s, string_truncate_safe(s.clone(), 6));
+ assert_eq!("❤", string_truncate_safe(s.clone(), 5));
+ assert_eq!("❤", string_truncate_safe(s.clone(), 4));
+ assert_eq!("❤", string_truncate_safe(s.clone(), 3));
+ assert_eq!("", string_truncate_safe(s.clone(), 2));
+ assert_eq!("", string_truncate_safe(s.clone(), 1));
+ assert_eq!("", string_truncate_safe(s.clone(), 0)); |
// In fuzzing its relatively challenging for the fuzzer to find cases where we have issues | ||
// in a BOLT 12 object but also have a right-sized nonce. So instead we allow any size | ||
// nonce. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... but most everything is being skipped. Like we aren't adding any nonce bytes or TLV records as input to the HMAC by returning early. We could at least do that and not change verify_metadata
.
lightning/src/offers/signer.rs
Outdated
let mut ok = metadata.len() == Nonce::LENGTH + Sha256::LEN | ||
&& fixed_time_eq(&metadata[Nonce::LENGTH..], &hmac.to_byte_array()); | ||
#[cfg(fuzzing)] | ||
if metadata[0] & 1 == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this panic if the metadata is empty?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grr, rewrote it too many times...
In the next commit we attempt to verify `InvoiceRequest`s when fuzzing so that we can test fetching the `InvoiceRequestFields`, but its useful to allow the verification to succeed more often first, which we do here.
a4fd137
to
5d1a197
Compare
$ git diff-tree -U1 a4fd1371e 5d1a197cf
diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs
index d958b68b1..329b90d20 100644
--- a/lightning/src/offers/signer.rs
+++ b/lightning/src/offers/signer.rs
@@ -411,3 +411,3 @@ fn verify_metadata<T: secp256k1::Signing>(
#[cfg(fuzzing)]
- if metadata[0] & 1 == 0 {
+ if metadata.is_empty() || metadata[0] & 1 == 0 {
ok = true;
@@ -429,3 +429,3 @@ fn hmac_for_message<'a>(
- if metadata.len() < Nonce::LENGTH {
+ let nonce = if metadata.len() < Nonce::LENGTH {
// In fuzzing its relatively challenging for the fuzzer to find cases where we have issues
@@ -433,12 +433,8 @@ fn hmac_for_message<'a>(
// nonce.
- if cfg!(fuzzing) {
- return Ok(hmac);
- } else {
+ if !cfg!(fuzzing) {
return Err(());
}
- }
-
- let nonce = match Nonce::try_from(&metadata[..Nonce::LENGTH]) {
- Ok(nonce) => nonce,
- Err(_) => return Err(()),
+ Nonce::try_from(&[42; Nonce::LENGTH][..]).unwrap()
+ } else {
+ Nonce::try_from(&metadata[..Nonce::LENGTH])?
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Feel free to squash.
This should allow us to reach the panic from two commits ago from the fuzzer.
3e9d966
to
5309176
Compare
Squashed again, thanks. |
if let Ok(ver) = invoice_request.clone().verify_using_metadata(&expanded_key, secp_ctx) { | ||
// Previously we had a panic where we'd truncate the payer note possibly cutting a | ||
// Unicode character in two here, so try to fetch fields if we can validate. | ||
ver.fields() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Somehow I expected this call not here directly, but a fuzzer that indirectly hits it via channel manager. But that would be a whole different fuzz test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea, hitting something that requires eg 3 specific TLVs to line up as a part of a much, much larger target is probably an infeasible level of complexity for a fuzzer.
Backported in #3757 |
v0.1.3 - Apr 30, 2025 - "Routing Unicode in 2025" Bug Fixes ========= * `Event::InvoiceReceived` is now only generated once for each `Bolt12Invoice` received matching a pending outbound payment. Previously it would be provided each time we received an invoice, which may happen many times if the sender sends redundant messages to improve success rates (lightningdevkit#3658). * LDK's router now more fully saturates paths which are subject to HTLC maximum restrictions after the first hop. In some rare cases this can result in finding paths when it would previously spuriously decide it cannot find enough diverse paths (lightningdevkit#3707, lightningdevkit#3755). Security ======== 0.1.3 fixes a denial-of-service vulnerability which cause a crash of an LDK-based node if an attacker has access to a valid `Bolt12Offer` which the LDK-based node created. * A malicious payer which requests a BOLT 12 Invoice from an LDK-based node (via the `Bolt12InvoiceRequest` message) can cause the panic of the LDK-based node due to the way `String::truncate` handles UTF-8 codepoints. The codepath can only be reached once the received `Botlt12InvoiceRequest` has been authenticated to be based on a valid `Bolt12Offer` which the same LDK-based node issued (lightningdevkit#3747, lightningdevkit#3750).
This is #3747 with one trivial code simplification and a fuzzer update which (after a few hundred million iterations) managed to hit the bug.