diff --git a/Cargo.lock b/Cargo.lock
index c05e45b24..a1f74fbac 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1962,6 +1962,13 @@ dependencies = [
"soroban-sdk",
]
+[[package]]
+name = "test_fuzz_afl"
+version = "27.0.5"
+dependencies = [
+ "soroban-sdk",
+]
+
[[package]]
name = "test_generics"
version = "27.0.5"
diff --git a/Makefile b/Makefile
index 20f959805..7f36fdbd1 100644
--- a/Makefile
+++ b/Makefile
@@ -46,6 +46,10 @@ build-test-wasms: fmt
build-fuzz:
cd tests/fuzz/fuzz && cargo +nightly fuzz check
+# Builds the afl fuzz test. Requires cargo-afl, see tests/fuzz_afl/README.md.
+build-fuzz-afl:
+ cd tests/fuzz_afl/fuzz && cargo afl build
+
readme:
cd soroban-sdk \
&& cargo +nightly rustdoc --features testutils -- -Zunstable-options -wjson \
diff --git a/soroban-sdk/src/testutils/arbitrary.rs b/soroban-sdk/src/testutils/arbitrary.rs
index 4fa38d1d7..33531f3af 100644
--- a/soroban-sdk/src/testutils/arbitrary.rs
+++ b/soroban-sdk/src/testutils/arbitrary.rs
@@ -1,15 +1,17 @@
-//! Support for randomized testing of Soroban contracts, with [`cargo-fuzz`] or
-//! [`proptest`].
+//! Support for randomized testing of Soroban contracts, with [`cargo-fuzz`],
+//! [`cargo-afl`], or [`proptest`].
//!
//! This module provides a pattern for generating Soroban contract types for the
//! purpose of fuzzing and property testing Soroban contracts. It is focused on
//! implementing the [`Arbitrary`] trait, which turns generated bytes into Rust
-//! values: `cargo-fuzz` consumes it directly, and `proptest` through a bridge.
+//! values: `cargo-fuzz` and `cargo-afl` consume it directly, and `proptest`
+//! through a bridge.
//!
//! The examples in this module are written for `cargo-fuzz`. For generating the
//! same values in a `proptest` property test, see the [`proptest`] module.
//!
//! [`cargo-fuzz`]: https://github.com/rust-fuzz/cargo-fuzz/
+//! [`cargo-afl`]: https://github.com/rust-fuzz/afl.rs/
//! [`Arbitrary`]: ::arbitrary::Arbitrary
//! [`proptest`]: crate::testutils::proptest
//!
@@ -21,10 +23,10 @@
//! This module is only available when the "testutils" Cargo feature is defined.
//!
//!
-//! ## About `cargo-fuzz` and `Arbitrary`
+//! ## About `cargo-fuzz`, `cargo-afl`, and `Arbitrary`
//!
-//! In its basic operation `cargo-fuzz` fuzz generates raw bytes and feeds them
-//! to a program-dependent fuzzer designed to exercise a program, in our case a
+//! In its basic operation a fuzzer generates raw bytes and feeds them to a
+//! program-dependent fuzz target designed to exercise a program, in our case a
//! Soroban contract.
//!
//! `cargo-fuzz` programs declare their entry points with a macro:
@@ -170,6 +172,85 @@
//! // fuzz the program based on the input
//! });
//! ```
+//!
+//!
+//! ## Fuzzing with `cargo-afl` instead of `cargo-fuzz`
+//!
+//! Everything above applies unchanged when fuzzing with [`cargo-afl`], which
+//! drives [AFL++] instead of libFuzzer: prototypes are generated by the same
+//! [`Arbitrary`] trait, and are converted to contract types with the same
+//! [`FromVal`] or [`IntoVal`] conversions. `cargo-afl` runs on stable Rust,
+//! whereas `cargo-fuzz` requires a nightly compiler.
+//!
+//! [AFL++]: https://aflplus.plus
+//!
+//! Install the tooling, which builds AFL++ from source and so needs a C
+//! compiler and LLVM available, and let AFL++ tune the machine for fuzzing:
+//!
+//! ```text
+//! cargo install cargo-afl --locked
+//! cargo afl system-config
+//! ```
+//!
+//! An AFL++ fuzz target is a crate with a `main` that calls the [`afl::fuzz!`]
+//! macro, and it depends on the `arbitrary` crate directly, because that is
+//! where the `Arbitrary` derive comes from and where `fuzz!` looks up the trait:
+//!
+//! ```toml
+//! [dependencies]
+//! afl = "0.18"
+//! arbitrary = { version = "~1.3.0", features = ["derive"] }
+//! soroban-sdk = { version = "*", features = ["testutils"] }
+//! my-contract = { path = ".." }
+//! ```
+//!
+//! [`afl::fuzz!`]: https://docs.rs/afl/latest/afl/macro.fuzz.html
+//!
+//! ```
+//! # macro_rules! fuzz {
+//! # (|$data:ident: $dty: ty| $body:block) => { };
+//! # }
+//! use arbitrary::Arbitrary;
+//! use soroban_sdk::testutils::arbitrary::SorobanArbitrary;
+//! use soroban_sdk::{Address, Env, IntoVal};
+//!
+//! #[derive(Arbitrary, Debug)]
+//! struct TestInput {
+//! deposit_amount: i128,
+//! claim_address:
::Prototype,
+//! }
+//!
+//! fn main() {
+//! fuzz!(|input: TestInput| {
+//! // Create the `Env` inside the closure, not outside: AFL++ reuses the
+//! // process for many inputs, and state created outside the closure
+//! // would leak from one input into the next.
+//! let env = Env::default();
+//! let claim_address: Address = input.claim_address.into_val(&env);
+//! // fuzz the contract based on the input
+//! });
+//! }
+//! ```
+//!
+//! Build with `cargo afl build`, which is `cargo build` with the AFL++
+//! instrumentation added, and fuzz the resulting binary with an input directory
+//! containing at least one seed input:
+//!
+//! ```text
+//! cargo afl build
+//! cargo afl fuzz -i in -o out target/debug/fuzz_target_1
+//! ```
+//!
+//! Fuzz debug builds, at least at first: they keep integer overflow checks and
+//! `debug_assert!`s enabled, and those catch bugs a release build allows.
+//!
+//! Crashing inputs are written to `out/default/crashes/`. The target reads an
+//! input on stdin when it is not being driven by AFL++, so a crash is replayed
+//! by feeding the file back in:
+//!
+//! ```text
+//! RUST_BACKTRACE=1 ./target/debug/fuzz_target_1 < out/default/crashes/id:000000*
+//! ```
/// A reexport of the `arbitrary` crate.
///
diff --git a/tests-expanded/test_fuzz_afl_tests.rs b/tests-expanded/test_fuzz_afl_tests.rs
new file mode 100644
index 000000000..f4db42280
--- /dev/null
+++ b/tests-expanded/test_fuzz_afl_tests.rs
@@ -0,0 +1,350 @@
+#![feature(prelude_import)]
+#![no_std]
+#[macro_use]
+extern crate core;
+#[prelude_import]
+use core::prelude::rust_2021::*;
+use soroban_sdk::{contract, contractimpl, U256};
+pub struct Contract;
+///ContractArgs is a type for building arg lists for functions defined in "Contract".
+pub struct ContractArgs;
+///ContractClient is a client for calling the contract defined in "Contract".
+pub struct ContractClient<'a> {
+ pub env: soroban_sdk::Env,
+ pub address: soroban_sdk::Address,
+ #[doc(hidden)]
+ set_auths: Option<&'a [soroban_sdk::xdr::SorobanAuthorizationEntry]>,
+ #[doc(hidden)]
+ mock_auths: Option<&'a [soroban_sdk::testutils::MockAuth<'a>]>,
+ #[doc(hidden)]
+ mock_all_auths: bool,
+ #[doc(hidden)]
+ allow_non_root_auth: bool,
+}
+impl<'a> ContractClient<'a> {
+ pub fn new(env: &soroban_sdk::Env, address: &soroban_sdk::Address) -> Self {
+ Self {
+ env: env.clone(),
+ address: address.clone(),
+ set_auths: None,
+ mock_auths: None,
+ mock_all_auths: false,
+ allow_non_root_auth: false,
+ }
+ }
+ /// Set authorizations in the environment which will be consumed by
+ /// contracts when they invoke `Address::require_auth` or
+ /// `Address::require_auth_for_args` functions.
+ ///
+ /// Requires valid signatures for the authorization to be successful.
+ /// To mock auth without requiring valid signatures, use `mock_auths`.
+ ///
+ /// See `soroban_sdk::Env::set_auths` for more details and examples.
+ pub fn set_auths(&self, auths: &'a [soroban_sdk::xdr::SorobanAuthorizationEntry]) -> Self {
+ Self {
+ env: self.env.clone(),
+ address: self.address.clone(),
+ set_auths: Some(auths),
+ mock_auths: self.mock_auths.clone(),
+ mock_all_auths: false,
+ allow_non_root_auth: false,
+ }
+ }
+ /// Mock authorizations in the environment which will cause matching invokes
+ /// of `Address::require_auth` and `Address::require_auth_for_args` to
+ /// pass.
+ ///
+ /// See `soroban_sdk::Env::set_auths` for more details and examples.
+ pub fn mock_auths(&self, mock_auths: &'a [soroban_sdk::testutils::MockAuth<'a>]) -> Self {
+ Self {
+ env: self.env.clone(),
+ address: self.address.clone(),
+ set_auths: self.set_auths.clone(),
+ mock_auths: Some(mock_auths),
+ mock_all_auths: false,
+ allow_non_root_auth: false,
+ }
+ }
+ /// Mock all calls to the `Address::require_auth` and
+ /// `Address::require_auth_for_args` functions in invoked contracts,
+ /// having them succeed as if authorization was provided.
+ ///
+ /// See `soroban_sdk::Env::mock_all_auths` for more details and
+ /// examples.
+ pub fn mock_all_auths(&self) -> Self {
+ Self {
+ env: self.env.clone(),
+ address: self.address.clone(),
+ set_auths: None,
+ mock_auths: None,
+ mock_all_auths: true,
+ allow_non_root_auth: false,
+ }
+ }
+ /// A version of `mock_all_auths` that allows authorizations that
+ /// are not present in the root invocation.
+ ///
+ /// Refer to `mock_all_auths` documentation for details and
+ /// prefer using `mock_all_auths` unless non-root authorization is
+ /// required.
+ ///
+ /// See `soroban_sdk::Env::mock_all_auths_allowing_non_root_auth`
+ /// for more details and examples.
+ pub fn mock_all_auths_allowing_non_root_auth(&self) -> Self {
+ Self {
+ env: self.env.clone(),
+ address: self.address.clone(),
+ set_auths: None,
+ mock_auths: None,
+ mock_all_auths: true,
+ allow_non_root_auth: true,
+ }
+ }
+}
+mod __contract_fn_set_registry {
+ use super::*;
+ extern crate std;
+ use std::collections::BTreeMap;
+ use std::sync::Mutex;
+ pub type F = soroban_sdk::testutils::ContractFunctionF;
+ static FUNCS: Mutex> = Mutex::new(BTreeMap::new());
+ pub fn register(name: &'static str, func: &'static F) {
+ FUNCS.lock().unwrap().insert(name, func);
+ }
+ pub fn call(
+ name: &str,
+ env: soroban_sdk::Env,
+ args: &[soroban_sdk::Val],
+ ) -> Option {
+ let fopt: Option<&'static F> = FUNCS.lock().unwrap().get(name).map(|f| f.clone());
+ fopt.map(|f| f(env, args))
+ }
+}
+impl soroban_sdk::testutils::ContractFunctionRegister for Contract {
+ fn register(name: &'static str, func: &'static __contract_fn_set_registry::F) {
+ __contract_fn_set_registry::register(name, func);
+ }
+}
+#[doc(hidden)]
+impl soroban_sdk::testutils::ContractFunctionSet for Contract {
+ fn call(
+ &self,
+ func: &str,
+ env: soroban_sdk::Env,
+ args: &[soroban_sdk::Val],
+ ) -> Option {
+ __contract_fn_set_registry::call(func, env, args)
+ }
+}
+impl Contract {
+ pub fn run(a: U256, b: U256) {
+ if a < b {
+ {
+ ::core::panicking::panic_fmt(format_args!("unexpected"));
+ }
+ }
+ }
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+pub mod __Contract__run__spec {
+ #[doc(hidden)]
+ #[allow(non_snake_case)]
+ #[allow(non_upper_case_globals)]
+ pub static __SPEC_XDR_FN_RUN: [u8; 56usize] = super::Contract::spec_xdr_run();
+}
+impl Contract {
+ #[allow(non_snake_case)]
+ pub const fn spec_xdr_run() -> [u8; 56usize] {
+ *b"\0\0\0\0\0\0\0\0\0\0\0\x03run\0\0\0\0\x02\0\0\0\0\0\0\0\x01a\0\0\0\0\0\0\x0c\0\0\0\0\0\0\0\x01b\0\0\0\0\0\0\x0c\0\0\0\0"
+ }
+}
+impl<'a> ContractClient<'a> {
+ pub fn run(&self, a: &U256, b: &U256) -> () {
+ use core::ops::Not;
+ let old_auth_manager = self
+ .env
+ .in_contract()
+ .not()
+ .then(|| self.env.host().snapshot_auth_manager().unwrap());
+ {
+ if let Some(set_auths) = self.set_auths {
+ self.env.set_auths(set_auths);
+ }
+ if let Some(mock_auths) = self.mock_auths {
+ self.env.mock_auths(mock_auths);
+ }
+ if self.mock_all_auths {
+ if self.allow_non_root_auth {
+ self.env.mock_all_auths_allowing_non_root_auth();
+ } else {
+ self.env.mock_all_auths();
+ }
+ }
+ }
+ use soroban_sdk::{FromVal, IntoVal};
+ let res = self.env.invoke_contract(
+ &self.address,
+ &{
+ #[allow(deprecated)]
+ const SYMBOL: soroban_sdk::Symbol = soroban_sdk::Symbol::short("run");
+ SYMBOL
+ },
+ ::soroban_sdk::Vec::from_array(
+ &self.env,
+ [a.into_val(&self.env), b.into_val(&self.env)],
+ ),
+ );
+ if let Some(old_auth_manager) = old_auth_manager {
+ self.env.host().set_auth_manager(old_auth_manager).unwrap();
+ }
+ res
+ }
+ pub fn try_run(
+ &self,
+ a: &U256,
+ b: &U256,
+ ) -> Result<
+ Result<(), <() as soroban_sdk::TryFromVal>::Error>,
+ Result,
+ > {
+ use core::ops::Not;
+ let old_auth_manager = self
+ .env
+ .in_contract()
+ .not()
+ .then(|| self.env.host().snapshot_auth_manager().unwrap());
+ {
+ if let Some(set_auths) = self.set_auths {
+ self.env.set_auths(set_auths);
+ }
+ if let Some(mock_auths) = self.mock_auths {
+ self.env.mock_auths(mock_auths);
+ }
+ if self.mock_all_auths {
+ if self.allow_non_root_auth {
+ self.env.mock_all_auths_allowing_non_root_auth();
+ } else {
+ self.env.mock_all_auths();
+ }
+ }
+ }
+ use soroban_sdk::{FromVal, IntoVal};
+ let res = self.env.try_invoke_contract(
+ &self.address,
+ &{
+ #[allow(deprecated)]
+ const SYMBOL: soroban_sdk::Symbol = soroban_sdk::Symbol::short("run");
+ SYMBOL
+ },
+ ::soroban_sdk::Vec::from_array(
+ &self.env,
+ [a.into_val(&self.env), b.into_val(&self.env)],
+ ),
+ );
+ if let Some(old_auth_manager) = old_auth_manager {
+ self.env.host().set_auth_manager(old_auth_manager).unwrap();
+ }
+ res
+ }
+}
+impl ContractArgs {
+ #[inline(always)]
+ #[allow(clippy::unused_unit)]
+ pub fn run<'i>(a: &'i U256, b: &'i U256) -> (&'i U256, &'i U256) {
+ (a, b)
+ }
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+#[deprecated(note = "use `ContractClient::new(&env, &contract_id).run` instead")]
+#[allow(deprecated)]
+pub fn __Contract__run__invoke_raw(
+ env: soroban_sdk::Env,
+ arg_0: soroban_sdk::Val,
+ arg_1: soroban_sdk::Val,
+) -> soroban_sdk::Val {
+ soroban_sdk::IntoValForContractFn::into_val_for_contract_fn(
+ ::run(
+ <_ as soroban_sdk::unwrap::UnwrapOptimized>::unwrap_optimized(
+ <_ as soroban_sdk::TryFromValForContractFn<
+ soroban_sdk::Env,
+ soroban_sdk::Val,
+ >>::try_from_val_for_contract_fn(&env, &arg_0),
+ ),
+ <_ as soroban_sdk::unwrap::UnwrapOptimized>::unwrap_optimized(
+ <_ as soroban_sdk::TryFromValForContractFn<
+ soroban_sdk::Env,
+ soroban_sdk::Val,
+ >>::try_from_val_for_contract_fn(&env, &arg_1),
+ ),
+ ),
+ &env,
+ )
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+#[deprecated(note = "use `ContractClient::new(&env, &contract_id).run` instead")]
+pub fn __Contract__run__invoke_raw_slice(
+ env: soroban_sdk::Env,
+ args: &[soroban_sdk::Val],
+) -> soroban_sdk::Val {
+ if args.len() != 2usize {
+ {
+ ::core::panicking::panic_fmt(format_args!(
+ "invalid number of input arguments: {0} expected, got {1}",
+ 2usize,
+ args.len(),
+ ));
+ };
+ }
+ #[allow(deprecated)]
+ __Contract__run__invoke_raw(env, args[0usize], args[1usize])
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+#[deprecated(note = "use `ContractClient::new(&env, &contract_id).run` instead")]
+pub extern "C" fn __Contract__run__invoke_raw_extern(
+ arg_0: soroban_sdk::Val,
+ arg_1: soroban_sdk::Val,
+) -> soroban_sdk::Val {
+ #[allow(deprecated)]
+ __Contract__run__invoke_raw(soroban_sdk::Env::default(), arg_0, arg_1)
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+#[allow(unused)]
+fn __Contract____acba25512100f80b56fc3ccd14c65be55d94800cda77585c5f41a887e398f9be_ctor() {
+ #[allow(unsafe_code)]
+ {
+ #[link_section = ".init_array"]
+ #[used]
+ #[allow(non_upper_case_globals, non_snake_case)]
+ #[doc(hidden)]
+ static f: extern "C" fn() -> ::ctor::__support::CtorRetType = {
+ #[link_section = ".text.startup"]
+ #[allow(non_snake_case)]
+ extern "C" fn f() -> ::ctor::__support::CtorRetType {
+ unsafe {
+ __Contract____acba25512100f80b56fc3ccd14c65be55d94800cda77585c5f41a887e398f9be_ctor();
+ };
+ core::default::Default::default()
+ }
+ f
+ };
+ }
+ {
+ ::register(
+ "run",
+ #[allow(deprecated)]
+ &__Contract__run__invoke_raw_slice,
+ );
+ }
+}
+#[rustc_main]
+#[coverage(off)]
+#[doc(hidden)]
+pub fn main() -> () {
+ extern crate test;
+ test::test_main_static(&[])
+}
diff --git a/tests-expanded/test_fuzz_afl_wasm32v1-none.rs b/tests-expanded/test_fuzz_afl_wasm32v1-none.rs
new file mode 100644
index 000000000..95d5497bf
--- /dev/null
+++ b/tests-expanded/test_fuzz_afl_wasm32v1-none.rs
@@ -0,0 +1,137 @@
+#![feature(prelude_import)]
+#![no_std]
+#[macro_use]
+extern crate core;
+#[prelude_import]
+use core::prelude::rust_2021::*;
+use soroban_sdk::{contract, contractimpl, U256};
+pub struct Contract;
+///ContractArgs is a type for building arg lists for functions defined in "Contract".
+pub struct ContractArgs;
+///ContractClient is a client for calling the contract defined in "Contract".
+pub struct ContractClient<'a> {
+ pub env: soroban_sdk::Env,
+ pub address: soroban_sdk::Address,
+ #[doc(hidden)]
+ _phantom: core::marker::PhantomData<&'a ()>,
+}
+impl<'a> ContractClient<'a> {
+ pub fn new(env: &soroban_sdk::Env, address: &soroban_sdk::Address) -> Self {
+ Self {
+ env: env.clone(),
+ address: address.clone(),
+ _phantom: core::marker::PhantomData,
+ }
+ }
+}
+impl Contract {
+ pub fn run(a: U256, b: U256) {
+ if a < b {
+ {
+ ::core::panicking::panic_fmt(format_args!("unexpected"));
+ }
+ }
+ }
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+pub mod __Contract__run__spec {
+ #[doc(hidden)]
+ #[allow(non_snake_case)]
+ #[allow(non_upper_case_globals)]
+ #[link_section = "contractspecv0"]
+ pub static __SPEC_XDR_FN_RUN: [u8; 56usize] = super::Contract::spec_xdr_run();
+}
+impl Contract {
+ #[allow(non_snake_case)]
+ pub const fn spec_xdr_run() -> [u8; 56usize] {
+ *b"\0\0\0\0\0\0\0\0\0\0\0\x03run\0\0\0\0\x02\0\0\0\0\0\0\0\x01a\0\0\0\0\0\0\x0c\0\0\0\0\0\0\0\x01b\0\0\0\0\0\0\x0c\0\0\0\0"
+ }
+}
+impl<'a> ContractClient<'a> {
+ pub fn run(&self, a: &U256, b: &U256) -> () {
+ use core::ops::Not;
+ use soroban_sdk::{FromVal, IntoVal};
+ let res = self.env.invoke_contract(
+ &self.address,
+ &{
+ #[allow(deprecated)]
+ const SYMBOL: soroban_sdk::Symbol = soroban_sdk::Symbol::short("run");
+ SYMBOL
+ },
+ ::soroban_sdk::Vec::from_array(
+ &self.env,
+ [a.into_val(&self.env), b.into_val(&self.env)],
+ ),
+ );
+ res
+ }
+ pub fn try_run(
+ &self,
+ a: &U256,
+ b: &U256,
+ ) -> Result<
+ Result<(), <() as soroban_sdk::TryFromVal>::Error>,
+ Result,
+ > {
+ use soroban_sdk::{FromVal, IntoVal};
+ let res = self.env.try_invoke_contract(
+ &self.address,
+ &{
+ #[allow(deprecated)]
+ const SYMBOL: soroban_sdk::Symbol = soroban_sdk::Symbol::short("run");
+ SYMBOL
+ },
+ ::soroban_sdk::Vec::from_array(
+ &self.env,
+ [a.into_val(&self.env), b.into_val(&self.env)],
+ ),
+ );
+ res
+ }
+}
+impl ContractArgs {
+ #[inline(always)]
+ #[allow(clippy::unused_unit)]
+ pub fn run<'i>(a: &'i U256, b: &'i U256) -> (&'i U256, &'i U256) {
+ (a, b)
+ }
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+#[deprecated(note = "use `ContractClient::new(&env, &contract_id).run` instead")]
+#[allow(deprecated)]
+pub fn __Contract__run__invoke_raw(
+ env: soroban_sdk::Env,
+ arg_0: soroban_sdk::Val,
+ arg_1: soroban_sdk::Val,
+) -> soroban_sdk::Val {
+ soroban_sdk::IntoValForContractFn::into_val_for_contract_fn(
+ ::run(
+ <_ as soroban_sdk::unwrap::UnwrapOptimized>::unwrap_optimized(
+ <_ as soroban_sdk::TryFromValForContractFn<
+ soroban_sdk::Env,
+ soroban_sdk::Val,
+ >>::try_from_val_for_contract_fn(&env, &arg_0),
+ ),
+ <_ as soroban_sdk::unwrap::UnwrapOptimized>::unwrap_optimized(
+ <_ as soroban_sdk::TryFromValForContractFn<
+ soroban_sdk::Env,
+ soroban_sdk::Val,
+ >>::try_from_val_for_contract_fn(&env, &arg_1),
+ ),
+ ),
+ &env,
+ )
+}
+#[doc(hidden)]
+#[allow(non_snake_case)]
+#[deprecated(note = "use `ContractClient::new(&env, &contract_id).run` instead")]
+#[export_name = "run"]
+pub extern "C" fn __Contract__run__invoke_raw_extern(
+ arg_0: soroban_sdk::Val,
+ arg_1: soroban_sdk::Val,
+) -> soroban_sdk::Val {
+ #[allow(deprecated)]
+ __Contract__run__invoke_raw(soroban_sdk::Env::default(), arg_0, arg_1)
+}
diff --git a/tests/fuzz_afl/Cargo.toml b/tests/fuzz_afl/Cargo.toml
new file mode 100644
index 000000000..5c2945b7b
--- /dev/null
+++ b/tests/fuzz_afl/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "test_fuzz_afl"
+version.workspace = true
+authors = ["Stellar Development Foundation "]
+license = "Apache-2.0"
+edition = "2021"
+publish = false
+rust-version.workspace = true
+
+[lib]
+# Adding rlib is required so that the test_fuzz_afl crate can be imported as a
+# library into the fuzzing crate. However, having multiple crate types is a
+# problem, it'll cause LTO optimizations to be disabled for the cdylib build.
+# TODO: Figure out what to do about this.
+crate-type = ["cdylib", "rlib"]
+doctest = false
+
+[dependencies]
+soroban-sdk = {path = "../../soroban-sdk"}
+
+[dev-dependencies]
+soroban-sdk = {path = "../../soroban-sdk", features = ["testutils"]}
diff --git a/tests/fuzz_afl/README.md b/tests/fuzz_afl/README.md
new file mode 100644
index 000000000..32da528a3
--- /dev/null
+++ b/tests/fuzz_afl/README.md
@@ -0,0 +1,255 @@
+# test_fuzz_afl
+
+A minimal Soroban contract, plus a fuzz test for it that runs under
+[AFL++] via the [afl.rs] crate (the `cargo-afl` tooling).
+
+If you have never fuzzed anything before, this README is for you: follow it
+top to bottom and you will watch a fuzzer find a bug in the contract in
+`src/lib.rs`.
+
+[AFL++]: https://aflplus.plus
+[afl.rs]: https://github.com/rust-fuzz/afl.rs
+
+## What is fuzzing?
+
+A fuzzer runs your code over and over with generated input, watching which
+branches the code takes, and mutating the input it feeds in so as to reach
+branches it has not reached yet. When an input makes the code panic, the
+fuzzer saves that input to disk so you can replay it. It is a way of finding
+the inputs you did not think to write a test for.
+
+Two fuzzers are commonly used with Rust, and the SDK supports both:
+
+- **libFuzzer**, driven by `cargo-fuzz`. See the `tests/fuzz` crate next
+ door for the same example written for it. It requires a nightly compiler.
+- **AFL++**, driven by `cargo-afl`, which is what this crate uses. It works
+ on stable Rust, runs each input in a separate forked process (so a crash
+ cannot corrupt the fuzzer itself), and has a terminal UI that shows what
+ the campaign is doing.
+
+Neither is better; they explore differently, and fuzzing a contract with
+both finds more than fuzzing it with either.
+
+## What is in this crate
+
+```
+tests/fuzz_afl
+├── Cargo.toml the contract crate, a normal member of the workspace
+├── src/lib.rs the contract being fuzzed
+└── fuzz the fuzz crate: a separate crate, and its own workspace
+ ├── Cargo.toml
+ ├── in/seed the starting input the fuzzer mutates ("seed corpus")
+ └── src/fuzz_target_1.rs the fuzz test itself
+```
+
+The contract has one function that panics on some inputs and not others:
+
+```rust
+pub fn run(a: U256, b: U256) {
+ if a < b {
+ panic!("unexpected")
+ }
+}
+```
+
+The fuzz test in `fuzz/src/fuzz_target_1.rs` turns the fuzzer's raw bytes
+into two `U256` values and calls `run` with them. The panic is the bug we
+expect the fuzzer to find.
+
+The fuzz crate is deliberately *not* part of the repository's Cargo
+workspace — it declares its own `[workspace]` — because it has to be built
+with special compiler flags that you do not want applied to everything else.
+
+## One-time setup
+
+You need a Rust toolchain ([rustup]), and on Linux the packages `build-essential`,
+`clang`, and `llvm` (on macOS, Xcode command line tools and `brew install llvm`).
+`cargo-afl` compiles AFL++ from source when you install it, which is why a C
+compiler is needed:
+
+```console
+cargo install cargo-afl --locked
+```
+
+Then let AFL++ tune the machine for fuzzing. This asks for your password,
+because it writes kernel settings that stop crash handlers from stealing
+crashes from the fuzzer:
+
+```console
+cargo afl system-config
+```
+
+If you would rather not run that, AFL++ will tell you at startup exactly
+which setting it is unhappy about.
+
+[rustup]: https://rustup.rs
+
+## Build the fuzz test
+
+All the commands below are run from the `fuzz` directory:
+
+```console
+cd tests/fuzz_afl/fuzz
+```
+
+Build with `cargo afl build` rather than `cargo build`. It is a normal
+`cargo build` with the AFL++ instrumentation added, which is how the fuzzer
+sees which branches an input reached:
+
+```console
+cargo afl build
+```
+
+That produces an instrumented binary at `target/debug/fuzz_target_1`.
+
+Note that this is a debug build. Fuzz in debug, at least at first: debug
+builds keep integer overflow checks and `debug_assert!`s turned on, and
+those catch bugs that a release build would silently allow. If you later
+want the extra speed, `cargo afl build --release` works too, and the binary
+lands in `target/release/fuzz_target_1`.
+
+## Run the fuzzer
+
+```console
+cargo afl fuzz -i in -o out target/debug/fuzz_target_1
+```
+
+- `-i in` is the directory of starting inputs. Mutating an existing input is
+ much more effective than starting from nothing, so a fuzzer always wants
+ at least one seed. Ours is a file of 64 zero characters, which decodes to
+ `a == b` and so does not panic.
+- `-o out` is where AFL++ writes everything it learns: the inputs it decides
+ are interesting, the crashes it finds, and its statistics. It is
+ `.gitignore`d.
+
+A full-screen status display takes over the terminal. The two numbers to
+watch when you are starting out:
+
+- **exec speed** — how many inputs per second are being tried. Expect
+ hundreds to a few thousand per second here; a Soroban `Env` is not cheap
+ to create.
+- **saved crashes** — how many distinct crashing inputs have been found. For
+ this contract it should tick up from 0 within seconds.
+
+Press `Ctrl-C` to stop. You can resume a campaign later by pointing at the
+same output directory with `-i-` instead of `-i in`.
+
+Two messages that look alarming but are normal here. AFL++ warns about
+"instability detected during calibration", and reports a **stability** figure
+below 100%: it means the same input did not take exactly the same path
+through the code twice. The Soroban host keeps some state for the lifetime of
+the process, and AFL++ reuses one process for many inputs, so a little
+instability is expected. It makes the fuzzer somewhat less efficient at
+finding new paths; it does not make the crashes it finds any less real. The
+other is the low **coverage** percentage: it is measured over all the code
+linked into the binary, most of which is the host environment, so a few
+percent is normal.
+
+## Reproduce a crash
+
+Crashing inputs are saved as files in `out/default/crashes/`:
+
+```console
+ls out/default/crashes
+```
+
+Ignore the `README.txt` there; the `id:000000,...` files are the inputs. The
+fuzz binary reads an input on stdin when it is not being driven by AFL++, so
+you can replay one directly and see the panic and its backtrace:
+
+```console
+RUST_BACKTRACE=1 ./target/debug/fuzz_target_1 < out/default/crashes/id:000000*
+```
+
+For this contract the output looks like this:
+
+```
+thread 'main' panicked at .../soroban-env-host-28.0.2/src/host.rs:892:9:
+HostError: Error(WasmVm, InvalidAction)
+
+Event log (newest first):
+ 0: [Diagnostic Event] topics:[error, Error(WasmVm, InvalidAction)], data:"escalating error to panic"
+ 1: [Diagnostic Event] topics:[error, Error(WasmVm, InvalidAction)], data:["contract call failed", run, [2179615797408...8145 76, 2179615797408...227248]]
+ ...
+```
+
+Note that the panic you see is the host escalating the contract's failure,
+not the contract's own `panic!("unexpected")`; a contract panic is opaque to
+the caller, so the message is a generic `HostError`. What tells you what
+happened is the event log: it names the function that failed, `run`, and the
+arguments it was called with — and there you can see that the fuzzer found a
+pair where `a < b`. The crash file is a reproducer you can keep, and the
+backtrace below the event log points at the line of the fuzz test that made
+the call.
+
+Two more commands are worth knowing once you have a crash:
+
+- `cargo afl tmin -i -o target/debug/fuzz_target_1`
+ shrinks one crashing input down to the smallest input that still crashes,
+ which usually makes it much easier to understand.
+- `cargo afl cmin -i out/default/queue -o corpus target/debug/fuzz_target_1`
+ reduces the directory of interesting inputs down to a small set with the
+ same coverage, a good corpus to keep and reuse as `-i` next time.
+
+## Writing a fuzz test for your own contract
+
+The fuzzer only knows how to produce bytes, and Soroban types such as
+`U256`, `Vec`, `Map`, or `Address` cannot be built from bytes alone — they
+live inside an `Env`. The SDK bridges that gap with the
+[`SorobanArbitrary`] trait: every contract type has an associated
+`Prototype` type that *can* be built from bytes, and that converts into the
+real type with `into_val(&env)`. So a fuzz test is:
+
+1. Declare a struct of prototypes and derive `Arbitrary` on it, which is
+ what lets the fuzzer generate it:
+
+ ```rust
+ #[derive(Arbitrary, Debug)]
+ struct Input {
+ a: ::Prototype,
+ b: ::Prototype,
+ }
+ ```
+
+2. Pass it to `fuzz!`, convert the prototypes into contract types, register
+ the contract, and call it:
+
+ ```rust
+ fn main() {
+ fuzz!(|input: Input| {
+ let env = Env::default();
+ let a: U256 = input.a.into_val(&env);
+ let b: U256 = input.b.into_val(&env);
+ let contract_id = env.register(Contract, ());
+ let client = ContractClient::new(&env, &contract_id);
+ let _ = client.run(&a, &b);
+ });
+ }
+ ```
+
+Things to keep in mind:
+
+- Create the `Env` **inside** `fuzz!`, not outside. AFL++ reuses the process
+ for many inputs ("persistent mode"), so state created outside the closure
+ would leak from one input into the next and make crashes hard to
+ reproduce.
+- Contract types of your own, those with `#[contracttype]`, get a
+ `Prototype` automatically, but only when the `soroban-sdk/testutils`
+ feature is on. That means your contract crate needs its own `testutils`
+ feature that enables `soroban-sdk/testutils`, and the fuzz crate must turn
+ it on.
+- A panicking contract call is what the fuzzer detects, so call `client.foo()`
+ and let it panic. If you use `client.try_foo()` you get a `Result` back and
+ a failing call is no longer a crash — useful when you want to check
+ something about the error instead, with `assert!`/`panic!` of your own.
+- The fuzz crate depends on `arbitrary` directly, because that is where the
+ `Arbitrary` derive comes from and `fuzz!` expects it to be nameable. Keep
+ its version matching the one `soroban-sdk` uses.
+
+[`SorobanArbitrary`]: https://docs.rs/soroban-sdk/latest/soroban_sdk/testutils/arbitrary/trait.SorobanArbitrary.html
+
+## Note for maintainers of this repository
+
+`make build-fuzz-afl` builds this fuzz test, and requires `cargo-afl` to be
+installed as above. Unlike the `cargo-fuzz` example in `tests/fuzz`, it is
+not built in CI, because building it means building AFL++ from source.
diff --git a/tests/fuzz_afl/fuzz/.gitignore b/tests/fuzz_afl/fuzz/.gitignore
new file mode 100644
index 000000000..5381c0af5
--- /dev/null
+++ b/tests/fuzz_afl/fuzz/.gitignore
@@ -0,0 +1,3 @@
+target
+out
+Cargo.lock
diff --git a/tests/fuzz_afl/fuzz/Cargo.toml b/tests/fuzz_afl/fuzz/Cargo.toml
new file mode 100644
index 000000000..7aa8e4dba
--- /dev/null
+++ b/tests/fuzz_afl/fuzz/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "test_fuzz_afl-fuzz"
+version = "0.0.0"
+publish = false
+edition = "2021"
+
+[dependencies]
+afl = "0.18"
+arbitrary = { version = "~1.3.0", features = ["derive"] }
+soroban-sdk = { path = "../../../soroban-sdk", features = [ "testutils" ]}
+test_fuzz_afl = { path = ".." }
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[profile.release]
+debug = 1
+
+[[bin]]
+name = "fuzz_target_1"
+path = "src/fuzz_target_1.rs"
+test = false
+doc = false
diff --git a/tests/fuzz_afl/fuzz/in/seed b/tests/fuzz_afl/fuzz/in/seed
new file mode 100644
index 000000000..9c7bc66b5
--- /dev/null
+++ b/tests/fuzz_afl/fuzz/in/seed
@@ -0,0 +1 @@
+0000000000000000000000000000000000000000000000000000000000000000
\ No newline at end of file
diff --git a/tests/fuzz_afl/fuzz/src/fuzz_target_1.rs b/tests/fuzz_afl/fuzz/src/fuzz_target_1.rs
new file mode 100644
index 000000000..10272d74b
--- /dev/null
+++ b/tests/fuzz_afl/fuzz/src/fuzz_target_1.rs
@@ -0,0 +1,29 @@
+use afl::fuzz;
+use arbitrary::Arbitrary;
+
+use soroban_sdk::{testutils::arbitrary::SorobanArbitrary, Env, IntoVal, U256};
+
+use test_fuzz_afl::{Contract, ContractClient};
+
+#[derive(Arbitrary, Debug)]
+struct Input {
+ a: ::Prototype,
+ b: ::Prototype,
+}
+
+fn main() {
+ // Every iteration gets a fresh `Env` so that no ledger state leaks from one
+ // input to the next. AFL++ runs the body of `fuzz!` in a persistent loop,
+ // so anything created outside of it would be shared between inputs.
+ fuzz!(|input: Input| {
+ let env = Env::default();
+
+ let a: U256 = input.a.into_val(&env);
+ let b: U256 = input.b.into_val(&env);
+
+ let contract_id = env.register(Contract, ());
+ let client = ContractClient::new(&env, &contract_id);
+
+ let _ = client.run(&a, &b);
+ });
+}
diff --git a/tests/fuzz_afl/src/lib.rs b/tests/fuzz_afl/src/lib.rs
new file mode 100644
index 000000000..f57d35015
--- /dev/null
+++ b/tests/fuzz_afl/src/lib.rs
@@ -0,0 +1,14 @@
+#![no_std]
+use soroban_sdk::{contract, contractimpl, U256};
+
+#[contract]
+pub struct Contract;
+
+#[contractimpl]
+impl Contract {
+ pub fn run(a: U256, b: U256) {
+ if a < b {
+ panic!("unexpected")
+ }
+ }
+}