// Copyright 2015 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 mgrconfig
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/google/syzkaller/pkg/config"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/prog"
_ "github.com/google/syzkaller/sys" // most mgrconfig users want targets too
"github.com/google/syzkaller/sys/targets"
)
type Config struct {
// Instance name (used for identification and as GCE instance prefix).
Name string `json:"name"`
// Target OS/arch, e.g. "linux/arm64" or "linux/amd64/386" (amd64 OS with 386 test process).
Target string `json:"target"`
// TCP address to serve HTTP stats page (e.g. "localhost:50000").
HTTP string `json:"http"`
// TCP address to serve RPC for fuzzer processes (optional).
RPC string `json:"rpc"`
Workdir string `json:"workdir"`
// Directory with kernel object files.
KernelObj string `json:"kernel_obj"`
// Kernel source directory (if not set defaults to KernelObj).
KernelSrc string `json:"kernel_src"`
// Arbitrary optional tag that is saved along with crash reports (e.g. branch/commit).
Tag string `json:"tag"`
// Linux image for VMs.
Image string `json:"image"`
// SSH key for the image (may be empty for some VM types).
SSHKey string `json:"sshkey"`
// SSH user ("root" by default).
SSHUser string `json:"ssh_user"`
HubClient string `json:"hub_client"`
HubAddr string `json:"hub_addr"`
HubKey string `json:"hub_key"`
// syz-manager will send crash emails to this list of emails using mailx (optional).
EmailAddrs []string `json:"email_addrs"`
DashboardClient string `json:"dashboard_client"`
DashboardAddr string `json:"dashboard_addr"`
DashboardKey string `json:"dashboard_key"`
// Path to syzkaller checkout (syz-manager will look for binaries in bin subdir).
Syzkaller string `json:"syzkaller"`
// Number of parallel processes inside of every VM.
Procs int `json:"procs"`
// Type of sandbox to use during fuzzing:
// "none": don't do anything special (has false positives, e.g. due to killing init), default
// "setuid": impersonate into user nobody (65534)
// "namespace": create a new namespace for fuzzer using CLONE_NEWNS/CLONE_NEWNET/CLONE_NEWPID/etc,
// requires building kernel with CONFIG_NAMESPACES, CONFIG_UTS_NS, CONFIG_USER_NS,
// CONFIG_PID_NS and CONFIG_NET_NS.
Sandbox string `json:"sandbox"`
// Use KCOV coverage (default: true).
Cover bool `json:"cover"`
// Reproduce, localize and minimize crashers (default: true).
Reproduce bool `json:"reproduce"`
EnabledSyscalls []string `json:"enable_syscalls"`
DisabledSyscalls []string `json:"disable_syscalls"`
// Don't save reports matching these regexps, but reboot VM after them,
// matched against whole report output.
Suppressions []string `json:"suppressions"`
// Completely ignore reports matching these regexps (don't save nor reboot),
// must match the first line of crash message.
Ignores []string `json:"ignores"`
// VM type (qemu, gce, android, isolated, etc).
Type string `json:"type"`
// VM-type-specific config.
VM json.RawMessage `json:"vm"`
// Implementation details beyond this point.
// Parsed Target:
TargetOS string `json:"-"`
TargetArch string `json:"-"`
TargetVMArch string `json:"-"`
// Syzkaller binaries that we are going to use:
SyzFuzzerBin string `json:"-"`
SyzExecprogBin string `json:"-"`
SyzExecutorBin string `json:"-"`
}
func LoadData(data []byte) (*Config, error) {
cfg, err := LoadPartialData(data)
if err != nil {
return nil, err
}
if err := Complete(cfg); err != nil {
return nil, err
}
return cfg, nil
}
func LoadFile(filename string) (*Config, error) {
cfg, err := LoadPartialFile(filename)
if err != nil {
return nil, err
}
if err := Complete(cfg); err != nil {
return nil, err
}
return cfg, nil
}
func LoadPartialData(data []byte) (*Config, error) {
cfg := defaultValues()
if err := config.LoadData(data, cfg); err != nil {
return nil, err
}
return loadPartial(cfg)
}
func LoadPartialFile(filename string) (*Config, error) {
cfg := defaultValues()
if err := config.LoadFile(filename, cfg); err != nil {
return nil, err
}
return loadPartial(cfg)
}
func defaultValues() *Config {
return &Config{
SSHUser: "root",
Cover: true,
Reproduce: true,
Sandbox: "none",
RPC: ":0",
Procs: 1,
}
}
func loadPartial(cfg *Config) (*Config, error) {
var err error
cfg.TargetOS, cfg.TargetVMArch, cfg.TargetArch, err = splitTarget(cfg.Target)
if err != nil {
return nil, err
}
return cfg, nil
}
func Complete(cfg *Config) error {
if cfg.TargetOS == "" || cfg.TargetVMArch == "" || cfg.TargetArch == "" {
return fmt.Errorf("target parameters are not filled in")
}
if cfg.Workdir == "" {
return fmt.Errorf("config param workdir is empty")
}
cfg.Workdir = osutil.Abs(cfg.Workdir)
if cfg.Syzkaller == "" {
return fmt.Errorf("config param syzkaller is empty")
}
if err := completeBinaries(cfg); err != nil {
return err
}
if cfg.HTTP == "" {
return fmt.Errorf("config param http is empty")
}
if cfg.Type == "" {
return fmt.Errorf("config param type is empty")
}
if cfg.Procs < 1 || cfg.Procs > 32 {
return fmt.Errorf("bad config param procs: '%v', want [1, 32]", cfg.Procs)
}
switch cfg.Sandbox {
case "none", "setuid", "namespace":
default:
return fmt.Errorf("config param sandbox must contain one of none/setuid/namespace")
}
if err := checkSSHParams(cfg); err != nil {
return err
}
cfg.KernelObj = osutil.Abs(cfg.KernelObj)
if cfg.KernelSrc == "" {
cfg.KernelSrc = cfg.KernelObj // assume in-tree build by default
}
cfg.KernelSrc = osutil.Abs(cfg.KernelSrc)
if cfg.HubClient != "" && (cfg.Name == "" || cfg.HubAddr == "" || cfg.HubKey == "") {
return fmt.Errorf("hub_client is set, but name/hub_addr/hub_key is empty")
}
if cfg.DashboardClient != "" && (cfg.Name == "" ||
cfg.DashboardAddr == "" ||
cfg.DashboardKey == "") {
return fmt.Errorf("dashboard_client is set, but name/dashboard_addr/dashboard_key is empty")
}
return nil
}
func checkSSHParams(cfg *Config) error {
if cfg.SSHUser == "" {
return fmt.Errorf("bad config syzkaller param: ssh user is empty")
}
if cfg.SSHKey == "" {
return nil
}
info, err := os.Stat(cfg.SSHKey)
if err != nil {
return err
}
if info.Mode()&0077 != 0 {
return fmt.Errorf("sshkey %v is unprotected, ssh will reject it, do chmod 0600", cfg.SSHKey)
}
return nil
}
func completeBinaries(cfg *Config) error {
sysTarget := targets.Get(cfg.TargetOS, cfg.TargetArch)
if sysTarget == nil {
return fmt.Errorf("unsupported OS/arch: %v/%v", cfg.TargetOS, cfg.TargetArch)
}
cfg.Syzkaller = osutil.Abs(cfg.Syzkaller)
exe := sysTarget.ExeExtension
targetBin := func(name, arch string) string {
return filepath.Join(cfg.Syzkaller, "bin", cfg.TargetOS+"_"+arch, name+exe)
}
cfg.SyzFuzzerBin = targetBin("syz-fuzzer", cfg.TargetVMArch)
cfg.SyzExecprogBin = targetBin("syz-execprog", cfg.TargetVMArch)
cfg.SyzExecutorBin = targetBin("syz-executor", cfg.TargetArch)
if !osutil.IsExist(cfg.SyzFuzzerBin) {
return fmt.Errorf("bad config syzkaller param: can't find %v", cfg.SyzFuzzerBin)
}
if !osutil.IsExist(cfg.SyzExecprogBin) {
return fmt.Errorf("bad config syzkaller param: can't find %v", cfg.SyzExecprogBin)
}
if !osutil.IsExist(cfg.SyzExecutorBin) {
return fmt.Errorf("bad config syzkaller param: can't find %v", cfg.SyzExecutorBin)
}
return nil
}
func splitTarget(target string) (string, string, string, error) {
if target == "" {
return "", "", "", fmt.Errorf("target is empty")
}
targetParts := strings.Split(target, "/")
if len(targetParts) != 2 && len(targetParts) != 3 {
return "", "", "", fmt.Errorf("bad config param target")
}
os := targetParts[0]
vmarch := targetParts[1]
arch := targetParts[1]
if len(targetParts) == 3 {
arch = targetParts[2]
}
return os, vmarch, arch, nil
}
func ParseEnabledSyscalls(target *prog.Target, enabled, disabled []string) (map[int]bool, error) {
syscalls := make(map[int]bool)
if len(enabled) != 0 {
for _, c := range enabled {
n := 0
for _, call := range target.Syscalls {
if matchSyscall(call.Name, c) {
syscalls[call.ID] = true
n++
}
}
if n == 0 {
return nil, fmt.Errorf("unknown enabled syscall: %v", c)
}
}
} else {
for _, call := range target.Syscalls {
syscalls[call.ID] = true
}
}
for _, c := range disabled {
n := 0
for _, call := range target.Syscalls {
if matchSyscall(call.Name, c) {
delete(syscalls, call.ID)
n++
}
}
if n == 0 {
return nil, fmt.Errorf("unknown disabled syscall: %v", c)
}
}
if len(syscalls) == 0 {
return nil, fmt.Errorf("all syscalls are disabled by disable_syscalls in config")
}
return syscalls, nil
}
func matchSyscall(name, pattern string) bool {
if pattern == name || strings.HasPrefix(name, pattern+"$") {
return true
}
if len(pattern) > 1 && pattern[len(pattern)-1] == '*' &&
strings.HasPrefix(name, pattern[:len(pattern)-1]) {
return true
}
return false
}