👩🏫Fuzzing Lessons
Increase your coverage using fuzzing lessons
The main goal of a fuzzer is to cover your code as well as possible. However, occasionally a fuzzer may struggle to reach certain regions of your code. In such cases, you may want to help the fuzzer by "teaching" it how to reach a line of code that it could not cover on its own.
To make this easy, Diligence Fuzzing allows you to record so-called fuzzing lessons. On a high level, a fuzzing lesson is recorded by observing how a human user would invoke the code to cover a given line of code. During the next fuzzing campaign, the fuzzer can use previously recorded lessons by replaying the observed invocations. This will allow the fuzzer to reach the given line of code but also other code that is reachable from there.
The user can record another lesson and repeat this process if other code regions are still not covered during the next campaign. Eventually, the user can help the fuzzer explore all the critical code regions by recording a small number of lessons.
Let us illustrate this process with a concrete example.
Imagine that we want to test the following contract (only intended for illustration purposes and not for production use):
The contract allows any user to destroy it (by invoking the destroy
function that, in addition, transfers any leftover Ether to the sender). However, this is only possible once the owner has shared their permission with at least one user by signing a hash value with their private key. The signature (represented by the inputs v
, r
, and s
of function permitDestroy
) is validated using Solidity's ecrecover
primitive; for a valid signature, it returns the signer's address.
When we start a fuzzing campaign for this contract, the fuzzer will not be able to cover the last line of function permitDestroy
(see the above screenshot). This is not surprising since the fuzzer would have to come up with a valid signature of the owner for a fixed hash value. This would require the fuzzer to guess the owner's private key and to sign a specific hash value to obtain valid inputs for v
, r
, and s
.
Luckily, we can use a fuzzing lesson to help the fuzzer overcome this obstacle. We create a small script that invokes the permitDestroy
function with valid inputs. Here, we use the following Hardhat script, but one could, for instance, also use a local web frontend to interact with the contract:
On a high level, the script first generates the hash value and then signs it with the owner's private key (0x000000000000000000000000000000000000000000000000000000000000affe
). Finally, it invokes the permitDestroy
function with the corresponding valid inputs.
Once we have written our script, we can start recording a lesson using our fuzzing CLI:
The description is optional but helpful in documenting what individual lessons are supposed to "teach".
Now, we can run the script using the following command:
And finally, we can save the recorded lesson:
The CLI saves the recorded lesson in a .fuzzing_lessons.json
file, and we can start a new campaign to verify that the lesson achieved the desired effect. Before starting the new campaign, the local node needs to be restarted and the deployment script needs to be rerun. After all, recording the fuzzing lesson has changed the state that will be used as the seed state for fuzzing.
The new campaign can now completely cover the function permitDestroy
(see the above screenshot). On top of this, it can now also fully cover the function destroy
. This demonstrates how a small hint can sometimes unlock new regions of the code that were not originally targeted by the lesson.
Last updated