Skip to content

Fuzz process onion failure #3683

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

Merged
Merged
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
1 change: 1 addition & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
hfuzz_target
target
hfuzz_workspace
corpus
1 change: 1 addition & 0 deletions fuzz/src/bin/gen_target.sh
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ GEN_TEST bolt11_deser
GEN_TEST onion_message
GEN_TEST peer_crypt
GEN_TEST process_network_graph
GEN_TEST process_onion_failure
GEN_TEST refund_deser
GEN_TEST router
GEN_TEST zbase32
120 changes: 120 additions & 0 deletions fuzz/src/bin/process_onion_failure_target.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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_attr(rustfmt, rustfmt_skip)]

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

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

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

extern crate lightning_fuzz;
use lightning_fuzz::process_onion_failure::*;

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

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

#[cfg(feature = "libfuzzer_fuzz")]
#[macro_use] extern crate libfuzzer_sys;
#[cfg(feature = "libfuzzer_fuzz")]
fuzz_target!(|data: &[u8]| {
process_onion_failure_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();
process_onion_failure_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];
process_onion_failure_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/process_onion_failure") {
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 || {
process_onion_failure_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/lib.rs
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ pub mod onion_hop_data;
pub mod onion_message;
pub mod peer_crypt;
pub mod process_network_graph;
pub mod process_onion_failure;
pub mod refund_deser;
pub mod router;
pub mod zbase32;
135 changes: 135 additions & 0 deletions fuzz/src/process_onion_failure.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use std::sync::Arc;

use bitcoin::{
key::Secp256k1,
secp256k1::{PublicKey, SecretKey},
};
use lightning::{
blinded_path::BlindedHop,
ln::{
channelmanager::{HTLCSource, PaymentId},
msgs::OnionErrorPacket,
},
routing::router::{BlindedTail, Path, RouteHop, TrampolineHop},
types::features::{ChannelFeatures, NodeFeatures},
util::logger::Logger,
};

// Imports that need to be added manually
use crate::utils::test_logger::{self};

/// Actual fuzz test, method signature and name are fixed
fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) {
let mut read_pos = 0;
macro_rules! get_slice {
($len: expr) => {{
let slice_len = $len as usize;
if data.len() < read_pos + slice_len {
return;
}
read_pos += slice_len;
&data[read_pos - slice_len..read_pos]
}};
}

macro_rules! get_u16 {
() => {
match get_slice!(2).try_into() {
Ok(val) => u16::from_be_bytes(val),
Err(_) => return,
}
};
}

macro_rules! get_bool {
() => {
get_slice!(1)[0] & 1 != 0
};
}

fn usize_to_32_bytes(input: usize) -> [u8; 32] {
let mut bytes = [0u8; 32];
let input_bytes = input.to_be_bytes();
bytes[..input_bytes.len()].copy_from_slice(&input_bytes);
bytes
}

fn usize_to_pubkey(input: usize) -> PublicKey {
let bytes = usize_to_32_bytes(1 + input);

let secp_ctx = Secp256k1::new();
let secret_key = SecretKey::from_slice(&bytes).unwrap();
secret_key.public_key(&secp_ctx)
}

let secp_ctx = Secp256k1::new();
let logger: Arc<dyn Logger> = Arc::new(test_logger::TestLogger::new("".to_owned(), out));

let session_priv = SecretKey::from_slice(&usize_to_32_bytes(213127)).unwrap();
let payment_id = PaymentId(usize_to_32_bytes(232299));

let mut hops = Vec::<RouteHop>::new();
let hop_count = get_slice!(1)[0] as usize % 30;
for i in 0..hop_count {
hops.push(RouteHop {
pubkey: usize_to_pubkey(i),
node_features: NodeFeatures::empty(),
short_channel_id: i as u64,
channel_features: ChannelFeatures::empty(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't bother setting the features as they aren't used for the onion failure processing. Mainly because it seemed not straight-fwd to do it.

fee_msat: 0,
cltv_expiry_delta: 0,
maybe_announced_channel: false,
});
}

let blinded_tail = if get_bool!() {
let mut trampoline_hops = Vec::<TrampolineHop>::new();
let trampoline_hop_count = get_slice!(1)[0] as usize % 30;
for i in 0..trampoline_hop_count {
trampoline_hops.push(TrampolineHop {
pubkey: usize_to_pubkey(1000 + i),
node_features: NodeFeatures::empty(),
fee_msat: 0,
cltv_expiry_delta: 0,
});
}
let mut blinded_hops = Vec::<BlindedHop>::new();
let blinded_hop_count = get_slice!(1)[0] as usize % 30;
for i in 0..blinded_hop_count {
blinded_hops.push(BlindedHop {
blinded_node_id: usize_to_pubkey(2000 + i),
encrypted_payload: get_slice!(get_u16!()).to_vec(),
});
}
Some(BlindedTail {
trampoline_hops,
hops: blinded_hops,
blinding_point: usize_to_pubkey(64354334),
excess_final_cltv_expiry_delta: 0,
final_value_msat: 0,
})
} else {
None
};

let path = Path { hops, blinded_tail };

let htlc_source =
HTLCSource::OutboundRoute { path, session_priv, first_hop_htlc_msat: 0, payment_id };

let failure_len = get_u16!();
let encrypted_packet = OnionErrorPacket { data: get_slice!(failure_len).into() };
Comment on lines +120 to +121
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we replace this with just reading the OnionErrorPacket using standard Readable from the remainder of the stream, rather than reading a length prefix?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there is a standard readable defined for OnionErrorPacket?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I guess we'd have to read an DecodedOnionErrorPacket?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read that, and then write it out again to get a raw byte array. I'm not sure if it makes it better.


lightning::ln::process_onion_failure(&secp_ctx, &logger, &htlc_source, encrypted_packet);
}

/// Method that needs to be added manually, {name}_test
pub fn process_onion_failure_test<Out: test_logger::Output>(data: &[u8], out: Out) {
do_test(data, out);
}

/// Method that needs to be added manually, {name}_run
#[no_mangle]
pub extern "C" fn process_onion_failure_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/targets.h
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ 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);
void process_onion_failure_run(const unsigned char* data, size_t data_len);
void refund_deser_run(const unsigned char* data, size_t data_len);
void router_run(const unsigned char* data, size_t data_len);
void zbase32_run(const unsigned char* data, size_t data_len);
78 changes: 43 additions & 35 deletions lightning/src/ln/channelmanager.rs
Original file line number Diff line number Diff line change
@@ -409,27 +409,6 @@ pub enum BlindedFailure {
FromBlindedNode,
}

/// Tracks the inbound corresponding to an outbound HTLC
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub(crate) struct HTLCPreviousHopData {
// Note that this may be an outbound SCID alias for the associated channel.
short_channel_id: u64,
user_channel_id: Option<u128>,
htlc_id: u64,
incoming_packet_shared_secret: [u8; 32],
phantom_shared_secret: Option<[u8; 32]>,
blinded_failure: Option<BlindedFailure>,
channel_id: ChannelId,

// These fields are consumed by `claim_funds_from_hop()` when updating a force-closed backwards
// channel with a preimage provided by the forward channel.
outpoint: OutPoint,
counterparty_node_id: Option<PublicKey>,
/// Used to preserve our backwards channel by failing back in case an HTLC claim in the forward
/// channel remains unconfirmed for too long.
cltv_expiry: Option<u32>,
}

#[derive(PartialEq, Eq)]
enum OnionPayload {
/// Indicates this incoming onion payload is for the purpose of paying an invoice.
@@ -674,21 +653,50 @@ impl_writeable_tlv_based_enum!(SentHTLCId,
},
);


/// Tracks the inbound corresponding to an outbound HTLC
#[allow(clippy::derive_hash_xor_eq)] // Our Hash is faithful to the data, we just don't have SecretKey::hash
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum HTLCSource {
PreviousHopData(HTLCPreviousHopData),
OutboundRoute {
path: Path,
session_priv: SecretKey,
/// Technically we can recalculate this from the route, but we cache it here to avoid
/// doing a double-pass on route when we get a failure back
first_hop_htlc_msat: u64,
payment_id: PaymentId,
},
mod fuzzy_channelmanager {
use super::*;

/// Tracks the inbound corresponding to an outbound HTLC
#[allow(clippy::derive_hash_xor_eq)] // Our Hash is faithful to the data, we just don't have SecretKey::hash
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HTLCSource {
PreviousHopData(HTLCPreviousHopData),
OutboundRoute {
path: Path,
session_priv: SecretKey,
/// Technically we can recalculate this from the route, but we cache it here to avoid
/// doing a double-pass on route when we get a failure back
first_hop_htlc_msat: u64,
payment_id: PaymentId,
},
}

/// Tracks the inbound corresponding to an outbound HTLC
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct HTLCPreviousHopData {
// Note that this may be an outbound SCID alias for the associated channel.
pub short_channel_id: u64,
pub user_channel_id: Option<u128>,
pub htlc_id: u64,
pub incoming_packet_shared_secret: [u8; 32],
pub phantom_shared_secret: Option<[u8; 32]>,
pub blinded_failure: Option<BlindedFailure>,
pub channel_id: ChannelId,

// These fields are consumed by `claim_funds_from_hop()` when updating a force-closed backwards
// channel with a preimage provided by the forward channel.
pub outpoint: OutPoint,
pub counterparty_node_id: Option<PublicKey>,
/// Used to preserve our backwards channel by failing back in case an HTLC claim in the forward
/// channel remains unconfirmed for too long.
pub cltv_expiry: Option<u32>,
}
}
#[cfg(fuzzing)]
pub use self::fuzzy_channelmanager::*;
#[cfg(not(fuzzing))]
pub(crate) use self::fuzzy_channelmanager::*;

#[allow(clippy::derive_hash_xor_eq)] // Our Hash is faithful to the data, we just don't have SecretKey::hash
impl core::hash::Hash for HTLCSource {
fn hash<H: core::hash::Hasher>(&self, hasher: &mut H) {
3 changes: 3 additions & 0 deletions lightning/src/ln/mod.rs
Original file line number Diff line number Diff line change
@@ -51,6 +51,9 @@ pub use onion_utils::create_payment_onion;
// without the node parameter being mut. This is incorrect, and thus newer rustcs will complain
// about an unnecessary mut. Thus, we silence the unused_mut warning in two test modules below.

#[cfg(fuzzing)]
pub use onion_utils::process_onion_failure;

#[cfg(test)]
#[allow(unused_mut)]
pub mod bolt11_payment_tests;
Loading