diff --git a/.gitignore b/.gitignore index ca0be4ce5..1cc64e885 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .idea/ target/ +fuzz/Cargo.lock .DS_Store diff --git a/Cargo.lock b/Cargo.lock index 41cd95f0d..dd312850b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "arbitrary" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -1580,6 +1589,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_arbitrary" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" +dependencies = [ + "proc-macro2 1.0.66", + "quote 1.0.33", + "syn 2.0.28", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -6576,6 +6596,7 @@ name = "sov-accounts" version = "0.1.0" dependencies = [ "anyhow", + "arbitrary", "borsh", "clap", "jsonrpsee 0.18.2", @@ -6852,6 +6873,7 @@ name = "sov-modules-api" version = "0.1.0" dependencies = [ "anyhow", + "arbitrary", "bech32 0.9.1", "bincode", "borsh", @@ -7011,6 +7033,7 @@ name = "sov-state" version = "0.1.0" dependencies = [ "anyhow", + "arbitrary", "bcs", "borsh", "hex", diff --git a/Cargo.toml b/Cargo.toml index 55919fbd7..6f78e1b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ jmt = { git = "https://github.com/penumbra-zone/jmt", commit = "46b4b00" } # External dependencies async-trait = "0.1.71" anyhow = "1.0.68" +arbitrary = { version = "1.3.0", features = ["derive"] } borsh = { version = "0.10.3", features = ["rc", "bytes"] } # TODO: Consider replacing this serialization format # https://github.com/Sovereign-Labs/sovereign-sdk/issues/283 diff --git a/Makefile b/Makefile index 836eae355..96e05665a 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,9 @@ lint-fix: ## cargo fmt, fix and clippy check-features: ## Checks that project compiles with all combinations of features. default is not needed because we never check `cfg(default)`, we only use it as an alias. cargo hack check --workspace --feature-powerset --exclude-features default +check-fuzz: ## Checks that fuzz member compiles + $(MAKE) -C fuzz check + find-unused-deps: ## Prints unused dependencies for project. Note: requires nightly cargo udeps --all-targets --all-features diff --git a/README.md b/README.md index 4a1ece285..153799504 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,10 @@ to have adapters for almost all Data Availability Layers and LLVM-compatible pro maintain adapters for [`Risc0`](https://www.risczero.com) (a ZKVM) and [`Celestia`](https://www.celestia.org) a (DA layer). The Avail project also maintains an adapter for their DA layer, which can be found [here](https://github.com/availproject/avail-sovereign-da-adapter). +## Testing + +An implementation of LLVM's libFUZZER is available under [fuzz/README.md](./fuzz/README.md). + ## Warning The Sovereign SDK is Alpha software. It has not been audited and should not be used in production under any circumstances. diff --git a/adapters/celestia/src/shares.rs b/adapters/celestia/src/shares.rs index 44ea1b799..3f543bfca 100644 --- a/adapters/celestia/src/shares.rs +++ b/adapters/celestia/src/shares.rs @@ -278,7 +278,13 @@ impl std::error::Error for ShareParsingError {} impl NamespaceGroup { pub fn from_b64(b64: &str) -> Result { + if b64.is_empty() { + error!("Empty input"); + return Err(ShareParsingError::ErrWrongLength); + } + let mut decoded = Vec::with_capacity((b64.len() + 3) / 4 * 3); + // unsafe { decoded.set_len((b64.len() / 4 * 3)) } if let Err(err) = B64_ENGINE.decode_slice(b64, &mut decoded) { info!("Error decoding NamespaceGroup from base64: {}", err); diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 000000000..16a41c54f --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,91 @@ +[package] +name = "sovereign-sdk-fuzz" +version = "0.1.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +serde_json = "1" +tempfile = "3" +rand = "0.8" + +# Sovereign-maintained dependencies. +celestia = { path = "../adapters/celestia" } +sov-modules-api = { path = "../module-system/sov-modules-api", features = ["arbitrary", "native"] } +sov-accounts = { path = "../module-system/module-implementations/sov-accounts", features = ["arbitrary", "native"] } +sov-bank = { path = "../module-system/module-implementations/sov-bank", features = ["native"] } +sov-state = { path = "../module-system/sov-state" } + +# Prevent this from interfering with workspaces. +[workspace] +members = ["."] + +[[bin]] +name = "namespace_group_from_b64" +path = "fuzz_targets/namespace_group_from_b64.rs" +test = false +doc = false + +[[bin]] +name = "parse_address" +path = "fuzz_targets/parse_address.rs" +test = false +doc = false + +[[bin]] +name = "address_bech_32_parse_serde" +path = "fuzz_targets/address_bech_32_parse_serde.rs" +test = false +doc = false + +[[bin]] +name = "address_bech_32_try_from_bytes" +path = "fuzz_targets/address_bech_32_try_from_bytes.rs" +test = false +doc = false + +[[bin]] +name = "share_deserialize" +path = "fuzz_targets/share_deserialize.rs" +test = false +doc = false + +[[bin]] +name = "bank_call" +path = "fuzz_targets/bank_call.rs" +test = false +doc = false + +[[bin]] +name = "accounts_call" +path = "fuzz_targets/accounts_call.rs" +test = false +doc = false + +[[bin]] +name = "accounts_call_random" +path = "fuzz_targets/accounts_call_random.rs" +test = false +doc = false + +[[bin]] +name = "bank_parse_call_message" +path = "fuzz_targets/bank_parse_call_message.rs" +test = false +doc = false + +[[bin]] +name = "accounts_parse_call_message" +path = "fuzz_targets/accounts_parse_call_message.rs" +test = false +doc = false + +[[bin]] +name = "accounts_parse_call_message_random" +path = "fuzz_targets/accounts_parse_call_message_random.rs" +test = false +doc = false diff --git a/fuzz/Makefile b/fuzz/Makefile new file mode 100644 index 000000000..f0e01a141 --- /dev/null +++ b/fuzz/Makefile @@ -0,0 +1,33 @@ +.PHONY: help + +BINARIES := $(shell sed -n '/^\[\[bin\]\]/,/^$$/ { /name\s*=\s*"\(.*\)"/s//\1/p }' Cargo.toml) +PROFILE ?= debug +ARGS ?= + +help: ## Display this help message + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +check: ## Checks that fuzz member compiles + cargo check + +build-target: ## Build the target fuzz + cargo rustc --bin $(TARGET) $(ARGS) -- \ + -C debuginfo=full \ + -C debug-assertions \ + -C passes='sancov-module' \ + -C llvm-args='-sanitizer-coverage-level=3' \ + -C llvm-args='-sanitizer-coverage-inline-8bit-counters' \ + -Z sanitizer=address + +build: ## Build the fuzz targets + @for t in $(BINARIES); do \ + $(MAKE) build-target TARGET=$$t; \ + done + +targets: ## Prints all fuzz targets + @for t in $(BINARIES); do \ + echo $$t; \ + done + +run: build-target ## Run the fuzz target + ./target/$(PROFILE)/$(TARGET) -artifact_prefix=artifacts/ diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 000000000..78efd5e93 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,66 @@ +# LLVM's libFuzzer + +This implementation is built upon [libfuzzer-sys](https://crates.io/crates/libfuzzer-sys). For more information, check [LLVM](https://llvm.org/docs/LibFuzzer.html) documentation. + +## Build + +To build the fuzz target, run the following command: + +```sh +make build +``` + +You can build in release mode via: + +```sh +make build ARGS=--release +``` + +Some special parameters are required to build the fuzz target. As example, let's build the `namespace_group_from_b64` fuzz target: + +```sh +cargo rustc --bin namespace_group_from_b64 \ + --manifest-path fuzz/Cargo.toml -- \ + -C debuginfo=full \ + -C debug-assertions \ + -C passes='sancov-module' \ + -C llvm-args='-sanitizer-coverage-level=3' \ + -C llvm-args='-sanitizer-coverage-inline-8bit-counters' \ + -Z sanitizer=address +``` + +We don't default these options as they depend on the `rustc` version and might change in the future. For the list of available targets, check [Cargo.toml](./fuzz/Cargo.toml) under the `bin` section. We are currently not using optimized binaries as it might impact on how rocksdb is built. If you want to activate optimization, add `--release` after `rustc`. + +Unfortunately, rustc doesn't support the `--bins` argument to build multiple binaries with custom compiler directives. We have to build every target individually. Below is a convenience [sed](https://www.gnu.org/software/sed/) script to build all targets. + +```sh +for t in `sed -n '/^\[\[bin\]\]/,/^$/ { /name\s*=\s*"\(.*\)"/s//\1/p }' fuzz/Cargo.toml` ; do cargo rustc --bin $t --manifest-path fuzz/Cargo.toml -- -C debuginfo=full -C debug-assertions -C passes='sancov-module' -C llvm-args='-sanitizer-coverage-level=3' -C llvm-args='-sanitizer-coverage-inline-8bit-counters' -Z sanitizer=address ; done +``` + +## Run + +Here is a sample command to fuzz a `namespace_group_from_b64`: + +```sh +make run TARGET=namespace_group_from_b64 +``` + +To run in release mode: + +```sh +make run TARGET=namespace_group_from_b64 PROFILE=release +``` + +To list the available targets, run: + +```sh +make targets +``` + +Once built, you can run the targets under the `fuzz/target/` directory. + +```sh +./fuzz/target/debug/namespace_group_from_b64 +``` + +It will run the fuzz until you interrupt the command (i.e. `CTRL-C`), and will record crashes under `fuzz/artifacts/*/crash-*`. If you find a crash, please report a new [bug](https://github.com/Sovereign-Labs/sovereign-sdk/issues/new?assignees=&labels=&projects=&template=bug_report.md&title=). diff --git a/fuzz/artifacts/accounts_call/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709 b/fuzz/artifacts/accounts_call/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709 new file mode 100644 index 000000000..e69de29bb diff --git a/fuzz/artifacts/bank_call/crash-55fd4bd554ac3dcc6a2a2719335bd95869b9f6f5 b/fuzz/artifacts/bank_call/crash-55fd4bd554ac3dcc6a2a2719335bd95869b9f6f5 new file mode 100644 index 000000000..f871261f0 Binary files /dev/null and b/fuzz/artifacts/bank_call/crash-55fd4bd554ac3dcc6a2a2719335bd95869b9f6f5 differ diff --git a/fuzz/artifacts/crash-03ff9dbf9c64fb7c125f0aec0b8b80a972907eb8 b/fuzz/artifacts/crash-03ff9dbf9c64fb7c125f0aec0b8b80a972907eb8 new file mode 100644 index 000000000..21499eb41 --- /dev/null +++ b/fuzz/artifacts/crash-03ff9dbf9c64fb7c125f0aec0b8b80a972907eb8 @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/fuzz/artifacts/crash-1261a81d957460d69943ae02e30528372736fc15 b/fuzz/artifacts/crash-1261a81d957460d69943ae02e30528372736fc15 new file mode 100644 index 000000000..d8698bc8c --- /dev/null +++ b/fuzz/artifacts/crash-1261a81d957460d69943ae02e30528372736fc15 @@ -0,0 +1 @@ +��������������������������������ˊe' \ No newline at end of file diff --git a/fuzz/artifacts/crash-1f039bacf8f860eb5507d9ee3a9879dfe316cf5e b/fuzz/artifacts/crash-1f039bacf8f860eb5507d9ee3a9879dfe316cf5e new file mode 100644 index 000000000..25fa3ed32 --- /dev/null +++ b/fuzz/artifacts/crash-1f039bacf8f860eb5507d9ee3a9879dfe316cf5e @@ -0,0 +1,2 @@ +��� +���������������������������������� \ No newline at end of file diff --git a/fuzz/artifacts/crash-44796f5e67307b5b18e648fdd016e885ebf50da9 b/fuzz/artifacts/crash-44796f5e67307b5b18e648fdd016e885ebf50da9 new file mode 100644 index 000000000..428f2581d --- /dev/null +++ b/fuzz/artifacts/crash-44796f5e67307b5b18e648fdd016e885ebf50da9 @@ -0,0 +1 @@ +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> \ No newline at end of file diff --git a/fuzz/artifacts/crash-5e665cf3759c0aabfc3e898f6294840582421b32 b/fuzz/artifacts/crash-5e665cf3759c0aabfc3e898f6294840582421b32 new file mode 100644 index 000000000..c1558a21a --- /dev/null +++ b/fuzz/artifacts/crash-5e665cf3759c0aabfc3e898f6294840582421b32 @@ -0,0 +1,2 @@ + +888888888888888�88888888888888888888 \ No newline at end of file diff --git a/fuzz/artifacts/crash-6929e000e5891085cf17a75de96dd505b6499083 b/fuzz/artifacts/crash-6929e000e5891085cf17a75de96dd505b6499083 new file mode 100644 index 000000000..e71d37245 --- /dev/null +++ b/fuzz/artifacts/crash-6929e000e5891085cf17a75de96dd505b6499083 @@ -0,0 +1,2 @@ +����~���������������������������� +� \ No newline at end of file diff --git a/fuzz/artifacts/crash-816595d1445fb45b609cca5417ba5f537c74ab43 b/fuzz/artifacts/crash-816595d1445fb45b609cca5417ba5f537c74ab43 new file mode 100644 index 000000000..7c80f26e7 Binary files /dev/null and b/fuzz/artifacts/crash-816595d1445fb45b609cca5417ba5f537c74ab43 differ diff --git a/fuzz/artifacts/crash-98daf6c68def387cffda2937a318cdfd9e956627 b/fuzz/artifacts/crash-98daf6c68def387cffda2937a318cdfd9e956627 new file mode 100644 index 000000000..fa179e477 Binary files /dev/null and b/fuzz/artifacts/crash-98daf6c68def387cffda2937a318cdfd9e956627 differ diff --git a/fuzz/artifacts/crash-b4a051390ba551b5349b6f233930f32f9e16bd85 b/fuzz/artifacts/crash-b4a051390ba551b5349b6f233930f32f9e16bd85 new file mode 100644 index 000000000..e74c45da3 Binary files /dev/null and b/fuzz/artifacts/crash-b4a051390ba551b5349b6f233930f32f9e16bd85 differ diff --git a/fuzz/artifacts/crash-e5f45f193d720ae7264383fcc0763cd945120fc1 b/fuzz/artifacts/crash-e5f45f193d720ae7264383fcc0763cd945120fc1 new file mode 100644 index 000000000..bd96e04d8 --- /dev/null +++ b/fuzz/artifacts/crash-e5f45f193d720ae7264383fcc0763cd945120fc1 @@ -0,0 +1,4 @@ + + + +�������������������������������� \ No newline at end of file diff --git a/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-2221b8862d9d37ec7c714a5df89b570c1356cdba b/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-2221b8862d9d37ec7c714a5df89b570c1356cdba new file mode 100644 index 000000000..c17cfdc13 --- /dev/null +++ b/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-2221b8862d9d37ec7c714a5df89b570c1356cdba @@ -0,0 +1 @@ +zgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRzggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggegggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggRRRRggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg4 \ No newline at end of file diff --git a/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709 b/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-da39a3ee5e6b4b0d3255bfef95601890afd80709 new file mode 100644 index 000000000..e69de29bb diff --git a/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-ef43788e032a15a049005ce4fd839b3777597338 b/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-ef43788e032a15a049005ce4fd839b3777597338 new file mode 100644 index 000000000..80ed51c71 --- /dev/null +++ b/fuzz/artifacts/fuzz_namespace_group_from_b64/crash-ef43788e032a15a049005ce4fd839b3777597338 @@ -0,0 +1 @@ +zggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggeggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggRRRRRRRRRRRRRRRRRRRRRRRRRRRgggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg4 \ No newline at end of file diff --git a/fuzz/artifacts/slow-unit-727720324b031a1f6408810b07628b558866fb00 b/fuzz/artifacts/slow-unit-727720324b031a1f6408810b07628b558866fb00 new file mode 100644 index 000000000..c94d0e853 --- /dev/null +++ b/fuzz/artifacts/slow-unit-727720324b031a1f6408810b07628b558866fb00 @@ -0,0 +1,2 @@ + +��ﱱ����������������������������� \ No newline at end of file diff --git a/fuzz/artifacts/slow-unit-a5b5c4ac13d3d8cbc8b4696cb715160998407a8f b/fuzz/artifacts/slow-unit-a5b5c4ac13d3d8cbc8b4696cb715160998407a8f new file mode 100644 index 000000000..7fa179b79 --- /dev/null +++ b/fuzz/artifacts/slow-unit-a5b5c4ac13d3d8cbc8b4696cb715160998407a8f @@ -0,0 +1 @@ +R���� diff --git a/fuzz/fuzz_targets/accounts_call.rs b/fuzz/fuzz_targets/accounts_call.rs new file mode 100644 index 000000000..6dff3c2d1 --- /dev/null +++ b/fuzz/fuzz_targets/accounts_call.rs @@ -0,0 +1,84 @@ +#![no_main] + +use std::collections::{HashMap, HashSet}; + +use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured}; +use libfuzzer_sys::{fuzz_target, Corpus}; +use rand::rngs::StdRng; +use rand::seq::SliceRandom; +use rand::{RngCore, SeedableRng}; +use sov_accounts::{AccountConfig, Accounts, CallMessage, UPDATE_ACCOUNT_MSG}; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_modules_api::{Context, Module, PrivateKey, Spec}; +use sov_state::WorkingSet; + +type C = DefaultContext; + +// Check well-formed calls +fuzz_target!(|input: (u16, [u8; 32], Vec)| -> Corpus { + let (iterations, seed, keys) = input; + if iterations < 1024 { + // pointless to setup & run a small iterations count + return Corpus::Reject; + } + + // this is a workaround to the restriction where `ed25519_dalek::Keypair` doesn't implement + // `Eq` or `Sort`; reduce the set to a unique collection of keys so duplicated accounts are not + // used. + let keys = keys + .into_iter() + .map(|k| (k.as_hex(), k)) + .collect::>() + .into_values() + .collect::>(); + + if keys.is_empty() { + return Corpus::Reject; + } + + let rng = &mut StdRng::from_seed(seed); + let mut seed = [0u8; 32]; + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ::Storage::with_path(tmpdir.path()).unwrap(); + let working_set = &mut WorkingSet::new(storage); + + let config: AccountConfig = keys.iter().map(|k| k.pub_key()).collect(); + let accounts: Accounts = Accounts::default(); + accounts.genesis(&config, working_set).unwrap(); + + // address list is constant for this test + let mut used = keys.iter().map(|k| k.as_hex()).collect::>(); + let mut state: HashMap<_, _> = keys.into_iter().map(|k| (k.default_address(), k)).collect(); + let addresses: Vec<_> = state.keys().copied().collect(); + + for _ in 0..iterations { + // we use slices for better select performance + let sender = addresses.choose(rng).unwrap(); + let context = C::new(*sender); + + // clear previous state + let previous = state.get(sender).unwrap().as_hex(); + used.remove(&previous); + + // generate an unused key + rng.fill_bytes(&mut seed); + let u = &mut Unstructured::new(&seed); + let mut secret = DefaultPrivateKey::arbitrary(u).unwrap(); + while used.contains(&secret.as_hex()) { + rng.fill_bytes(&mut seed); + let u = &mut Unstructured::new(&seed); + secret = DefaultPrivateKey::arbitrary(u).unwrap(); + } + used.insert(secret.as_hex()); + + let public = secret.pub_key(); + let sig = secret.sign(&UPDATE_ACCOUNT_MSG); + state.insert(*sender, secret); + + let msg = CallMessage::::UpdatePublicKey(public.clone(), sig); + accounts.call(msg, &context, working_set).unwrap(); + } + + Corpus::Keep +}); diff --git a/fuzz/fuzz_targets/accounts_call_random.rs b/fuzz/fuzz_targets/accounts_call_random.rs new file mode 100644 index 000000000..66a896538 --- /dev/null +++ b/fuzz/fuzz_targets/accounts_call_random.rs @@ -0,0 +1,26 @@ +#![no_main] + +use libfuzzer_sys::arbitrary::Unstructured; +use libfuzzer_sys::fuzz_target; +use sov_accounts::{Accounts, CallMessage}; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::{Module, Spec}; +use sov_state::WorkingSet; + +type C = DefaultContext; + +// Check arbitrary, random calls +fuzz_target!(|input: (&[u8], Vec<(C, CallMessage)>)| { + let tmpdir = tempfile::tempdir().unwrap(); + let storage = ::Storage::with_path(tmpdir.path()).unwrap(); + let working_set = &mut WorkingSet::new(storage); + + let (seed, msgs) = input; + let u = &mut Unstructured::new(seed); + let accounts: Accounts = Accounts::arbitrary_workset(u, working_set).unwrap(); + + for (ctx, msg) in msgs { + // assert malformed calls won't panic + accounts.call(msg, &ctx, working_set).ok(); + } +}); diff --git a/fuzz/fuzz_targets/accounts_parse_call_message.rs b/fuzz/fuzz_targets/accounts_parse_call_message.rs new file mode 100644 index 000000000..0467112e1 --- /dev/null +++ b/fuzz/fuzz_targets/accounts_parse_call_message.rs @@ -0,0 +1,13 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sov_accounts::CallMessage; +use sov_modules_api::default_context::DefaultContext; + +type C = DefaultContext; + +fuzz_target!(|input: CallMessage| { + let json = serde_json::to_vec(&input).unwrap(); + let msg = serde_json::from_slice::>(&json).unwrap(); + assert_eq!(input, msg); +}); diff --git a/fuzz/fuzz_targets/accounts_parse_call_message_random.rs b/fuzz/fuzz_targets/accounts_parse_call_message_random.rs new file mode 100644 index 000000000..a928f4c7d --- /dev/null +++ b/fuzz/fuzz_targets/accounts_parse_call_message_random.rs @@ -0,0 +1,11 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sov_accounts::CallMessage; +use sov_modules_api::default_context::DefaultContext; + +type C = DefaultContext; + +fuzz_target!(|input: &[u8]| { + serde_json::from_slice::>(input).ok(); +}); diff --git a/fuzz/fuzz_targets/address_bech_32_parse_serde.rs b/fuzz/fuzz_targets/address_bech_32_parse_serde.rs new file mode 100644 index 000000000..a01d11178 --- /dev/null +++ b/fuzz/fuzz_targets/address_bech_32_parse_serde.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sov_modules_api::AddressBech32; + +fuzz_target!(|data: &[u8]| { + serde_json::from_slice::(data).ok(); +}); diff --git a/fuzz/fuzz_targets/address_bech_32_try_from_bytes.rs b/fuzz/fuzz_targets/address_bech_32_try_from_bytes.rs new file mode 100644 index 000000000..9546a74f3 --- /dev/null +++ b/fuzz/fuzz_targets/address_bech_32_try_from_bytes.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sov_modules_api::AddressBech32; + +fuzz_target!(|data: &[u8]| { + let _ = AddressBech32::try_from(data); +}); diff --git a/fuzz/fuzz_targets/bank_call.rs b/fuzz/fuzz_targets/bank_call.rs new file mode 100644 index 000000000..c999c262b --- /dev/null +++ b/fuzz/fuzz_targets/bank_call.rs @@ -0,0 +1,25 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sov_bank::{Bank, CallMessage}; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::Module; +use sov_state::{ProverStorage, WorkingSet}; + +type C = DefaultContext; + +fuzz_target!(|input: (&[u8], [u8; 32])| { + let (data, sender) = input; + if let Ok(msgs) = serde_json::from_slice::>>(data) { + let tmpdir = tempfile::tempdir().unwrap(); + let mut working_set = WorkingSet::new(ProverStorage::with_path(tmpdir.path()).unwrap()); + let ctx = C { + sender: sender.into(), + }; + + let bank = Bank::default(); + for msg in msgs { + bank.call(msg, &ctx, &mut working_set).ok(); + } + } +}); diff --git a/fuzz/fuzz_targets/bank_parse_call_message.rs b/fuzz/fuzz_targets/bank_parse_call_message.rs new file mode 100644 index 000000000..13962b9cd --- /dev/null +++ b/fuzz/fuzz_targets/bank_parse_call_message.rs @@ -0,0 +1,11 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sov_bank::CallMessage; +use sov_modules_api::default_context::DefaultContext; + +type C = DefaultContext; + +fuzz_target!(|input: &[u8]| { + serde_json::from_slice::>(input).ok(); +}); diff --git a/fuzz/fuzz_targets/namespace_group_from_b64.rs b/fuzz/fuzz_targets/namespace_group_from_b64.rs new file mode 100644 index 000000000..50e8d3e1a --- /dev/null +++ b/fuzz/fuzz_targets/namespace_group_from_b64.rs @@ -0,0 +1,11 @@ +#![no_main] +#[macro_use] +extern crate libfuzzer_sys; + +use celestia::shares::NamespaceGroup; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + let _ = NamespaceGroup::from_b64(s).ok(); + } +}); diff --git a/fuzz/fuzz_targets/parse_address.rs b/fuzz/fuzz_targets/parse_address.rs new file mode 100644 index 000000000..a5b74959d --- /dev/null +++ b/fuzz/fuzz_targets/parse_address.rs @@ -0,0 +1,15 @@ +#![no_main] +#[macro_use] +extern crate libfuzzer_sys; + +use std::str::FromStr; + +use sov_modules_api::AddressBech32; + +fuzz_target!(|data: &[u8]| { + if let Ok(data) = std::str::from_utf8(data) { + if let Ok(addr) = AddressBech32::from_str(data) { + addr.to_string(); + } + } +}); diff --git a/fuzz/fuzz_targets/share_deserialize.rs b/fuzz/fuzz_targets/share_deserialize.rs new file mode 100644 index 000000000..e08cfd1ec --- /dev/null +++ b/fuzz/fuzz_targets/share_deserialize.rs @@ -0,0 +1,8 @@ +#![no_main] + +use celestia::shares::Share; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + serde_json::from_slice::(data).ok(); +}); diff --git a/module-system/module-implementations/sov-accounts/Cargo.toml b/module-system/module-implementations/sov-accounts/Cargo.toml index 5b3849f3e..918e7500b 100644 --- a/module-system/module-implementations/sov-accounts/Cargo.toml +++ b/module-system/module-implementations/sov-accounts/Cargo.toml @@ -13,6 +13,7 @@ resolver = "2" [dependencies] anyhow = { workspace = true } +arbitrary = { workspace = true, optional = true } borsh = { workspace = true, features = ["rc"] } schemars = { workspace = true, optional = true } serde = { workspace = true, optional = true } @@ -30,5 +31,6 @@ tempfile = { workspace = true } [features] default = [] +arbitrary = ["dep:arbitrary", "sov-state/arbitrary"] serde = ["dep:serde", "dep:serde_json"] -native = ["serde", "dep:jsonrpsee", "dep:schemars", "dep:clap", "sov-state/native", "sov-modules-api/native"] \ No newline at end of file +native = ["serde", "dep:jsonrpsee", "dep:schemars", "dep:clap", "sov-state/native", "sov-modules-api/native"] diff --git a/module-system/module-implementations/sov-accounts/src/call.rs b/module-system/module-implementations/sov-accounts/src/call.rs index 1b716284c..23f9b7c1d 100644 --- a/module-system/module-implementations/sov-accounts/src/call.rs +++ b/module-system/module-implementations/sov-accounts/src/call.rs @@ -1,5 +1,5 @@ use anyhow::{ensure, Result}; -use sov_modules_api::{CallResponse, Signature}; +use sov_modules_api::{CallResponse, Context, Signature}; use sov_state::WorkingSet; use crate::Accounts; @@ -20,7 +20,7 @@ pub const UPDATE_ACCOUNT_MSG: [u8; 32] = [1; 32]; ) )] #[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Clone)] -pub enum CallMessage { +pub enum CallMessage { /// Updates a public key for the corresponding Account. /// The sender must be in possession of the new key. UpdatePublicKey( @@ -31,7 +31,7 @@ pub enum CallMessage { ), } -impl Accounts { +impl Accounts { pub(crate) fn update_public_key( &self, new_pub_key: C::PublicKey, @@ -72,3 +72,23 @@ impl Accounts { Ok(()) } } + +#[cfg(all(feature = "arbitrary", feature = "native"))] +impl<'a, C> arbitrary::Arbitrary<'a> for CallMessage +where + C: Context, + C::PrivateKey: arbitrary::Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + use sov_modules_api::PrivateKey; + + let secret = C::PrivateKey::arbitrary(u)?; + let public = secret.pub_key(); + + let payload_len = u.arbitrary_len::()?; + let payload = u.bytes(payload_len)?; + let signature = secret.sign(payload); + + Ok(Self::UpdatePublicKey(public, signature)) + } +} diff --git a/module-system/module-implementations/sov-accounts/src/lib.rs b/module-system/module-implementations/sov-accounts/src/lib.rs index 34b45891b..f15b1fbbd 100644 --- a/module-system/module-implementations/sov-accounts/src/lib.rs +++ b/module-system/module-implementations/sov-accounts/src/lib.rs @@ -10,18 +10,27 @@ pub mod query; mod tests; pub use call::{CallMessage, UPDATE_ACCOUNT_MSG}; -use sov_modules_api::{Error, ModuleInfo}; +use sov_modules_api::{Context, Error, ModuleInfo}; use sov_state::WorkingSet; /// Initial configuration for sov-accounts module. -pub struct AccountConfig { +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AccountConfig { /// Public keys to initialize the rollup. pub pub_keys: Vec, } +impl FromIterator for AccountConfig { + fn from_iter>(iter: T) -> Self { + Self { + pub_keys: iter.into_iter().collect(), + } + } +} + /// An account on the rollup. #[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Copy, Clone)] -pub struct Account { +pub struct Account { /// The address of the account. pub addr: C::Address, /// The current nonce value associated with the account. @@ -31,7 +40,7 @@ pub struct Account { /// A module responsible for managing accounts on the rollup. #[cfg_attr(feature = "native", derive(sov_modules_api::ModuleCallJsonSchema))] #[derive(ModuleInfo, Clone)] -pub struct Accounts { +pub struct Accounts { /// The address of the sov-accounts module. #[address] pub address: C::Address, @@ -45,7 +54,7 @@ pub struct Accounts { pub(crate) accounts: sov_state::StateMap>, } -impl sov_modules_api::Module for Accounts { +impl sov_modules_api::Module for Accounts { type Context = C; type Config = AccountConfig; @@ -73,3 +82,56 @@ impl sov_modules_api::Module for Accounts { } } } + +#[cfg(feature = "arbitrary")] +impl<'a, C> arbitrary::Arbitrary<'a> for Account +where + C: Context, + C::Address: arbitrary::Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let addr = u.arbitrary()?; + let nonce = u.arbitrary()?; + Ok(Self { addr, nonce }) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a, C> arbitrary::Arbitrary<'a> for AccountConfig +where + C: Context, + C::PublicKey: arbitrary::Arbitrary<'a>, +{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // TODO we might want a dedicated struct that will generate the private key counterpart so + // payloads can be signed and verified + Ok(Self { + pub_keys: u.arbitrary_iter()?.collect::>()?, + }) + } +} + +#[cfg(feature = "arbitrary")] +impl<'a, C> Accounts +where + C: Context, + C::Address: arbitrary::Arbitrary<'a>, + C::PublicKey: arbitrary::Arbitrary<'a>, +{ + /// Creates an arbitrary set of accounts and stores it under `working_set`. + pub fn arbitrary_workset( + u: &mut arbitrary::Unstructured<'a>, + working_set: &mut WorkingSet, + ) -> arbitrary::Result { + use sov_modules_api::Module; + + let config: AccountConfig = u.arbitrary()?; + let accounts = Accounts::default(); + + accounts + .genesis(&config, working_set) + .map_err(|_| arbitrary::Error::IncorrectFormat)?; + + Ok(accounts) + } +} diff --git a/module-system/sov-modules-api/Cargo.toml b/module-system/sov-modules-api/Cargo.toml index 34c157896..deda777fa 100644 --- a/module-system/sov-modules-api/Cargo.toml +++ b/module-system/sov-modules-api/Cargo.toml @@ -14,6 +14,7 @@ resolver = "2" [dependencies] jsonrpsee = { workspace = true, optional = true } anyhow = { workspace = true } +arbitrary = { workspace = true, optional = true } sov-state = { path = "../sov-state", version = "0.1" } sov-rollup-interface = { path = "../../rollup-interface", version = "0.1" } sov-modules-macros = { path = "../sov-modules-macros", version = "0.1", optional = true } diff --git a/module-system/sov-modules-api/src/default_context.rs b/module-system/sov-modules-api/src/default_context.rs index c5593e7f6..bd15d0af6 100644 --- a/module-system/sov-modules-api/src/default_context.rs +++ b/module-system/sov-modules-api/src/default_context.rs @@ -12,6 +12,7 @@ use crate::default_signature::{DefaultPublicKey, DefaultSignature}; use crate::{Address, Context, PublicKey, Spec}; #[cfg(feature = "native")] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct DefaultContext { pub sender: Address, diff --git a/module-system/sov-modules-api/src/default_signature.rs b/module-system/sov-modules-api/src/default_signature.rs index 8f03d9131..5b2cc54a1 100644 --- a/module-system/sov-modules-api/src/default_signature.rs +++ b/module-system/sov-modules-api/src/default_signature.rs @@ -112,6 +112,41 @@ pub mod private_key { self.pub_key().to_address::
() } } + + #[cfg(feature = "arbitrary")] + impl<'a> arbitrary::Arbitrary<'a> for DefaultPrivateKey { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + use rand::rngs::StdRng; + use rand::SeedableRng; + + // it is important to generate the secret deterministically from the arbitrary argument + // so keys and signatures will be reproductible for a given seed. this unlocks fuzzy + // replay + let seed = <[u8; 32]>::arbitrary(u)?; + let rng = &mut StdRng::from_seed(seed); + let key_pair = Keypair::generate(rng); + + Ok(Self { key_pair }) + } + } + + #[cfg(feature = "arbitrary")] + impl<'a> arbitrary::Arbitrary<'a> for DefaultPublicKey { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + DefaultPrivateKey::arbitrary(u).map(|p| p.pub_key()) + } + } + + #[cfg(feature = "arbitrary")] + impl<'a> arbitrary::Arbitrary<'a> for DefaultSignature { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + // the secret/public pair is lost; it is impossible to verify this signature + // to run a verification, generate the keys+payload individually + let payload_len = u.arbitrary_len::()?; + let payload = u.bytes(payload_len)?; + DefaultPrivateKey::arbitrary(u).map(|s| s.sign(payload)) + } + } } #[cfg_attr(feature = "native", derive(schemars::JsonSchema))] diff --git a/module-system/sov-modules-api/src/lib.rs b/module-system/sov-modules-api/src/lib.rs index 773c8c098..a62a281da 100644 --- a/module-system/sov-modules-api/src/lib.rs +++ b/module-system/sov-modules-api/src/lib.rs @@ -70,6 +70,7 @@ impl BasicAddress for Address {} impl RollupAddress for Address {} #[cfg_attr(feature = "native", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(PartialEq, Clone, Copy, Eq, borsh::BorshDeserialize, borsh::BorshSerialize, Hash)] pub struct Address { addr: [u8; 32], diff --git a/module-system/sov-state/Cargo.toml b/module-system/sov-state/Cargo.toml index 65575f77f..12950032e 100644 --- a/module-system/sov-state/Cargo.toml +++ b/module-system/sov-state/Cargo.toml @@ -13,6 +13,7 @@ resolver = "2" [dependencies] anyhow = { workspace = true } +arbitrary = { workspace = true, optional = true } borsh = { workspace = true } bcs = { workspace = true } serde = { workspace = true } diff --git a/module-system/sov-state/src/lib.rs b/module-system/sov-state/src/lib.rs index 577bd79f8..da556515c 100644 --- a/module-system/sov-state/src/lib.rs +++ b/module-system/sov-state/src/lib.rs @@ -45,8 +45,8 @@ pub use crate::witness::{ArrayWitness, TreeWitnessReader, Witness}; // All the collection types in this crate are backed by the same storage instance, this means that insertions of the same key // to two different `StorageMaps` would collide with each other. We solve it by instantiating every collection type with a unique // prefix that is prepended to each key. - #[derive(borsh::BorshDeserialize, borsh::BorshSerialize, Debug, PartialEq, Eq, Clone)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Prefix { prefix: AlignedVec, } diff --git a/module-system/sov-state/src/map.rs b/module-system/sov-state/src/map.rs index dee4a2ba3..aa545e253 100644 --- a/module-system/sov-state/src/map.rs +++ b/module-system/sov-state/src/map.rs @@ -171,3 +171,35 @@ where working_set.delete_value(self.prefix(), key); } } + +#[cfg(feature = "arbitrary")] +impl<'a, K, V, VC> StateMap +where + K: arbitrary::Arbitrary<'a> + Hash + Eq, + V: arbitrary::Arbitrary<'a> + Hash + Eq, + VC: StateValueCodec + Default, +{ + pub fn arbitrary_workset( + u: &mut arbitrary::Unstructured<'a>, + working_set: &mut WorkingSet, + ) -> arbitrary::Result + where + S: Storage, + { + use arbitrary::Arbitrary; + + let prefix = Prefix::arbitrary(u)?; + let len = u.arbitrary_len::<(K, V)>()?; + let codec = VC::default(); + let map = StateMap::with_codec(prefix, codec); + + (0..len).try_fold(map, |map, _| { + let key = K::arbitrary(u)?; + let value = V::arbitrary(u)?; + + map.set(&key, &value, working_set); + + Ok(map) + }) + } +} diff --git a/module-system/sov-state/src/utils.rs b/module-system/sov-state/src/utils.rs index 1ca65306f..1dfc152a7 100644 --- a/module-system/sov-state/src/utils.rs +++ b/module-system/sov-state/src/utils.rs @@ -8,15 +8,22 @@ pub struct AlignedVec { } impl AlignedVec { - // Creates a new AlignedVec whose length is aligned to 4 bytes. + /// The length of the chunks of the aligned vector. + pub const ALIGNMENT: usize = 4; + + // Creates a new AlignedVec whose length is aligned to [Self::ALIGNMENT] bytes. pub fn new(vector: Vec) -> Self { - // TODO pad the vector to Self { inner: vector } } // Extends self with the contents of the other AlignedVec. pub fn extend(&mut self, other: &Self) { // TODO check if the standard extend method does the right thing. + // debug_assert_eq!( + // self.inner.len() % Self::ALIGNMENT, + // 0, + // "`AlignedVec` is expected to have well-formed chunks" + // ); self.inner.extend(&other.inner); } @@ -39,3 +46,10 @@ impl AsRef> for AlignedVec { &self.inner } } + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for AlignedVec { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + u.arbitrary().map(Self::new) + } +}