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

feat: replay crash from post deployment #464

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion chain/test_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ func NewTestChain(genesisAlloc types.GenesisAlloc, testChainConfig *config.TestC
// Create an in-memory database
db := rawdb.NewMemoryDatabase()
dbConfig := &triedb.Config{
HashDB: hashdb.Defaults,
HashDB: hashdb.Defaults,
Preimages: true,
// TODO Add cleanCacheSize of 256 depending on the resolution of this issue https://github.com/ethereum/go-ethereum/issues/30099
// PathDB: pathdb.Defaults,
}
Expand Down
196 changes: 196 additions & 0 deletions cmd/replay.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package cmd

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

"github.com/crytic/medusa/cmd/exitcodes"
"github.com/crytic/medusa/logging"
"github.com/crytic/medusa/logging/colors"

"github.com/crytic/medusa/fuzzing"
"github.com/crytic/medusa/fuzzing/calls"
"github.com/crytic/medusa/fuzzing/config"
"github.com/crytic/medusa/fuzzing/contracts"
"github.com/spf13/cobra"
)

// replayCmd represents the command provider for fuzzing
var replayCmd = &cobra.Command{
Use: "replay",
Short: "Replay a fuzzing campaign",
Long: `Replay a fuzzing campaign`,
Args: cmdValidateFuzzArgs,
ValidArgsFunction: cmdValidFuzzArgs,
RunE: cmdRunReplay,
SilenceUsage: true,
SilenceErrors: true,
}

func init() {
// Add all the flags allowed for the fuzz command
err := addReplayFlags()
if err != nil {
cmdLogger.Panic("Failed to initialize the fuzz command", err)
}

// Add the fuzz command and its associated flags to the root command
rootCmd.AddCommand(replayCmd)
}

