Skip to content

Commit 77fbcc8

Browse files
committed
fix ABI resolution issue and use in mutations issue
1 parent 89ba2ff commit 77fbcc8

File tree

3 files changed

+69
-22
lines changed

3 files changed

+69
-22
lines changed

fuzzing/corpus/corpus.go

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ import (
2828
// Corpus describes an archive of fuzzer-generated artifacts used to further fuzzing efforts. These artifacts are
2929
// reusable across fuzzer runs. Changes to the fuzzer/chain configuration or definitions within smart contracts
3030
// may create incompatibilities with corpus items.
31+
// warmupSequence tracks a stored corpus entry and whether it should rejoin the mutation chooser after replay.
32+
type warmupSequence struct {
33+
sequence calls.CallSequence
34+
useInMutations bool
35+
}
36+
3137
type Corpus struct {
3238
// storageDirectory describes the directory to save corpus callSequenceFiles within.
3339
storageDirectory string
@@ -45,7 +51,7 @@ type Corpus struct {
4551
// unexecutedCallSequences defines the callSequences which have not yet been executed by the fuzzer. As each item
4652
// is selected for execution by the fuzzer on startup, it is removed. This way, all call sequences loaded from disk
4753
// are executed to check for test failures.
48-
unexecutedCallSequences []calls.CallSequence
54+
unexecutedCallSequences []warmupSequence
4955

5056
// mutationTargetSequenceChooser is a provider that allows for weighted random selection of callSequences. If a
5157
// call sequence was not found to be compatible with this run, it is not added to the chooser.
@@ -68,7 +74,7 @@ func NewCorpus(corpusDirectory string) (*Corpus, error) {
6874
coverageMaps: coverage.NewCoverageMaps(),
6975
callSequenceFiles: newCorpusDirectory[calls.CallSequence](""),
7076
testResultSequenceFiles: newCorpusDirectory[calls.CallSequence](""),
71-
unexecutedCallSequences: make([]calls.CallSequence, 0),
77+
unexecutedCallSequences: make([]warmupSequence, 0),
7278
logger: logging.GlobalLogger.NewSubLogger("module", "corpus"),
7379
}
7480

@@ -511,17 +517,23 @@ func (c *Corpus) CheckSequenceCoverageAndUpdate(callSequence calls.CallSequence,
511517
// MarkSequenceValidated records that a call sequence loaded from disk has been successfully replayed by a worker.
512518
// The sequence is cloned, stripped of runtime metadata, and registered with the mutation chooser so it can participate
513519
// in future mutations.
514-
func (c *Corpus) MarkSequenceValidated(sequence calls.CallSequence, mutationChooserWeight *big.Int) error {
520+
func (c *Corpus) MarkSequenceValidated(sequence calls.CallSequence, mutationChooserWeight *big.Int, useInMutations bool) error {
515521
if mutationChooserWeight == nil {
516522
mutationChooserWeight = big.NewInt(1)
517523
}
518524

525+
c.callSequencesLock.Lock()
526+
defer c.callSequencesLock.Unlock()
527+
528+
if !useInMutations {
529+
return nil
530+
}
531+
519532
clonedSequence, err := sequence.Clone()
520533
if err != nil {
521534
return err
522535
}
523536

524-
// Strip runtime-only references that should not persist in the corpus chooser.
525537
for _, element := range clonedSequence {
526538
if element == nil {
527539
continue
@@ -530,9 +542,6 @@ func (c *Corpus) MarkSequenceValidated(sequence calls.CallSequence, mutationChoo
530542
element.ExecutionTrace = nil
531543
}
532544

533-
c.callSequencesLock.Lock()
534-
defer c.callSequencesLock.Unlock()
535-
536545
if c.mutationTargetSequenceChooser == nil {
537546
c.mutationTargetSequenceChooser = randomutils.NewWeightedRandomChooser[calls.CallSequence]()
538547
}
@@ -599,12 +608,18 @@ func (c *Corpus) PrepareForWarmup(baseTestChain *chain.TestChain, contractDefini
599608
totalSequences := len(c.callSequenceFiles.files) + len(c.testResultSequenceFiles.files)
600609

601610
c.callSequencesLock.Lock()
602-
c.unexecutedCallSequences = make([]calls.CallSequence, 0, totalSequences)
611+
c.unexecutedCallSequences = make([]warmupSequence, 0, totalSequences)
603612
for _, sequenceFileData := range c.testResultSequenceFiles.files {
604-
c.unexecutedCallSequences = append(c.unexecutedCallSequences, sequenceFileData.data)
613+
c.unexecutedCallSequences = append(c.unexecutedCallSequences, warmupSequence{
614+
sequence: sequenceFileData.data,
615+
useInMutations: false,
616+
})
605617
}
606618
for _, sequenceFileData := range c.callSequenceFiles.files {
607-
c.unexecutedCallSequences = append(c.unexecutedCallSequences, sequenceFileData.data)
619+
c.unexecutedCallSequences = append(c.unexecutedCallSequences, warmupSequence{
620+
sequence: sequenceFileData.data,
621+
useInMutations: true,
622+
})
608623
}
609624
activeSequences := len(c.unexecutedCallSequences)
610625
c.callSequencesLock.Unlock()
@@ -617,11 +632,11 @@ func (c *Corpus) PrepareForWarmup(baseTestChain *chain.TestChain, contractDefini
617632
// failures. If a call sequence is returned, it will not be returned by this method again.
618633
// Returns a call sequence loaded from disk which has not yet been executed, to check for test failures. If all
619634
// sequences in the corpus have been executed, this will return nil.
620-
func (c *Corpus) UnexecutedCallSequence() *calls.CallSequence {
635+
func (c *Corpus) UnexecutedCallSequence() (*calls.CallSequence, bool) {
621636
// Prior to thread locking, if we have no un-executed call sequences, quit.
622637
// This is a speed optimization, as thread locking on a central component affects performance.
623638
if len(c.unexecutedCallSequences) == 0 {
624-
return nil
639+
return nil, false
625640
}
626641

627642
// Acquire a thread lock for the duration of this method.
@@ -631,15 +646,15 @@ func (c *Corpus) UnexecutedCallSequence() *calls.CallSequence {
631646
// Check that we have an item now that the thread is locked. This must be performed again as an item could've
632647
// been removed between time of check (the prior exit condition) and time of use (thread locked operations).
633648
if len(c.unexecutedCallSequences) == 0 {
634-
return nil
649+
return nil, false
635650
}
636651

637652
// Otherwise obtain the first item and remove it from the slice.
638653
firstSequence := c.unexecutedCallSequences[0]
639654
c.unexecutedCallSequences = c.unexecutedCallSequences[1:]
640655

641656
// Return the first sequence
642-
return &firstSequence
657+
return &firstSequence.sequence, firstSequence.useInMutations
643658
}
644659

645660
// Flush writes corpus changes to disk. Returns an error if one occurs.

fuzzing/fuzzer_worker.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,16 +259,34 @@ func (fw *FuzzerWorker) updateMethods() {
259259
}
260260
}
261261

262-
// bindContractsForSequence ensures each call sequence element references the deployed contract known to the worker.
263-
func (fw *FuzzerWorker) bindContractsForSequence(sequence calls.CallSequence) {
262+
// bindContractsForSequence ensures each call sequence element references the deployed contract known to the worker and
263+
// resolves ABI metadata needed for serialization.
264+
func (fw *FuzzerWorker) bindContractsForSequence(sequence calls.CallSequence) error {
264265
for _, element := range sequence {
265-
if element == nil || element.Call == nil || element.Call.To == nil {
266+
if element == nil || element.Call == nil {
266267
continue
267268
}
268-
if contractDefinition, ok := fw.deployedContracts[*element.Call.To]; ok {
269-
element.Contract = contractDefinition
269+
// Deployments have no target address, so there is no contract to resolve.
270+
if element.Call.To == nil {
271+
continue
272+
}
273+
274+
contractDefinition, ok := fw.deployedContracts[*element.Call.To]
275+
if !ok {
276+
return fmt.Errorf("contract at address %s is not registered with worker %d", element.Call.To.String(), fw.workerIndex)
277+
}
278+
279+
element.Contract = contractDefinition
280+
281+
if abiValues := element.Call.DataAbiValues; abiValues != nil {
282+
if abiValues.Method == nil {
283+
if err := abiValues.Resolve(contractDefinition.CompiledContract().Abi); err != nil {
284+
return fmt.Errorf("failed to resolve ABI for contract %s: %w", contractDefinition.Name(), err)
285+
}
286+
}
270287
}
271288
}
289+
return nil
272290
}
273291

274292
// testNextCallSequence tests a call message sequence against the underlying FuzzerWorker's Chain and calls every
@@ -297,8 +315,12 @@ func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, err
297315
return nil, err
298316
}
299317
isWarmupSequence := fw.sequenceGenerator.LastSequenceFromWarmup()
318+
warmupUseInMutations := fw.sequenceGenerator.WarmupSequenceUseInMutations()
300319
if isWarmupSequence {
301-
fw.bindContractsForSequence(fw.sequenceGenerator.CurrentSequence())
320+
if err = fw.bindContractsForSequence(fw.sequenceGenerator.CurrentSequence()); err != nil {
321+
fw.fuzzer.logger.Debug("[Worker ", fw.workerIndex, "] Skipping warmup sequence: ", err)
322+
return nil, nil
323+
}
302324
}
303325

304326
// Define our shrink requests we'll collect during execution.
@@ -371,7 +393,7 @@ func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, err
371393

372394
// If this was not a new call sequence, indicate not to save the shrunken result to the corpus again.
373395
if isWarmupSequence && len(shrinkCallSequenceRequests) == 0 && len(executedSequence) > 0 {
374-
err = fw.fuzzer.corpus.MarkSequenceValidated(executedSequence, fw.getNewCorpusCallSequenceWeight())
396+
err = fw.fuzzer.corpus.MarkSequenceValidated(executedSequence, fw.getNewCorpusCallSequenceWeight(), warmupUseInMutations)
375397
if err != nil {
376398
return nil, err
377399
}

fuzzing/fuzzer_worker_sequence_generator.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ type CallSequenceGenerator struct {
2929
// lastSequenceFromWarmup indicates whether the most recent sequence originated from the corpus warmup queue.
3030
lastSequenceFromWarmup bool
3131

32+
// warmupSequenceUseInMutations indicates whether the current warmup sequence should be added to the mutation chooser.
33+
warmupSequenceUseInMutations bool
34+
3235
// fetchIndex describes the current position in the baseSequence which defines the next element to be mutated and
3336
// returned when calling PopSequenceElement.
3437
fetchIndex int
@@ -197,13 +200,15 @@ func (g *CallSequenceGenerator) InitializeNextSequence() (bool, error) {
197200
g.fetchIndex = 0
198201
g.prefetchModifyCallFunc = nil
199202
g.lastSequenceFromWarmup = false
203+
g.warmupSequenceUseInMutations = false
200204

201205
// Check if there are any previously un-executed corpus call sequences. If there are, the fuzzer should execute
202206
// those first.
203-
unexecutedSequence := g.worker.fuzzer.corpus.UnexecutedCallSequence()
207+
unexecutedSequence, useInMutations := g.worker.fuzzer.corpus.UnexecutedCallSequence()
204208
if unexecutedSequence != nil {
205209
g.baseSequence = *unexecutedSequence
206210
g.lastSequenceFromWarmup = true
211+
g.warmupSequenceUseInMutations = useInMutations
207212
return false, nil
208213
}
209214

@@ -243,6 +248,11 @@ func (g *CallSequenceGenerator) LastSequenceFromWarmup() bool {
243248
return g.lastSequenceFromWarmup
244249
}
245250

251+
// WarmupSequenceUseInMutations indicates whether the current warmup sequence should be added to the mutation chooser.
252+
func (g *CallSequenceGenerator) WarmupSequenceUseInMutations() bool {
253+
return g.warmupSequenceUseInMutations
254+
}
255+
246256
// CurrentSequence returns the base sequence currently managed by the generator.
247257
func (g *CallSequenceGenerator) CurrentSequence() calls.CallSequence {
248258
return g.baseSequence

0 commit comments

Comments
 (0)