
Macros Rust et sous-commande Cargo pour automatiser le fuzzing avec afl.rs, y compris la génération de corpus et l'implémentation du harnais, intégrées au framework de test de Rust.
test-fuzz est une sous-commande Cargo et un ensemble de macros Rust permettant d'automatiser certaines tâches liées au fuzzing avec afl.rs, notamment :
test-fuzz accomplit ces tâches (en partie) en utilisant les fonctionnalités de test de Rust. Par exemple, pour générer un corpus de fuzzing, test-fuzz enregistre les arguments d'une cible à chaque appel effectué lors d'une invocation de cargo test. De même, test-fuzz implémente un harnais de fuzzing comme test supplémentaire dans un binaire généré par cargo-test. Cette intégration étroite avec les fonctionnalités de test de Rust est ce qui motive le nom test-fuzz.
Sommaire
test_fuzz macrotest_fuzz_impl macrocargo test-fuzz commandtest-fuzz package featuresInstallez cargo-test-fuzz et afl.rs avec la commande suivante :```sh
cargo install cargo-test-fuzz cargo-afl
## Overview
Le fuzzing avec `test-fuzz` se résume essentiellement à trois étapes :\*
1. **Identifier une cible de fuzzing** :
- Ajoutez les `dependencies` suivantes au fichier `Cargo.toml` de la crate cible :
```toml
serde = "*"
test-fuzz = "*"
```
- Faites précéder la fonction cible de la macro [`test_fuzz`] :
```rust
#[test_fuzz::test_fuzz]
fn foo(...) {
...
}
```
2. **Générer un corpus** en exécutant `cargo test` : ```
cargo test
cargo test-fuzz: ```
cargo test-fuzz foo
* Une étape préliminaire supplémentaire peut être nécessaire après un redémarrage :```sh cargo afl system-config
Notez que la commande ci-dessus exécute `sudo` en interne. Par conséquent, vous pouvez être invité à saisir votre mot de passe.
## Composants
### Macro `test_fuzz`
Faire précéder une fonction de la macro `test_fuzz` indique que cette fonction est une cible de fuzzing.
Les principaux effets de la macro `test_fuzz` sont :
- Ajouter une instrumentation à la cible pour sérialiser ses arguments et les écrire dans un fichier de corpus à chaque appel de la cible. L'instrumentation est protégée par `#[cfg(test)]` afin que les fichiers de corpus ne soient générés que lors de l'exécution des tests (voir cependant [`enable_in_production`] ci-dessous).
- Ajouter un test qui lit et désérialise les arguments depuis l'entrée standard et applique la cible sur ceux-ci. Le test vérifie une variable d'environnement, définie par [`cargo test-fuzz`], afin qu'il ne bloque pas en tentant de lire l'entrée standard lors d'une invocation normale de `cargo test`. Le test est contenu dans un module pour réduire la probabilité d'une collision de noms. Actuellement, le nom du module est `target_fuzz`, où `target` est le nom de la cible (voir cependant [`rename`] ci-dessous).
#### Arguments
##### `bounds = "where_predicates"`
Imposez `where_predicates` (par exemple, des contraintes de trait) sur la structure utilisée pour sérialiser/désérialiser les arguments. Cela peut être nécessaire, par exemple, si le type d'argument d'une cible est un type associé. Pour un exemple, voir [associated_type.rs] dans ce dépôt.
##### `generic_args = "parameters"`
Utilisez `parameters` comme paramètres de type de la cible lors du fuzzing. Exemple :```rust
#[test_fuzz(generic_args = "String")]
fn foo<T: Clone + Debug + Serialize>(x: &T) {
...
}
Note: Les arguments de la cible doivent être sérialisables pour chaque instanciation de ses paramètres de type. Mais les arguments de la cible ne doivent être désérialisables que lorsque la cible est instanciée avec parameters.
impl_generic_args = "parameters"Utilisez parameters comme paramètres de type Self de la cible lors du fuzzing. Exemple:```rust
#[test_fuzz_impl]
impl<T: Clone + Debug + Serialize> for Foo {
#[test_fuzz(impl_generic_args = "String")]
fn bar(&self, x: &T) {
...
}
}
Remarque : les arguments de la cible doivent être sérialisables pour **chaque** instanciation de ses paramètres de type `Self`. Mais les arguments de la cible ne doivent être désérialisables que lorsque le `Self` de la cible est instancié avec `parameters`.
##### `convert = "X, Y"`
Lors de la sérialisation des arguments de la cible, convertissez les valeurs de type `X` en type `Y` en utilisant l'implémentation de `From<X>` par `Y`, ou les valeurs de type `&X` en type `Y` en utilisant l'implémentation par `Y` du trait non standard `test_fuzz::FromRef<X>`. Lors de la désérialisation, reconvertissez ces valeurs en type `X` en utilisant l'implémentation par `Y` du trait non standard `test_fuzz::Into<X>`.
Autrement dit, l'utilisation de `convert = "X, Y"` doit être accompagnée de certaines implémentations. Si `X` implémente [`Clone`], alors `Y` peut implémenter ce qui suit :```rust
impl From<X> for Y {
fn from(x: X) -> Self {
...
}
}
Si X n'implémente pas Clone, alors Y doit implémenter ce qui suit :```rust
impl test_fuzz::FromRef for Y {
fn from_ref(x: &X) -> Self {
...
}
}
De plus, `Y` doit implémenter ce qui suit (que `X` implémente ou non [`Clone`]) :```rust
impl test_fuzz::Into<X> for Y {
fn into(self) -> X {
...
}
}
La définition de test_fuzz::Into est identique à celle de std::convert::Into. La raison d'utiliser un trait non standard est d'éviter les conflits qui pourraient découler d'implémentations fourre-tout des traits standard.
enable_in_productionGénérer des fichiers de corpus lorsque les tests ne sont pas exécutés, à condition que la variable d'environnement TEST_FUZZ_WRITE soit définie. Par défaut, les fichiers de corpus ne sont générés que lors de l'exécution des tests, que TEST_FUZZ_WRITE soit définie ou non. Lorsque vous exécutez une cible depuis l'extérieur de son répertoire de package, définissez TEST_FUZZ_MANIFEST_PATH sur le chemin du fichier Cargo.toml du package.
AVERTISSEMENT : Définir enable_in_production pourrait introduire un vecteur de déni de service. Par exemple, définir cette option pour une fonction appelée de nombreuses fois avec des arguments différents pourrait remplir le disque. La vérification de TEST_FUZZ_WRITE a pour but d'offrir une certaine protection contre cette possibilité. Néanmoins, examinez attentivement cette option avant de l'utiliser.
execute_with = "function"Plutôt que d'appeler la cible directement :
FnOnce() -> R, où R est le type de retour de la cible, de sorte que l'appel de la closure appelle la cible ;function avec la closure.Appeler la cible de cette manière permet à function de configurer l'environnement de l'appel. Cela peut être utile, par exemple, pour fuzzer [les externalités de Substrate].
no_auto_generateNe pas essayer de générer automatiquement des fichiers de corpus pour la cible.
only_generic_argsEnregistrer les arguments génériques de la cible lors de l'exécution des tests, mais ne pas générer de fichiers de corpus et ne pas implémenter de harnais de fuzzing. Cela peut être utile lorsque la cible est une fonction générique, mais qu'on ne sait pas quels paramètres de type devraient être utilisés pour le fuzzing.
Le flux de travail prévu est le suivant : activez only_generic_args, puis exécutez cargo test suivi de cargo test-fuzz --display generic-args. L'un des arguments génériques obtenus pourrait être utilisable comme parameters de generic_args. De même, les arguments génériques résultant de cargo test-fuzz --display impl-generic-args pourraient être utilisables comme parameters de impl_generic_args.
Notez cependant que le simple fait qu'une cible ait été appelée avec certains paramètres pendant les tests n'implique pas que les arguments de la cible soient sérialisables/désérialisables lorsque ces paramètres sont utilisés. Les résultats de --display generic-args/--display impl-generic-args ne sont qu'indicatifs.
rename = "name"Traiter la cible comme si son nom était name lors de l'ajout d'un module à la portée englobante. L'expansion de la macro test_fuzz ajoute une définition de module à la portée englobante. Par défaut, le module est nommé comme suit :
impl, le module est nommé target_fuzz__, où target est le nom de la cible.impl, le module est nommé path_target_fuzz__, où path est le dernier segment du chemin du type Self du impl.Cependant, l'utilisation de cette option fait que le module est nommé name_fuzz__ à la place. Exemple :```rust
#[test_fuzz(rename = "bar")]
fn foo() {}
// Without the use of rename, a name collision and compile error would result.
mod foo_fuzz__ {}
#### Attributs de champ Serde sur les arguments de fonction
La macro `test_fuzz` permet d'appliquer [Serde field attributes] aux arguments de fonction. Cela fournit un autre outil pour traiter les types difficiles.
L'exemple suivant illustre cela. Les traits `serde::Serialize` et `serde::Deserialize` ne peuvent pas être dérivés pour `Context` car celui-ci contient un `Mutex`. Cependant, `Context` implémente `Default`. Ainsi, l'application de `#[serde(skip)]` à l'argument `Context` fait qu'il est ignoré lors de la sérialisation et prend sa valeur par défaut lors de la désérialisation.```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);
}
Notez que lorsque des attributs de champ Serde sont appliqués à un argument, la macro test_fuzz n'effectue aucune autre conversions sur l'argument.
test_fuzz_implChaque fois que la macro test_fuzz est utilisée dans un bloc impl,
le impl doit être précédé de la macro test_fuzz_impl. Exemple :```rust
#[test_fuzz_impl]
impl Foo {
#[test_fuzz]
fn bar(&self, x: &str) {
...
}
}
La raison de cette exigence est la suivante. L'expansion de la macro [`test_fuzz`] ajoute une définition de module à la portée englobante. Cependant, une définition de module ne peut pas apparaître à l'intérieur d'un bloc `impl`. Faire précéder le bloc `impl` par la macro `test_fuzz_impl` fait en sorte que le module soit ajouté à l'extérieur du bloc `impl`.
Si vous voyez une erreur comme celle-ci, cela signifie probablement qu'une utilisation de la macro `test_fuzz_impl` manque :```
error: module is not supported in `trait`s or `impl`s
test_fuzz_impl n'a actuellement aucune option.
cargo test-fuzzLa commande cargo test-fuzz est utilisée pour interagir avec les cibles de fuzzing, et pour manipuler leurs corpus, crashes, blocages et files de travail. Exemples d'invocations :
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.
Lorsque vous utilisez l'option `--display`, toute sortie écrite sur stderr par la cible est affichée. Cela inclut la sortie des instructions `eprintln!`, ainsi que celle des macros de débogage comme `dbg!`. Cela peut être utile pour comprendre ce qui se passe dans votre code lors du traitement d'entrées spécifiques.
Les options `--display` et `--replay` peuvent être utilisées ensemble, ce qui vous permet à la fois de visualiser et de rejouer les entrées du corpus en une seule commande, par exemple :```
cargo test-fuzz foo --display corpus --replay corpus
Avertissement : Ces utilitaires sont exclus du versionnage sémantique et peuvent être supprimés dans les futures versions de test-fuzz.
dont_care!La macro dont_care! peut être utilisée pour implémenter serde::Serialize/serde::Deserialize pour des types faciles à construire et dont vous ne vous souciez pas d'enregistrer les valeurs. Intuitivement, dont_care!($ty, $expr) signifie :
$ty lors de la sérialisation.$ty avec $expr lors de la désérialisation.Plus précisément, dont_care!($ty, $expr) se développe comme suit :```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) } }
Si `$ty` est une structure unitaire, alors `$expr` peut être omis. Autrement dit, `dont_care!($ty)` est équivalent à `dont_care!($ty, $ty)`.
#### `leak!`
La macro `leak!` peut aider à sérialiser des arguments cibles qui sont des références et dont les types implémentent le trait [`ToOwned`]. Elle est destinée à être utilisée avec l'option [`convert`].
Plus précisément, une invocation de la forme suivante déclare un type `LeakedX`, et implémente pour celui-ci les traits `From` et `test_fuzz::Into` :```rust
leak!(X, LeakedX);
On peut ensuite utiliser LeakedX avec l'option convert comme suit :```rust
#[test_fuzz::test_fuzz(convert = "&X, LeakedX")
Un exemple où `X` est [`Path`] apparaît dans [conversion.rs] dans ce dépôt.
Plus généralement, une invocation de la forme `leak!($ty, $ident)` se développe en ce qui suit :```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 et deserialize_ref fonctionnent de manière similaire à leak!, mais elles sont destinées à être utilisées avec les attributs de champ serialize_with et deserialize_with de Serde (respectivement).```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` et `deserialize_ref_mut` sont similaires à `serialize_ref` et `deserialize_ref` (respectivement), sauf qu'ils opèrent sur des références mutables au lieu de références immuables.
## Fonctionnalités du paquet `test-fuzz`
Les fonctionnalités de cette section s'appliquent à l'ensemble du paquet `test-fuzz`. Activez-les dans la spécification de dépendance de `test-fuzz` comme décrit dans [The Cargo Book]. Par exemple, pour activer la fonctionnalité `cast_checks`, utilisez :```toml
test-fuzz = { version = "*", features = ["cast_checks"] }
Le paquet test-fuzz prend actuellement en charge les fonctionnalités suivantes :
cast_checksUtilisez cast_checks pour vérifier automatiquement les fonctions cibles pour les casts invalides.
Notez que cette fonctionnalité n'active cast_checks que pour les fonctions annotées avec la test_fuzz macro, et non pour les fonctions qu'elles appellent.
test-fuzz peut sérialiser les arguments d'une cible dans plusieurs formats Serde. Voici les fonctionnalités utilisées pour sélectionner un format.
cargo-test-fuzz peut générer automatiquement des valeurs pour les types qui implémentent certains traits. Si tous les types des arguments d'une cible implémentent de tels traits, cargo-test-fuzz peut générer automatiquement des fichiers de corpus pour la cible.
Les traits actuellement pris en charge par cargo-test-fuzz et les valeurs générées pour ceux-ci sont les suivants :
| Trait(s) | Valeur(s) |
|---|---|
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() |
Légende
Add - core::ops::AddBounded - num_traits::bounds::BoundedDefault - std::default::DefaultDiv - core::ops::DivOne - num_traits::OneSub - core::ops::SubTwo - test_fuzz::runtime::traits::Two (essentiellement Add + One)TEST_FUZZ_LOGPendant l'expansion de la macro :
TEST_FUZZ_LOG est défini à 1, écrire toutes les cibles de fuzz instrumentées et les définitions de modules sur la sortie standard.TEST_FUZZ_LOG est défini avec le nom d'une crate, écrire les cibles de fuzz instrumentées et les définitions de modules de cette crate sur la sortie standard.Cela peut être utile pour le débogage.
TEST_FUZZ_MANIFEST_PATHLors de l'exécution d'une cible depuis l'extérieur de son répertoire de paquet, recherchez le fichier Cargo.toml du paquet à cet emplacement. Il peut être nécessaire de définir cette variable d'environnement lorsque enable_in_production est utilisé.
TEST_FUZZ_WRITEGénérer des fichiers de corpus lorsque les tests ne sont pas exécutés pour les cibles pour lesquelles enable_in_production est défini.
Les arguments d'une cible doivent implémenter le trait Clone. La raison de cette exigence est que les arguments sont nécessaires à deux endroits : dans une fonction interne à test-fuzz qui écrit les fichiers de corpus, et dans le corps de la fonction cible. Pour résoudre ce conflit, les arguments sont clonés avant d'être passés à la première.
En général, les arguments d'une cible doivent implémenter les traits serde::Serialize et serde::Deserialize, par exemple en les dérivant. Nous disons « en général » parce que test-fuzz sait gérer certains cas particuliers qui ne seraient normalement pas sérialisables/désérialisables. Par exemple, un argument de type &str est converti en String lors de la sérialisation, puis reconverti en &str lors de la désérialisation. Voir aussi generic_args et impl_generic_args plus haut.
Les harnais de fuzzing implémentés par test-fuzz n'initialisent pas les variables globales. Bien que execute_with apporte un remède partiel, ce n'est pas une solution complète. En général, fuzzer une fonction qui repose sur des variables globales nécessite des méthodes ad hoc.
Ces options sont incompatibles au sens suivant. Si le type d'un argument d'une cible de fuzz est un paramètre de type, convert essaiera de correspondre au paramètre de type, et non au type auquel ce paramètre est défini. Prendre en charge ce dernier cas semblerait exiger de simuler la substitution de type comme le ferait le compilateur. Cependant, cela n'est pas implémenté actuellement.
#[cfg(test)] n'est pas activé pour les tests d'intégration. Si votre cible n'est testée que par des tests d'intégration, envisagez d'utiliser enable_in_production et TEST_FUZZ_WRITE pour générer un corpus. (Notez toutefois l'avertissement accompagnant enable_in_production.)
Si vous connaissez le paquet dans lequel se trouve votre cible, passer -p <package> à cargo test/cargo test-fuzz peut réduire considérablement les temps de compilation. De même, si vous savez que votre cible n'est appelée que depuis un seul test d'intégration, passer --test <name> peut réduire les temps de compilation.
Rust ne vous permettra pas d'implémenter serde::Serialize pour les types d'autres dépôts. Mais vous pouvez peut-être patcher d'autres dépôts pour rendre leurs types sérialisables. De plus, cargo-clone peut être utile pour récupérer les dépôts des dépendances.
Les attributs Serde peuvent être utiles pour implémenter serde::Serialize/serde::Deserialize pour les types difficiles.
Nous nous réservons le droit de modifier le format des corpus, des plantages, des blocages et des files de travail, et de considérer ces modifications comme non cassantes.
test-fuzz est sous licence et distribué sous la licence AGPLv3 avec l'Exception Macros et fonctions inline. En clair, utiliser la test_fuzz macro, la test_fuzz_impl macro ou les fonctions et macros de commodité de test-fuzz dans votre logiciel n'exige pas que votre logiciel soit couvert par la licence AGPLv3.