Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ fuzz-fvm:
# run fuzz tests in the fvm package
cd ./fvm && CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz=Fuzz -run ^$$

# FUZZ_TIME controls how long each fuzz target runs (default 5m for local dev; override for CI).
# Example: make fuzz-FuzzCBORDecoder FUZZ_TIME=10m
FUZZ_TIME ?= 5m

# fuzz-<FuzzFunctionName>: run a single named fuzz target for FUZZ_TIME.
# The package is auto-discovered by locating the func declaration in *_test.go files.
# Example: make fuzz-FuzzCBORDecoder
fuzz-%:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repo already has a native fuzz harness that nothing runs in CI: FuzzTransactionComputationLimit in fvm/fvm_fuzz_test.go. Add { name: FuzzTransactionComputationLimit, pkg: fvm } to the nightly workflow matrix. It also works with this fuzz-% target, making fuzz-fvm mostly redundant.

Its likely no-one ran fuzz-fvm in a while, so that code might also be stale

@FUNC=$*; \
PKG=$$(grep -rl "func $${FUNC}(" --include="*_test.go" . | head -1 | xargs -I{} dirname {} | sed 's|^\./||'); \
if [ -z "$${PKG}" ]; then echo "Error: no fuzz function $${FUNC} found in *_test.go files"; exit 1; fi; \
CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz=$${FUNC} -fuzztime=$(FUZZ_TIME) "./$${PKG}/"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without -run '^$$', go test -fuzz first runs the package's entire unit-test suite (minutes for ledger); the fuzz-fvm target above already skips it. -fuzz is also an unanchored regex and go test errors if it matches more than one target.

Suggested change
CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz=$${FUNC} -fuzztime=$(FUZZ_TIME) "./$${PKG}/"
CGO_CFLAGS=$(CRYPTO_FLAG) go test -fuzz="^$${FUNC}$$" -fuzztime=$(FUZZ_TIME) -run '^$$' "./$${PKG}/"


.PHONY: test
test: verify-mocks unittest-main

Expand Down
80 changes: 80 additions & 0 deletions access/validator/validator_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package validator_test

import (
"context"
"testing"

"github.com/onflow/flow-go/access/validator"
"github.com/onflow/flow-go/model/flow"
"github.com/onflow/flow-go/module/metrics"
"github.com/onflow/flow-go/utils/unittest"
)

// fixedBlocks is a minimal Blocks implementation for fuzz testing that always
// returns the same fixed header, keeping the harness deterministic and offline.
type fixedBlocks struct {
header *flow.Header
}

func (b *fixedBlocks) HeaderByID(_ flow.Identifier) (*flow.Header, error) {
return b.header, nil
}

func (b *fixedBlocks) FinalizedHeader() (*flow.Header, error) {
return b.header, nil
}

func (b *fixedBlocks) SealedHeader() (*flow.Header, error) {
return b.header, nil
}

func (b *fixedBlocks) IndexedHeight() (uint64, error) {
return b.header.Height, nil
}

func FuzzTransactionValidatorValidate(f *testing.F) {
header := unittest.BlockHeaderFixture()
blocks := &fixedBlocks{header: header}
chain := flow.Testnet.Chain()
opts := validator.TransactionValidationOptions{
Expiry: flow.DefaultTransactionExpiry,
ExpiryBuffer: 0,
AllowEmptyReferenceBlockID: false,
AllowUnknownReferenceBlockID: true,
MaxGasLimit: flow.DefaultMaxTransactionGasLimit,
CheckScriptsParse: false,
MaxTransactionByteSize: flow.DefaultMaxTransactionByteSize,
MaxCollectionByteSize: flow.DefaultMaxCollectionByteSize,
CheckPayerBalanceMode: validator.Disabled,
}

v, err := validator.NewTransactionValidator(blocks, chain, metrics.NewNoopCollector(), opts, nil)
if err != nil {
f.Fatalf("failed to build validator: %v", err)
}

// seed: valid transaction from fixture
seed := unittest.TransactionBodyFixture()
f.Add(seed.Script, seed.GasLimit, seed.Payer.Bytes())

// seed: minimal empty script
f.Add([]byte("access(all) fun main() {}"), uint64(10), unittest.AddressFixture().Bytes())
f.Add([]byte{}, uint64(0), []byte{})

f.Fuzz(func(t *testing.T, script []byte, gasLimit uint64, payerBytes []byte) {
tx := unittest.TransactionBodyFixture()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: TransactionBodyFixture draws ReferenceBlockID from crypto/rand inside the fuzz body, so a corpus entry does not reconstruct the same transaction across runs. Build the fixture once outside f.Fuzz and copy it per iteration so crashers stay reproducible.

tx.Script = script
tx.GasLimit = gasLimit
if len(payerBytes) >= flow.AddressLength {
copy(tx.Payer[:], payerBytes[:flow.AddressLength])
}

err1 := v.Validate(context.Background(), &tx)
err2 := v.Validate(context.Background(), &tx)

// accept/reject decision must be deterministic across two calls
if (err1 == nil) != (err2 == nil) {
t.Fatalf("Validate returned non-deterministic result: first=%v second=%v", err1, err2)
}
})
}
60 changes: 60 additions & 0 deletions ledger/trie_encoder_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ledger_test

import (
"testing"

"github.com/onflow/flow-go/ledger"
"github.com/onflow/flow-go/ledger/common/testutils"
)

func FuzzDecodeTrieProof(f *testing.F) {
// seed: encoded valid proof from fixture
p, _ := testutils.TrieProofFixture()
f.Add(ledger.EncodeTrieProof(p))

// seed: empty
f.Add([]byte{})
// seed: minimal truncated input
f.Add([]byte{0x00, 0x01})

f.Fuzz(func(t *testing.T, data []byte) {
proof, err := ledger.DecodeTrieProof(data)
if err != nil {
// malformed input must return error, not partial state
if proof != nil {
t.Fatal("DecodeTrieProof returned non-nil proof alongside error")
}
return
}
// valid decode: proof must be non-nil
if proof == nil {
t.Fatal("DecodeTrieProof returned nil proof with nil error")
}
})
}

func FuzzDecodeTrieBatchProof(f *testing.F) {
// seed: encoded valid batch proof from fixture
bp, _ := testutils.TrieBatchProofFixture()
f.Add(ledger.EncodeTrieBatchProof(bp))

// seed: empty
f.Add([]byte{})
// seed: minimal truncated input
f.Add([]byte{0x00, 0x01})

f.Fuzz(func(t *testing.T, data []byte) {
batch, err := ledger.DecodeTrieBatchProof(data)
if err != nil {
// malformed input must return error, not partial state
if batch != nil {
t.Fatal("DecodeTrieBatchProof returned non-nil batch alongside error")
}
return
}
// valid decode: batch must be non-nil
if batch == nil {
t.Fatal("DecodeTrieBatchProof returned nil batch with nil error")
}
})
}
31 changes: 31 additions & 0 deletions model/fingerprint/fingerprint_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package fingerprint

import (
"bytes"
"testing"
)

// fuzzEntity is a simple exported-enough struct for mutation fuzzing.
type fuzzEntity struct {
Data []byte
N uint64
}

func FuzzFingerprint(f *testing.F) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This harness fuzzes RLP encoding of a local two-field struct and asserts two calls in one process agree. That cannot realistically fail and exercises no untrusted-input boundary. Fingerprint real protocol entities with fuzz-mutated fields, or drop the target — as is it burns a nightly CI slot with no chance of signal.

// seeds: fixture-like entities mirroring fingerprint_test.go cases
f.Add([]byte("abc"), uint64(0))
f.Add([]byte{0x01, 0xff}, uint64(42))
f.Add([]byte{}, uint64(0))
f.Add([]byte("flow-go-boundary-fuzz"), uint64(9999))

f.Fuzz(func(t *testing.T, data []byte, n uint64) {
e := &fuzzEntity{Data: data, N: n}

first := Fingerprint(e)
second := Fingerprint(e)

if !bytes.Equal(first, second) {
t.Fatalf("Fingerprint not deterministic: first=%x second=%x", first, second)
}
})
}
48 changes: 48 additions & 0 deletions network/codec/cbor/decoder_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package cbor_test

import (
"bytes"
"testing"

"github.com/onflow/flow-go/model/messages"
"github.com/onflow/flow-go/network/codec/cbor"
"github.com/onflow/flow-go/utils/unittest"
)

func FuzzCBORDecoder(f *testing.F) {
c := cbor.NewCodec()

// seed: valid block proposal round-tripped through the encoder
proposal := messages.Proposal(*unittest.ProposalFixture())
var buf bytes.Buffer
if err := c.NewEncoder(&buf).Encode(&proposal); err == nil {
f.Add(buf.Bytes())
}
Comment on lines +18 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: if this encode fails, the only structurally valid seed silently disappears and the fuzzer degrades to blind byte mutation. Use f.Fatalf on error.


// seed: empty
f.Add([]byte{})
// seed: single null byte
f.Add([]byte{0x00})
// seed: cbor-encoded empty byte slice (mirrors decoder_test.go cases)
f.Add([]byte{0x80})

f.Fuzz(func(t *testing.T, data []byte) {
msg, err := c.NewDecoder(bytes.NewReader(data)).Decode()
if err != nil {
// any error is acceptable; the decoder must not panic
return
}

// round-trip: re-encoding a valid decoded message must decode to an equal value
var roundtrip bytes.Buffer
if err := c.NewEncoder(&roundtrip).Encode(msg); err != nil {
// encoder may reject the value if the interface type has no registered code — treat as non-fatal
return
}
msg2, err := c.NewDecoder(&roundtrip).Decode()
if err != nil {
t.Fatalf("round-trip decode failed after successful encode: %v", err)
}
_ = msg2
Comment on lines +36 to +46

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says re-encoding must decode to an equal value, but msg2 is never compared to msg. Assert equality or fix the comment. The Encode error branch is also dead: Decode only returns types registered in codec.InterfaceFromMessageCode, so a re-encode failure is an invariant violation and should be t.Fatalf, not a silent return.

})
}
Loading