Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Improve XCM fuzzer #6190

Merged
merged 10 commits into from
Feb 23, 2023
5 changes: 5 additions & 0 deletions xcm/xcm-simulator/fuzzer/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
hfuzz_target
hfuzz_workspace
cargo
coverage
ccov.zip
1 change: 1 addition & 0 deletions xcm/xcm-simulator/fuzzer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
codec = { package = "parity-scale-codec", version = "3.0.0" }
honggfuzz = "0.5.55"
arbitrary = "1.2.0"
scale-info = { version = "2.1.2", features = ["derive"] }

frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" }
Expand Down
34 changes: 34 additions & 0 deletions xcm/xcm-simulator/fuzzer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# XCM Simulator Fuzzer

This project will fuzz-test the XCM simulator. It can catch reachable panics, timeouts as well as integer overflows and underflows.

## Install dependencies

```
cargo install honggfuzz
```

## Run the fuzzer

```
cargo hfuzz run xcm-fuzzer
louismerlin marked this conversation as resolved.
Show resolved Hide resolved
```

## Run a single input

```
cargo hfuzz run-debug xcm-fuzzer hfuzz_workspace/xcm-fuzzer/fuzzer_input_file
```

## Generate coverage

In this directory, run these four commands:

```
RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" CARGO_INCREMENTAL=0 SKIP_WASM_BUILD=1 CARGO_HOME=./cargo cargo build
../../../target/debug/xcm-fuzzer hfuzz_workspace/xcm-fuzzer/input/
zip -0 ccov.zip `find ../../../target/ \( -name "*.gc*" -o -name "test-*.gc*" \) -print`
grcov ccov.zip -s ../../../ -t html --llvm --branch --ignore-not-existing -o ./coverage
```

The code coverage will be in `./coverage/index.html`.
143 changes: 105 additions & 38 deletions xcm/xcm-simulator/fuzzer/src/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ mod parachain;
mod relay_chain;

use codec::DecodeLimit;
use polkadot_core_primitives::AccountId;
use polkadot_parachain::primitives::Id as ParaId;
use sp_runtime::traits::AccountIdConversion;
use xcm_simulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain, TestExt};

use frame_support::assert_ok;
use xcm::{latest::prelude::*, MAX_XCM_DECODE_DEPTH};

pub const ALICE: sp_runtime::AccountId32 = sp_runtime::AccountId32::new([0u8; 32]);
use arbitrary::{Arbitrary, Error, Unstructured};

pub const INITIAL_BALANCE: u128 = 1_000_000_000;

decl_test_parachain! {
Expand All @@ -46,6 +48,15 @@ decl_test_parachain! {
}
}

decl_test_parachain! {
pub struct ParaC {
Runtime = parachain::Runtime,
XcmpMessageHandler = parachain::MsgQueue,
DmpMessageHandler = parachain::MsgQueue,
new_ext = para_ext(3),
}
}

