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: Experimental valuegeneration techniques #384

Draft
wants to merge 57 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b83bc3f
Move CallFrame to utils
Jun 18, 2024
d26cd47
Add ValueGenerationTracer
Jun 18, 2024
0024d74
Import CallFrame from utils for execution_tracer
Jun 18, 2024
385b6fb
Initialize ValueGenerationTracer in fuzzer_worker.go
Jun 18, 2024
af12408
Add a unit test for ValueGenerationTracer
Jun 18, 2024
faf0952
Enable ValueGenerationTracer in configs
Jun 18, 2024
937a7ad
Enable ValueGenerationTracer in medusa.json
Jun 18, 2024
39f7d8e
Modify existing contract to fit our test case
Jun 18, 2024
20ef5ce
Add necessary functions to get event values
Jun 18, 2024
63cf261
Add captureEnteredCallFrame in CaptureEnter
Jun 18, 2024
c60029f
grab events
anishnaik Jun 18, 2024
1695d01
make generating events a ValueGenerationTrace capability
anishnaik Jun 18, 2024
cdab504
Change emitted event values to check for changes in valueSet
s4nsec Jun 21, 2024
92b7699
Store captured event values and their types in a data structure and a…
s4nsec Jun 21, 2024
9118e21
Remove SetValueSet function
s4nsec Jun 24, 2024
6c6bdd2
Modify assert_immediate.sol to check return value collection function…
s4nsec Jun 24, 2024
aeabcdd
Modify fuzzer configs to make debugging easier
s4nsec Jun 24, 2024
f23d608
Remove deprecated storage alternatives for event values
s4nsec Jun 24, 2024
7f56635
Provide a new structure to store both event and return values
s4nsec Jun 24, 2024
0b2f9e6
Get return value of the current call frame
s4nsec Jun 24, 2024
7d1febf
Call getCallFrameReturnValue from captureExitedCallFrame
s4nsec Jun 24, 2024
fc21f10
Provide collected event and return values to MessageResults
s4nsec Jun 24, 2024
492e4c4
Write colleced transaction output values(return, event data) to curre…
s4nsec Jun 24, 2024
0cf6a86
Use AddTransactionOutputValuesToValueSet in fuzzer worker
s4nsec Jun 24, 2024
395667e
Rename ValueGenerationTracingEnabled parameter to ExperimentalValueGe…
s4nsec Jun 27, 2024
01c1de3
Modify the logic to preserve base value set
s4nsec Jun 27, 2024
ba1c045
Set TransactionOutputValues to []any
s4nsec Jun 27, 2024
aeef268
Perform modifications on ValueGenerationTracer methods to adapt to ne…
s4nsec Jun 27, 2024
be7c5b1
Emit and return values of various types
s4nsec Jun 27, 2024
f8874e7
Add a test to check if base value set is preserved throughout the fuz…
s4nsec Jun 27, 2024
233630d
Move ExperimantalValueGenerationEnabled to TestingConfig
s4nsec Jul 1, 2024
5c55b7f
Fix overwriting return values with event values issue
s4nsec Jul 1, 2024
84dd114
Use eventOrReturnValue as variable name to avoid ambiguity
s4nsec Jul 1, 2024
cb7d04f
Grab only if ReturnError is nil
s4nsec Jul 1, 2024
b1c3d0f
Don't expect values when it is contract creation
s4nsec Jul 1, 2024
93799c0
Move ExperimentalValueGenerationEnabled to TestingConfig
s4nsec Jul 17, 2024
ab6d871
Remove type TransactionOutputValues
s4nsec Jul 17, 2024
f2019f5
Add comment descriptions to ValueGenerationTracer members
s4nsec Jul 17, 2024
072c8cb
Remove stale comments and print statements
s4nsec Jul 17, 2024
d8cce90
Prevent return values from being reset
s4nsec Jul 17, 2024
5e10173
Make adding transaction output values value_set.go's responsibility
s4nsec Jul 17, 2024
b496c43
Use Add instead of deprecated AddTransactionOutputValuesToValueSet
s4nsec Jul 17, 2024
f9dd9a0
Use fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled
s4nsec Jul 17, 2024
00e8edb
Check whether all values are added and if the base value set is prese…
s4nsec Jul 17, 2024
826aa8f
Reset assert_immediate.sol to default
s4nsec Jul 17, 2024
288337e
Add a contract to test event and return value collection
s4nsec Jul 17, 2024
30f9419
Add to value set inside the unit test
s4nsec Jul 22, 2024
a68363c
Merge remote-tracking branch 'origin/master' into valuegeneration-tracer
s4nsec Jul 23, 2024
0e1763f
Adapt value generation tracer to interface changes
s4nsec Jul 23, 2024
8ac6695
Add new mutation strategy - callSeqGenFuncDuplicateAtRandom
s4nsec Jul 30, 2024
2851e96
code cleanup
anishnaik Jul 30, 2024
14f677b
fix test contract
anishnaik Jul 30, 2024
64e3ca2
wipe value set before test
anishnaik Jul 31, 2024
e94a668
Add expected values to test whether those values are added to the val…
s4nsec Jul 31, 2024
42ab8d3
Merge branch 'master' into valuegeneration-tracer
anishnaik Aug 1, 2024
f12eb1f
Enable JSON coverage reports for valuegeneration tracer benchmarking …
anishnaik Aug 1, 2024
5e2c637
Add latest medusa config
s4nsec Aug 7, 2024
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
1 change: 1 addition & 0 deletions docs/src/static/medusa.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"callSequenceLength": 100,
"corpusDirectory": "",
"coverageEnabled": true,
"experimentalValueGenerationEnabled": true,
"targetContracts": [],
"targetContractsBalances": [],
"constructorArgs": {},
Expand Down
4 changes: 4 additions & 0 deletions fuzzing/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ type TestingConfig struct {
// OptimizationTesting describes the configuration used for optimization testing.
OptimizationTesting OptimizationTestingConfig `json:"optimizationTesting"`

// ExperimentalValueGenerationEnabled describes the configuration used for testing of collection
// and addition of interesting values found during EVM execution to base value set
ExperimentalValueGenerationEnabled bool `json:"experimentalValueGenerationEnabled"`

// TargetFunctionSignatures is a list function signatures call the fuzzer should exclusively target by omitting calls to other signatures.
// The signatures should specify the contract name and signature in the ABI format like `Contract.func(uint256,bytes32)`.
TargetFunctionSignatures []string `json:"targetFunctionSignatures"`
Expand Down
1 change: 1 addition & 0 deletions fuzzing/config/config_defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func GetDefaultProjectConfig(platform string) (*ProjectConfig, error) {
TraceAll: false,
TargetFunctionSignatures: []string{},
ExcludeFunctionSignatures: []string{},
ExperimentalValueGenerationEnabled: false,
AssertionTesting: AssertionTestingConfig{
Enabled: true,
TestViewMethods: false,
Expand Down
2 changes: 1 addition & 1 deletion fuzzing/executiontracer/execution_tracer.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package executiontracer

import (
"github.com/crytic/medusa/utils"
"math/big"

"github.com/crytic/medusa/chain"
"github.com/crytic/medusa/fuzzing/contracts"
"github.com/crytic/medusa/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
Expand Down
56 changes: 56 additions & 0 deletions fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -936,3 +936,59 @@ func TestExcludeFunctionSignatures(t *testing.T) {
}
}})
}

