Add coverage-guided C fuzzers (#203)

* Start to add fuzzers

* Update path to trusted setup in gen_corpus

* Update path one more time

* Add support for two more targets

* Add fuzzers for remaining targets

* Clean up a little

* Add README

* Fix typos

* De-dup fuzzing files

* Add newline at the end of base_fuzz

* Make generic rule for displaying targets

* Use regular make command

* Remove duplicate targets

* Update .gitignore file
This commit is contained in:
Justin Traglia 2023-03-13 10:45:02 +00:00 committed by GitHub
parent 4f0546af81
commit 7d170f9939
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 579 additions and 0 deletions

2
fuzz/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!/.gitignore

91
fuzz/Makefile Normal file
View File

@ -0,0 +1,91 @@
###############################################################################
# Configuration Options
###############################################################################
CC = clang
FIELD_ELEMENTS_PER_BLOB ?= 4096
CFLAGS += -I../inc
CFLAGS += -Wall -Wextra -Werror -O0
CFLAGS += -DFIELD_ELEMENTS_PER_BLOB=$(FIELD_ELEMENTS_PER_BLOB)
CFLAGS += -g -fsanitize=fuzzer
BLST = -L../lib -lblst
LIBBLST = ../lib/libblst.a
# Default to no threads, to use all cores specify -1.
THREADS ?= 0
ifeq ($(THREADS), -1)
override THREADS = $(shell nproc --all)
endif
# On macOS, you need to use clang from the llvm package.
ifneq ($(OS),Windows_NT)
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
ENV_OPTS += PATH=$(shell brew --prefix llvm)/bin:$(PATH)
endif
endif
###############################################################################
# Helper targets
###############################################################################
.PHONY: targets
targets:
@echo Available targets:
@# List targets | filter fuzz targets | remove deps | add prefix
@make -qp | grep "^fuzz_" | sed 's/:.*//' | sed 's/^/ - /' | sort | uniq
$(LIBBLST):
@echo [+] Building blst
@make -C../src blst
.PRECIOUS: %/corpus
%/corpus:
@echo [+] Generating corpus
@cd gen_corpus && go run .
.PRECIOUS: %/fuzz
%/fuzz: %/fuzz.c ../src/c_kzg_4844.c %/corpus $(LIBBLST)
@echo [+] Compiling $* fuzzer
@$(ENV_OPTS) $(CC) $(CFLAGS) -o $@ $< $(BLST)
.PHONY: run_fuzz_%
run_fuzz_%: %/fuzz
@echo [+] Starting $* fuzzer
@-./$< \
-artifact_prefix=./$*/ \
-workers=$(THREADS) \
-jobs=$(THREADS) \
-max_len=$(LEN) \
./$*/corpus
###############################################################################
# Fuzzing targets
###############################################################################
# Length is (4096 * 32).
fuzz_blob_to_kzg_commitment: LEN=131072
fuzz_blob_to_kzg_commitment: run_fuzz_blob_to_kzg_commitment
# Length is (4096 * 32) + 32.
fuzz_compute_kzg_proof: LEN=131104
fuzz_compute_kzg_proof: run_fuzz_compute_kzg_proof
# Length is (4096 * 32) + 48.
fuzz_compute_blob_kzg_proof: LEN=131120
fuzz_compute_blob_kzg_proof: run_fuzz_compute_blob_kzg_proof
# Length is 48 + 32 + 32 + 48.
fuzz_verify_kzg_proof: LEN=160
fuzz_verify_kzg_proof: run_fuzz_verify_kzg_proof
# Length is (4096 * 32) + 48 + 48.
fuzz_verify_blob_kzg_proof: LEN=131168
fuzz_verify_blob_kzg_proof: run_fuzz_verify_blob_kzg_proof
# Length is (3 * 4096 * 32) + (3 * 48) + (3 * 48).
fuzz_verify_blob_kzg_proof_batch: LEN=393504
fuzz_verify_blob_kzg_proof_batch: run_fuzz_verify_blob_kzg_proof_batch

116
fuzz/README.md Normal file
View File

@ -0,0 +1,116 @@
# Fuzzing
This directory contains coverage-guided fuzzers for KZG functions. It uses
LLVM's [libFuzzer](https://llvm.org/docs/LibFuzzer.html) for the heavy lifting.
Each directory is named after a target and contains a single file (`fuzz.c`)
that implements a `LLVMFuzzerTestOneInput` function. These are relatively
simple; if the input matches the size requirements, it passes the data to the
target function. There is a Makefile that compiles and starts the fuzzer, which
means it should be pretty easy.
## Dependencies
This is expected to run on Linux/macOS, it is not expected to work on Windows.
In additional to `build-essentials` and `clang`, this requires `llvm` be
installed:
### Linux
```
sudo apt install llvm
```
### macOS
```
brew install llvm
```
## Targets
Currently, only the public KZG interface functions are fuzzable:
```
$ make
Available targets:
- fuzz_blob_to_kzg_commitment
- fuzz_compute_blob_kzg_proof
- fuzz_compute_kzg_proof
- fuzz_verify_blob_kzg_proof
- fuzz_verify_blob_kzg_proof_batch
- fuzz_verify_kzg_proof
```
To run a fuzzer, run `make fuzz_<func>` like:
```
$ make fuzz_verify_kzg_proof
[+] Building blst
+ cc -O2 -fno-builtin -fPIC -Wall -Wextra -Werror -c ./src/server.c
+ cc -O2 -fno-builtin -fPIC -Wall -Wextra -Werror -c ./build/assembly.S
+ ar rc libblst.a assembly.o server.o
[+] Generating corpus
[+] Compiling verify_kzg_proof fuzzer
[+] Starting verify_kzg_proof fuzzer
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 855755358
INFO: Loaded 1 modules (228 inline 8-bit counters): 228 [0x1025a00f8, 0x1025a01dc),
INFO: Loaded 1 PC tables (228 PCs): 228 [0x1025a01e0,0x1025a1020),
INFO: 1 files found in ./verify_kzg_proof/corpus
INFO: seed corpus: files: 1 min: 160b max: 160b total: 160b rss: 28Mb
#2 pulse ft: 17 exec/s: 1 rss: 29Mb
#2 INITED cov: 17 ft: 17 corp: 1/160b exec/s: 1 rss: 29Mb
#4 pulse cov: 17 ft: 17 corp: 1/160b lim: 160 exec/s: 2 rss: 29Mb
#5 NEW cov: 19 ft: 20 corp: 2/320b lim: 160 exec/s: 2 rss: 29Mb L: 160/160 MS: 3 ChangeASCIIInt-ChangeBit-ChangeBit-
#7 NEW cov: 20 ft: 21 corp: 3/477b lim: 160 exec/s: 3 rss: 29Mb L: 157/160 MS: 2 ChangeByte-EraseBytes-
#8 pulse cov: 20 ft: 21 corp: 3/477b lim: 160 exec/s: 4 rss: 29Mb
#13 NEW cov: 21 ft: 23 corp: 4/637b lim: 160 exec/s: 6 rss: 29Mb L: 160/160 MS: 1 ChangeBit-
#16 pulse cov: 21 ft: 23 corp: 4/637b lim: 160 exec/s: 8 rss: 29Mb
...
```
There are a few steps:
* Build the blst library.
* Generate initial corpora files.
* Compile the fuzzer.
* Start the fuzzer.
Reference [this page](https://llvm.org/docs/LibFuzzer.html#output) for a guide on reading the output.
To stop the fuzzer, press ctrl-C on your keyboard. It will print something like:
```
...
#65536 pulse cov: 25 ft: 29 corp: 7/961b lim: 160 exec/s: 16384 rss: 29Mb
#131072 pulse cov: 25 ft: 29 corp: 7/961b lim: 160 exec/s: 18724 rss: 29Mb
^C==11616== libFuzzer: run interrupted; exiting
make: [run_fuzz_verify_kzg_proof] Error 72 (ignored)
```
If your system has multiple cores, it's easy to run fuzzers on multiple threads.
Append `THREADS=<n>` where `n` is the number of threads you would like there to
be. If you wish to use all available CPU cores, specify `-1` as the count.
```
$ make fuzz_verify_kzg_proof THREADS=4
[+] Starting verify_kzg_proof fuzzer
./verify_kzg_proof/fuzz -artifact_prefix=./verify_kzg_proof/ -max_len=160 ./verify_kzg_proof/corpus >fuzz-0.log 2>&1
./verify_kzg_proof/fuzz -artifact_prefix=./verify_kzg_proof/ -max_len=160 ./verify_kzg_proof/corpus >fuzz-2.log 2>&1
./verify_kzg_proof/fuzz -artifact_prefix=./verify_kzg_proof/ -max_len=160 ./verify_kzg_proof/corpus >fuzz-1.log 2>&1
./verify_kzg_proof/fuzz -artifact_prefix=./verify_kzg_proof/ -max_len=160 ./verify_kzg_proof/corpus >fuzz-3.log 2>&1
```
When you press ctrl-C it will stop all the fuzzers and print their output to
your console sequentially. You will most likely need to scroll up to see their
outputs.
When operating in parallel (threads) the fuzzers use a shared corpus and are
intelligent enough to learn from other threads that have progressed further.
When you see a line that starts with "RELOAD" that fuzzer process is updating
its corpus with findings from other threads.
### Findings
If there is a crash or timeout, the fuzzer will write a file to the target
directory containing the input data associated with that crash/timeout. If this
happens, please report the finding via an issue on GitHub.

34
fuzz/base_fuzz.h Normal file
View File

@ -0,0 +1,34 @@
/*
* This file contains fuzzing tests for C-KZG-4844.
*/
#pragma once
#include "../src/c_kzg_4844.c"
///////////////////////////////////////////////////////////////////////////////
// Globals
///////////////////////////////////////////////////////////////////////////////
KZGSettings s;
///////////////////////////////////////////////////////////////////////////////
// Trusted setup configuration
///////////////////////////////////////////////////////////////////////////////
static void initialize(void) {
static bool initialized = false;
if (!initialized) {
FILE *fp;
C_KZG_RET ret;
/* Open the mainnet trusted setup file */
fp = fopen("../src/trusted_setup.txt", "r");
assert(fp != NULL);
/* Load that trusted setup file */
ret = load_trusted_setup_file(&s, fp);
assert(ret == C_KZG_OK);
fclose(fp);
initialized = true;
}
}

View File

@ -0,0 +1,17 @@
#include "../base_fuzz.h"
static const size_t BLOB_OFFSET = 0;
static const size_t INPUT_SIZE = BLOB_OFFSET + BYTES_PER_BLOB;
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
initialize();
if (size == INPUT_SIZE) {
KZGCommitment commitment;
blob_to_kzg_commitment(
&commitment,
(const Blob *)(data + BLOB_OFFSET),
&s
);
}
return 0;
}

View File

@ -0,0 +1,19 @@
#include "../base_fuzz.h"
static const size_t BLOB_OFFSET = 0;
static const size_t COMMITMENT_OFFSET = BYTES_PER_BLOB;
static const size_t INPUT_SIZE = COMMITMENT_OFFSET + BYTES_PER_COMMITMENT;
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
initialize();
if (size == INPUT_SIZE) {
KZGProof proof;
compute_blob_kzg_proof(
&proof,
(const Blob *)(data + BLOB_OFFSET),
(const Bytes48 *)(data + COMMITMENT_OFFSET),
&s
);
}
return 0;
}

