aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/report/decompile.go
blob: 6134a1fd97abc8b8b70b8d257adbcf4228abeaa5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// Copyright 2021 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package report

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/google/syzkaller/pkg/osutil"
	"github.com/google/syzkaller/sys/targets"
)

type DecompilerFlagMask uint64

// Extra flags that control the flow of decompilation.
const (
	FlagForceArmThumbMode DecompilerFlagMask = 1 << iota
)

const objdumpCallTimeout = 10 * time.Second

type DecompiledOpcode struct {
	Offset          int
	IsBad           bool
	Instruction     string
	FullDescription string
}

// Decompiles a byte array with opcodes into human-readable descriptions.
// Target must specify the environment from which the opcodes were taken.
func DecompileOpcodes(rawOpcodes []byte, flags DecompilerFlagMask, target *targets.Target) ([]DecompiledOpcode, error) {
	args, err := objdumpBuildArgs(flags, target)
	if err != nil {
		return nil, err
	}

	outBytes, err := objdumpExecutor(rawOpcodes, args, target)
	if err != nil {
		return nil, err
	}

	list := objdumpParseOutput(outBytes)
	if len(list) == 0 && len(rawOpcodes) > 0 {
		return nil, fmt.Errorf("no instructions found while the total size is %v bytes", len(rawOpcodes))
	}
	return list, nil
}

func objdumpExecutor(rawOpcodes []byte, args []string, target *targets.Target) ([]byte, error) {
	fileName, err := osutil.TempFile("syz-opcode-decompiler")
	if err != nil {
		return nil, fmt.Errorf("failed to create temp file: %w", err)
	}
	defer os.Remove(fileName)

	err = osutil.WriteFile(fileName, rawOpcodes)
	if err != nil {
		return nil, fmt.Errorf("failed to write to temp file: %w", err)
	}

	return osutil.RunCmd(objdumpCallTimeout, "", target.Objdump, append(args, fileName)...)
}

// nolint: lll
var objdumpAsmLineRegexp = regexp.MustCompile(`\s+([a-fA-F0-9]+)\:\s+((?:[a-fA-F0-9]{2,8}\s*)*[a-fA-F0-9]{2,8})\s+(.*?)\s*$`)

func objdumpParseOutput(rawOutput []byte) []DecompiledOpcode {
	ret := []DecompiledOpcode{}
	for s := bufio.NewScanner(bytes.NewReader(rawOutput)); s.Scan(); {
		result := objdumpAsmLineRegexp.FindStringSubmatch(string(s.Bytes()))
		if result == nil {
			continue
		}
		offset, err := strconv.ParseUint(result[1], 16, 64)
		if err != nil {
			continue
		}
		const objdumpBadInstruction = "(bad)"
		ret = append(ret, DecompiledOpcode{
			Offset:          int(offset),
			IsBad:           result[3] == objdumpBadInstruction,
			Instruction:     result[3],
			FullDescription: strings.TrimRight(result[0], " \t"),
		})
	}
	return ret
}

func objdumpBuildArgs(flags DecompilerFlagMask, target *targets.Target) ([]string, error) {
	// objdump won't be able to decompile a raw binary file unless we specify the exact
	// architecture through the -m parameter.
	ret := []string{"-b", "binary", "-D"}
	switch target.Arch {
	case targets.ARM64:
		ret = append(ret, "-maarch64")
	case targets.ARM:
		ret = append(ret, "-marm")
		if flags&FlagForceArmThumbMode != 0 {
			ret = append(ret, "-M", "force-thumb")
		}
	case targets.I386:
		ret = append(ret, "-mi386")
	case targets.AMD64:
		ret = append(ret, "-mi386", "-Mx86-64")
	case targets.MIPS64LE:
		ret = append(ret, "-mmips")
	case targets.PPC64LE:
		ret = append(ret, "-mppc")
	case targets.S390x:
		ret = append(ret, "-m", "s390:64-bit")
	case targets.RiscV64:
		ret = append(ret, "-mriscv")
	default:
		return nil, fmt.Errorf("cannot build objdump args for %#v", target.Arch)
	}

	return ret, nil
}