// cmdRunReplay executes the CLI fuzz command and navigates through the following possibilities:
// #1: We will search for either a custom config file (via --config) or the default (medusa.json).
// If we find it, read it. If we can't read it, throw an error.
// #2: If a custom file was provided (--config was used), and we can't find the file, throw an error.
// #3: If medusa.json can't be found, use the default project configuration.
func cmdRunReplay(cmd *cobra.Command, args []string) error {
var projectConfig *config.ProjectConfig

// Check to see if --config flag was used and store the value of --config flag
configFlagUsed := cmd.Flags().Changed("config")
configPath, err := cmd.Flags().GetString("config")
if err != nil {
cmdLogger.Error("Failed to run the fuzz command", err)
return err
}

// If --config was not used, look for `medusa.json` in the current work directory
if !configFlagUsed {
workingDirectory, err := os.Getwd()
if err != nil {
cmdLogger.Error("Failed to run the fuzz command", err)
return err
}
configPath = filepath.Join(workingDirectory, DefaultProjectConfigFilename)
}

// Check to see if the file exists at configPath
_, existenceError := os.Stat(configPath)

// Possibility #1: File was found
if existenceError == nil {
// Try to read the configuration file and throw an error if something goes wrong
cmdLogger.Info("Reading the configuration file at: ", colors.Bold, configPath, colors.Reset)
// Use the default compilation platform if the config file doesn't specify one
projectConfig, err = config.ReadProjectConfigFromFile(configPath, DefaultCompilationPlatform)
if err != nil {
cmdLogger.Error("Failed to run the fuzz command", err)
return err
}
}

// Possibility #2: If the --config flag was used, and we couldn't find the file, we'll throw an error
if configFlagUsed && existenceError != nil {
cmdLogger.Error("Failed to run the fuzz command", err)
return existenceError
}

// Possibility #3: --config flag was not used and medusa.json was not found, so use the default project config
if !configFlagUsed && existenceError != nil {
cmdLogger.Warn(fmt.Sprintf("Unable to find the config file at %v, will use the default project configuration for the "+
"%v compilation platform instead", configPath, DefaultCompilationPlatform))

projectConfig, err = config.GetDefaultProjectConfig(DefaultCompilationPlatform)
if err != nil {
cmdLogger.Error("Failed to run the fuzz command", err)
return err
}
}

// Update the project configuration given whatever flags were set using the CLI
err = updateProjectConfigWithFuzzFlags(cmd, projectConfig)
if err != nil {
cmdLogger.Error("Failed to run the fuzz command", err)
return err
}

// Change our working directory to the parent directory of the project configuration file
// This is important as when we compile for a given platform, the paths may be relative to wherever the
// configuration is supplied from. Providing a file path explicitly is optional anyways, so we _should_
// be in the config directory when running this.
err = os.Chdir(filepath.Dir(configPath))
if err != nil {
cmdLogger.Error("Failed to run the fuzz command", err)
return err
}

if !projectConfig.Fuzzing.CoverageEnabled {
cmdLogger.Warn("Disabling coverage may limit efficacy of fuzzing. Consider enabling coverage for better results.")
}

// Create our fuzzing
fuzzer, fuzzErr := fuzzing.NewFuzzer(*projectConfig)
if fuzzErr != nil {
return exitcodes.NewErrorWithExitCode(fuzzErr, exitcodes.ExitCodeHandledError)
}

// Stop our fuzzing on keyboard interrupts
// c := make(chan os.Signal, 1)
// signal.Notify(c, os.Interrupt)
// go func() {
// <-c
// fuzzer.Stop()
// }()

// // Start the fuzzing process with our cancellable context.
// fuzzErr = fuzzer.Start()
chain, err := fuzzer.CreateTestChainWithAllocFile()

if err != nil {
return err
}
// Read the file data.
b, err := os.ReadFile("crash.json")
if err != nil {
return err
}

fmt.Println("Loaded data", string(b))
// Parse the call sequence data.
var sequence calls.CallSequence
err = json.Unmarshal(b, &sequence)
if err != nil {
return err
}

fmt.Println("Loaded sequence", len(sequence))

// fetchElementFunc := func(currentIndex int) (*calls.CallSequenceElement, error) {
// // If we are at the end of our sequence, return nil indicating we should stop executing.
// if currentIndex >= len(sequence) {
// return nil, nil
// }

// // If we are deploying a contract and not targeting one with this call, there should be no work to do.
// currentSequenceElement := sequence[currentIndex]

// return currentSequenceElement, nil

// }

executed, err := calls.ExecuteCallSequenceWithExecutionTracer(chain, contracts.Contracts{}, sequence, true)
if err != nil {

logging.GlobalLogger.Panic(err)
}
for _, call := range executed {
if call.ExecutionTrace != nil {
logging.GlobalLogger.Info(call.ExecutionTrace.Log())
} else {
logging.GlobalLogger.Info("No trace for call")
}
}

if fuzzErr != nil {
return exitcodes.NewErrorWithExitCode(fuzzErr, exitcodes.ExitCodeHandledError)
}

// If we have no error and failed test cases, we'll want to return a special exit code
if fuzzErr == nil && len(fuzzer.TestCasesWithStatus(fuzzing.TestCaseStatusFailed)) > 0 {
return exitcodes.NewErrorWithExitCode(fuzzErr, exitcodes.ExitCodeTestFailed)
}

return fuzzErr
}
114 changes: 114 additions & 0 deletions cmd/replay_flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cmd

import (
"fmt"

"github.com/crytic/medusa/fuzzing/config"
"github.com/spf13/cobra"
)

// addFuzzFlags adds the various flags for the fuzz command
func addReplayFlags() error {
// Get the default project config and throw an error if we cant
defaultConfig, err := config.GetDefaultProjectConfig(DefaultCompilationPlatform)
if err != nil {
return err
}

// Prevent alphabetical sorting of usage message
replayCmd.Flags().SortFlags = false

// Config file
replayCmd.Flags().String("config", "", "path to config file")

// Number of workers
replayCmd.Flags().Int("workers", 0,
fmt.Sprintf("number of fuzzer workers (unless a config file is provided, default is %d)", defaultConfig.Fuzzing.Workers))

// Timeout
replayCmd.Flags().Int("timeout", 0,
fmt.Sprintf("number of seconds to run the fuzzer campaign for (unless a config file is provided, default is %d). 0 means that timeout is not enforced", defaultConfig.Fuzzing.Timeout))

// Test limit
replayCmd.Flags().Uint64("test-limit", 0,
fmt.Sprintf("number of transactions to test before exiting (unless a config file is provided, default is %d). 0 means that test limit is not enforced", defaultConfig.Fuzzing.TestLimit))

// Tx sequence length
replayCmd.Flags().Int("seq-len", 0,
fmt.Sprintf("maximum transactions to run in sequence (unless a config file is provided, default is %d)", defaultConfig.Fuzzing.CallSequenceLength))

// Corpus directory
replayCmd.Flags().String("corpus-dir", "",
fmt.Sprintf("directory path for corpus items and coverage reports (unless a config file is provided, default is %q)", defaultConfig.Fuzzing.CorpusDirectory))

// Trace all
replayCmd.Flags().Bool("trace-all", false,
fmt.Sprintf("print the execution trace for every element in a shrunken call sequence instead of only the last element (unless a config file is provided, default is %t)", defaultConfig.Fuzzing.Testing.TraceAll))

// Logging color
replayCmd.Flags().Bool("no-color", false, "disabled colored terminal output")

return nil
}

