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
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
GOLANGCI := $(GOPATH)/bin/golangci-lint

.PHONY: analyser
analyser:
go build -o ./bin/analyser ./cmd/analyser/main.go
.PHONY: analyzer
analyzer:
go build -o ./bin/analyzer ./main.go

.PHONY: get
get:
Expand Down
90 changes: 77 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ Even if `condition2` is never met during runtime, VM Compat will still detect `d
This ensures that all possible execution paths are analyzed,
making the tool effective in identifying compatibility concerns proactively.

## Prerequisites

VM Compat requires `llvm-objdump` to be installed for disassembly. Install it using the following commands:

### Linux (Ubuntu/Debian)
```sh
sudo apt update && sudo apt install llvm
```

### macOS
```sh
brew install llvm
```

Ensure that `llvm-objdump` is accessible in your system's `PATH`. If necessary, update the `PATH` variable:

```sh
export PATH="$(brew --prefix llvm)/bin:$PATH"
```

## Installation

To install VM Compat, clone the repository and build the binary:
Expand All @@ -54,25 +74,70 @@ cd vm-compat
make analyser
```

## CLI Flags
## Usage

VM Compat provides several command-line flags to control its behavior:
### General CLI Usage

| Flag | Description | Default |
| -------------------------- | ---------------------------------------------------------------------- |-----------------------------------|
| `-vm-profile` | Path to the VM profile config file. | `./profile/cannon/cannon-64.yaml` |
| `-analyzer` | Type of analysis to perform. Options: `opcode`, `syscall` | analyzes both by default |
| `-disassembly-output-path` | File path to store the disassembled opcode assembly code. | None |
| `-format` | Output format. Options: `json`, `text` | `text` |
| `-report-output-path` | Path to store the analysis report. If not provided, outputs to stdout. | Stdout |
```sh
./bin/analyzer [global options] command [command options]
```

## Usage
### Commands

- `analyze`: Checks the program compatibility against the VM profile.
- `trace`: Generates a stack trace for a given function.
- `help, h`: Shows a list of commands or help for one command.

Run VM Compat with the Go source file you want to analyze:
### Command-Specific Usage

#### Analyze Command

```sh
./bin/analyser -analyzer=opcode -format=text -disassembly-output-path=sample.asm -vm-profile=./profile/cannon/cannon-64.yaml ./examples/sample.go
./bin/analyzer analyze [command options]
```

#### Analyze Options

| Option | Description | Default |
|---------------------------------|--------------------------------------------------------------------|---------|
| `--vm-profile value` | Path to the VM profile config file (required). | None |
| `--analysis-type value` | Type of analysis to perform. Options: `opcode`, `syscall`. | All |
| `--disassembly-output-path` | File path to store the disassembled assembly code. | None |
| `--format value` | Output format. Options: `json`, `text`. | `text` |
| `--report-output-path value` | Output file path for report. Default: stdout. | None |
| `--with-trace` | Enable full stack trace output. | `false` |
| `--help, -h` | Show help. | None |

#### Trace Command

```sh
./bin/analyzer trace [command options]
```

#### Trace Options

| Option | Description | Default |
|-----------------------|----------------------------------------------------------------------------------------|---------|
| `--vm-profile value` | Path to the VM profile config file (required). | None |
| `--function value` | Name of the function to trace. Include package name (e.g., `syscall.read`). (required) | None |
| `--help, -h` | Show help. | None |

## Example Usage

### Running an Analysis

```sh
./bin/analyzer analyze --with-trace=true --format=text --analysis-type=syscall --disassembly-output-path=sample.asm --vm-profile ./profile/cannon/cannon-64.yaml ./examples/sample.go

```

### Running a Trace