View File

@ -0,0 +1,21 @@
#include "../base_fuzz.h"
static const size_t BLOB_OFFSET = 0;
static const size_t Z_OFFSET = BLOB_OFFSET + BYTES_PER_BLOB;
static const size_t INPUT_SIZE = Z_OFFSET + BYTES_PER_FIELD_ELEMENT;
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
initialize();
if (size == INPUT_SIZE) {
KZGProof proof;
Bytes32 y;
compute_kzg_proof(
&proof,
&y,
(const Blob *)(data + BLOB_OFFSET),
(const Bytes32 *)(data + Z_OFFSET),
&s
);
}
return 0;
}

8
fuzz/gen_corpus/go.mod Normal file
View File

@ -0,0 +1,8 @@
module gen_corpus
go 1.19
require (
github.com/ethereum/c-kzg-4844 v0.0.0-20230310094217-da83e45e9cef // indirect
github.com/supranational/blst v0.3.11-0.20230124161941-ca03e11a3ff2 // indirect
)

10
fuzz/gen_corpus/go.sum Normal file
View File

@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/ethereum/c-kzg-4844 v0.0.0-20230310094217-da83e45e9cef h1:o/RGxt4PuTHLxwOBS30DFdADBwOZV7J5zoT9CTG/mBU=
github.com/ethereum/c-kzg-4844 v0.0.0-20230310094217-da83e45e9cef/go.mod h1:ECrzdyCNp8d5bLD8Id8Y0mZ8+HDZ0LIdshCrDDTO944=
github.com/ethereum/c-kzg-4844/bindings/go v0.0.0-20230126171313-363c7d7593b4 h1:B2mpK+MNqgPqk2/KNi1LbqwtZDy5F7iy0mynQiBr8VA=
github.com/ethereum/c-kzg-4844/bindings/go v0.0.0-20230126171313-363c7d7593b4/go.mod h1:y4GA2JbAUama1S4QwYjC2hefgGLU8Ul0GMtL/ADMF1c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/supranational/blst v0.3.11-0.20230124161941-ca03e11a3ff2 h1:wh1wzwAhZBNiZO37uWS/nDaKiIwHz4mDo4pnA+fqTO0=
github.com/supranational/blst v0.3.11-0.20230124161941-ca03e11a3ff2/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