// TestExperimentalValueGeneration runs tests to ensure whether interesting values collected
// during EVM execution is added to the base value set. In addition, it makes sure that the base value set is reset to
// default after the end of each call sequence execution
func TestExperimentalValueGeneration(t *testing.T) {
filePaths := []string{
"testdata/contracts/valuegeneration_tracing/event_and_return_value_emission.sol",
}

for _, filePath := range filePaths {
runFuzzerTest(t, &fuzzerSolcFileTest{
filePath: filePath,
configUpdates: func(config *config.ProjectConfig) {
config.Fuzzing.TargetContracts = []string{"TestContract"}
config.Fuzzing.TestLimit = 500
config.Fuzzing.Testing.PropertyTesting.Enabled = false
config.Fuzzing.Testing.OptimizationTesting.Enabled = false
config.Fuzzing.Workers = 1
config.Fuzzing.Testing.ExperimentalValueGenerationEnabled = true
},
method: func(f *fuzzerTestContext) {
valueSet := f.fuzzer.baseValueSet
f.fuzzer.Events.WorkerCreated.Subscribe(func(event FuzzerWorkerCreatedEvent) error {
event.Worker.Events.FuzzerWorkerChainSetup.Subscribe(func(event FuzzerWorkerChainSetupEvent) error {
event.Worker.chain.Events.PendingBlockAddedTx.Subscribe(func(event chain.PendingBlockAddedTxEvent) error {
if valueGenerationResults, ok := event.Block.MessageResults[event.TransactionIndex-1].AdditionalResults["ValueGenerationTracerResults"].([]any); ok {
f.fuzzer.workers[0].valueSet.Add(valueGenerationResults)
res := experimentalValuesAddedToBaseValueSet(f, valueGenerationResults)
assert.True(t, res)
}
// just check if these values are added to value set
//msgResult[event.TransactionIndex].AdditionalResults
// make sure to use CallSequenceTested event to see if the base value set
// is reset at the end of each sequence
//fmt.Printf("MsgResult: %v\n", msgResult[event.TransactionIndex].AdditionalResults)
return nil
})
// This will make sure that the base value set is reset after the end of execution of each
// call sequence
event.Worker.Events.CallSequenceTested.Subscribe(func(event FuzzerWorkerCallSequenceTestedEvent) error {
sequenceValueSet := f.fuzzer.baseValueSet
assert.EqualValues(t, valueSet, sequenceValueSet)
return nil
})
return nil
})
return nil
})
err := f.fuzzer.Start()

assert.NoError(t, err)

},
})
}
}
31 changes: 31 additions & 0 deletions fuzzing/fuzzer_test_methods_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package fuzzing

