From be314e0ef3db176707caf2d7f5a667aa8572c16a Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Wed, 24 Jul 2024 23:34:02 -0500 Subject: [PATCH] feat: replay crash from post deployment --- chain/test_chain.go | 3 +- cmd/replay.go | 196 +++++++++++++++++++++++ cmd/replay_flags.go | 114 +++++++++++++ fuzzing/calls/call_sequence_execution.go | 23 ++- fuzzing/fuzzer.go | 63 +++++++- 5 files changed, 393 insertions(+), 6 deletions(-) create mode 100644 cmd/replay.go create mode 100644 cmd/replay_flags.go diff --git a/chain/test_chain.go b/chain/test_chain.go index 0137c6c3..54b79a60 100644 --- a/chain/test_chain.go +++ b/chain/test_chain.go @@ -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, } diff --git a/cmd/replay.go b/cmd/replay.go new file mode 100644 index 00000000..2310982c --- /dev/null +++ b/cmd/replay.go @@ -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 +} diff --git a/cmd/replay_flags.go b/cmd/replay_flags.go new file mode 100644 index 00000000..5415deb9 --- /dev/null +++ b/cmd/replay_flags.go @@ -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 { + 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 +} diff --git a/fuzzing/calls/call_sequence_execution.go b/fuzzing/calls/call_sequence_execution.go index ca983f0d..775f3fec 100644 --- a/fuzzing/calls/call_sequence_execution.go +++ b/fuzzing/calls/call_sequence_execution.go @@ -1,7 +1,9 @@ package calls import ( + "encoding/json" "fmt" + "os" "github.com/crytic/medusa/chain" "github.com/crytic/medusa/fuzzing/contracts" @@ -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 @@ -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 diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 960ebfe2..5217ba3b 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -2,9 +2,9 @@ package fuzzing import ( "context" + "encoding/json" "errors" "fmt" - "github.com/ethereum/go-ethereum/crypto" "math/big" "math/rand" "os" @@ -16,6 +16,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/crypto" + "github.com/crytic/medusa/fuzzing/executiontracer" "github.com/crytic/medusa/fuzzing/coverage" @@ -25,6 +27,7 @@ import ( "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/utils/randomutils" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/crytic/medusa/chain" @@ -40,6 +43,31 @@ import ( "golang.org/x/exp/slices" ) +type Alloc map[common.Address]types.Account + +func (g Alloc) OnRoot(common.Hash) {} + +func (g Alloc) OnAccount(addr *common.Address, dumpAccount state.DumpAccount) { + if addr == nil { + return + } + balance, _ := new(big.Int).SetString(dumpAccount.Balance, 0) + var storage map[common.Hash]common.Hash + if dumpAccount.Storage != nil { + storage = make(map[common.Hash]common.Hash, len(dumpAccount.Storage)) + for k, v := range dumpAccount.Storage { + storage[k] = common.HexToHash(v) + } + } + genesisAccount := types.Account{ + Code: dumpAccount.Code, + Storage: storage, + Balance: balance, + Nonce: dumpAccount.Nonce, + } + g[*addr] = genesisAccount +} + // Fuzzer represents an Ethereum smart contract fuzzing provider. type Fuzzer struct { // ctx describes the context for the fuzzing run, used to cancel running operations. @@ -386,6 +414,27 @@ func (f *Fuzzer) createTestChain() (*chain.TestChain, error) { return testChain, err } +func (f *Fuzzer) CreateTestChainWithAllocFile() (*chain.TestChain, error) { + inFile, err := os.Open("alloc.json") + if err != nil { + return nil, err + } + defer inFile.Close() + decoder := json.NewDecoder(inFile) + var genesisAlloc types.GenesisAlloc + if err := decoder.Decode(&genesisAlloc); err != nil { + return nil, err + } + fmt.Println("Genesis alloc loaded from alloc.json") + + // Create our test chain with our basic allocations and passed medusa's chain configuration + testChain, err := chain.NewTestChain(genesisAlloc, &f.config.Fuzzing.TestChainConfig) + + // Set our block gas limit + testChain.BlockGasLimit = f.config.Fuzzing.BlockGasLimit + return testChain, err +} + // chainSetupFromCompilations is a TestChainSetupFunc which sets up the base test chain state by deploying // all compiled contract definitions. This includes any successful compilations as a result of the Fuzzer.config // definitions, as well as those added by Fuzzer.AddCompilationTargets. The contract deployment order is defined by @@ -515,6 +564,18 @@ func chainSetupFromCompilations(fuzzer *Fuzzer, testChain *chain.TestChain) (*ex return nil, fmt.Errorf("%v was specified in the target contracts but was not found in the compilation artifacts", contractName) } } + + // Write the state after deployment to alloc.json + dumpdb := testChain.State().Copy() + collector := make(Alloc) + dumpdb.DumpToCollector(collector, nil) + b, err := json.MarshalIndent(collector, "", " ") + os.WriteFile("alloc.json", b, 0644) + + if err != nil { + return nil, err + } + return nil, nil }