Skip to content

Commit

Permalink
Merge pull request lightningdevkit#3054 from TheBlueMatt/2024-04-fuzz…
Browse files Browse the repository at this point in the history
…-bolt11

Add fuzzing coverage for BOLT11 invoice deserialization
  • Loading branch information
TheBlueMatt authored May 8, 2024
2 parents 676f242 + 8db1226 commit 806b7f0
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 124 deletions.
3 changes: 0 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,10 @@ jobs:
- name: Pin the regex dependency
run: |
cd fuzz && cargo update -p regex --precise "1.9.6" --verbose && cd ..
cd lightning-invoice/fuzz && cargo update -p regex --precise "1.9.6" --verbose
- name: Sanity check fuzz targets on Rust ${{ env.TOOLCHAIN }}
run: cd fuzz && RUSTFLAGS="--cfg=fuzzing" cargo test --verbose --color always
- name: Run fuzzers
run: cd fuzz && ./ci-fuzz.sh && cd ..
- name: Run lightning-invoice fuzzers
run: cd lightning-invoice/fuzz && RUSTFLAGS="--cfg=fuzzing" cargo test --verbose && ./ci-fuzz.sh

linting:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ stdin_fuzz = []

[dependencies]
lightning = { path = "../lightning", features = ["regex", "hashbrown", "_test_utils"] }
lightning-invoice = { path = "../lightning-invoice" }
lightning-rapid-gossip-sync = { path = "../lightning-rapid-gossip-sync" }
bitcoin = { version = "0.30.2", features = ["secp-lowmemory"] }
hex = { package = "hex-conservative", version = "0.1.1", default-features = false }
Expand Down
113 changes: 113 additions & 0 deletions fuzz/src/bin/bolt11_deser_target.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

// This file is auto-generated by gen_target.sh based on target_template.txt
// To modify it, modify target_template.txt and run gen_target.sh instead.

#![cfg_attr(feature = "libfuzzer_fuzz", no_main)]

#[cfg(not(fuzzing))]
compile_error!("Fuzz targets need cfg=fuzzing");

extern crate lightning_fuzz;
use lightning_fuzz::bolt11_deser::*;

#[cfg(feature = "afl")]
#[macro_use] extern crate afl;
#[cfg(feature = "afl")]
fn main() {
fuzz!(|data| {
bolt11_deser_run(data.as_ptr(), data.len());
});
}

#[cfg(feature = "honggfuzz")]
#[macro_use] extern crate honggfuzz;
#[cfg(feature = "honggfuzz")]
fn main() {
loop {
fuzz!(|data| {
bolt11_deser_run(data.as_ptr(), data.len());
});
}
}

#[cfg(feature = "libfuzzer_fuzz")]
#[macro_use] extern crate libfuzzer_sys;
#[cfg(feature = "libfuzzer_fuzz")]
fuzz_target!(|data: &[u8]| {
bolt11_deser_run(data.as_ptr(), data.len());
});

#[cfg(feature = "stdin_fuzz")]
fn main() {
use std::io::Read;

let mut data = Vec::with_capacity(8192);
std::io::stdin().read_to_end(&mut data).unwrap();
bolt11_deser_run(data.as_ptr(), data.len());
}

#[test]
fn run_test_cases() {
use std::fs;
use std::io::Read;
use lightning_fuzz::utils::test_logger::StringBuffer;

use std::sync::{atomic, Arc};
{
let data: Vec<u8> = vec![0];
bolt11_deser_run(data.as_ptr(), data.len());
}
let mut threads = Vec::new();
let threads_running = Arc::new(atomic::AtomicUsize::new(0));
if let Ok(tests) = fs::read_dir("test_cases/bolt11_deser") {
for test in tests {
let mut data: Vec<u8> = Vec::new();
let path = test.unwrap().path();
fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap();
threads_running.fetch_add(1, atomic::Ordering::AcqRel);

let thread_count_ref = Arc::clone(&threads_running);
let main_thread_ref = std::thread::current();
threads.push((path.file_name().unwrap().to_str().unwrap().to_string(),
std::thread::spawn(move || {
let string_logger = StringBuffer::new();

let panic_logger = string_logger.clone();
let res = if ::std::panic::catch_unwind(move || {
bolt11_deser_test(&data, panic_logger);
}).is_err() {
Some(string_logger.into_string())
} else { None };
thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel);
main_thread_ref.unpark();
res
})
));
while threads_running.load(atomic::Ordering::Acquire) > 32 {
std::thread::park();
}
}
}
let mut failed_outputs = Vec::new();
for (test, thread) in threads.drain(..) {
if let Some(output) = thread.join().unwrap() {
println!("\nOutput of {}:\n{}\n", test, output);
failed_outputs.push(test);
}
}
if !failed_outputs.is_empty() {
println!("Test cases which failed: ");
for case in failed_outputs {
println!("{}", case);
}
panic!();
}
}
1 change: 1 addition & 0 deletions fuzz/src/bin/gen_target.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ GEN_TEST full_stack
GEN_TEST invoice_deser
GEN_TEST invoice_request_deser
GEN_TEST offer_deser
GEN_TEST bolt11_deser
GEN_TEST onion_message
GEN_TEST peer_crypt
GEN_TEST process_network_graph
Expand Down
75 changes: 75 additions & 0 deletions fuzz/src/bolt11_deser.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// This file is Copyright its original authors, visible in version control
// history.
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.