import (
"github.com/ethereum/go-ethereum/common"
"math/big"
"testing"

"github.com/crytic/medusa/compilation"
Expand Down Expand Up @@ -111,3 +113,32 @@ func expectEventEmitted[T any](f *fuzzerTestContext, eventEmitter *events.EventE
assert.Greater(f.t, f.eventCounter[eventType], 0, "Event was not emitted at all")
})
}

// experimentalValuesAddedToBaseValueSet will ensure whether collected EVM values during execution of the current
// transaction is added to the base value set
func experimentalValuesAddedToBaseValueSet(f *fuzzerTestContext, valueGenerationResults any) bool {
var allAdded bool
currentValueSet := f.fuzzer.workers[0].valueSet
if valGenResults, ok := valueGenerationResults.([]any); ok {
for _, eventOrReturnValues := range valGenResults {
if values, ok := eventOrReturnValues.([]any); ok {
for _, value := range values {
switch v := value.(type) {
case *big.Int:
allAdded = currentValueSet.ContainsInteger(v)
case common.Address:
allAdded = currentValueSet.ContainsAddress(v)
case string:
allAdded = currentValueSet.ContainsString(v)
case []byte:
allAdded = currentValueSet.ContainsBytes(v)
default:
// TODO(Sanan): A byte array with just one element is considered as default, fix.
continue
}
}
}
}
}
return allAdded
}
31 changes: 31 additions & 0 deletions fuzzing/fuzzer_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package fuzzing

