Skip to content
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

Add fuzzing coverage for BOLT11 invoice deserialization #3054

Merged
merged 3 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading