Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Capture return values during call sequence execution #533

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
23 changes: 23 additions & 0 deletions fuzzing/calls/call_sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,29 @@
return method, err
}

// DecodedReturnValues returns the Go-equivalent decoded return values for the CallSequenceElement's return data
func (cse *CallSequenceElement) DecodedReturnValues() ([]any, error) {
// First, retrieve the method that was called by the call sequence element
method, err := cse.Method()
if err != nil {
return nil, err
}

// Retrieve the ABI-encoded return data
encodedReturnData := cse.ChainReference.Block.MessageResults[cse.ChainReference.TransactionIndex].ExecutionResult.ReturnData
if encodedReturnData == nil || len(encodedReturnData) == 0 {

Check failure on line 232 in fuzzing/calls/call_sequence.go

View workflow job for this annotation

GitHub Actions / lint

S1009: should omit nil check; len() for []byte is defined as zero (gosimple)
return nil, nil
}

// Decode the return data
decodedReturnValues, err := method.Outputs.Unpack(encodedReturnData)
if err != nil {
return nil, err
}

return decodedReturnValues, nil
}

// String returns a displayable string representing the CallSequenceElement.
func (cse *CallSequenceElement) String() string {
// Obtain our contract name
Expand Down
2 changes: 1 addition & 1 deletion fuzzing/coverage/coverage_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (t *CoverageTracer) NativeTracer() *chain.TestChainTracer {
return t.nativeTracer
}

// CaptureTxStart is called upon the start of transaction execution, as defined by tracers.Tracer.
// OnTxStart is called upon the start of transaction execution, as defined by tracers.Tracer.
func (t *CoverageTracer) OnTxStart(vm *tracing.VMContext, tx *coretypes.Transaction, from common.Address) {
// Reset our call frame states
t.callDepth = 0
Expand Down
2 changes: 1 addition & 1 deletion fuzzing/fuzzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ func defaultCallSequenceGeneratorConfigFunc(fuzzer *Fuzzer, valueSet *valuegener
GenerateRandomAddressBias: 0.5,
GenerateRandomIntegerBias: 0.5,
GenerateRandomStringBias: 0.5,
GenerateRandomBytesBias: 0.5,
GenerateRandomBytesBias: 0.1,
MutateAddressProbability: 0.1,
MutateArrayStructureProbability: 0.1,
MutateBoolProbability: 0.1,
Expand Down
19 changes: 17 additions & 2 deletions fuzzing/fuzzer_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,14 @@ func (fw *FuzzerWorker) updateMethods() {
// deployed in the Chain.
// Returns the length of the call sequence tested, any requests for call sequence shrinking, or an error if one occurs.
func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCallSequenceRequest, error) {
// We will make a copy of the worker's base value set so that we can rollback to it at the end of the call sequence
originalValueSet := fw.valueSet.Clone()

// After testing the sequence, we'll want to rollback changes to reset our testing state.
var err error
defer func() {
// Reset the value set back to the original
fw.valueSet = originalValueSet
if err == nil {
err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber)
}
Expand All @@ -282,11 +287,21 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall

// Our "post execution check function" method will check coverage and call all testing functions. If one returns a
// request for a shrunk call sequence, we exit our call sequence execution immediately to go fulfill the shrink
// request.
// request. Additionally, the execution check function will also attempt to add any return data to the value set for
// this call sequence. Note that the value set is reset after each call sequence (see the defer section above)
executionCheckFunc := func(currentlyExecutedSequence calls.CallSequence) (bool, error) {
// Get the last call sequence element that was executed
latestCallSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1]
// Get the decoded return values and add it to the base value set
// Don't throw an error since we care more about coverage than adding the return values to the base value set
decodedReturnValues, err := latestCallSequenceElement.DecodedReturnValues()
if decodedReturnValues != nil && err == nil {
fw.valueSet.Add(decodedReturnValues)
}

// Check for updates to coverage and corpus.
// If we detect coverage changes, add this sequence with weight as 1 + sequences tested (to avoid zero weights)
err := fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true)
err = fw.fuzzer.corpus.CheckSequenceCoverageAndUpdate(currentlyExecutedSequence, fw.getNewCorpusCallSequenceWeight(), true)
if err != nil {
return true, err
}
Expand Down
36 changes: 24 additions & 12 deletions fuzzing/valuegeneration/generator_mutational.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ type MutationalValueGeneratorConfig struct {
// GenerateRandomStringBias defines the probability in which a string generated by the value generator is entirely
// random, rather than mutated. Value range is [0.0, 1.0].
GenerateRandomStringBias float32
// GenerateRandomStringBias defines the probability in which a byte array generated by the value generator is
// GenerateRandomBytesBias defines the probability in which a byte array generated by the value generator is
// entirely random, rather than mutated. Value range is [0.0, 1.0].
GenerateRandomBytesBias float32

Expand Down Expand Up @@ -244,14 +244,20 @@ var bytesMutationMethods = []func(*MutationalValueGenerator, []byte, ...[]byte)
},
}

// mutateBytesInternal takes a byte array and returns either a random new byte array, or a mutated value based off the
// input.
// mutateBytesInternal takes a byte array and a length. This function returns either a fixed length byte array (based on
// the provided length) or a byte slice. The returned byte array/slice is either randomly generated or mutated using
// the provided input.
// If a nil input is provided, this method uses an existing base value set value as the starting point for mutation.
func (g *MutationalValueGenerator) mutateBytesInternal(b []byte) []byte {
func (g *MutationalValueGenerator) mutateBytesInternal(b []byte, length int) []byte {
// If we have no inputs or our bias directs us to, use the random generator instead
inputs := g.valueSet.Bytes()
randomGeneratorDecision := g.randomProvider.Float32()
if len(inputs) == 0 || randomGeneratorDecision < g.config.GenerateRandomBytesBias {
// If the length is non-zero, generate a fixed byte array
if length > 0 {
return g.RandomValueGenerator.GenerateFixedBytes(length)
}
// Otherwise, generate a random byte slice
return g.RandomValueGenerator.GenerateBytes()
}

Expand All @@ -269,6 +275,12 @@ func (g *MutationalValueGenerator) mutateBytesInternal(b []byte) []byte {
input = bytesMutationMethods[g.randomProvider.Intn(len(bytesMutationMethods))](g, input, inputs...)
}

// If we want a fixed-byte array and the mutated input is smaller than the requested length, then generate a random
// byte array
// TODO: This should be improved and maybe it is better to pad with zeros
if length > 0 && len(input) < length {
return g.RandomValueGenerator.GenerateFixedBytes(length)
}
return input
}

Expand Down Expand Up @@ -415,7 +427,7 @@ func (g *MutationalValueGenerator) MutateBool(bl bool) bool {

// GenerateBytes generates bytes and returns them.
func (g *MutationalValueGenerator) GenerateBytes() []byte {
return g.mutateBytesInternal(nil)
return g.mutateBytesInternal(nil, 0)
}

// MutateBytes takes a dynamic-sized byte array input and returns a mutated value based off the input.
Expand All @@ -428,20 +440,20 @@ func (g *MutationalValueGenerator) MutateBytes(b []byte) []byte {
if randomGeneratorDecision < g.config.MutateBytesGenerateNewBias {
return g.GenerateBytes()
} else {
return g.mutateBytesInternal(b)
return g.mutateBytesInternal(b, 0)
}
}
return b
}

// MutateFixedBytes takes a fixed-sized byte array input and returns a mutated value based off the input.
func (g *MutationalValueGenerator) MutateFixedBytes(b []byte) []byte {
// Determine whether to perform mutations against this input or just return it as-is.
randomGeneratorDecision := g.randomProvider.Float32()
if randomGeneratorDecision < g.config.MutateFixedBytesProbability {
return g.GenerateFixedBytes(len(b))
}
return b
return g.mutateBytesInternal(b, len(b))
}

// GenerateFixedBytes generates a fixed-sized byte array to use when populating inputs.
func (g *MutationalValueGenerator) GenerateFixedBytes(length int) []byte {
return g.mutateBytesInternal(nil, length)
}

// GenerateString generates strings and returns them.
Expand Down
51 changes: 51 additions & 0 deletions fuzzing/valuegeneration/value_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package valuegeneration

import (
"encoding/hex"
"github.com/crytic/medusa/utils/reflectionutils"
"hash"
"math/big"
"reflect"

"github.com/ethereum/go-ethereum/common"
"golang.org/x/crypto/sha3"
Expand Down Expand Up @@ -172,3 +174,52 @@ func (vs *ValueSet) RemoveBytes(b []byte) {

delete(vs.bytes, hashStr)
}

// Add adds one or more values. Note the values must be a primitive type (signed/unsigned integer, address, string,
// bytes, fixed bytes)
func (vs *ValueSet) Add(values []any) {
// Iterate across each value and assert on its type
for _, value := range values {
switch v := value.(type) {
case uint8:
vs.AddInteger(new(big.Int).SetUint64(uint64(v)))
case uint16:
vs.AddInteger(new(big.Int).SetUint64(uint64(v)))
case uint32:
vs.AddInteger(new(big.Int).SetUint64(uint64(v)))
case uint64:
vs.AddInteger(new(big.Int).SetUint64(v))
case int8:
vs.AddInteger(new(big.Int).SetInt64(int64(v)))
case int16:
vs.AddInteger(new(big.Int).SetInt64(int64(v)))
case int32:
vs.AddInteger(new(big.Int).SetInt64(int64(v)))
case int64:
vs.AddInteger(new(big.Int).SetInt64(v))
case *big.Int:
vs.AddInteger(v)
case common.Address:
vs.AddAddress(v)
case bool:
if value == true {
vs.AddInteger(new(big.Int).SetUint64(1))
} else {
vs.AddInteger(new(big.Int).SetUint64(0))
}
case string:
vs.AddString(v)
case []byte:
vs.AddBytes(v)
default:
// We need to be able to capture fixed bytes. Unfortunately, the only way to do so is using reflection
r := reflect.TypeOf(value)
// If we have a fixed array of uint8 (aka byte), then we will convert it into a slice and add to value set
if r.Kind() == reflect.Array && r.Elem().Kind() == reflect.Uint8 {
b := reflectionutils.ArrayToSlice(reflect.ValueOf(value)).([]byte)
vs.AddBytes(b)
}
continue
}
}
}
Loading