import (
"fmt"
"github.com/crytic/medusa/fuzzing/valuegenerationtracer"
"math/big"
"math/rand"

Expand Down Expand Up @@ -29,6 +30,10 @@ type FuzzerWorker struct {
// coverageTracer describes the tracer used to collect coverage maps during fuzzing campaigns.
coverageTracer *coverage.CoverageTracer

// valueGenerationTracer represents the structure that is used for collecting "interesting" values during EVM
// execution, such as emitted event and return values of executed functions in one sequence.
valueGenerationTracer *valuegenerationtracer.ValueGenerationTracer

// testingBaseBlockNumber refers to the block number at which all contracts for testing have been deployed, prior
// to any fuzzing activity. This block number is reverted to after testing each call sequence to reset state.
testingBaseBlockNumber uint64
Expand Down Expand Up @@ -260,12 +265,21 @@ 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) {
// Copy the existing value set if experimental value generation is enabled
var originalValueSet *valuegeneration.ValueSet
if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled {
originalValueSet = fw.valueSet.Clone()
}
// After testing the sequence, we'll want to rollback changes to reset our testing state.
var err error
defer func() {
if err == nil {
err = fw.chain.RevertToBlockNumber(fw.testingBaseBlockNumber)
}
// Reset the value set if experimental value generation is enabled
if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled {
fw.valueSet = originalValueSet
}
}()

// Initialize a new sequence within our sequence generator.
Expand Down Expand Up @@ -294,6 +308,16 @@ func (fw *FuzzerWorker) testNextCallSequence() (calls.CallSequence, []ShrinkCall
return true, err
}

// Add event and return values to the value set if experimental value generation is enabled
if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled {
lastExecutedSequenceElement := currentlyExecutedSequence[len(currentlyExecutedSequence)-1]

if values, ok := lastExecutedSequenceElement.ChainReference.MessageResults().AdditionalResults["ValueGenerationTracerResults"].([]any); ok {
fw.valueSet.Add(values)
}

}

anishnaik marked this conversation as resolved.
Show resolved Hide resolved
// Loop through each test function, signal our worker tested a call, and collect any requests to shrink
// this call sequence.
for _, callSequenceTestFunc := range fw.fuzzer.Hooks.CallSequenceTestFuncs {
Expand Down Expand Up @@ -557,6 +581,13 @@ func (fw *FuzzerWorker) run(baseTestChain *chain.TestChain) (bool, error) {
fw.coverageTracer = coverage.NewCoverageTracer()
initializedChain.AddTracer(fw.coverageTracer.NativeTracer(), true, false)
}

// If we enabled experimental value generation, create a tracer to collect interesting values during EVM
// execution and connect it to the chain
if fw.fuzzer.config.Fuzzing.Testing.ExperimentalValueGenerationEnabled {
fw.valueGenerationTracer = valuegenerationtracer.NewValueGenerationTracer(fw.fuzzer.contractDefinitions)
anishnaik marked this conversation as resolved.
Show resolved Hide resolved
initializedChain.AddTracer(fw.valueGenerationTracer.NativeTracer(), true, false)
}
return nil
})

Expand Down
26 changes: 24 additions & 2 deletions fuzzing/fuzzer_worker_sequence_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package fuzzing

import (
"fmt"
"math/big"

"github.com/crytic/medusa/fuzzing/calls"
"github.com/crytic/medusa/fuzzing/valuegeneration"
"github.com/crytic/medusa/utils"
"github.com/crytic/medusa/utils/randomutils"
"math/big"
)

// CallSequenceGenerator generates call sequences iteratively per element, for use in fuzzing campaigns. It is attached
Expand Down Expand Up @@ -164,6 +163,13 @@ func NewCallSequenceGenerator(worker *FuzzerWorker, config *CallSequenceGenerato
},
new(big.Int).SetUint64(config.RandomMutatedCorpusTailWeight),
),
randomutils.NewWeightedRandomChoice(
CallSequenceGeneratorMutationStrategy{
CallSequenceGeneratorFunc: callSeqGenFuncDuplicateAtRandom,
PrefetchModifyCallFunc: prefetchModifyCallFuncMutate,
},
new(big.Int).SetUint64(config.RandomMutatedCorpusTailWeight),
),
randomutils.NewWeightedRandomChoice(
CallSequenceGeneratorMutationStrategy{
CallSequenceGeneratorFunc: callSeqGenFuncSpliceAtRandom,
Expand Down Expand Up @@ -368,6 +374,22 @@ func callSeqGenFuncCorpusTail(sequenceGenerator *CallSequenceGenerator, sequence
return nil
}

// callSeqGenFuncDuplicateAtRandom is a CallSequenceGeneratorFunc which prepares a CallSequenceGenerator to generate a sequence
// which duplicates a call sequence element at index N and inserts it at N+1
// if random index is len(sequence)-1, it inserts the duplicated call sequence element at N-1
func callSeqGenFuncDuplicateAtRandom(sequenceGenerator *CallSequenceGenerator, sequence calls.CallSequence) error {
randIndex := sequenceGenerator.worker.randomProvider.Intn(len(sequence))
duplicatedElement := sequence[randIndex]

if randIndex == len(sequence)-1 {
sequence[randIndex-1] = duplicatedElement
} else {
sequence[randIndex+1] = duplicatedElement
}

return nil
}

// callSeqGenFuncSpliceAtRandom is a CallSequenceGeneratorFunc which prepares a CallSequenceGenerator to generate a
// sequence which is based off of two corpus call sequence entries, from which a random length head and tail are
// respectively sliced and joined together.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
pragma solidity ^0.8.0;

contract AnotherContract {
// This function returns a variety of values that need to be captured in the value set
function testAnotherFunction(uint256 x) public pure returns (uint256, int256, string memory, address, bytes memory, bytes4) {
// Fix the values we want to return
uint256 myUint = 3;
int256 myInt = 4;
string memory myStr = "another string";
address myAddr = address(0x5678);
bytes memory myBytes = "another byte array";
bytes4 fixedBytes = "word";

return (myUint, myInt, myStr, myAddr, myBytes, fixedBytes);
}
}

contract TestContract {
AnotherContract public anotherContract;

event EventValues(uint indexed myUint, int myInt, string myStr, address myAddr, bytes myBytes, bytes4 fixedBytes);

// Deploy AnotherContract within the TestContract
constructor() {
anotherContract = new AnotherContract();
}

function testFunction(uint x) public {
// Fix the values we want to emit
uint256 myUint = 1;
int256 myInt = 2;
string memory myStr = "string";
address myAddr = address(0x1234);
bytes memory myBytes = "byte array";
bytes4 fixedBytes = "byte";

// Call an external contract
anotherContract.testAnotherFunction(x);

// Emit an event in this call frame
emit EventValues(myUint, myInt, myStr, myAddr, myBytes, fixedBytes);

// ASSERTION: We always fail when you call this function.
assert(false);

}
}
19 changes: 19 additions & 0 deletions fuzzing/valuegeneration/value_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,22 @@ func (vs *ValueSet) RemoveBytes(b []byte) {

delete(vs.bytes, hashStr)
}

// Add adds one or more values. Note the values could be any primitive type (integer, address, string, 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 *big.Int:
vs.AddInteger(v)
case common.Address:
vs.AddAddress(v)
case string:
vs.AddString(v)
case []byte:
vs.AddBytes(v)
default:
continue
}
}
}
Loading
Loading