use crate::utils::test_logger;
use bitcoin::bech32::{u5, FromBase32, ToBase32};
use bitcoin::secp256k1::{Secp256k1, SecretKey};
use lightning_invoice::{
Bolt11Invoice, RawBolt11Invoice, RawDataPart, RawHrp, RawTaggedField, TaggedField,
};
use std::str::FromStr;

#[inline]
pub fn do_test<Out: test_logger::Output>(data: &[u8], _out: Out) {
// Read a fake HRP length byte
let hrp_len = std::cmp::min(*data.get(0).unwrap_or(&0) as usize, data.len());
if let Ok(s) = std::str::from_utf8(&data[..hrp_len]) {
let hrp = match RawHrp::from_str(s) {
Ok(hrp) => hrp,
Err(_) => return,
};
let bech32 =
data.iter().skip(hrp_len).map(|x| u5::try_from_u8(x % 32).unwrap()).collect::<Vec<_>>();
let invoice_data = match RawDataPart::from_base32(&bech32) {
Ok(invoice) => invoice,
Err(_) => return,
};

// Our data encoding is not worse than the input
assert!(invoice_data.to_base32().len() <= bech32.len());

// Our data serialization is loss-less
assert_eq!(
RawDataPart::from_base32(&invoice_data.to_base32())
.expect("faild parsing out own encoding"),
invoice_data
);

if invoice_data.tagged_fields.iter().any(|field| {
matches!(field, RawTaggedField::KnownSemantics(TaggedField::PayeePubKey(_)))
}) {
// We could forge a signature using the fact that signing is insecure in fuzz mode, but
// easier to just skip and rely on the fact that no-PayeePubKey invoices do pubkey
// recovery
return;
}

let raw_invoice = RawBolt11Invoice { hrp, data: invoice_data };
let signed_raw_invoice = match raw_invoice.sign(|hash| {
let private_key = SecretKey::from_slice(&[42; 32]).unwrap();
Ok::<_, ()>(Secp256k1::new().sign_ecdsa_recoverable(hash, &private_key))
}) {
Ok(inv) => inv,
Err(_) => return,
};

if let Ok(invoice) = Bolt11Invoice::from_signed(signed_raw_invoice) {
invoice.amount_milli_satoshis();
}
}
}

pub fn bolt11_deser_test<Out: test_logger::Output>(data: &[u8], out: Out) {
do_test(data, out);
}

#[no_mangle]
pub extern "C" fn bolt11_deser_run(data: *const u8, datalen: usize) {
do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {});
}
1 change: 1 addition & 0 deletions fuzz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod indexedmap;
pub mod invoice_deser;
pub mod invoice_request_deser;
pub mod offer_deser;
pub mod bolt11_deser;
pub mod onion_message;
pub mod peer_crypt;
pub mod process_network_graph;
Expand Down
1 change: 1 addition & 0 deletions fuzz/targets.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ void full_stack_run(const unsigned char* data, size_t data_len);
void invoice_deser_run(const unsigned char* data, size_t data_len);
void invoice_request_deser_run(const unsigned char* data, size_t data_len);
void offer_deser_run(const unsigned char* data, size_t data_len);
void bolt11_deser_run(const unsigned char* data, size_t data_len);
void onion_message_run(const unsigned char* data, size_t data_len);
void peer_crypt_run(const unsigned char* data, size_t data_len);
void process_network_graph_run(const unsigned char* data, size_t data_len);
Expand Down
2 changes: 0 additions & 2 deletions lightning-invoice/fuzz/.gitignore

This file was deleted.

28 changes: 0 additions & 28 deletions lightning-invoice/fuzz/Cargo.toml

This file was deleted.

19 changes: 0 additions & 19 deletions lightning-invoice/fuzz/ci-fuzz.sh

This file was deleted.

69 changes: 0 additions & 69 deletions lightning-invoice/fuzz/fuzz_targets/serde_data_part.rs

This file was deleted.

10 changes: 7 additions & 3 deletions lightning-invoice/src/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ mod hrp_sm {
}

impl States {
fn next_state(&self, read_symbol: char) -> Result<States, super::Bolt11ParseError> {
fn next_state(&self, read_byte: u8) -> Result<States, super::Bolt11ParseError> {
let read_symbol = match char::from_u32(read_byte.into()) {
Some(symb) if symb.is_ascii() => symb,
_ => return Err(super::Bolt11ParseError::MalformedHRP),
};
match *self {
States::Start => {
if read_symbol == 'l' {
Expand Down Expand Up @@ -119,7 +123,7 @@ mod hrp_sm {
*range = Some(new_range);
}

fn step(&mut self, c: char) -> Result<(), super::Bolt11ParseError> {
fn step(&mut self, c: u8) -> Result<(), super::Bolt11ParseError> {
let next_state = self.state.next_state(c)?;
match next_state {
States::ParseCurrencyPrefix => {
Expand Down Expand Up @@ -158,7 +162,7 @@ mod hrp_sm {

pub fn parse_hrp(input: &str) -> Result<(&str, &str, &str), super::Bolt11ParseError> {
let mut sm = StateMachine::new();
for c in input.chars() {
for c in input.bytes() {
sm.step(c)?;
}

Expand Down
1 change: 1 addition & 0 deletions rustfmt_excluded_files
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
./fuzz/src/bech32_parse.rs
./fuzz/src/bin/base32_target.rs
./fuzz/src/bin/bech32_parse_target.rs
./fuzz/src/bin/bolt11_deser_target.rs
./fuzz/src/bin/chanmon_consistency_target.rs
./fuzz/src/bin/chanmon_deser_target.rs
./fuzz/src/bin/fromstr_to_netaddress_target.rs
Expand Down

0 comments on commit 806b7f0

Please sign in to comment.