// updateProjectConfigWithFuzzFlags will update the given projectConfig with any CLI arguments that were provided to the fuzz command
func updateProjectConfigWithReplayFlags(cmd *cobra.Command, projectConfig *config.ProjectConfig) error {

Check failure on line 55 in cmd/replay_flags.go

View workflow job for this annotation

GitHub Actions / lint

func `updateProjectConfigWithReplayFlags` is unused (unused)
var err error

// Update number of workers
if cmd.Flags().Changed("workers") {
projectConfig.Fuzzing.Workers, err = cmd.Flags().GetInt("workers")
if err != nil {
return err
}
}

// Update timeout
if cmd.Flags().Changed("timeout") {
projectConfig.Fuzzing.Timeout, err = cmd.Flags().GetInt("timeout")
if err != nil {
return err
}
}

// Update test limit
if cmd.Flags().Changed("test-limit") {
projectConfig.Fuzzing.TestLimit, err = cmd.Flags().GetUint64("test-limit")
if err != nil {
return err
}
}

// Update sequence length
if cmd.Flags().Changed("seq-len") {
projectConfig.Fuzzing.CallSequenceLength, err = cmd.Flags().GetInt("seq-len")
if err != nil {
return err
}
}

// Update corpus directory
if cmd.Flags().Changed("corpus-dir") {
projectConfig.Fuzzing.CorpusDirectory, err = cmd.Flags().GetString("corpus-dir")
if err != nil {
return err
}
}

// Update trace all enablement
if cmd.Flags().Changed("trace-all") {
projectConfig.Fuzzing.Testing.TraceAll, err = cmd.Flags().GetBool("trace-all")
if err != nil {
return err
}
}

// Update logging color mode
if cmd.Flags().Changed("no-color") {
projectConfig.Logging.NoColor, err = cmd.Flags().GetBool("no-color")
if err != nil {
return err
}
}
return nil
}
23 changes: 19 additions & 4 deletions fuzzing/calls/call_sequence_execution.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package calls

import (
"encoding/json"
"fmt"
"os"

"github.com/crytic/medusa/chain"
"github.com/crytic/medusa/fuzzing/contracts"
Expand All @@ -26,15 +28,23 @@ type ExecuteCallSequenceExecutionCheckFunc func(currentExecutedSequence CallSequ
// A "post element executed check" function is provided to check whether execution should stop after each element is
// executed.
// Returns the call sequence which was executed and an error if one occurs.
func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc ExecuteCallSequenceFetchElementFunc, executionCheckFunc ExecuteCallSequenceExecutionCheckFunc, additionalTracers ...*chain.TestChainTracer) (CallSequence, error) {
func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc ExecuteCallSequenceFetchElementFunc, executionCheckFunc ExecuteCallSequenceExecutionCheckFunc, additionalTracers ...*chain.TestChainTracer) (callSequenceExecuted CallSequence, err error) {
defer func() {
if recover() != nil {
// Marshal the data
jsonEncodedData, _ := json.MarshalIndent(callSequenceExecuted, "", " ")

// Write the JSON encoded data.
err = os.WriteFile("crash1.json", jsonEncodedData, os.ModePerm)
fmt.Println("Recovered from panic in ExecuteCallSequenceIteratively")
}
}()

// If there is no fetch element function provided, throw an error
if fetchElementFunc == nil {
return nil, fmt.Errorf("could not execute call sequence on chain as the 'fetch element function' provided was nil")
}

// Create a call sequence to track all elements executed throughout this operation.
var callSequenceExecuted CallSequence

// Create a variable to track if the post-execution check operation requested we break execution.
execCheckFuncRequestedBreak := false

Expand Down Expand Up @@ -64,6 +74,11 @@ func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc Exe
}
}

// // randomly panic
// if i == 3 {
// panic("random panic")
// }

// If we have no pending block to add a tx containing our call to, we must create one.
if chain.PendingBlock() == nil {
// The minimum step between blocks must be 1 in block number and timestamp, so we ensure this is the
Expand Down
Loading
Loading