
Macro Rust e sottocomando Cargo per automatizzare il fuzzing con afl.rs, inclusa la generazione di corpus e l'implementazione di harness, integrati con il framework di test di Rust.
test-fuzz è un sottocomando di Cargo e una raccolta di macro Rust per automatizzare alcune attività relative al fuzzing con afl.rs, tra cui:
test-fuzz realizza queste attività (in parte) utilizzando le strutture di test di Rust. Ad esempio, per generare un corpus di fuzzing, test-fuzz registra gli argomenti di un target ogni volta che viene chiamato durante un'invocazione di cargo test. Analogamente, test-fuzz implementa un harness di fuzzing come test aggiuntivo in un binario generato da cargo-test. Questa stretta integrazione con le strutture di test di Rust è ciò che motiva il nome test-fuzz.
Contenuti
test_fuzz macrotest_fuzz_impl macrocargo test-fuzz comando]test-fuzz]Installa cargo-test-fuzz e afl.rs con il seguente comando:```sh
cargo install cargo-test-fuzz cargo-afl
## Panoramica
Il fuzzing con `test-fuzz` consiste essenzialmente in tre passaggi:\*
1. **Identificare un target di fuzzing**:
- Aggiungere le seguenti `dependencies` al file `Cargo.toml` della crate target:
```toml
serde = "*"
test-fuzz = "*"
```
- Anteporre alla funzione target la macro [`test_fuzz`]:
```rust
#[test_fuzz::test_fuzz]
fn foo(...) {
...
}
```
2. **Generare un corpus** eseguendo `cargo test`: ```
cargo test
cargo test-fuzz: ```
cargo test-fuzz foo
* Potrebbe essere necessario un ulteriore passaggio preliminare dopo un riavvio:```sh cargo afl system-config
Si noti che il comando precedente esegue `sudo` internamente. Pertanto, potrebbe esserti richiesto di inserire la password.
## Componenti
### Macro `test_fuzz`
Anteporre la macro `test_fuzz` a una funzione indica che la funzione è un fuzz target.
Gli effetti principali della macro `test_fuzz` sono:
- Aggiunge strumentazione al target per serializzare i suoi argomenti e scriverli in un file di corpus ogni volta che il target viene chiamato. La strumentazione è protetta da `#[cfg(test)]`, così che i file di corpus vengano generati solo durante l'esecuzione dei test (vedi comunque [`enable_in_production`] di seguito).
- Aggiunge un test che legge e deserializza gli argomenti dallo standard input e applica il target agli stessi. Il test controlla una variabile d'ambiente, impostata da [`cargo test-fuzz`], in modo che il test non si blocchi tentando di leggere dallo standard input durante una normale invocazione di `cargo test`. Il test è racchiuso in un modulo per ridurre la probabilità di una collisione di nomi. Attualmente, il nome del modulo è `target_fuzz`, dove `target` è il nome del target (vedi comunque [`rename`] di seguito).
#### Argomenti
##### `bounds = "where_predicates"`
Impone `where_predicates` (ad esempio, trait bounds) sulla struct usata per serializzare/deserializzare gli argomenti. Questo può essere necessario, ad esempio, se il tipo di un argomento del target è un tipo associato. Per un esempio, vedere [associated_type.rs] in questo repository.
##### `generic_args = "parameters"`
Usa `parameters` come parametri di tipo del target durante il fuzzing. Esempio:```rust
#[test_fuzz(generic_args = "String")]
fn foo<T: Clone + Debug + Serialize>(x: &T) {
...
}
Nota: gli argomenti del target devono essere serializzabili per ogni istanziazione dei suoi parametri di tipo. Ma gli argomenti del target devono essere deserializzabili solo quando il target è istanziato con parameters.
impl_generic_args = "parameters"Usa parameters come parametri di tipo Self del target durante il fuzzing. Esempio:```rust
#[test_fuzz_impl]
impl<T: Clone + Debug + Serialize> for Foo {
#[test_fuzz(impl_generic_args = "String")]
fn bar(&self, x: &T) {
...
}
}
Nota: gli argomenti del target devono essere serializzabili per **ogni** istanziazione dei parametri di tipo `Self`. Ma gli argomenti del target devono essere deserializzabili solo quando il `Self` del target è istanziato con `parameters`.
##### `convert = "X, Y"`
Quando si serializzano gli argomenti del target, convertire i valori di tipo `X` in tipo `Y` usando l'implementazione di `From<X>` per `Y`, oppure quelli di tipo `&X` in tipo `Y` usando l'implementazione del trait non standard `test_fuzz::FromRef<X>` per `Y`. Quando si deserializzano, riconvertire quei valori al tipo `X` usando l'implementazione del trait non standard `test_fuzz::Into<X>` per `Y`.
Cioè, l'uso di `convert = "X, Y"` deve essere accompagnato da determinate implementazioni. Se `X` implementa [`Clone`], allora `Y` può implementare quanto segue:```rust
impl From<X> for Y {
fn from(x: X) -> Self {
...
}
}
Se X non implementa Clone, allora Y deve implementare quanto segue:```rust
impl test_fuzz::FromRef for Y {
fn from_ref(x: &X) -> Self {
...
}
}
Inoltre, `Y` deve implementare quanto segue (indipendentemente dal fatto che `X` implementi [`Clone`]):```rust
impl test_fuzz::Into<X> for Y {
fn into(self) -> X {
...
}
}
The definition of test_fuzz::Into is identical to that of std::convert::Into. The reason for using a non-standard trait is to avoid conflicts that could arise from blanket implementations of standard traits.
enable_in_productionGenerate corpus files when not running tests, provided the environment variable TEST_FUZZ_WRITE is set. The default is to generate corpus files only when running tests, regardless of whether TEST_FUZZ_WRITE is set. When running a target from outside its package directory, set TEST_FUZZ_MANIFEST_PATH to the path of the package's Cargo.toml file.
WARNING: Setting enable_in_production could introduce a denial-of-service vector. For example, setting this option for a function that is called many times with different arguments could fill up the disk. The check of TEST_FUZZ_WRITE is meant to provide some defense against this possibility. Nonetheless, consider this option carefully before using it.
execute_with = "function"Rather than call the target directly:
FnOnce() -> R, where R is the target's return type, so that calling the closure calls the target;function with the closure.Calling the target in this way allows function to set up the call's environment. This can be useful, e.g., for fuzzing Substrate externalities.
no_auto_generateDo not try to auto-generate corpus files for the target.
only_generic_argsRecord the target's generic args when running tests, but do not generate corpus files and do not implement a fuzzing harness. This can be useful when the target is a generic function, but it is unclear what type parameters should be used for fuzzing.
The intended workflow is: enable only_generic_args, then run cargo test followed by cargo test-fuzz --display generic-args. One of the resulting generic args might be usable as generic_args's parameters. Similarly, generic args resulting from cargo test-fuzz --display impl-generic-args might be usable as impl_generic_args's parameters.
Note, however, that just because a target was called with certain parameters during tests, it does not imply the target's arguments are serializable/deserializable when those parameters are used. The results of --display generic-args/--display impl-generic-args are merely suggestive.
rename = "name"Treat the target as though its name is name when adding a module to the enclosing scope. Expansion of the test_fuzz macro adds a module definition to the enclosing scope. By default, the module is named as follows:
impl block, the module is named target_fuzz__, where target is the name of the target.impl block, the module is named path_target_fuzz__, where path is the last segment of the impl's Self type path.However, use of this option causes the module to instead be named name_fuzz__. Example:```rust
#[test_fuzz(rename = "bar")]
fn foo() {}
// Without the use of rename, a name collision and compile error would result.
mod foo_fuzz__ {}
#### Attributi di campo Serde sugli argomenti di funzione
La macro `test_fuzz` consente di applicare [Serde field attributes] agli argomenti di funzione. Questo fornisce un altro strumento per gestire tipi difficili.
Di seguito è riportato un esempio. I trait `serde::Serialize` e `serde::Deserialize` non possono essere derivati per `Context` perché contiene un `Mutex`. Tuttavia, `Context` implementa `Default`. Quindi applicare `#[serde(skip)]` all'argomento `Context` fa sì che venga saltato durante la serializzazione e che assuma il suo valore predefinito durante la deserializzazione.```rust
use std::sync::Mutex;
// Traits `serde::Serialize` and `serde::Deserialize` cannot be derived for `Context` because it
// contains a `Mutex`.
#[derive(Default)]
struct Context {
lock: Mutex<()>,
}
impl Clone for Context {
fn clone(&self) -> Self {
Self {
lock: Mutex::new(()),
}
}
}
#[test_fuzz::test_fuzz]
fn target(#[serde(skip)] context: Context, x: i32) {
assert!(x >= 0);
}
Nota che quando gli attributi di campo Serde vengono applicati a un argomento, la macro test_fuzz non esegue altre [conversioni] sull'argomento.
test_fuzz_implOgni volta che la macro test_fuzz viene usata in un blocco impl,
il blocco impl deve essere preceduto dalla macro test_fuzz_impl. Esempio:```rust
#[test_fuzz_impl]
impl Foo {
#[test_fuzz]
fn bar(&self, x: &str) {
...
}
}
Il motivo di questo requisito è il seguente. L'espansione della macro [`test_fuzz`] aggiunge una definizione di modulo all'ambito circostante. Tuttavia, una definizione di modulo non può apparire all'interno di un blocco `impl`. Anteporre la macro `test_fuzz_impl` all'`impl` fa sì che il modulo venga aggiunto all'esterno del blocco `impl`.
Se vedi un errore come il seguente, probabilmente significa che manca un uso della macro `test_fuzz_impl`:```
error: module is not supported in `trait`s or `impl`s
test_fuzz_impl attualmente non ha opzioni.
cargo test-fuzzIl comando cargo test-fuzz viene utilizzato per interagire con i fuzz target e per manipolare i relativi corpora, crash, hang e code di lavoro. Esempi di invocazioni includono:
foo ```
cargo test-fuzz foo --display corpus
foo ```
cargo test-fuzz foo
foo ```
cargo test-fuzz foo --replay crashes
Usage: cargo test-fuzz [OPTIONS] [TARGETNAME] [-- ...]
Arguments: [TARGETNAME] String that fuzz target's name must contain [ARGS]... Arguments for the fuzzer
Options:
--backtrace Display backtraces
--consolidate Move one target's crashes, hangs, and work queue to its corpus; to
consolidate all targets, use --consolidate-all
--coverage Generate coverage for corpus, crashes, hangs, or work queue. Note
that generating coverage for instrumented fuzz targets is not
supported.
--cpus Fuzz using at most cpus; default is all but one
--display Display corpus, crashes, generic args, impl generic args, hangs,
or work queue. By default, an uninstrumented fuzz target is used.
To display with instrumentation, append -instrumented to
, e.g., --display corpus-instrumented.
--exact Target name is an exact name rather than a substring
--exit-code Exit with 0 if the time limit was reached, 1 for other
programmatic aborts, and 2 if an error occurred; implies --no-ui,
does not imply --run-until-crash or --max-total-time
--features Space or comma separated list of features to activate
--list List fuzz targets
--manifest-path Path to Cargo.toml
--max-total-time Fuzz at most of time (equivalent to -- -V )
--no-default-features Do not activate the default feature
--no-run Compile, but don't fuzz
--no-ui Disable user interface
-p, --package Package containing fuzz target
--persistent Enable persistent mode fuzzing
--pretty Pretty-print debug output when generating coverage, displaying, or
replaying
--release Build in release mode
--replay Replay corpus, crashes, hangs, or work queue. By default, an
uninstrumented fuzz target is used. To replay with
instrumentation, append -instrumented to , e.g.,
--replay corpus-instrumented.
--reset Clear fuzzing data for one target, but leave corpus intact; to
reset all targets, use --reset-all
--resume Resume target's last fuzzing session
--run-until-crash Stop fuzzing once a crash is found
--slice If there are not sufficiently many cpus to fuzz all targets
simultaneously, fuzz them in intervals of [default:
1200]
--test Integration test containing fuzz target
--timeout Number of seconds to consider a hang when fuzzing or replaying
(equivalent to -- -t <TIMEOUT * 1000> when fuzzing)
--verbose Show build output when generating coverage, displaying, or
replaying
-h, --help Print help
-V, --version Print version
Try cargo afl fuzz --help to see additional fuzzer options.
Quando si usa l'opzione `--display`, viene mostrato qualsiasi output scritto su stderr dal target. Questo include l'output dalle istruzioni `eprintln!`, così come dalle macro di debug come `dbg!`. Ciò può essere utile per capire cosa succede nel tuo codice quando elabora input specifici.
Le opzioni `--display` e `--replay` possono essere passate insieme, consentendo di visualizzare e riprodurre le voci del corpus in un unico comando, ad esempio:```
cargo test-fuzz foo --display corpus --replay corpus
Avviso: Queste utilità sono escluse dal versionamento semantico e potrebbero essere rimosse nelle versioni future di test-fuzz.
dont_care!La macro dont_care! può essere utilizzata per implementare serde::Serialize/serde::Deserialize per tipi facili da costruire e di cui non si desidera registrare i valori. Intuitivamente, dont_care!($ty, $expr) dice:
$ty durante la serializzazione.$ty con $expr durante la deserializzazione.Più specificamente, dont_care!($ty, $expr) si espande nel seguente modo:```rust
impl serde::Serialize for $ty {
fn serialize(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for $ty { fn deserialize(deserializer: D) -> std::result::Result<Self, D::Error> where D: serde::Deserializer<'de>, { <()>::deserialize(deserializer).map(|_| $expr) } }
Se `$ty` è uno unit struct, allora `$expr` può essere omesso. Cioè, `dont_care!($ty)` è equivalente a `dont_care!($ty, $ty)`.
#### `leak!`
La macro `leak!` può aiutare a serializzare argomenti target che sono riferimenti e i cui tipi implementano il trait [`ToOwned`]. È pensata per essere usata con l'opzione [`convert`].
Nello specifico, un'invocazione della forma seguente dichiara un tipo `LeakedX` e implementa i trait `From` e `test_fuzz::Into` per esso:```rust
leak!(X, LeakedX);
Si può quindi usare LeakedX con l'opzione convert come segue:```rust
#[test_fuzz::test_fuzz(convert = "&X, LeakedX")
Un esempio in cui `X` è [`Path`] appare in [conversion.rs] in questo repository.
Più in generale, un'invocazione della forma `leak!($ty, $ident)` si espande nel seguente modo:```rust
#[derive(Clone, std::fmt::Debug, serde::Deserialize, serde::Serialize)]
struct $ident(<$ty as ToOwned>::Owned);
impl From<&$ty> for $ident {
fn from(ty: &$ty) -> Self {
Self(ty.to_owned())
}
}
impl test_fuzz::Into<&$ty> for $ident {
fn into(self) -> &'static $ty {
Box::leak(Box::new(self.0))
}
}
serialize_ref / deserialize_refserialize_ref e deserialize_ref funzionano in modo simile a leak!, ma sono pensati per essere usati con gli attributi di campo serialize_with e deserialize_with di Serde (rispettivamente).```rust
fn serialize_ref<S, T>(x: &&T, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: serde::Serialize,
{
::serialize(*x, serializer)
}
fn deserialize_ref<'de, D, T>(deserializer: D) -> Result<&'static T, D::Error> where D: serde::Deserializer<'de>, T: serde:🇩🇪:DeserializeOwned + std::fmt::Debug, { let x = ::deserialize(deserializer)?; Ok(Box::leak(Box::new(x))) }
#### `serialize_ref_mut` / `deserialize_ref_mut`
`serialize_ref_mut` e `deserialize_ref_mut` sono simili a `serialize_ref` e `deserialize_ref` (rispettivamente), tranne per il fatto che operano su riferimenti mutabili invece che immutabili.
## Funzionalità del pacchetto `test-fuzz`
Le funzionalità in questa sezione si applicano al pacchetto `test-fuzz` nel suo complesso. Abilitalo nella specifica delle dipendenze di `test-fuzz` come descritto in [The Cargo Book]. Ad esempio, per abilitare la funzionalità `cast_checks`, usa:```toml
test-fuzz = { version = "*", features = ["cast_checks"] }
Il package test-fuzz supporta attualmente le seguenti funzionalità:
cast_checksUsa cast_checks per controllare automaticamente le funzioni target per cast non validi.
Nota che questa funzionalità abilita cast_checks solo per le funzioni annotate con la test_fuzz macro, non per le funzioni che queste chiamano.
test-fuzz può serializzare gli argomenti dei target in più formati Serde. Le seguenti sono le funzionalità usate per selezionare un formato.
cargo-test-fuzz può generare automaticamente valori per i tipi che implementano determinati trait. Se tutti i tipi degli argomenti di un target implementano tali trait, cargo-test-fuzz può generare automaticamente file di corpus per il target.
I trait che cargo-test-fuzz supporta attualmente e i valori generati per essi sono i seguenti:
Legenda
Add - core::ops::AddBounded - num_traits::bounds::BoundedDefault - std::default::DefaultDiv - core::ops::DivOne - num_traits::OneSub - core::ops::SubTEST_FUZZ_LOGDurante l'espansione della macro:
TEST_FUZZ_LOG è impostato su 1, scrivi tutti i fuzz target strumentati e le definizioni dei moduli sullo standard output.TEST_FUZZ_LOG è impostato sul nome di una crate, scrivi i fuzz target strumentati e le definizioni dei moduli di quella crate sullo standard output.Questo può essere utile per il debug.
TEST_FUZZ_MANIFEST_PATHQuando si esegue un target dall'esterno della directory del suo package, individua il file Cargo.toml del package in questa posizione. Potrebbe essere necessario impostare questa variabile d'ambiente quando si usa enable_in_production.
TEST_FUZZ_WRITEGenera i file di corpus quando non si eseguono i test per quei target per i quali è impostato enable_in_production.
Gli argomenti di un target devono implementare il trait Clone. Il motivo di questo requisito è che gli argomenti sono necessari in due punti: in una funzione interna a test-fuzz che scrive i file di corpus e nel corpo della funzione target. Per risolvere questo conflitto, gli argomenti vengono clonati prima di essere passati alla prima.
In generale, gli argomenti di un target devono implementare i trait serde::Serialize e serde::Deserialize, ad esempio deriving them. Diciamo "in generale" perché test-fuzz sa come gestire alcuni casi speciali che normalmente non sarebbero serializzabili/deserializzabili. Ad esempio, un argomento di tipo &str viene convertito in String durante la serializzazione e di nuovo in &str durante la deserializzazione. Vedi anche generic_args e impl_generic_args sopra.
Gli harness di fuzzing implementati da test-fuzz non inizializzano le variabili globali. Sebbene execute_with fornisca qualche rimedio, non è una soluzione completa. In generale, fare fuzzing su una funzione che dipende da variabili globali richiede metodi ad hoc.
convert e generic_args / impl_generic_argsQueste opzioni sono incompatibili nel senso seguente. Se il tipo di un argomento di un fuzz target è un parametro di tipo, convert proverà a corrispondere al parametro di tipo, non al tipo al quale il parametro è impostato. Supportare quest'ultimo sembrerebbe richiedere di simulare la sostituzione dei tipi come farebbe il compilatore. Tuttavia, questo non è attualmente implementato.
#[cfg(test)] is not enabled per i test di integrazione. Se il tuo target è testato solo da test di integrazione, considera l'uso di enable_in_production e TEST_FUZZ_WRITE per generare un corpus. (Nota comunque l'avviso che accompagna enable_in_production).
Se conosci il package in cui si trova il tuo target, passare -p <package> a cargo test/cargo test-fuzz può ridurre notevolmente i tempi di compilazione. Analogamente, se sai che il tuo target viene chiamato da un solo test di integrazione, passare --test <name> può ridurre i tempi di compilazione.
Rust won't allow you to implementare per i tipi di altri repository. Ma potresti riuscire a altri repository per rendere serializzabili i loro tipi. Inoltre, può essere utile per recuperare i repository delle dipendenze.
Ci riserviamo il diritto di modificare il formato di corpus, crash, hang e code di lavoro e di considerare tali modifiche come non breaking.
test-fuzz è concesso in licenza e distribuito sotto licenza AGPLv3 con la Macros and Inline Functions Exception. In parole semplici, usare la test_fuzz macro, la test_fuzz_impl macro o le convenience functions and macros di test-fuzz nel tuo software non richiede che quest'ultimo sia coperto dalla licenza AGPLv3.
| Trait(s) | Valore(i) |
|---|
Bounded | T::min_value(), T::max_value() |
Bounded + Add + One | T::min_value() + T::one() |
Bounded + Add + Div + Two | T::min_value() / T::two() + T::max_value() / T::two() |
Bounded + Add + Div + Two + One | T::min_value() / T::two() + T::max_value() / T::two() + T::one() |
Bounded + Sub + One | T::max_value() - T::one() |
Default | T::default() |
Two - test_fuzz::runtime::traits::Two (essenzialmente Add + One)serde::SerializeSerde attributes possono essere utili per implementare serde::Serialize/serde::Deserialize per tipi difficili.