From b3914bcd337011e4bf36653902ad14d770c7b341 Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Fri, 7 Mar 2025 19:39:48 +0600 Subject: [PATCH 1/6] implemented TraceAllAsmCaller --- analyzer/opcode/opcode.go | 29 ++++++------ analyzer/syscall/asm_syscall.go | 41 ++++++++--------- common/stack_tracer.go | 78 +++++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 33 deletions(-) diff --git a/analyzer/opcode/opcode.go b/analyzer/opcode/opcode.go index 8e23166..7c46104 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,19 +45,22 @@ 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()), + for _, source := range sources { + issue := &analyzer.Issue{ + Severity: analyzer.IssueSeverityCritical, + CallStack: source, + Message: 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 + } + if !withTrace { + source.CallStack = nil + } + issues = append(issues, issue) } - if common.ShouldIgnoreSource(source, op.profile.IgnoredFunctions) { - issue.Severity = analyzer.IssueSeverityWarning - } - if !withTrace { - source.CallStack = nil - } - issues = append(issues, issue) + } } } diff --git a/analyzer/syscall/asm_syscall.go b/analyzer/syscall/asm_syscall.go index e6d61dc..d65fdca 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,26 +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 - } - if !withTrace { - source.CallStack = nil + 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.Issue{ + Severity: severity, + Message: message, + CallStack: source, + Impact: potentialImpactMsg, + Reference: analyzerWorkingPrincipalURL, + }) } - issues = append(issues, &analyzer.Issue{ - Severity: severity, - Message: message, - CallStack: source, - Impact: potentialImpactMsg, - Reference: analyzerWorkingPrincipalURL, - }) } } } diff --git a/common/stack_tracer.go b/common/stack_tracer.go index 1239dac..bb7918d 100644 --- a/common/stack_tracer.go +++ b/common/stack_tracer.go @@ -7,6 +7,7 @@ import ( "github.com/ChainSafe/vm-compat/analyzer" "github.com/ChainSafe/vm-compat/asmparser" + "github.com/ChainSafe/vm-compat/common/lifo" ) // TraceAsmCaller correctly tracks function calls in the execution stack. @@ -60,6 +61,83 @@ 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) + + if len(sources) >= 10 { + return + } + + if endCond(segment.Label()) { + sources = append(sources, currentStack.Copy()) + } else { + for _, seg := range graph.ParentsOf(segment) { + visit(seg) + } + } + + currentStack.Pop() + 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 +} + func ShouldIgnoreSource(callStack *analyzer.CallStack, functions []string) bool { if callStack != nil { if slices.Contains(functions, callStack.Function) { From e06afc7e7f0b581e9cd49547b2878140c93039ae Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Fri, 7 Mar 2025 19:54:37 +0600 Subject: [PATCH 2/6] lint fixed --- analyzer/opcode/opcode.go | 1 - 1 file changed, 1 deletion(-) diff --git a/analyzer/opcode/opcode.go b/analyzer/opcode/opcode.go index 7c46104..25f9705 100644 --- a/analyzer/opcode/opcode.go +++ b/analyzer/opcode/opcode.go @@ -60,7 +60,6 @@ func (op *opcode) Analyze(path string, withTrace bool) ([]*analyzer.Issue, error } issues = append(issues, issue) } - } } } From 4269ccb47badc5a4a61a448a055edad57425a917 Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Mon, 10 Mar 2025 14:49:05 +0600 Subject: [PATCH 3/6] fix: ignored path variation on internal package --- common/stack_tracer.go | 24 ++++++++++++++++++------ renderer/text.go | 10 ++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/common/stack_tracer.go b/common/stack_tracer.go index bb7918d..84e0672 100644 --- a/common/stack_tracer.go +++ b/common/stack_tracer.go @@ -4,6 +4,7 @@ import ( "fmt" "path/filepath" "slices" + "strings" "github.com/ChainSafe/vm-compat/analyzer" "github.com/ChainSafe/vm-compat/asmparser" @@ -91,20 +92,31 @@ func TraceAllAsmCaller( seen[segment] = true currentStack.Push(segment) - if len(sources) >= 10 { - return - } - if endCond(segment.Label()) { sources = append(sources, currentStack.Copy()) } else { - for _, seg := range graph.ParentsOf(segment) { + parents := graph.ParentsOf(segment) + // 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() - seen[segment] = false + // We don't want to revisit the runtime and internal packages as the number of paths can be up to billions. + if !strings.Contains(segment.Label(), "runtime") && + strings.Contains(segment.Label(), "internal") { + seen[segment] = false + } } visit(segment) 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++ } From e7add32ba3b1ef066c149dfc9e7eda4b7d889a27 Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Mon, 10 Mar 2025 15:06:03 +0600 Subject: [PATCH 4/6] fix: profile updated --- common/stack_tracer.go | 5 +++-- profile/cannon/cannon-multithreaded-32.yaml | 1 + profile/cannon/cannon-multithreaded-64.yaml | 1 + profile/cannon/cannon-singlethreaded-32.yaml | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/stack_tracer.go b/common/stack_tracer.go index 84e0672..84c87d2 100644 --- a/common/stack_tracer.go +++ b/common/stack_tracer.go @@ -92,10 +92,11 @@ func TraceAllAsmCaller( seen[segment] = true currentStack.Push(segment) - if endCond(segment.Label()) { + parents := graph.ParentsOf(segment) + + if endCond(segment.Label()) && len(parents) == 0 { sources = append(sources, currentStack.Copy()) } else { - parents := graph.ParentsOf(segment) // sort the parents for consistent output slices.SortFunc(parents, func(a, b asmparser.Segment) int { if a.Address() > b.Address() { diff --git a/profile/cannon/cannon-multithreaded-32.yaml b/profile/cannon/cannon-multithreaded-32.yaml index 50bd4b3..591c102 100644 --- a/profile/cannon/cannon-multithreaded-32.yaml +++ b/profile/cannon/cannon-multithreaded-32.yaml @@ -10,6 +10,7 @@ ignored_functions: - 'runtime.rtsigprocmask' - 'runtime.munmap' - 'runtime.exit' + - 'runtime.sysFaultOS' allowed_opcodes: - opcode: '0x2' funct: [] diff --git a/profile/cannon/cannon-multithreaded-64.yaml b/profile/cannon/cannon-multithreaded-64.yaml index bb7defe..381cead 100644 --- a/profile/cannon/cannon-multithreaded-64.yaml +++ b/profile/cannon/cannon-multithreaded-64.yaml @@ -5,6 +5,7 @@ ignored_functions: - 'syscall.setrlimit' - 'runtime.morestack' - 'runtime.abort' + - 'runtime.sysFaultOS' allowed_opcodes: - opcode: '0x2' diff --git a/profile/cannon/cannon-singlethreaded-32.yaml b/profile/cannon/cannon-singlethreaded-32.yaml index 09bcd34..8c1a073 100644 --- a/profile/cannon/cannon-singlethreaded-32.yaml +++ b/profile/cannon/cannon-singlethreaded-32.yaml @@ -24,6 +24,7 @@ ignored_functions: - 'github.com/prometheus/client_model/go.init.1' - 'flag.init' - 'runtime.check' + - 'runtime.sysFaultOS' allowed_opcodes: - opcode: '0x2' funct: [] From 5330ebbdcdcc0c468cb9b3a0a8bddcbf1ab0c3f1 Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Mon, 10 Mar 2025 19:39:26 +0600 Subject: [PATCH 5/6] fix: optimized entrypoint to generate optimal stack trace --- common/entrypoint.go | 24 ++++++++++++--- common/stack_tracer.go | 31 +++++++++++++++++--- profile/cannon/cannon-multithreaded-32.yaml | 1 + profile/cannon/cannon-multithreaded-64.yaml | 1 + profile/cannon/cannon-singlethreaded-32.yaml | 1 + 5 files changed, 50 insertions(+), 8 deletions(-) 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 84c87d2..d96b4c1 100644 --- a/common/stack_tracer.go +++ b/common/stack_tracer.go @@ -2,6 +2,7 @@ package common import ( "fmt" + "os/exec" "path/filepath" "slices" "strings" @@ -11,6 +12,10 @@ import ( "github.com/ChainSafe/vm-compat/common/lifo" ) +var ( + stdPkgs, _ = getStandardPackages() +) + // TraceAsmCaller correctly tracks function calls in the execution stack. func TraceAsmCaller( filePath string, @@ -94,7 +99,7 @@ func TraceAllAsmCaller( parents := graph.ParentsOf(segment) - if endCond(segment.Label()) && len(parents) == 0 { + if endCond(segment.Label()) { sources = append(sources, currentStack.Copy()) } else { // sort the parents for consistent output @@ -113,9 +118,10 @@ func TraceAllAsmCaller( } currentStack.Pop() - // We don't want to revisit the runtime and internal packages as the number of paths can be up to billions. - if !strings.Contains(segment.Label(), "runtime") && - strings.Contains(segment.Label(), "internal") { + + // 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 } } @@ -151,6 +157,23 @@ func TraceAllAsmCaller( 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 591c102..6635ea1 100644 --- a/profile/cannon/cannon-multithreaded-32.yaml +++ b/profile/cannon/cannon-multithreaded-32.yaml @@ -11,6 +11,7 @@ ignored_functions: - '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 381cead..ba6d172 100644 --- a/profile/cannon/cannon-multithreaded-64.yaml +++ b/profile/cannon/cannon-multithreaded-64.yaml @@ -6,6 +6,7 @@ ignored_functions: - '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 8c1a073..6a41508 100644 --- a/profile/cannon/cannon-singlethreaded-32.yaml +++ b/profile/cannon/cannon-singlethreaded-32.yaml @@ -25,6 +25,7 @@ ignored_functions: - 'flag.init' - 'runtime.check' - 'runtime.sysFaultOS' + - 'runtime.netpollinit' allowed_opcodes: - opcode: '0x2' funct: [] From a6ab0df58e2c7f7391b14038ec81cbefa4e56eee Mon Sep 17 00:00:00 2001 From: sadiq1971 Date: Mon, 10 Mar 2025 23:13:30 +0600 Subject: [PATCH 6/6] fix: entrypoint of mips 32 --- common/entrypoint.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/entrypoint.go b/common/entrypoint.go index 8d04672..c2ac005 100644 --- a/common/entrypoint.go +++ b/common/entrypoint.go @@ -23,7 +23,9 @@ func ProgramEntrypoint(arch string) func(function string) bool { function == "runtime.morestack" || function == "runtime.systemstack" || function == "runtime.gopanic" || - function == "runtime.chanrecv" + function == "runtime.chanrecv" || + function == "runtime.startm" || // 32 bit specific + function == "runtime.sysAlloc" // // 32 bit specific } case "mips64": return func(function string) bool {