Skip to content
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
83 changes: 83 additions & 0 deletions analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
// Package analyzer provides an interface for analyzing source code for compatibility issues.
package analyzer

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sort"
"strings"
)

// Analyzer represents the interface for the analyzer.
type Analyzer interface {
// Analyze analyzes the provided source code and returns any issues found.
Expand All @@ -26,6 +34,62 @@ type Issue struct {
Severity IssueSeverity `json:"severity"`
Impact string `json:"impact,omitempty"`
Reference string `json:"reference,omitempty"`
Hash string `json:"hash"`
}

// Opt is a functional option for configuring an Issue.
type Opt func(*Issue)

// WithImpact sets the impact of the issue.
func WithImpact(impact string) Opt {
return func(i *Issue) {
i.Impact = impact
}
}

// WithReference sets the reference for the issue.
func WithReference(reference string) Opt {
return func(i *Issue) {
i.Reference = reference
}
}

// WithCallStack sets the call stack for the issue.
func WithCallStack(callStack *CallStack) Opt {
return func(i *Issue) {
i.CallStack = callStack
}
}

// WithSeverity sets the severity for the issue.
func WithSeverity(severity IssueSeverity) Opt {
return func(i *Issue) {
i.Severity = severity
}
}

// WithMessage sets the message for the issue.
func WithMessage(message string) Opt {
return func(i *Issue) {
i.Message = message
}
}

// NewIssue creates a new issue with the provided severity, message, and source.
func NewIssue(opts ...Opt) *Issue {
issue := new(Issue)
for _, opt := range opts {
opt(issue)
}
issue.PopulateHash()
return issue
}

// PopulateHash generates a hash for the issue and populates it into the issue
func (i *Issue) PopulateHash() {
h := sha256.New()
_, _ = fmt.Fprintf(h, "%s:%s", i.Message, i.CallStack.Trace())
i.Hash = hex.EncodeToString(h.Sum(nil))
}

// CallStack represents a location in the code where the issue originates.
Expand All @@ -37,6 +101,14 @@ type CallStack struct {
CallStack *CallStack `json:"callStack,omitempty"` // The trace of calls leading to this source.
}

func (src *CallStack) Trace() string {
sub := src.Function
if src.CallStack != nil {
sub = fmt.Sprintf("%s:%s", sub, src.CallStack.Trace())
}
return sub
}

// Copy creates a deep copy of the CallStack.
func (src *CallStack) Copy() *CallStack {
if src == nil {
Expand Down Expand Up @@ -66,3 +138,14 @@ func (src *CallStack) AddCallStack(stack *CallStack) {
}
src.CallStack.AddCallStack(stack)
}

// SortIssues sorts the issues by severity and hash.
func SortIssues(issues []*Issue) []*Issue {
sort.Slice(issues, func(i, j int) bool {
if issues[i].Severity != issues[j].Severity {
return issues[i].Severity < issues[j].Severity
}
return strings.Compare(issues[i].Hash, issues[j].Hash) < 0
})
return issues
}
18 changes: 11 additions & 7 deletions analyzer/opcode/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,23 @@ func (op *opcode) Analyze(path string, withTrace bool) ([]*analyzer.Issue, error
if err != nil { // non-reachable portion ignored
continue
}
issue := &analyzer.Issue{
Severity: analyzer.IssueSeverityCritical,
CallStack: source,
Message: fmt.Sprintf("Potential Incompatible Opcode Detected: Opcode: %s, Funct: %s",
instruction.OpcodeHex(), instruction.Funct()),

opts := []analyzer.Opt{
analyzer.WithSeverity(analyzer.IssueSeverityCritical),
analyzer.WithCallStack(source),
analyzer.WithMessage(
fmt.Sprintf("Potential Incompatible Opcode Detected: Opcode: %s, Funct: %s",
instruction.OpcodeHex(), instruction.Funct()),
),
}
if common.ShouldIgnoreSource(source, op.profile.IgnoredFunctions) {
issue.Severity = analyzer.IssueSeverityWarning
opts = append(opts, analyzer.WithSeverity(analyzer.IssueSeverityWarning))
}
if !withTrace {
source.CallStack = nil
}
issues = append(issues, issue)

issues = append(issues, analyzer.NewIssue(opts...))
}
}
}
Expand Down
15 changes: 8 additions & 7 deletions analyzer/syscall/asm_syscall.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,14 @@ func (a *asmSyscallAnalyser) Analyze(path string, withTrace bool) ([]*analyzer.I
if !withTrace {
source.CallStack = nil
}
issues = append(issues, &analyzer.Issue{
Severity: severity,
Message: message,
CallStack: source,
Impact: potentialImpactMsg,
Reference: analyzerWorkingPrincipalURL,
})

issues = append(issues, analyzer.NewIssue(
analyzer.WithImpact(potentialImpactMsg),
analyzer.WithReference(analyzerWorkingPrincipalURL),
analyzer.WithCallStack(source),
analyzer.WithSeverity(severity),
analyzer.WithMessage(message),
))
}
}
}
Expand Down
13 changes: 8 additions & 5 deletions analyzer/syscall/go_syscall.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@ func (a *goSyscallAnalyser) Analyze(path string, withTrace bool) ([]*analyzer.Is
message = fmt.Sprintf("Potential NOOP Syscall Detected: %d", syscll.num)
}