196
fuzz/gen_corpus/main.go Normal file
View File

@ -0,0 +1,196 @@
package main
import (
"bytes"
"math/rand"
"os"
"path"
ckzg "github.com/ethereum/c-kzg-4844/bindings/go"
)
///////////////////////////////////////////////////////////////////////////////
// Helper functions
///////////////////////////////////////////////////////////////////////////////
func GetRandFieldElement(seed int64) ckzg.Bytes32 {
rand.Seed(seed)
bytes := make([]byte, 31)
_, err := rand.Read(bytes)
if err != nil {
panic("failed to get random field element")
}
// This leaves the last byte in fieldElementBytes as
// zero, which guarantees it's a canonical field element.
var fieldElementBytes ckzg.Bytes32
copy(fieldElementBytes[:], bytes)
return fieldElementBytes
}
func GetRandCommitment(seed int64) ckzg.KZGCommitment {
commitment, ret := ckzg.BlobToKZGCommitment(GetRandBlob(seed))
if ret != ckzg.C_KZG_OK {
panic("failed to get random commitment")
}
return commitment
}
func GetRandProof(seed int64) ckzg.KZGProof {
commitment := ckzg.Bytes48(GetRandCommitment(seed))
proof, ret := ckzg.ComputeBlobKZGProof(GetRandBlob(seed), commitment)
if ret != ckzg.C_KZG_OK {
panic("failed to get random proof")
}
return proof
}
func GetRandBlob(seed int64) ckzg.Blob {
var blob ckzg.Blob
for i := 0; i < ckzg.BytesPerBlob; i += ckzg.BytesPerFieldElement {
fieldElementBytes := GetRandFieldElement(seed + int64(i))
copy(blob[i:i+ckzg.BytesPerFieldElement], fieldElementBytes[:])
}
return blob
}
///////////////////////////////////////////////////////////////////////////////
// Generators
///////////////////////////////////////////////////////////////////////////////
func GenerateCorpus_BlobToKZGCommitment() {
blob := GetRandBlob(0)
dir := "../blob_to_kzg_commitment/corpus"
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic(err)
}
err = os.WriteFile(path.Join(dir, "init"), blob[:], 0644)
if err != nil {
panic(err)
}
}
func GenerateCorpus_ComputeKZGProof() {
blob := GetRandBlob(0)
z := GetRandFieldElement(1)
dir := "../compute_kzg_proof/corpus"
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic(err)
}
data := bytes.Join([][]byte{blob[:], z[:]}, []byte{})
err = os.WriteFile(path.Join(dir, "init"), data, 0644)
if err != nil {
panic(err)
}
}
func GenerateCorpus_ComputeBlobKZGProof() {
blob := GetRandBlob(0)
commitment := GetRandCommitment(1)
dir := "../compute_blob_kzg_proof/corpus"
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic(err)
}
data := bytes.Join([][]byte{blob[:], commitment[:]}, []byte{})
err = os.WriteFile(path.Join(dir, "init"), data, 0644)
if err != nil {
panic(err)
}
}
func GenerateCorpus_VerifyKZGProof() {
commitment := GetRandCommitment(0)
z := GetRandFieldElement(1)
y := GetRandFieldElement(2)
proof := GetRandProof(3)
dir := "../verify_kzg_proof/corpus"
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic(err)
}
data := bytes.Join([][]byte{commitment[:], z[:], y[:], proof[:]}, []byte{})
err = os.WriteFile(path.Join(dir, "init"), data, 0644)
if err != nil {
panic(err)
}
}
func GenerateCorpus_VerifyBlobKZGProof() {
blob := GetRandBlob(0)
commitment := GetRandCommitment(1)
proof := GetRandProof(2)
dir := "../verify_blob_kzg_proof/corpus"
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic(err)
}
data := bytes.Join([][]byte{blob[:], commitment[:], proof[:]}, []byte{})
err = os.WriteFile(path.Join(dir, "init"), data, 0644)
if err != nil {
panic(err)
}
}
func GenerateCorpus_VerifyBlobKZGProofBatch() {
const n = 3
var blobs [n][]byte
var commitments [n][]byte
var proofs [n][]byte
for i := range blobs {
blob := GetRandBlob(int64(i) + 0)
commitment := GetRandCommitment(int64(i) + 1)
proof := GetRandProof(int64(i) + 2)
blobs[i] = blob[:]
commitments[i] = commitment[:]
proofs[i] = proof[:]
}
dir := "../verify_blob_kzg_proof_batch/corpus"
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
panic(err)
}
blobsBytes := bytes.Join(blobs[:], []byte{})
commitmentsBytes := bytes.Join(commitments[:], []byte{})
proofsBytes := bytes.Join(proofs[:], []byte{})
data := bytes.Join([][]byte{blobsBytes, commitmentsBytes, proofsBytes}, []byte{})
err = os.WriteFile(path.Join(dir, "init"), data, 0644)
if err != nil {
panic(err)
}
}
///////////////////////////////////////////////////////////////////////////////
// Entry point
///////////////////////////////////////////////////////////////////////////////
func main() {
ret := ckzg.LoadTrustedSetupFile("../../src/trusted_setup.txt")
if ret != ckzg.C_KZG_OK {
panic("failed to load trusted setup")
}
defer ckzg.FreeTrustedSetup()
GenerateCorpus_BlobToKZGCommitment()
GenerateCorpus_ComputeKZGProof()
GenerateCorpus_ComputeBlobKZGProof()
GenerateCorpus_VerifyKZGProof()
GenerateCorpus_VerifyBlobKZGProof()
GenerateCorpus_VerifyBlobKZGProofBatch()
}