```sh
./bin/analyzer trace --vm-profile=./profile/cannon/cannon-64.yaml --function=syscall.read

````

To create vm specific profile, follow [this](./profile/readme.md)

## Example Output
Expand Down Expand Up @@ -102,6 +167,5 @@ To create vm specific profile, follow [this](./profile/readme.md)
1. [CRITICAL] Incompatible Syscall Detected: 5006
- Sources:
-> zsyscall_linux_mips64.go:1677 : (syscall.lstat)
-> syscall_linux_mips64x.go:154 : (syscall.Lstat)
...
```
15 changes: 9 additions & 6 deletions analyser/analyser.go → analyzer/analyzer.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
// Package analyser provides an interface for analyzing source code for compatibility issues.
package analyser
// Package analyzer provides an interface for analyzing source code for compatibility issues.
package analyzer

// Analyzer represents the interface for the analyzer.
type Analyzer interface {
// Analyze analyzes the provided source code and returns any issues found.
// TODO: better to update the code to take a reader interface instead of path
Analyze(path string) ([]*Issue, error)
Analyze(path string, withTrace bool) ([]*Issue, error)

// TraceStack generates callstack for a function to debug
TraceStack(path string, function string) (*IssueSource, error)
}

// IssueSeverity represents the severity level of an issue.
Expand All @@ -18,9 +21,9 @@ const (

// Issue represents a single issue found by the analyzer.
type Issue struct {
Sources []*IssueSource `json:"sources"`
Message string `json:"message"` // A description of the issue.
Severity IssueSeverity `json:"severity"`
Sources *IssueSource `json:"sources"`
Message string `json:"message"` // A description of the issue.
Severity IssueSeverity `json:"severity"`
}

// IssueSource represents a location in the code where the issue originates.
Expand Down
28 changes: 20 additions & 8 deletions analyser/opcode/opcode.go → analyzer/opcode/opcode.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package opcode implements analyser.Analyzer for detecting incompatible opcodes.
// Package opcode implements analyzer.Analyzer for detecting incompatible opcodes.
package opcode

import (
Expand All @@ -7,7 +7,7 @@ import (
"slices"
"strings"

"github.com/ChainSafe/vm-compat/analyser"
"github.com/ChainSafe/vm-compat/analyzer"
"github.com/ChainSafe/vm-compat/asmparser"
"github.com/ChainSafe/vm-compat/asmparser/mips"
"github.com/ChainSafe/vm-compat/common"
Expand All @@ -18,11 +18,11 @@ type opcode struct {
profile *profile.VMProfile
}

func NewAnalyser(profile *profile.VMProfile) analyser.Analyzer {
func NewAnalyser(profile *profile.VMProfile) analyzer.Analyzer {
return &opcode{profile: profile}
}

func (op *opcode) Analyze(path string) ([]*analyser.Issue, error) {
func (op *opcode) Analyze(path string, withTrace bool) ([]*analyzer.Issue, error) {
var err error
var callGraph asmparser.CallGraph

Expand All @@ -39,13 +39,20 @@ func (op *opcode) Analyze(path string) ([]*analyser.Issue, error) {
if err != nil {
return nil, err
}
issues := make([]*analyser.Issue, 0)
issues := make([]*analyzer.Issue, 0)
for _, segment := range callGraph.Segments() {
for _, instruction := range segment.Instructions() {
if !op.isAllowedOpcode(instruction.OpcodeHex(), instruction.Funct()) {
issues = append(issues, &analyser.Issue{
Severity: analyser.IssueSeverityCritical,
Sources: common.TraceAsmCaller(absPath, instruction, segment, callGraph, make([]*analyser.IssueSource, 0), 0),
source, err := common.TraceAsmCaller(absPath, callGraph, segment.Label())
if err != nil { // non-reachable portion ignored
continue
}
if !withTrace {
source.CallStack = nil
}
issues = append(issues, &analyzer.Issue{
Severity: analyzer.IssueSeverityCritical,
Sources: source,
Message: fmt.Sprintf("Incompatible Opcode Detected: Opcode: %s, Funct: %s",
instruction.OpcodeHex(), instruction.Funct()),
})
Expand All @@ -55,6 +62,11 @@ func (op *opcode) Analyze(path string) ([]*analyser.Issue, error) {
return issues, nil
}

// TraceStack generates callstack for a function to debug
func (op *opcode) TraceStack(path string, function string) (*analyzer.IssueSource, error) {
return nil, fmt.Errorf("stack trace is not supported for assembly code")
}

func (op *opcode) isAllowedOpcode(opcode, funct string) bool {
return slices.ContainsFunc(op.profile.AllowedOpcodes, func(instr profile.OpcodeInstruction) bool {
if !strings.EqualFold(instr.Opcode, opcode) {
Expand Down
33 changes: 23 additions & 10 deletions analyser/syscall/asm_syscall.go → analyzer/syscall/asm_syscall.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"path/filepath"
"slices"

"github.com/ChainSafe/vm-compat/analyser"
"github.com/ChainSafe/vm-compat/analyzer"
"github.com/ChainSafe/vm-compat/asmparser"
"github.com/ChainSafe/vm-compat/asmparser/mips"
"github.com/ChainSafe/vm-compat/common"
Expand All @@ -21,14 +21,14 @@ type asmSyscallAnalyser struct {
}

// NewAssemblySyscallAnalyser initializes an analyser for assembly syscalls.
func NewAssemblySyscallAnalyser(profile *profile.VMProfile) analyser.Analyzer {
func NewAssemblySyscallAnalyser(profile *profile.VMProfile) analyzer.Analyzer {
return &asmSyscallAnalyser{profile: profile}
}

// Analyze scans an assembly file for syscalls and detects compatibility issues.
//
//nolint:cyclop
func (a *asmSyscallAnalyser) Analyze(path string) ([]*analyser.Issue, error) {
func (a *asmSyscallAnalyser) Analyze(path string, withTrace bool) ([]*analyzer.Issue, error) {
var (
err error
callGraph asmparser.CallGraph
Expand All @@ -45,7 +45,7 @@ func (a *asmSyscallAnalyser) Analyze(path string) ([]*analyser.Issue, error) {
return nil, fmt.Errorf("error parsing assembly file: %w", err)
}

issues := make([]*analyser.Issue, 0)
issues := make([]*analyzer.Issue, 0)

absPath, err := filepath.Abs(path)
if err != nil {
Expand All @@ -72,20 +72,33 @@ func (a *asmSyscallAnalyser) Analyze(path string) ([]*analyser.Issue, error) {
if slices.Contains(a.profile.AllowedSycalls, syscallNum) {
continue
}
// Better to develop a new algo to check all segments at once like go_syscall
source, err := common.TraceAsmCaller(absPath, callGraph, segment.Label())
if err != nil { // non-reachable portion ignored
continue
}
if !withTrace {
source.CallStack = nil
}

severity := analyser.IssueSeverityCritical
message := fmt.Sprintf("Incompatible Syscall Detected: %d", syscallNum)
severity := analyzer.IssueSeverityCritical
message := fmt.Sprintf("Potential Incompatible Syscall Detected: %d", syscallNum)
if slices.Contains(a.profile.NOOPSyscalls, syscallNum) {
message = fmt.Sprintf("NOOP Syscall Detected: %d", syscallNum)
severity = analyser.IssueSeverityWarning
message = fmt.Sprintf("Potential NOOP Syscall Detected: %d", syscallNum)
severity = analyzer.IssueSeverityWarning
}

issues = append(issues, &analyser.Issue{
issues = append(issues, &analyzer.Issue{
Severity: severity,
Message: message,
Sources: common.TraceAsmCaller(absPath, instruction, segment, callGraph, make([]*analyser.IssueSource, 0), 0),
Sources: source,
})
}
}
return issues, nil
}

// TraceStack generates callstack for a function to debug
func (a *asmSyscallAnalyser) TraceStack(path string, function string) (*analyzer.IssueSource, error) {
return nil, fmt.Errorf("stack trace is not supported for assembly code")
}
Loading