decl_test_relay_chain! {
pub struct Relay {
Runtime = relay_chain::Runtime,
Expand All @@ -60,10 +71,35 @@ decl_test_network! {
parachains = vec![
(1, ParaA),
(2, ParaB),
(3, ParaC),
],
}
}

// An XCM message that will be generated by the fuzzer through the Arbitrary trait
struct XcmMessage {
// Source chain
source: u32,
// Destination chain
destination: u32,
// XCM message
message: Xcm<()>,
}

impl<'a> Arbitrary<'a> for XcmMessage {
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self, Error> {
let source: u32 = u.arbitrary()?;
let destination: u32 = u.arbitrary()?;
let mut encoded_message: &[u8] = u.arbitrary()?;
if let Ok(message) =
DecodeLimit::decode_with_depth_limit(MAX_XCM_DECODE_DEPTH, &mut encoded_message)
{
return Ok(XcmMessage { source, destination, message })
}
mrcnski marked this conversation as resolved.
Show resolved Hide resolved
Err(Error::IncorrectFormat)
}
}

pub fn para_account_id(id: u32) -> relay_chain::AccountId {
ParaId::from(id).into_account_truncating()
}
Expand All @@ -73,9 +109,11 @@ pub fn para_ext(para_id: u32) -> sp_io::TestExternalities {

let mut t = frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();

pallet_balances::GenesisConfig::<Runtime> { balances: vec![(ALICE, INITIAL_BALANCE)] }
.assimilate_storage(&mut t)
.unwrap();
pallet_balances::GenesisConfig::<Runtime> {
balances: (0..6).map(|i| ([i; 32].into(), INITIAL_BALANCE)).collect(),
}
.assimilate_storage(&mut t)
.unwrap();

let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| {
Expand All @@ -90,11 +128,13 @@ pub fn relay_ext() -> sp_io::TestExternalities {

let mut t = frame_system::GenesisConfig::default().build_storage::<Runtime>().unwrap();

pallet_balances::GenesisConfig::<Runtime> {
balances: vec![(ALICE, INITIAL_BALANCE), (para_account_id(1), INITIAL_BALANCE)],
}
.assimilate_storage(&mut t)
.unwrap();
let mut balances: Vec<(AccountId, u128)> = vec![];
balances.append(&mut (1..=3).map(|i| (para_account_id(i), INITIAL_BALANCE)).collect());
balances.append(&mut (0..6).map(|i| ([i; 32].into(), INITIAL_BALANCE)).collect());

pallet_balances::GenesisConfig::<Runtime> { balances }
.assimilate_storage(&mut t)
.unwrap();

let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| System::set_block_number(1));
Expand All @@ -104,46 +144,70 @@ pub fn relay_ext() -> sp_io::TestExternalities {
pub type RelayChainPalletXcm = pallet_xcm::Pallet<relay_chain::Runtime>;
pub type ParachainPalletXcm = pallet_xcm::Pallet<parachain::Runtime>;

fn run_one_input(mut data: &[u8]) {
fn run_input(xcm_messages: [XcmMessage; 5]) {
MockNet::reset();
if let Ok(m) = Xcm::decode_all_with_depth_limit(MAX_XCM_DECODE_DEPTH, &mut data) {
#[cfg(not(fuzzing))]
{
println!("Executing message {:?}", m);

#[cfg(not(fuzzing))]
println!();
KiChjang marked this conversation as resolved.
Show resolved Hide resolved

for xcm_message in xcm_messages {
if xcm_message.source % 4 == 0 {
// We get the destination for the message
let parachain_id = (xcm_message.destination % 3) + 1;
let destination: MultiLocation = Parachain(parachain_id).into();
#[cfg(not(fuzzing))]
{
println!(" source: Relay Chain");
println!(" destination: Parachain {parachain_id}");
println!(" message: {:?}", xcm_message.message);
}
Relay::execute_with(|| {
assert_ok!(RelayChainPalletXcm::send_xcm(Here, destination, xcm_message.message));
})
} else {
// We get the source's execution method
let execute_with = match xcm_message.source % 4 {
1 => ParaA::execute_with,
2 => ParaB::execute_with,
_ => ParaC::execute_with,
};
// We get the destination for the message
let destination: MultiLocation = match xcm_message.destination % 4 {
n @ 1..=3 => (Parent, Parachain(n)).into(),
_ => Parent.into(),
};
#[cfg(not(fuzzing))]
{
let destination_str = match xcm_message.destination % 4 {
n @ 1..=3 => format!("Parachain {n}"),
_ => "Relay Chain".to_string(),
};
println!(" source: Parachain {}", xcm_message.source % 4);
println!(" destination: {}", destination_str);
println!(" message: {:?}", xcm_message.message);
}
// We execute the message with the appropriate source and destination
execute_with(|| {
assert_ok!(ParachainPalletXcm::send_xcm(Here, destination, xcm_message.message));
});
}
ParaA::execute_with(|| {
assert_ok!(ParachainPalletXcm::send_xcm(Here, Parent, m));
});
Relay::execute_with(|| {});
#[cfg(not(fuzzing))]
println!();
}
Relay::execute_with(|| {});
}

fn main() {
#[cfg(fuzzing)]
{
loop {
honggfuzz::fuzz!(|data: &[u8]| {
run_one_input(data);
});
honggfuzz::fuzz!(|xcm_messages: [XcmMessage; 5]| {
run_input(xcm_messages);
})
}
}
#[cfg(not(fuzzing))]
{
//This code path can be used to generate a line-code coverage report in HTML
//that depicts which lines are executed by at least one input in the current fuzzing queue.
//To generate this code coverage report, run the following commands:
/*
```
export CARGO_INCREMENTAL=0
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
export RUSTDOCFLAGS="-Cpanic=abort"
rustup override set nightly
SKIP_WASM_BUILD=1 cargo build
./xcm/xcm-simulator/fuzzer/target/debug/xcm-fuzzer hfuzz_workspace/xcm-fuzzer/input
zip -0 ccov.zip `find ../../target/debug \( -name "*.gc*" -o -name "test-*.gc*" \) -print`
grcov ccov.zip -s / -t html --llvm --branch --ignore-not-existing -o ../../target/debug/coverage/
```
*/
use std::{env, fs, fs::File, io::Read};
let args: Vec<_> = env::args().collect();
let md = fs::metadata(&args[1]).unwrap();
Expand All @@ -152,15 +216,18 @@ fn main() {
.unwrap()
.map(|x| x.unwrap().path().to_str().unwrap().to_string())
.collect::<Vec<String>>(),
false => (&args[1..]).to_vec(),
false => (args[1..]).to_vec(),
};
println!("All_files {:?}", all_files);
for argument in all_files {
println!("Now doing file {:?}", argument);
let mut buffer: Vec<u8> = Vec::new();
let mut f = File::open(argument).unwrap();
f.read_to_end(&mut buffer).unwrap();
run_one_input(&buffer.as_slice());
let mut unstructured = Unstructured::new(&buffer);
if let Ok(xcm_messages) = unstructured.arbitrary() {
run_input(xcm_messages);
}
}
}
}