issues = append(issues, &analyzer.Issue{
Severity: severity,
CallStack: stackTrace,
Message: message,
})
issues = append(
issues,
analyzer.NewIssue(
analyzer.WithSeverity(severity),
analyzer.WithCallStack(stackTrace),
analyzer.WithMessage(message),
),
)
}

return issues, nil
Expand Down
39 changes: 39 additions & 0 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/ChainSafe/vm-compat/analyzer"
"github.com/ChainSafe/vm-compat/analyzer/opcode"
"github.com/ChainSafe/vm-compat/analyzer/syscall"
"github.com/ChainSafe/vm-compat/comparer"
"github.com/ChainSafe/vm-compat/disassembler"
"github.com/ChainSafe/vm-compat/disassembler/manager"
"github.com/ChainSafe/vm-compat/profile"
Expand Down Expand Up @@ -50,6 +51,12 @@ var (
Required: false,
Value: false,
}

BaselineReport = &cli.StringFlag{
Name: "baseline-report",
Usage: "Path to the baseline report",
Required: false,
}
)

func CreateAnalyzeCommand(action cli.ActionFunc) *cli.Command {
Expand All @@ -65,6 +72,7 @@ func CreateAnalyzeCommand(action cli.ActionFunc) *cli.Command {
FormatFlag,
ReportOutputPathFlag,
TraceFlag,
BaselineReport,
},
}
}
Expand All @@ -84,6 +92,7 @@ func AnalyzeCompatibility(ctx *cli.Context) error {
reportOutputPath := ctx.Path(ReportOutputPathFlag.Name)
analysisType := ctx.String(AnalysisTypeFlag.Name)
withTrace := ctx.Bool(TraceFlag.Name)
baselineReport := ctx.Path(BaselineReport.Name)

disassemblyPath, err = disassemble(prof, source, disassemblyPath)
if err != nil {
Expand All @@ -95,6 +104,14 @@ func AnalyzeCompatibility(ctx *cli.Context) error {
return fmt.Errorf("analysis failed: %w", err)
}

if baselineReport != "" {
err = compareReport(issues, format, reportOutputPath, prof, baselineReport)
if err != nil {
return fmt.Errorf("error comparing reports: %w", err)
}
return nil
}

if err := writeReport(issues, format, reportOutputPath, prof); err != nil {
return fmt.Errorf("unable to write report: %w", err)
}
Expand Down Expand Up @@ -168,3 +185,25 @@ func writeReport(issues []*analyzer.Issue, format, outputPath string, prof *prof

return rendererInstance.Render(issues, output)
}

func compareReport(issues []*analyzer.Issue, format, outputPath string, prof *profile.VMProfile, baselineReport string) error {
absPath, err := filepath.Abs(baselineReport)
if err != nil {
return fmt.Errorf("error determining absolute path of baseline report: %w", err)
}

baselineIssuesFile, err := os.OpenFile(absPath, os.O_RDONLY, 0600)
if err != nil {
return fmt.Errorf("error loading baseline report: %w", err)
}
defer func() {
_ = baselineIssuesFile.Close()
}()

issues, err = comparer.NewJSONComparer().CompareReport(issues, baselineIssuesFile)
if err != nil {
return fmt.Errorf("error comparing reports: %w", err)
}

return writeReport(issues, format, outputPath, prof)
}
52 changes: 52 additions & 0 deletions comparer/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Package comparer provides a way to compare current issues with the baseline report.
package comparer

import (
"encoding/json"
"fmt"
"io"

"github.com/ChainSafe/vm-compat/analyzer"
)

// Comparer defines the interface for comparing the issues
type Comparer interface {
CompareReport(issues []*analyzer.Issue, reader io.Reader) ([]*analyzer.Issue, error)

Format() string
}

type jsonComparer struct{}

func NewJSONComparer() Comparer {
return &jsonComparer{}
}

func (r *jsonComparer) CompareReport(issues []*analyzer.Issue, reader io.Reader) ([]*analyzer.Issue, error) {
baseLineIssues := make([]*analyzer.Issue, 0)
data, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("error reading data while decoding issues: %w", err)
}
err = json.Unmarshal(data, &baseLineIssues)
if err != nil {
return nil, fmt.Errorf("error decoding issues: %w", err)
}

baselineIssuesMap := make(map[string]*analyzer.Issue)
for _, issue := range baseLineIssues {
baselineIssuesMap[issue.Hash] = issue
}

newIssues := make([]*analyzer.Issue, 0)
for _, newIssue := range issues {
if _, ok := baselineIssuesMap[newIssue.Hash]; !ok {
newIssues = append(newIssues, newIssue)
}
}
return newIssues, nil
}

func (r *jsonComparer) Format() string {
return "json"
}
2 changes: 1 addition & 1 deletion renderer/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ func NewJSONRenderer() Renderer {
}

func (r *JSONRenderer) Render(issues []*analyzer.Issue, output io.Writer) error {
return json.NewEncoder(output).Encode(issues)
return json.NewEncoder(output).Encode(analyzer.SortIssues(issues))
}

func (r *JSONRenderer) Format() string {
Expand Down
Loading