diff --git a/analyzer/opcode/opcode.go b/analyzer/opcode/opcode.go index f3d7afa..7807149 100644 --- a/analyzer/opcode/opcode.go +++ b/analyzer/opcode/opcode.go @@ -36,7 +36,7 @@ func (op *opcode) Analyze(path string, withTrace bool) ([]*analyzer.Issue, error for _, segment := range callGraph.Segments() { for _, instruction := range segment.Instructions() { if !op.isAllowedOpcode(instruction.OpcodeHex(), instruction.Funct()) { - source, err := common.TraceAsmCaller( + sources, err := common.TraceAllAsmCaller( absPath, callGraph, segment.Label(), @@ -45,23 +45,23 @@ func (op *opcode) Analyze(path string, withTrace bool) ([]*analyzer.Issue, error if err != nil { // non-reachable portion ignored continue } - - 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) { - opts = append(opts, analyzer.WithSeverity(analyzer.IssueSeverityWarning)) + for _, source := range sources { + 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) { + opts = append(opts, analyzer.WithSeverity(analyzer.IssueSeverityWarning)) + } + if !withTrace { + source.CallStack = nil + } + issues = append(issues, analyzer.NewIssue(opts...)) } - if !withTrace { - source.CallStack = nil - } - - issues = append(issues, analyzer.NewIssue(opts...)) } } } diff --git a/analyzer/syscall/asm_syscall.go b/analyzer/syscall/asm_syscall.go index e34b11d..fb4ff5f 100644 --- a/analyzer/syscall/asm_syscall.go +++ b/analyzer/syscall/asm_syscall.go @@ -58,7 +58,7 @@ func (a *asmSyscallAnalyser) Analyze(path string, withTrace bool) ([]*analyzer.I if slices.Contains(a.profile.AllowedSycalls, syscall.Number) { continue } - source, err := common.TraceAsmCaller( + sources, err := common.TraceAllAsmCaller( absPath, callGraph, syscall.Segment.Label(), @@ -67,27 +67,27 @@ func (a *asmSyscallAnalyser) Analyze(path string, withTrace bool) ([]*analyzer.I if err != nil { // non-reachable portion ignored continue } - - severity := analyzer.IssueSeverityCritical - if common.ShouldIgnoreSource(source, a.profile.IgnoredFunctions) { - severity = analyzer.IssueSeverityWarning - } - message := fmt.Sprintf("Potential Incompatible Syscall Detected: %d", syscall.Number) - if slices.Contains(a.profile.NOOPSyscalls, syscall.Number) { - message = fmt.Sprintf("Potential NOOP Syscall Detected: %d", syscall.Number) - severity = analyzer.IssueSeverityWarning + for _, source := range sources { + severity := analyzer.IssueSeverityCritical + if common.ShouldIgnoreSource(source, a.profile.IgnoredFunctions) { + severity = analyzer.IssueSeverityWarning + } + message := fmt.Sprintf("Potential Incompatible Syscall Detected: %d", syscall.Number) + if slices.Contains(a.profile.NOOPSyscalls, syscall.Number) { + message = fmt.Sprintf("Potential NOOP Syscall Detected: %d", syscall.Number) + severity = analyzer.IssueSeverityWarning + } + if !withTrace { + source.CallStack = nil + } + issues = append(issues, analyzer.NewIssue( + analyzer.WithImpact(potentialImpactMsg), + analyzer.WithReference(analyzerWorkingPrincipalURL), + analyzer.WithCallStack(source), + analyzer.WithSeverity(severity), + analyzer.WithMessage(message), + )) } - if !withTrace { - source.CallStack = nil - } - - issues = append(issues, analyzer.NewIssue( - analyzer.WithImpact(potentialImpactMsg), - analyzer.WithReference(analyzerWorkingPrincipalURL), - analyzer.WithCallStack(source), - analyzer.WithSeverity(severity), - analyzer.WithMessage(message), - )) } } } diff --git a/common/entrypoint.go b/common/entrypoint.go index 4d7d116..8d04672 100644 --- a/common/entrypoint.go +++ b/common/entrypoint.go @@ -15,14 +15,30 @@ func ProgramEntrypoint(arch string) func(function string) bool { function == "runtime.mstart" || strings.Contains(function, "main.main") || // main and closures or anonymous functions strings.Contains(function, ".init.") || // all init functions - strings.HasSuffix(function, ".init") // vars + strings.HasSuffix(function, ".init") || // vars + // Bellow functions though are not the starting point, but those have a lot of trigger points + // and for a sample go program is expected to be called. So, to reduce stack trace, it's fine to make them as entry points + function == "runtime.gcStart" || + function == "runtime.mallocgc" || + function == "runtime.morestack" || + function == "runtime.systemstack" || + function == "runtime.gopanic" || + function == "runtime.chanrecv" } case "mips64": return func(function string) bool { - return function == "runtime.rt0_go" || // start point of a go program - strings.Contains(function, "main.main") || // main and closures or anonymous functions + return strings.Contains(function, "main.main") || // main and closures or anonymous functions strings.Contains(function, ".init.") || // all init functions - strings.HasSuffix(function, ".init") // vars + strings.HasSuffix(function, ".init") || // vars + function == "runtime.rt0_go" || // start point of any go program + // Bellow functions though are not the starting point, but those have a lot of trigger points + // and for a sample go program is expected to be called. So, to reduce stack trace, it's fine to make them as entry points + function == "runtime.gcStart" || + function == "runtime.mallocgc" || + function == "runtime.morestack" || + function == "runtime.systemstack" || + function == "runtime.gopanic" || + function == "runtime.chanrecv" } } return func(function string) bool { diff --git a/common/stack_tracer.go b/common/stack_tracer.go index 1239dac..d96b4c1 100644 --- a/common/stack_tracer.go +++ b/common/stack_tracer.go @@ -2,11 +2,18 @@ package common import ( "fmt" + "os/exec" "path/filepath" "slices" + "strings" "github.com/ChainSafe/vm-compat/analyzer" "github.com/ChainSafe/vm-compat/asmparser" + "github.com/ChainSafe/vm-compat/common/lifo" +) + +var ( + stdPkgs, _ = getStandardPackages() ) // TraceAsmCaller correctly tracks function calls in the execution stack. @@ -60,6 +67,113 @@ func TraceAsmCaller( return src, nil } +// TraceAllAsmCaller correctly tracks all possible function calls in the execution stack. +func TraceAllAsmCaller( + filePath string, + graph asmparser.CallGraph, + function string, + endCond func(string) bool, +) ([]*analyzer.CallStack, error) { + var segment asmparser.Segment + for _, seg := range graph.Segments() { + if seg.Label() == function { + segment = seg + break + } + } + if segment == nil { + return nil, fmt.Errorf("could not find %s in %s", function, filePath) + } + sources := make([]*lifo.Stack[asmparser.Segment], 0) + currentStack := lifo.Stack[asmparser.Segment]{} + seen := make(map[asmparser.Segment]bool) + + var visit func(segment asmparser.Segment) + + visit = func(segment asmparser.Segment) { + if seen[segment] { + return + } + seen[segment] = true + currentStack.Push(segment) + + parents := graph.ParentsOf(segment) + + if endCond(segment.Label()) { + sources = append(sources, currentStack.Copy()) + } else { + // sort the parents for consistent output + slices.SortFunc(parents, func(a, b asmparser.Segment) int { + if a.Address() > b.Address() { + return 1 + } else if a.Address() < b.Address() { + return -1 + } + return 0 + }) + + for _, seg := range parents { + visit(seg) + } + } + + currentStack.Pop() + + // We don't want to revisit the sdk packages as the number of paths can be up to billions. + pkg := strings.Split(segment.Label(), ".")[0] + if !stdPkgs[pkg] { + seen[segment] = false + } + } + + visit(segment) + + if len(sources) == 0 { + return nil, fmt.Errorf("no trace found to root for the given function") + } + // build call stacks from the sources + traces := make([]*analyzer.CallStack, len(sources)) + for i, source := range sources { + var callStack *analyzer.CallStack + for !source.IsEmpty() { + if seg, ok := source.Pop(); ok { + stack := &analyzer.CallStack{ + File: filepath.Base(filePath), + Line: seg.Instructions()[0].Line() - 1, // function start line + AbsPath: filePath, + Function: seg.Label(), + } + if callStack == nil { + callStack = stack + } else { + stack.CallStack = callStack + callStack = stack + } + } + } + traces[i] = callStack + } + + return traces, nil +} + +// getStandardPackages fetches all standard library packages and stores them in a map for fast lookup. +func getStandardPackages() (map[string]bool, error) { + cmd := exec.Command("go", "list", "std") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + stdPackages := make(map[string]bool) + for _, pkg := range strings.Split(string(output), "\n") { + if pkg != "" { + stdPackages[pkg] = true + } + } + return stdPackages, nil +} + func ShouldIgnoreSource(callStack *analyzer.CallStack, functions []string) bool { if callStack != nil { if slices.Contains(functions, callStack.Function) { diff --git a/profile/cannon/cannon-multithreaded-32.yaml b/profile/cannon/cannon-multithreaded-32.yaml index 50bd4b3..6635ea1 100644 --- a/profile/cannon/cannon-multithreaded-32.yaml +++ b/profile/cannon/cannon-multithreaded-32.yaml @@ -10,6 +10,8 @@ ignored_functions: - 'runtime.rtsigprocmask' - 'runtime.munmap' - 'runtime.exit' + - 'runtime.sysFaultOS' + - 'runtime.netpollinit' allowed_opcodes: - opcode: '0x2' funct: [] diff --git a/profile/cannon/cannon-multithreaded-64.yaml b/profile/cannon/cannon-multithreaded-64.yaml index bb7defe..ba6d172 100644 --- a/profile/cannon/cannon-multithreaded-64.yaml +++ b/profile/cannon/cannon-multithreaded-64.yaml @@ -5,6 +5,8 @@ ignored_functions: - 'syscall.setrlimit' - 'runtime.morestack' - 'runtime.abort' + - 'runtime.sysFaultOS' + - 'runtime.netpollinit' allowed_opcodes: - opcode: '0x2' diff --git a/profile/cannon/cannon-singlethreaded-32.yaml b/profile/cannon/cannon-singlethreaded-32.yaml index 09bcd34..6a41508 100644 --- a/profile/cannon/cannon-singlethreaded-32.yaml +++ b/profile/cannon/cannon-singlethreaded-32.yaml @@ -24,6 +24,8 @@ ignored_functions: - 'github.com/prometheus/client_model/go.init.1' - 'flag.init' - 'runtime.check' + - 'runtime.sysFaultOS' + - 'runtime.netpollinit' allowed_opcodes: - opcode: '0x2' funct: [] diff --git a/renderer/text.go b/renderer/text.go index 938c173..4174216 100644 --- a/renderer/text.go +++ b/renderer/text.go @@ -82,10 +82,16 @@ func (r *TextRenderer) Render(issues []*analyzer.Issue, output io.Writer) error if len(groupedIssue[0].Reference) > 0 { report.WriteString(fmt.Sprintf(" - Referance: %s \n", groupedIssue[0].Reference)) } - report.WriteString(" - CallStack:") + report.WriteString(" - CallStacks:") - for _, issue := range groupedIssue { + length := len(groupedIssue) + for i, issue := range groupedIssue { report.WriteString(fmt.Sprintf("%s\n", buildCallStack(output, issue.CallStack, ""))) + // Don't spam the text output + if i > 1 { + report.WriteString(fmt.Sprintf("\n ..... and %d more \n", length-i)) + break + } } issueCounter++ }