syzkaller/syz-fuzzer/testing.go
Zach Riggle 0eca949a6c RFC: android: Add support for untrusted_app sandboxing (#697)
executor: add support for android_untrusted_app sandbox

This adds a new sandbox type, 'android_untrusted_app', which restricts
syz-executor to the privileges which are available to third-party applications,
e.g. those installed from the Google Play store.

In particular, this uses the UID space reserved for applications (instead of
the 'setuid' sandbox, which uses the traditional 'nobody' user / 65534)
as well as a set of groups which the Android-specific kernels are aware of,
and finally ensures that the SELinux context is set appropriately.

Dependencies on libselinux are avoided by manually implementing the few
functions that are needed to change the context of the current process,
and arbitrary files.  The underlying mechanisms are relatively simple.

Fixes google/syzkaller#643

Test: make presubmit
Bug: http://b/112900774
2018-09-17 11:33:11 +02:00

303 lines
8.7 KiB
Go

// Copyright 2017 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 main
import (
"fmt"
"io/ioutil"
"strings"
"time"
"github.com/google/syzkaller/pkg/host"
"github.com/google/syzkaller/pkg/ipc"
"github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/osutil"
"github.com/google/syzkaller/pkg/rpctype"
"github.com/google/syzkaller/pkg/runtest"
"github.com/google/syzkaller/prog"
"github.com/google/syzkaller/sys"
)
type checkArgs struct {
target *prog.Target
sandbox string
gitRevision string
targetRevision string
enabledCalls []int
allSandboxes bool
ipcConfig *ipc.Config
ipcExecOpts *ipc.ExecOpts
}
func testImage(hostAddr string, args *checkArgs) {
log.Logf(0, "connecting to host at %v", hostAddr)
conn, err := rpctype.Dial(hostAddr)
if err != nil {
log.Fatalf("failed to connect: %v", err)
}
conn.Close()
if _, err := checkMachine(args); err != nil {
log.Fatalf("%v", err)
}
}
func runTest(target *prog.Target, manager *rpctype.RPCClient, name, executor string) {
pollReq := &rpctype.RunTestPollReq{Name: name}
for {
req := new(rpctype.RunTestPollRes)
if err := manager.Call("Manager.Poll", pollReq, req); err != nil {
log.Fatalf("Manager.Poll call failed: %v", err)
}
if len(req.Bin) == 0 && len(req.Prog) == 0 {
return
}
test := convertTestReq(target, req)
if test.Err == nil {
runtest.RunTest(test, executor)
}
reply := &rpctype.RunTestDoneArgs{
Name: name,
ID: req.ID,
Output: test.Output,
Info: test.Info,
}
if test.Err != nil {
reply.Error = test.Err.Error()
}
if err := manager.Call("Manager.Done", reply, nil); err != nil {
log.Fatalf("Manager.Done call failed: %v", err)
}
}
}
func convertTestReq(target *prog.Target, req *rpctype.RunTestPollRes) *runtest.RunRequest {
test := &runtest.RunRequest{
Cfg: req.Cfg,
Opts: req.Opts,
Repeat: req.Repeat,
}
if len(req.Bin) != 0 {
bin, err := osutil.TempFile("syz-runtest")
if err != nil {
test.Err = err
return test
}
if err := osutil.WriteExecFile(bin, req.Bin); err != nil {
test.Err = err
return test
}
test.Bin = bin
}
if len(req.Prog) != 0 {
p, err := target.Deserialize(req.Prog)
if err != nil {
test.Err = err
return test
}
test.P = p
}
return test
}
func checkMachine(args *checkArgs) (*rpctype.CheckArgs, error) {
// Machine checking can be very slow on some machines (qemu without kvm, KMEMLEAK linux, etc),
// so print periodic heartbeats for vm.MonitorExecution so that it does not decide that we are dead.
done := make(chan bool)
defer close(done)
go func() {
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
fmt.Printf("executing program\n")
}
}
}()
if err := checkRevisions(args); err != nil {
return nil, err
}
features, err := host.Check(args.target)
if err != nil {
return nil, err
}
if feat := features[host.FeatureCoverage]; !feat.Enabled &&
args.ipcConfig.Flags&ipc.FlagSignal != 0 {
return nil, fmt.Errorf("coverage is not supported (%v)", feat.Reason)
}
if feat := features[host.FeatureSandboxSetuid]; !feat.Enabled &&
args.ipcConfig.Flags&ipc.FlagSandboxSetuid != 0 {
return nil, fmt.Errorf("sandbox=setuid is not supported (%v)", feat.Reason)
}
if feat := features[host.FeatureSandboxNamespace]; !feat.Enabled &&
args.ipcConfig.Flags&ipc.FlagSandboxNamespace != 0 {
return nil, fmt.Errorf("sandbox=namespace is not supported (%v)", feat.Reason)
}
if feat := features[host.FeatureSandboxAndroidUntrustedApp]; !feat.Enabled &&
args.ipcConfig.Flags&ipc.FlagSandboxAndroidUntrustedApp != 0 {
return nil, fmt.Errorf("sandbox=android_untrusted_app is not supported (%v)", feat.Reason)
}
if err := checkSimpleProgram(args); err != nil {
return nil, err
}
res := &rpctype.CheckArgs{
Features: features,
EnabledCalls: make(map[string][]int),
DisabledCalls: make(map[string][]rpctype.SyscallReason),
}
sandboxes := []string{args.sandbox}
if args.allSandboxes {
if features[host.FeatureSandboxSetuid].Enabled {
sandboxes = append(sandboxes, "setuid")
}
if features[host.FeatureSandboxNamespace].Enabled {
sandboxes = append(sandboxes, "namespace")
}
}
for _, sandbox := range sandboxes {
enabledCalls, disabledCalls, err := buildCallList(args.target, args.enabledCalls, sandbox)
if err != nil {
return nil, err
}
res.EnabledCalls[sandbox] = enabledCalls
res.DisabledCalls[sandbox] = disabledCalls
}
if args.allSandboxes {
var enabled []int
for _, id := range res.EnabledCalls["none"] {
switch args.target.Syscalls[id].Name {
default:
enabled = append(enabled, id)
case "syz_emit_ethernet", "syz_extract_tcp_res":
// Tun is not setup without sandbox, this is a hacky way to workaround this.
}
}
res.EnabledCalls[""] = enabled
}
return res, nil
}
func checkRevisions(args *checkArgs) error {
log.Logf(0, "checking revisions...")
executorArgs := strings.Split(args.ipcConfig.Executor, " ")
executorArgs = append(executorArgs, "version")
cmd := osutil.Command(executorArgs[0], executorArgs[1:]...)
cmd.Stderr = ioutil.Discard
if _, err := cmd.StdinPipe(); err != nil { // for the case executor is wrapped with ssh
return err
}
out, err := osutil.Run(time.Minute, cmd)
if err != nil {
return fmt.Errorf("failed to run executor version: %v", err)
}
vers := strings.Split(strings.TrimSpace(string(out)), " ")
if len(vers) != 4 {
return fmt.Errorf("executor version returned bad result: %q", string(out))
}
if args.target.Arch != vers[1] {
return fmt.Errorf("mismatching target/executor arches: %v vs %v", args.target.Arch, vers[1])
}
if sys.GitRevision != vers[3] {
return fmt.Errorf("mismatching fuzzer/executor git revisions: %v vs %v",
sys.GitRevision, vers[3])
}
if args.gitRevision != "" && args.gitRevision != sys.GitRevision {
return fmt.Errorf("mismatching manager/fuzzer git revisions: %v vs %v",
args.gitRevision, sys.GitRevision)
}
if args.target.Revision != vers[2] {
return fmt.Errorf("mismatching fuzzer/executor system call descriptions: %v vs %v",
args.target.Revision, vers[2])
}
if args.targetRevision != "" && args.targetRevision != args.target.Revision {
return fmt.Errorf("mismatching manager/fuzzer system call descriptions: %v vs %v",
args.targetRevision, args.target.Revision)
}
return nil
}
func checkSimpleProgram(args *checkArgs) error {
log.Logf(0, "testing simple program...")
env, err := ipc.MakeEnv(args.ipcConfig, 0)
if err != nil {
return fmt.Errorf("failed to create ipc env: %v", err)
}
defer env.Close()
p := args.target.GenerateSimpleProg()
output, info, failed, hanged, err := env.Exec(args.ipcExecOpts, p)
if err != nil {
return fmt.Errorf("program execution failed: %v\n%s", err, output)
}
if hanged {
return fmt.Errorf("program hanged:\n%s", output)
}
if failed {
return fmt.Errorf("program failed:\n%s", output)
}
if len(info) == 0 {
return fmt.Errorf("no calls executed:\n%s", output)
}
if info[0].Errno != 0 {
return fmt.Errorf("simple call failed: %+v\n%s", info[0], output)
}
if args.ipcConfig.Flags&ipc.FlagSignal != 0 && len(info[0].Signal) < 2 {
return fmt.Errorf("got no coverage:\n%s", output)
}
if len(info[0].Signal) < 1 {
return fmt.Errorf("got no fallback coverage:\n%s", output)
}
return nil
}
func buildCallList(target *prog.Target, enabledCalls []int, sandbox string) (
enabled []int, disabled []rpctype.SyscallReason, err error) {
calls := make(map[*prog.Syscall]bool)
if len(enabledCalls) != 0 {
for _, n := range enabledCalls {
if n >= len(target.Syscalls) {
return nil, nil, fmt.Errorf("unknown enabled syscall %v", n)
}
calls[target.Syscalls[n]] = true
}
} else {
for _, c := range target.Syscalls {
calls[c] = true
}
}
_, unsupported, err := host.DetectSupportedSyscalls(target, sandbox)
if err != nil {
return nil, nil, fmt.Errorf("failed to detect host supported syscalls: %v", err)
}
for c := range calls {
if reason, ok := unsupported[c]; ok {
log.Logf(1, "unsupported syscall: %v: %v", c.Name, reason)
disabled = append(disabled, rpctype.SyscallReason{
ID: c.ID,
Reason: reason,
})
delete(calls, c)
}
}
_, unsupported = target.TransitivelyEnabledCalls(calls)
for c := range calls {
if reason, ok := unsupported[c]; ok {
log.Logf(1, "transitively unsupported: %v: %v", c.Name, reason)
disabled = append(disabled, rpctype.SyscallReason{
ID: c.ID,
Reason: reason,
})
delete(calls, c)
}
}
if len(calls) == 0 {
return nil, nil, fmt.Errorf("all system calls are disabled")
}
for c := range calls {
enabled = append(enabled, c.ID)
}
return enabled, disabled, nil
}