Skip to content

Commit dea79d9

Browse files
authored
Merge branch 'master' into dependabot/go_modules/go.etcd.io/bbolt-1.4.3
2 parents e79b73b + f1c981e commit dea79d9

File tree

9 files changed

+235
-199
lines changed

9 files changed

+235
-199
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ jobs:
105105

106106
steps:
107107
- name: Download binaries
108-
uses: actions/download-artifact@v5
108+
uses: actions/download-artifact@v6
109109
with:
110110
pattern: medusa-*
111111
merge-multiple: true

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## medusa build process
2-
FROM golang:1.23 AS medusa
2+
FROM golang:1.24 AS medusa
33

44
WORKDIR /src
55
COPY . /src/medusa/

fuzzing/corpus/corpus.go

Lines changed: 100 additions & 161 deletions
Large diffs are not rendered by default.

fuzzing/corpus/corpus_files.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ package corpus
33
import (
44
"encoding/json"
55
"fmt"
6-
"github.com/crytic/medusa/utils"
76
"os"
87
"path/filepath"
98
"strings"
109
"sync"
10+
11+
"github.com/crytic/medusa/utils"
1112
)
1213

1314
// corpusFile represents corpus data and its state on the filesystem.
@@ -176,7 +177,7 @@ func (cd *corpusDirectory[T]) writeFiles() error {
176177
// Write the JSON encoded data.
177178
err = os.WriteFile(filePath, jsonEncodedData, os.ModePerm)
178179
if err != nil {
179-
return fmt.Errorf("An error occurred while writing corpus data to file: %v\n", err)
180+
return fmt.Errorf("an error occurred while writing corpus data to file: %v", err)
180181
}
181182

182183
// Update our written to disk status.

fuzzing/fuzzer.go

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,7 @@ func (f *Fuzzer) spawnWorkersLoop(baseTestChain *chain.TestChain) error {
814814
if err == nil && workerCreatedErr != nil {
815815
err = workerCreatedErr
816816
}
817+
817818
if err == nil {
818819
// Publish an event indicating we created a worker.
819820
workerCreatedErr = f.Events.WorkerCreated.Publish(FuzzerWorkerCreatedEvent{Worker: worker})
@@ -894,13 +895,6 @@ func (f *Fuzzer) Start() error {
894895
f.ctx, f.ctxCancelFunc = context.WithTimeout(f.ctx, time.Duration(f.config.Fuzzing.Timeout)*time.Second)
895896
}
896897

897-
// Set up the corpus
898-
f.logger.Info("Initializing corpus")
899-
f.corpus, err = corpus.NewCorpus(f.config.Fuzzing.CorpusDirectory)
900-
if err != nil {
901-
f.logger.Error("Failed to create the corpus", err)
902-
return err
903-
}
904898
// Start the revert reporter
905899
f.revertReporter.Start(f.ctx)
906900

@@ -933,28 +927,25 @@ func (f *Fuzzer) Start() error {
933927
}
934928
f.logger.Info("Finished setting up test chain")
935929

936-
// Initialize our coverage maps by measuring the coverage we get from the corpus.
937-
var corpusActiveSequences, corpusTotalSequences int
938-
if totalCallSequences, testResults := f.corpus.CallSequenceEntryCount(); totalCallSequences > 0 || testResults > 0 {
939-
f.logger.Info("Running call sequences in the corpus")
940-
}
941-
startTime := time.Now()
942-
corpusActiveSequences, corpusTotalSequences, err = f.corpus.Initialize(baseTestChain, f.contractDefinitions)
943-
if corpusTotalSequences > 0 {
944-
f.logger.Info("Finished running call sequences in the corpus in ", time.Since(startTime).Round(time.Second))
930+
// Create and initialize the corpus
931+
f.logger.Info("Creating corpus...")
932+
f.corpus, err = corpus.NewCorpus(f.config.Fuzzing.CorpusDirectory)
933+
if err != nil {
934+
f.logger.Error("Failed to create the corpus", err)
935+
return err
945936
}
937+
err = f.corpus.Initialize(baseTestChain, f.contractDefinitions)
946938
if err != nil {
947939
f.logger.Error("Failed to initialize the corpus", err)
948940
return err
949941
}
950942

951-
// Log corpus health statistics, if we have any existing sequences.
952-
if corpusTotalSequences > 0 {
953-
f.logger.Info(
954-
colors.Bold, "corpus: ", colors.Reset,
955-
"health: ", colors.Bold, int(float32(corpusActiveSequences)/float32(corpusTotalSequences)*100.0), "%", colors.Reset, ", ",
956-
"sequences: ", colors.Bold, corpusTotalSequences, " (", corpusActiveSequences, " valid, ", corpusTotalSequences-corpusActiveSequences, " invalid)", colors.Reset,
957-
)
943+
// Log that we will initialize corpus if there are any call sequences or test results
944+
if totalCallSequences, testResults := f.corpus.CallSequenceEntryCount(); totalCallSequences > 0 || testResults > 0 {
945+
f.logger.Info("Initializing corpus...")
946+
947+
// Monitor corpus initialization
948+
go f.monitorCorpusInitialization()
958949
}
959950

960951
// Start the corpus pruner.
@@ -1081,6 +1072,45 @@ func (f *Fuzzer) Terminate() {
10811072
}
10821073
}
10831074

1075+
// monitorCorpusInitialization monitors the corpus initialization process and logs the corpus health when it is complete.
1076+
// This goroutine is short-lived and exits when the corpus is initialized.
1077+
func (f *Fuzzer) monitorCorpusInitialization() {
1078+
// There is nothing to do if there are no corpus elements or unexecuted call sequences
1079+
totalSequences, totalTestResults := f.corpus.CallSequenceEntryCount()
1080+
if !f.corpus.InitializingCorpus() || totalSequences == 0 || totalTestResults == 0 {
1081+
return
1082+
}
1083+
1084+
// Capture an approximate start time
1085+
startTime := time.Now()
1086+
for !utils.CheckContextDone(f.ctx) && !utils.CheckContextDone(f.emergencyCtx) {
1087+
// Go to sleep if corpus is still initializing
1088+
if f.corpus.InitializingCorpus() {
1089+
time.Sleep(200 * time.Millisecond)
1090+
continue
1091+
}
1092+
1093+
// Calculate the necessary variables for corpus health
1094+
totalSequences, totalTestResults := f.corpus.CallSequenceEntryCount()
1095+
totalCorpusEntries := totalSequences + totalTestResults
1096+
validSequences := f.corpus.ValidCallSequences()
1097+
invalidSequences := int(totalSequences - int(validSequences))
1098+
1099+
// Log how much time it took to initialize the corpus
1100+
f.logger.Info("Finished running call sequences in the corpus in ", time.Since(startTime).Round(time.Second))
1101+
1102+
// Log the overall corpus health
1103+
f.logger.Info(
1104+
colors.Bold, "corpus: ", colors.Reset,
1105+
"health: ", colors.Bold, int(float32(validSequences)/float32(totalCorpusEntries)*100), "%", colors.Reset, ", ",
1106+
"sequences: ", colors.Bold, totalCorpusEntries, " (", validSequences, " valid, ", invalidSequences, " invalid)", colors.Reset,
1107+
)
1108+
1109+
// Now we can exit the goroutine
1110+
break
1111+
}
1112+
}
1113+
10841114
// printMetricsLoop prints metrics to the console in a loop until ctx signals a stopped operation.
10851115
func (f *Fuzzer) printMetricsLoop() {
10861116
// Define our start time

fuzzing/fuzzer_worker.go

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,46 @@ func (fw *FuzzerWorker) updateMethods() {
259259
}
260260
}
261261

262+
// bindCorpusElement ensures that the de-serialized corpus element is ready for runtime use.
263+
// The index for the element is provided and the base sequence used for execution is updated in-place.
264+
// It resolves the contract definition and ABI metadata needed for runtime execution. If the function
265+
// returns an error, the call sequence/corpus item is marked as invalid and will not be used for mutations.
266+
func (fw *FuzzerWorker) bindCorpusElement(currentIndex int) error {
267+
// Guard clause
268+
if currentIndex >= len(fw.sequenceGenerator.baseSequence) {
269+
return nil
270+
}
271+
272+
// Obtain the corpus element
273+
element := fw.sequenceGenerator.baseSequence[currentIndex]
274+
275+
// If it is a contract creation, there is nothing to do.
276+
if element.Call.To == nil {
277+
return nil
278+
}
279+
280+
contractDefinition, ok := fw.deployedContracts[*element.Call.To]
281+
if !ok {
282+
return fmt.Errorf("contract at address %v could not be resolved", element.Call.To.String())
283+
}
284+
element.Contract = contractDefinition
285+
286+
// Next, if our sequence element uses ABI values to produce call data, our deserialized data is not yet
287+
// sufficient for runtime use, until we use it to resolve runtime references.
288+
if abiValues := element.Call.DataAbiValues; abiValues != nil {
289+
// Resolve the ABI values.
290+
if err := abiValues.Resolve(contractDefinition.CompiledContract().Abi); err != nil {
291+
return fmt.Errorf("error resolving method in contract '%v': %v", element.Contract.Name(), err)
292+
}
293+
}
294+
295+
return nil
296+
}
297+
262298
// testNextCallSequence tests a call message sequence against the underlying FuzzerWorker's Chain and calls every
263299
// CallSequenceTestFunc registered with the parent Fuzzer to update any test results. If any call message in the
264300
// sequence is nil, a call message will be created in its place, targeting a state changing method of a contract
265-
// deployed in the Chain.
301+
// deployed in the Chain. Note that this function also replays the corpus call sequences before entering the main fuzzing loop.
266302
// Returns any requests for call sequence shrinking or an error if one occurs.
267303
func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, error) {
268304
// 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
@@ -290,6 +326,17 @@ func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, err
290326

291327
// Our "fetch next call" method will generate new calls as needed, if we are generating a new sequence.
292328
fetchElementFunc := func(currentIndex int) (*calls.CallSequenceElement, error) {
329+
// We need to prepare the corpus element for runtime execution if we are replaying a corpus sequence
330+
if !isNewSequence {
331+
err := fw.bindCorpusElement(currentIndex)
332+
333+
// We will separately capture the error and log that the corpus element is disabled
334+
if err != nil {
335+
return nil, err
336+
}
337+
}
338+
339+
// Now, we are ready to fetch the next element from the sequence generator
293340
return fw.sequenceGenerator.PopSequenceElement()
294341
}
295342

@@ -340,10 +387,16 @@ func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, err
340387
}
341388

342389
// Execute our call sequence.
343-
_, err = calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc)
390+
var executedSequence calls.CallSequence
391+
executedSequence, err = calls.ExecuteCallSequenceIteratively(fw.chain, fetchElementFunc, executionCheckFunc)
344392

345393
// If we encountered an error, report it.
346394
if err != nil {
395+
// If a corpus element fails to execute, log it (in debug mode) and exit the function early
396+
if !isNewSequence {
397+
fw.fuzzer.logger.Debug("[Worker ", fw.workerIndex, "] corpus element has been disabled due to an error:", err)
398+
return nil, nil
399+
}
347400
return nil, err
348401
}
349402

@@ -352,7 +405,17 @@ func (fw *FuzzerWorker) testNextCallSequence() ([]ShrinkCallSequenceRequest, err
352405
return nil, nil
353406
}
354407

355-
// If this was not a new call sequence, indicate not to save the shrunken result to the corpus again.
408+
// We successfully executed a corpus element
409+
if !isNewSequence {
410+
fw.fuzzer.corpus.IncrementValid()
411+
// If there are no shrink requests that means this is not a test result call sequence, so we can mark it for mutation.
412+
if len(shrinkCallSequenceRequests) == 0 {
413+
// We don't really want an error here to stop fuzzing, so we ignore it.
414+
_ = fw.fuzzer.corpus.MarkCallSequenceForMutation(executedSequence, big.NewInt(1))
415+
}
416+
}
417+
418+
// We don't want to save shrink results from corpus sequences since we already did.
356419
if !isNewSequence {
357420
for i := 0; i < len(fw.shrinkCallSequenceRequests); i++ {
358421
shrinkCallSequenceRequests[i].RecordResultInCorpus = false

fuzzing/fuzzer_worker_sequence_generator.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,11 @@ func (g *CallSequenceGenerator) PopSequenceElement() (*calls.CallSequenceElement
262262
}
263263
}
264264

265-
// Update the element with the current nonce for the associated chain.
266-
element.Call.FillFromTestChainProperties(g.worker.chain)
265+
// Update the element with the current nonce for the associated chain only if we are not replaying a corpus sequence.
266+
// TODO: This feels a little hacky
267+
if !g.worker.fuzzer.corpus.InitializingCorpus() {
268+
element.Call.FillFromTestChainProperties(g.worker.chain)
269+
}
267270

268271
// Update our base sequence, advance our position, and return the processed element from this round.
269272
g.baseSequence[g.fetchIndex] = element

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ require (
1212
github.com/pkg/errors v0.9.1
1313
github.com/rs/zerolog v1.34.0
1414
github.com/shopspring/decimal v1.4.0
15-
github.com/spf13/cobra v1.9.1
16-
github.com/spf13/pflag v1.0.6
15+
github.com/spf13/cobra v1.10.1
16+
github.com/spf13/pflag v1.0.9
1717
github.com/stretchr/testify v1.11.1
1818
go.etcd.io/bbolt v1.4.3
1919
golang.org/x/crypto v0.43.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,10 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl
182182
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
183183
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
184184
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
185-
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
186-
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
187-
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
188-
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
185+
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
186+
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
187+
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
188+
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
189189
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
190190
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
191191
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

0 commit comments

Comments
 (0)