View File

@ -0,0 +1,21 @@
#include "../base_fuzz.h"
static const size_t BLOB_OFFSET = 0;
static const size_t COMMITMENT_OFFSET = BLOB_OFFSET + BYTES_PER_BLOB;
static const size_t PROOF_OFFSET = COMMITMENT_OFFSET + BYTES_PER_COMMITMENT;
static const size_t INPUT_SIZE = PROOF_OFFSET + BYTES_PER_PROOF;
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
initialize();
if (size == INPUT_SIZE) {
bool ok;
verify_blob_kzg_proof(
&ok,
(const Blob *)(data + BLOB_OFFSET),
(const Bytes48 *)(data + COMMITMENT_OFFSET),
(const Bytes48 *)(data + PROOF_OFFSET),
&s
);
}
return 0;
}

View File

@ -0,0 +1,21 @@
#include "../base_fuzz.h"
static const size_t BLOBS_OFFSET = 0;
static const size_t COMMITMENTS_OFFSET = BLOBS_OFFSET + BYTES_PER_BLOB;
static const size_t PROOFS_OFFSET = COMMITMENTS_OFFSET + BYTES_PER_COMMITMENT;
static const size_t INPUT_SIZE = PROOFS_OFFSET + BYTES_PER_PROOF;
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
initialize();
size_t count = size / INPUT_SIZE;
bool ok;
verify_blob_kzg_proof_batch(
&ok,
(const Blob *)(data + BLOBS_OFFSET * count),
(const Bytes48 *)(data + COMMITMENTS_OFFSET * count),
(const Bytes48 *)(data + PROOFS_OFFSET * count),
count,
&s
);
return 0;
}

View File

@ -0,0 +1,23 @@
#include "../base_fuzz.h"
static const size_t COMMITMENT_OFFSET = 0;
static const size_t Z_OFFSET = COMMITMENT_OFFSET + BYTES_PER_COMMITMENT;
static const size_t Y_OFFSET = Z_OFFSET + BYTES_PER_FIELD_ELEMENT;
static const size_t PROOF_OFFSET = Y_OFFSET + BYTES_PER_FIELD_ELEMENT;
static const size_t INPUT_SIZE = PROOF_OFFSET + BYTES_PER_PROOF;
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
initialize();
if (size == INPUT_SIZE) {
bool ok;
verify_kzg_proof(
&ok,
(const Bytes48 *)(data + COMMITMENT_OFFSET),
(const Bytes32 *)(data + Z_OFFSET),
(const Bytes32 *)(data + Y_OFFSET),
(const Bytes48 *)(data + PROOF_OFFSET),
&s
);
}
return 0;
}