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: display success/revert hit count in coverage report #364

Merged
merged 14 commits into from
Aug 22, 2024
Merged
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
58 changes: 33 additions & 25 deletions fuzzing/coverage/coverage_maps.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package coverage

import (
"bytes"
"golang.org/x/exp/slices"
"sync"

compilationTypes "github.com/crytic/medusa/compilation/types"
"github.com/crytic/medusa/utils"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"sync"
)

// CoverageMaps represents a data structure used to identify instruction execution coverage of various smart contracts
Expand Down Expand Up @@ -168,8 +169,8 @@ func (cm *CoverageMaps) Update(coverageMaps *CoverageMaps) (bool, bool, error) {
return successCoverageChanged, revertedCoverageChanged, nil
}

// SetAt sets the coverage state of a given program counter location within code coverage data.
func (cm *CoverageMaps) SetAt(codeAddress common.Address, codeLookupHash common.Hash, codeSize int, pc uint64) (bool, error) {
// UpdateAt updates the hit count of a given program counter location within code coverage data.
func (cm *CoverageMaps) UpdateAt(codeAddress common.Address, codeLookupHash common.Hash, codeSize int, pc uint64) (bool, error) {
// If the code size is zero, do nothing
if codeSize == 0 {
return false, nil
Expand Down Expand Up @@ -210,7 +211,7 @@ func (cm *CoverageMaps) SetAt(codeAddress common.Address, codeLookupHash common.
}

// Set our coverage in the map and return our change state
changedInMap, err = coverageMap.setCoveredAt(codeSize, pc)
changedInMap, err = coverageMap.updateCoveredAt(codeSize, pc)
return addedNewMap || changedInMap, err
}

Expand Down Expand Up @@ -285,18 +286,18 @@ func (cm *ContractCoverageMap) update(coverageMap *ContractCoverageMap) (bool, b
return successfulCoverageChanged, revertedCoverageChanged, nil
}

// setCoveredAt sets the coverage state at a given program counter location within a ContractCoverageMap used for
// updateCoveredAt updates the hit counter at a given program counter location within a ContractCoverageMap used for
// "successful" coverage (non-reverted).
// Returns a boolean indicating whether new coverage was achieved, or an error if one occurred.
func (cm *ContractCoverageMap) setCoveredAt(codeSize int, pc uint64) (bool, error) {
func (cm *ContractCoverageMap) updateCoveredAt(codeSize int, pc uint64) (bool, error) {
// Set our coverage data for the successful path.
return cm.successfulCoverage.setCoveredAt(codeSize, pc)
return cm.successfulCoverage.updateCoveredAt(codeSize, pc)
}

// CoverageMapBytecodeData represents a data structure used to identify instruction execution coverage of some init
// or runtime bytecode.
type CoverageMapBytecodeData struct {
executedFlags []byte
executedFlags []uint
}

// Reset resets the bytecode coverage map data to be empty.
Expand All @@ -310,27 +311,29 @@ func (cm *CoverageMapBytecodeData) Equal(b *CoverageMapBytecodeData) bool {
// Return an equality comparison on the data, ignoring size checks by stopping at the end of the shortest slice.
// We do this to avoid comparing arbitrary length constructor arguments appended to init bytecode.
smallestSize := utils.Min(len(cm.executedFlags), len(b.executedFlags))
return bytes.Equal(cm.executedFlags[:smallestSize], b.executedFlags[:smallestSize])
// TODO: Currently we are checking equality by making sure the two maps have the same hit counts
// it may make sense to just check that both of them are greater than zero
return slices.Equal(cm.executedFlags[:smallestSize], b.executedFlags[:smallestSize])
}

// IsCovered checks if a given program counter location is covered by the map.
// Returns a boolean indicating if the program counter was executed on this map.
func (cm *CoverageMapBytecodeData) IsCovered(pc int) bool {
// HitCount returns the number of times that the provided program counter (PC) has been hit. If zero is returned, then
// the PC has not been hit, the map is empty, or the PC is out-of-bounds
func (cm *CoverageMapBytecodeData) HitCount(pc int) uint {
// If the coverage map bytecode data is nil, this is not covered.
if cm == nil {
return false
return 0
}

// If this map has no execution data or is out of bounds, it is not covered.
if cm.executedFlags == nil || len(cm.executedFlags) <= pc {
return false
return 0
}

// Otherwise, return the execution flag
return cm.executedFlags[pc] != 0
// Otherwise, return the hit count
return cm.executedFlags[pc]
}

// update creates updates the current CoverageMapBytecodeData with the provided one.
// update updates the hit count of the current CoverageMapBytecodeData with the provided one.
// Returns a boolean indicating whether new coverage was achieved, or an error if one was encountered.
func (cm *CoverageMapBytecodeData) update(coverageMap *CoverageMapBytecodeData) (bool, error) {
// If the coverage map execution data provided is nil, exit early
Expand All @@ -347,28 +350,33 @@ func (cm *CoverageMapBytecodeData) update(coverageMap *CoverageMapBytecodeData)
// Update each byte which represents a position in the bytecode which was covered.
changed := false
for i := 0; i < len(cm.executedFlags) && i < len(coverageMap.executedFlags); i++ {
// Only update the map if we haven't seen this coverage before
if cm.executedFlags[i] == 0 && coverageMap.executedFlags[i] != 0 {
cm.executedFlags[i] = 1
cm.executedFlags[i] += coverageMap.executedFlags[i]
changed = true
}
}
return changed, nil
}

// setCoveredAt sets the coverage state at a given program counter location within a CoverageMapBytecodeData.
// updateCoveredAt updates the hit count at a given program counter location within a CoverageMapBytecodeData.
// Returns a boolean indicating whether new coverage was achieved, or an error if one occurred.
func (cm *CoverageMapBytecodeData) setCoveredAt(codeSize int, pc uint64) (bool, error) {
func (cm *CoverageMapBytecodeData) updateCoveredAt(codeSize int, pc uint64) (bool, error) {
// If the execution flags don't exist, create them for this code size.
if cm.executedFlags == nil {
cm.executedFlags = make([]byte, codeSize)
cm.executedFlags = make([]uint, codeSize)
}

// If our program counter is in range, determine if we achieved new coverage for the first time, and update it.
// If our program counter is in range, determine if we achieved new coverage for the first time or increment the hit counter.
if pc < uint64(len(cm.executedFlags)) {
if cm.executedFlags[pc] == 0 {
cm.executedFlags[pc] = 1
// Increment the hit counter
cm.executedFlags[pc] += 1

// This is the first time we have hit this PC, so return true
if cm.executedFlags[pc] == 1 {
return true, nil
}
// We have seen this PC before, return false
return false, nil
}

Expand Down
2 changes: 1 addition & 1 deletion fuzzing/coverage/coverage_tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ func (t *CoverageTracer) OnOpcode(pc uint64, op byte, gas, cost uint64, scope tr
}

// Record coverage for this location in our map.
_, coverageUpdateErr := callFrameState.pendingCoverageMap.SetAt(address, *callFrameState.lookupHash, codeSize, pc)
_, coverageUpdateErr := callFrameState.pendingCoverageMap.UpdateAt(address, *callFrameState.lookupHash, codeSize, pc)
if coverageUpdateErr != nil {
logging.GlobalLogger.Panic("Coverage tracer failed to update coverage map while tracing state", coverageUpdateErr)
}
Expand Down
4 changes: 2 additions & 2 deletions fuzzing/coverage/report_template.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,12 @@
{{/* Output two cells for the reverted/non-reverted execution status */}}
<td class="row-reverted-status unselectable">
{{if $line.IsCovered}}
<div title="The source line executed without reverting.">√</div>
<div title="The source line executed without reverting.">√ {{$line.SuccessHitCount}}</div>
{{end}}
</td>
<td class="row-reverted-status unselectable">
{{if $line.IsCoveredReverted}}
<div title="The source line executed, but was reverted.">⟳</div>
<div title="The source line executed, but was reverted.">⟳ {{$line.RevertHitCount}}</div>
{{end}}
</td>

Expand Down
24 changes: 16 additions & 8 deletions fuzzing/coverage/source_analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ type SourceLineAnalysis struct {
// IsCovered indicates whether the source line has been executed without reverting.
IsCovered bool

// SuccessHitCount describes how many times this line was executed successfully
SuccessHitCount uint

// RevertHitCount describes how many times this line reverted during execution
RevertHitCount uint

// IsCoveredReverted indicates whether the source line has been executed before reverting.
IsCoveredReverted bool
}
Expand Down Expand Up @@ -214,12 +220,12 @@ func analyzeContractSourceCoverage(compilation types.Compilation, sourceAnalysis
continue
}

// Check if the source map element was executed.
sourceMapElementCovered := false
sourceMapElementCoveredReverted := false
// Capture the hit count of the source map element.
succHitCount := uint(0)
revertHitCount := uint(0)
if contractCoverageData != nil {
sourceMapElementCovered = contractCoverageData.successfulCoverage.IsCovered(instructionOffsetLookup[sourceMapElement.Index])
sourceMapElementCoveredReverted = contractCoverageData.revertedCoverage.IsCovered(instructionOffsetLookup[sourceMapElement.Index])
succHitCount = contractCoverageData.successfulCoverage.HitCount(instructionOffsetLookup[sourceMapElement.Index])
revertHitCount = contractCoverageData.revertedCoverage.HitCount(instructionOffsetLookup[sourceMapElement.Index])
}

// Obtain the source file this element maps to.
Expand All @@ -232,9 +238,11 @@ func analyzeContractSourceCoverage(compilation types.Compilation, sourceAnalysis
// Mark the line active/executable.
sourceLine.IsActive = true

// Set its coverage state
sourceLine.IsCovered = sourceLine.IsCovered || sourceMapElementCovered
sourceLine.IsCoveredReverted = sourceLine.IsCoveredReverted || sourceMapElementCoveredReverted
// Set its coverage state and increment hit counts
sourceLine.SuccessHitCount += succHitCount
sourceLine.RevertHitCount += revertHitCount
sourceLine.IsCovered = sourceLine.IsCovered || sourceLine.SuccessHitCount > 0
sourceLine.IsCoveredReverted = sourceLine.IsCoveredReverted || sourceLine.RevertHitCount > 0

// Indicate we matched a source line, so when we stop matching sequentially, we know we can exit
// early.
Expand Down
12 changes: 10 additions & 2 deletions fuzzing/fuzzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -839,8 +839,16 @@ func TestCorpusReplayability(t *testing.T) {
assertCorpusCallSequencesCollected(f, true)
newCoverage := f.fuzzer.corpus.CoverageMaps()

// Check to see if original and new coverage are the same.
assert.True(t, originalCoverage.Equal(newCoverage))
// Check to see if original and new coverage are the same (disregarding hit count)
successCovIncreased, revertCovIncreased, err := originalCoverage.Update(newCoverage)
assert.False(t, successCovIncreased)
assert.False(t, revertCovIncreased)
assert.NoError(t, err)

successCovIncreased, revertCovIncreased, err = newCoverage.Update(originalCoverage)
assert.False(t, successCovIncreased)
assert.False(t, revertCovIncreased)
assert.NoError(t, err)

// Verify that the fuzzer finished after fewer sequences than there are in the corpus
assert.LessOrEqual(t, f.fuzzer.metrics.SequencesTested().Uint64(), uint64(originalCorpusSequenceCount))
Expand Down