diff --git a/fuzzing/fuzzer.go b/fuzzing/fuzzer.go index 8c0269c1..cb9692cd 100644 --- a/fuzzing/fuzzer.go +++ b/fuzzing/fuzzer.go @@ -12,6 +12,7 @@ import ( "math/big" "math/rand" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -723,13 +724,24 @@ func (f *Fuzzer) printMetricsLoop() { // Calculate time elapsed since the last update secondsSinceLastUpdate := time.Since(lastPrintedTime).Seconds() + // Obtain memory usage stats + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + memoryUsedMB := memStats.Alloc / 1024 / 1024 + memoryTotalMB := memStats.Sys / 1024 / 1024 + // Print a metrics update - f.logger.Info(colors.Bold, "fuzz: ", colors.Reset, - "elapsed: ", colors.Bold, time.Since(startTime).Round(time.Second).String(), colors.Reset, - ", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset, - ", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset, - ", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset, - ", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset) + logBuffer := logging.NewLogBuffer() + logBuffer.Append(colors.Bold, "fuzz: ", colors.Reset) + logBuffer.Append("elapsed: ", colors.Bold, time.Since(startTime).Round(time.Second).String(), colors.Reset) + logBuffer.Append(", calls: ", colors.Bold, fmt.Sprintf("%d (%d/sec)", callsTested, uint64(float64(new(big.Int).Sub(callsTested, lastCallsTested).Uint64())/secondsSinceLastUpdate)), colors.Reset) + logBuffer.Append(", seq/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(sequencesTested, lastSequencesTested).Uint64())/secondsSinceLastUpdate)), colors.Reset) + logBuffer.Append(", coverage: ", colors.Bold, fmt.Sprintf("%d", f.corpus.ActiveMutableSequenceCount()), colors.Reset) + if f.logger.Level() <= zerolog.DebugLevel { + logBuffer.Append(", mem: ", colors.Bold, fmt.Sprintf("%v/%v MB", memoryUsedMB, memoryTotalMB), colors.Reset) + logBuffer.Append(", resets/s: ", colors.Bold, fmt.Sprintf("%d", uint64(float64(new(big.Int).Sub(workerStartupCount, lastWorkerStartupCount).Uint64())/secondsSinceLastUpdate)), colors.Reset) + } + f.logger.Info(logBuffer.Elements()...) // Update our delta tracking metrics lastPrintedTime = time.Now() diff --git a/fuzzing/fuzzer_test.go b/fuzzing/fuzzer_test.go index 978ecbc0..55c845fd 100644 --- a/fuzzing/fuzzer_test.go +++ b/fuzzing/fuzzer_test.go @@ -1,11 +1,13 @@ package fuzzing import ( + "encoding/hex" "github.com/crytic/medusa/chain" "github.com/crytic/medusa/events" "github.com/crytic/medusa/fuzzing/calls" "github.com/crytic/medusa/fuzzing/valuegeneration" "github.com/crytic/medusa/utils" + "github.com/ethereum/go-ethereum/common" "math/big" "math/rand" "testing" @@ -595,6 +597,75 @@ func TestValueGenerationSolving(t *testing.T) { } } +// TestASTValueExtraction runs a test to ensure appropriate AST values can be mined out of a compiled source's AST. +func TestASTValueExtraction(t *testing.T) { + // Define our expected values to be mined. + expectedAddresses := []common.Address{ + common.HexToAddress("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D"), + common.HexToAddress("0x1234567890123456789012345678901234567890"), + } + expectedIntegers := []string{ + // Unsigned integer tests + "111", // no denomination + "1", // 1 wei (base unit) + "2000000000", // 2 gwei + "5000000000000000000", // 5 ether + "6", // 6 seconds (base unit) + "420", // 7 minutes + "28800", // 8 hours + "777600", // 9 days + "6048000", // 10 weeks + + // Signed integer tests + "-111", // no denomination + "-1", // 1 wei (base unit) + "-2000000000", // 2 gwei + "-5000000000000000000", // 5 ether + "-6", // 6 seconds (base unit) + "-420", // 7 minutes + "-28800", // 8 hours + "-777600", // 9 days + "-6048000", // 10 weeks + } + expectedStrings := []string{ + "testString", + "testString2", + } + expectedByteSequences := make([][]byte, 0) // no tests yet + + // Run the fuzzer test + runFuzzerTest(t, &fuzzerSolcFileTest{ + filePath: "testdata/contracts/value_generation/ast_value_extraction.sol", + configUpdates: func(config *config.ProjectConfig) { + config.Fuzzing.TestLimit = 1 // stop immediately to simply see what values were mined. + config.Fuzzing.Testing.AssertionTesting.Enabled = true + config.Fuzzing.Testing.PropertyTesting.Enabled = false + }, + method: func(f *fuzzerTestContext) { + // Start the fuzzer + err := f.fuzzer.Start() + assert.NoError(t, err) + + // Verify all of our expected values exist + valueSet := f.fuzzer.BaseValueSet() + for _, expectedAddr := range expectedAddresses { + assert.True(t, valueSet.ContainsAddress(expectedAddr), "Value set did not contain expected address: %v", expectedAddr.String()) + } + for _, expectedIntegerStr := range expectedIntegers { + expectedInteger, ok := new(big.Int).SetString(expectedIntegerStr, 10) + assert.True(t, ok, "Could not parse provided expected integer string in test: \"%v\"", expectedIntegerStr) + assert.True(t, valueSet.ContainsInteger(expectedInteger), "Value set did not contain expected integer: %v", expectedInteger.String()) + } + for _, expectedString := range expectedStrings { + assert.True(t, valueSet.ContainsString(expectedString), "Value set did not contain expected string: \"%v\"", expectedString) + } + for _, expectedByteSequence := range expectedByteSequences { + assert.True(t, valueSet.ContainsBytes(expectedByteSequence), "Value set did not contain expected bytes: \"%v\"", hex.EncodeToString(expectedByteSequence)) + } + }, + }) +} + // TestVMCorrectness runs tests to ensure block properties are reported consistently within the EVM, as it's configured // by the chain.TestChain. func TestVMCorrectness(t *testing.T) { diff --git a/fuzzing/testdata/contracts/value_generation/ast_value_extraction.sol b/fuzzing/testdata/contracts/value_generation/ast_value_extraction.sol new file mode 100644 index 00000000..02f2e99c --- /dev/null +++ b/fuzzing/testdata/contracts/value_generation/ast_value_extraction.sol @@ -0,0 +1,48 @@ +// This contract verifies the fuzzer can extract AST literals of different subdenominations from the file. +contract TestContract { + function addressValues() public { + address x = 0x7109709ECfa91a80626fF3989D68f67F5b1DD12D; + assert(x != address(0x1234567890123456789012345678901234567890)); + } + function uintValues() public { + // Use all integer denoms + uint x = 111; + x = 1 wei; + x = 2 gwei; + //x = 3 szabo; + //x = 4 finney; + x = 5 ether; + x = 6 seconds; + x = 7 minutes; + x = 8 hours; + x = 9 days; + x = 10 weeks; + //x = 11 years; + + // Dummy assertion that should always pass. + assert(x != 0); + } + function intValues() public { + // Use all integer denoms + int x = -111; + x = -1 wei; + x = -2 gwei; + //x = -3 szabo; + //x = -4 finney; + x = -5 ether; + x = -6 seconds; + x = -7 minutes; + x = -8 hours; + x = -9 days; + x = -10 weeks; + //x = -11 years; + + // Dummy assertion that should always pass. + assert(x != 0); + } + function stringValues() public { + string memory s = "testString"; + s = "testString2"; + assert(true); + } +} diff --git a/fuzzing/valuegeneration/value_set.go b/fuzzing/valuegeneration/value_set.go index 77e78b99..883aab73 100644 --- a/fuzzing/valuegeneration/value_set.go +++ b/fuzzing/valuegeneration/value_set.go @@ -64,6 +64,12 @@ func (vs *ValueSet) AddAddress(a common.Address) { vs.addresses[a] = nil } +// ContainsAddress checks if an address is contained in the ValueSet. +func (vs *ValueSet) ContainsAddress(a common.Address) bool { + _, contains := vs.addresses[a] + return contains +} + // RemoveAddress removes an address item from the ValueSet. func (vs *ValueSet) RemoveAddress(a common.Address) { delete(vs.addresses, a) @@ -85,6 +91,12 @@ func (vs *ValueSet) AddInteger(b *big.Int) { vs.integers[b.String()] = b } +// ContainsInteger checks if an integer is contained in the ValueSet. +func (vs *ValueSet) ContainsInteger(b *big.Int) bool { + _, contains := vs.integers[b.String()] + return contains +} + // RemoveInteger removes an integer item from the ValueSet. func (vs *ValueSet) RemoveInteger(b *big.Int) { delete(vs.integers, b.String()) @@ -106,6 +118,12 @@ func (vs *ValueSet) AddString(s string) { vs.strings[s] = nil } +// ContainsString checks if a string is contained in the ValueSet. +func (vs *ValueSet) ContainsString(s string) bool { + _, contains := vs.strings[s] + return contains +} + // RemoveString removes a string item from the ValueSet. func (vs *ValueSet) RemoveString(s string) { delete(vs.strings, s) @@ -133,6 +151,18 @@ func (vs *ValueSet) AddBytes(b []byte) { vs.bytes[hashStr] = b } +// ContainsBytes checks if a byte sequence is contained in the ValueSet. +func (vs *ValueSet) ContainsBytes(b []byte) bool { + // Calculate hash and reset our hash provider + vs.hashProvider.Write(b) + hashStr := hex.EncodeToString(vs.hashProvider.Sum(nil)) + vs.hashProvider.Reset() + + // Check if the key exists in our lookup + _, contains := vs.bytes[hashStr] + return contains +} + // RemoveBytes removes a byte sequence item from the ValueSet. func (vs *ValueSet) RemoveBytes(b []byte) { // Calculate hash and reset our hash provider diff --git a/fuzzing/valuegeneration/value_set_from_ast.go b/fuzzing/valuegeneration/value_set_from_ast.go index 11eb9c5a..0051956c 100644 --- a/fuzzing/valuegeneration/value_set_from_ast.go +++ b/fuzzing/valuegeneration/value_set_from_ast.go @@ -2,6 +2,7 @@ package valuegeneration import ( "github.com/ethereum/go-ethereum/common" + "github.com/shopspring/decimal" "math/big" "strings" ) @@ -20,8 +21,16 @@ func (vs *ValueSet) SeedFromAst(ast any) { return // fail silently to continue walking } + // Extract the subdenomination type + tempSubdenomination, obtainedSubdenomination := node["subdenomination"].(string) + var literalSubdenomination *string + if obtainedSubdenomination { + literalSubdenomination = &tempSubdenomination + } + // Seed ValueSet with literals if literalKind == "number" { + // If it has a 0x prefix, it won't have decimals if strings.HasPrefix(literalValue, "0x") { if b, ok := big.NewInt(0).SetString(literalValue[2:], 16); ok { vs.AddInteger(b) @@ -29,7 +38,8 @@ func (vs *ValueSet) SeedFromAst(ast any) { vs.AddAddress(common.BigToAddress(b)) } } else { - if b, ok := big.NewInt(0).SetString(literalValue, 10); ok { + if decValue, err := decimal.NewFromString(literalValue); err == nil { + b := getAbsoluteValueFromDenominatedValue(decValue, literalSubdenomination) vs.AddInteger(b) vs.AddInteger(new(big.Int).Neg(b)) vs.AddAddress(common.BigToAddress(b)) @@ -42,6 +52,50 @@ func (vs *ValueSet) SeedFromAst(ast any) { }) } +// getAbsoluteValueFromDenominatedValue converts a given decimal number in a provided denomination to a big.Int +// that represents its actual calculated value. +// Note: Decimals must be used as big.Float is prone to similar mantissa-related precision issues as float32/float64. +// Returns the calculated value given the floating point number in a given denomination. +func getAbsoluteValueFromDenominatedValue(number decimal.Decimal, denomination *string) *big.Int { + // If the denomination is nil, we do nothing + if denomination == nil { + return number.BigInt() + } + + // Otherwise, switch on the type and obtain a multiplier + var multiplier decimal.Decimal + switch *denomination { + case "wei": + multiplier = decimal.NewFromFloat32(1) + case "gwei": + multiplier = decimal.NewFromFloat32(1e9) + case "szabo": + multiplier = decimal.NewFromFloat32(1e12) + case "finney": + multiplier = decimal.NewFromFloat32(1e15) + case "ether": + multiplier = decimal.NewFromFloat32(1e18) + case "seconds": + multiplier = decimal.NewFromFloat32(1) + case "minutes": + multiplier = decimal.NewFromFloat32(60) + case "hours": + multiplier = decimal.NewFromFloat32(60 * 60) + case "days": + multiplier = decimal.NewFromFloat32(60 * 60 * 24) + case "weeks": + multiplier = decimal.NewFromFloat32(60 * 60 * 24 * 7) + case "years": + multiplier = decimal.NewFromFloat32(60 * 60 * 24 * 7 * 365) + default: + multiplier = decimal.NewFromFloat32(1) + } + + // Obtain the transformed number as an integer. + transformedValue := number.Mul(multiplier) + return transformedValue.BigInt() +} + // walkAstNodes walks/iterates across an AST for each node, calling the provided walk function with each discovered node // as an argument. func walkAstNodes(ast any, walkFunc func(node map[string]any)) { diff --git a/go.mod b/go.mod index 1aec9600..83b9d462 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.29.0 + github.com/shopspring/decimal v1.3.1 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index 1c621d8b..7721ec5e 100644 --- a/go.sum +++ b/go.sum @@ -259,6 +259,8 @@ github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtm github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=