github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/vminfo/syscalls.go (about)

     1  // Copyright 2024 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package vminfo
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  	"syscall"
    11  
    12  	"github.com/google/syzkaller/pkg/flatrpc"
    13  	"github.com/google/syzkaller/pkg/fuzzer/queue"
    14  	"github.com/google/syzkaller/pkg/ipc"
    15  	"github.com/google/syzkaller/pkg/mgrconfig"
    16  	"github.com/google/syzkaller/prog"
    17  	"github.com/google/syzkaller/sys/targets"
    18  )
    19  
    20  // checkContext arranges checking of presence/support of all target syscalls.
    21  // The actual checking is done by OS-specific impl.syscallCheck,
    22  // while checkContext invokes that function for each syscall in a special manner
    23  // and provides primitives for reading target VM files, checking if a file can be opened,
    24  // executing test programs on the target VM, etc.
    25  //
    26  // The external interface of this type contains only 2 methods:
    27  // startCheck - starts impl.syscallCheck goroutines and collects all test programs in progs,
    28  // finishCheck - accepts results of program execution, unblocks impl.syscallCheck goroutines,
    29  //
    30  //	waits and returns results of checking.
    31  type checkContext struct {
    32  	ctx      context.Context
    33  	impl     checker
    34  	cfg      *mgrconfig.Config
    35  	target   *prog.Target
    36  	sandbox  flatrpc.ExecEnv
    37  	executor queue.Executor
    38  	fs       filesystem
    39  	// Once checking of a syscall is finished, the result is sent to syscalls.
    40  	// The main goroutine will wait for exactly pendingSyscalls messages.
    41  	syscalls        chan syscallResult
    42  	pendingSyscalls int
    43  	features        chan featureResult
    44  }
    45  
    46  type syscallResult struct {
    47  	call   *prog.Syscall
    48  	reason string
    49  }
    50  
    51  func newCheckContext(ctx context.Context, cfg *mgrconfig.Config, impl checker,
    52  	executor queue.Executor) *checkContext {
    53  	sandbox, err := ipc.SandboxToFlags(cfg.Sandbox)
    54  	if err != nil {
    55  		panic(fmt.Sprintf("failed to parse sandbox: %v", err))
    56  	}
    57  	return &checkContext{
    58  		ctx:      ctx,
    59  		impl:     impl,
    60  		cfg:      cfg,
    61  		target:   cfg.Target,
    62  		sandbox:  sandbox,
    63  		executor: executor,
    64  		syscalls: make(chan syscallResult),
    65  		features: make(chan featureResult, 100),
    66  	}
    67  }
    68  
    69  func (ctx *checkContext) start(fileInfos []*flatrpc.FileInfo) {
    70  	ctx.fs = createVirtualFilesystem(fileInfos)
    71  	for _, id := range ctx.cfg.Syscalls {
    72  		call := ctx.target.Syscalls[id]
    73  		if call.Attrs.Disabled {
    74  			continue
    75  		}
    76  		ctx.pendingSyscalls++
    77  		syscallCheck := ctx.impl.syscallCheck
    78  		if strings.HasPrefix(call.CallName, "syz_ext_") {
    79  			// Non-mainline pseudo-syscalls in executor/common_ext.h can't have
    80  			// the checking function and are assumed to be unconditionally supported.
    81  			syscallCheck = alwaysSupported
    82  		}
    83  		// HostFuzzer targets can't run Go binaries on the targets,
    84  		// so we actually run on the host on another OS. The same for targets.TestOS OS.
    85  		if ctx.cfg.SysTarget.HostFuzzer || ctx.target.OS == targets.TestOS {
    86  			syscallCheck = alwaysSupported
    87  		}
    88  		go func() {
    89  			var reason string
    90  			deps := ctx.cfg.SysTarget.PseudoSyscallDeps[call.CallName]
    91  			if len(deps) != 0 {
    92  				reason = ctx.supportedSyscalls(deps)
    93  			}
    94  			// Only check the call if all its dependencies are satisfied.
    95  			if reason == "" {
    96  				reason = syscallCheck(ctx, call)
    97  			}
    98  			ctx.syscalls <- syscallResult{call, reason}
    99  		}()
   100  	}
   101  	ctx.startFeaturesCheck()
   102  }
   103  
   104  func (ctx *checkContext) wait(featureInfos []*flatrpc.FeatureInfo) (
   105  	map[*prog.Syscall]bool, map[*prog.Syscall]string, Features, error) {
   106  	enabled := make(map[*prog.Syscall]bool)
   107  	disabled := make(map[*prog.Syscall]string)
   108  	for i := 0; i < ctx.pendingSyscalls; i++ {
   109  		res := <-ctx.syscalls
   110  		if res.reason == "" {
   111  			enabled[res.call] = true
   112  		} else {
   113  			disabled[res.call] = res.reason
   114  		}
   115  	}
   116  	features, err := ctx.finishFeatures(featureInfos)
   117  	return enabled, disabled, features, err
   118  }
   119  
   120  func (ctx *checkContext) rootCanOpen(file string) string {
   121  	return ctx.canOpenImpl(file, nil, true)
   122  }
   123  
   124  func (ctx *checkContext) canOpen(file string) string {
   125  	return ctx.canOpenImpl(file, nil, false)
   126  }
   127  
   128  func (ctx *checkContext) canWrite(file string) string {
   129  	return ctx.canOpenImpl(file, []uint64{ctx.val("O_WRONLY")}, false)
   130  }
   131  
   132  func (ctx *checkContext) canOpenImpl(file string, modes []uint64, root bool) string {
   133  	if len(modes) == 0 {
   134  		modes = ctx.allOpenModes()
   135  	}
   136  	var calls []string
   137  	for _, mode := range modes {
   138  		call := fmt.Sprintf("openat(0x%x, &AUTO='%s', 0x%x, 0x0)", ctx.val("AT_FDCWD"), file, mode)
   139  		calls = append(calls, call)
   140  	}
   141  	info := ctx.execRaw(calls, prog.StrictUnsafe, root)
   142  	for _, call := range info.Calls {
   143  		if call.Errno == 0 {
   144  			return ""
   145  		}
   146  	}
   147  	who := ""
   148  	if root {
   149  		who = "root "
   150  	}
   151  	return fmt.Sprintf("%vfailed to open %s: %v", who, file, syscall.Errno(info.Calls[0].Errno))
   152  }
   153  
   154  func (ctx *checkContext) supportedSyscalls(names []string) string {
   155  	var calls []string
   156  	for _, name := range names {
   157  		if strings.HasPrefix(name, "syz_") {
   158  			panic("generic syscall check used for pseudo-syscall: " + name)
   159  		}
   160  		calls = append(calls, name+"()")
   161  	}
   162  	info := ctx.execRaw(calls, prog.NonStrictUnsafe, false)
   163  	for i, res := range info.Calls {
   164  		if res.Errno == int(syscall.ENOSYS) {
   165  			return fmt.Sprintf("syscall %v is not present", names[i])
   166  		}
   167  	}
   168  	return ""
   169  }
   170  
   171  func supportedOpenat(ctx *checkContext, call *prog.Syscall) string {
   172  	fname, ok := extractStringConst(call.Args[1].Type)
   173  	if !ok || fname[0] != '/' {
   174  		return ""
   175  	}
   176  	modes := ctx.allOpenModes()
   177  	// Attempt to extract flags from the syscall description.
   178  	if mode, ok := call.Args[2].Type.(*prog.ConstType); ok {
   179  		modes = []uint64{mode.Val}
   180  	}
   181  	var calls []string
   182  	for _, mode := range modes {
   183  		call := fmt.Sprintf("openat(0x%0x, &AUTO='%v', 0x%x, 0x0)", ctx.val("AT_FDCWD"), fname, mode)
   184  		calls = append(calls, call)
   185  	}
   186  	return ctx.anyCallSucceeds(calls, fmt.Sprintf("failed to open %v", fname))
   187  }
   188  
   189  func (ctx *checkContext) allOpenModes() []uint64 {
   190  	// Various open modes we need to try if we don't have a concrete mode.
   191  	// Some files can be opened only for reading, some only for writing,
   192  	// and some only in non-blocking mode.
   193  	// Note: some of these consts are different for different arches.
   194  	return []uint64{ctx.val("O_RDONLY"), ctx.val("O_WRONLY"), ctx.val("O_RDWR"),
   195  		ctx.val("O_RDONLY") | ctx.val("O_NONBLOCK")}
   196  }
   197  
   198  func (ctx *checkContext) callSucceeds(call string) string {
   199  	return ctx.anyCallSucceeds([]string{call}, call+" failed")
   200  }
   201  
   202  func (ctx *checkContext) execCall(call string) syscall.Errno {
   203  	info := ctx.execRaw([]string{call}, prog.StrictUnsafe, false)
   204  	return syscall.Errno(info.Calls[0].Errno)
   205  }
   206  
   207  func (ctx *checkContext) anyCallSucceeds(calls []string, msg string) string {
   208  	info := ctx.execRaw(calls, prog.StrictUnsafe, false)
   209  	for _, call := range info.Calls {
   210  		if call.Errno == 0 {
   211  			return ""
   212  		}
   213  	}
   214  	return fmt.Sprintf("%s: %v", msg, syscall.Errno(info.Calls[0].Errno))
   215  }
   216  
   217  func (ctx *checkContext) onlySandboxNone() string {
   218  	if ctx.sandbox != 0 {
   219  		return "only supported under root with sandbox=none"
   220  	}
   221  	return ""
   222  }
   223  
   224  func (ctx *checkContext) onlySandboxNoneOrNamespace() string {
   225  	if ctx.sandbox != 0 && ctx.sandbox != flatrpc.ExecEnvSandboxNamespace {
   226  		return "only supported under root with sandbox=none/namespace"
   227  	}
   228  	return ""
   229  }
   230  
   231  func (ctx *checkContext) val(name string) uint64 {
   232  	val, ok := ctx.target.ConstMap[name]
   233  	if !ok {
   234  		panic(fmt.Sprintf("const %v is not present", name))
   235  	}
   236  	return val
   237  }
   238  
   239  func (ctx *checkContext) execRaw(calls []string, mode prog.DeserializeMode, root bool) *ipc.ProgInfo {
   240  	sandbox := ctx.sandbox
   241  	if root {
   242  		sandbox = 0
   243  	}
   244  	info := &ipc.ProgInfo{}
   245  	for remain := calls; len(remain) != 0; {
   246  		// Don't put too many syscalls into a single program,
   247  		// it will have higher chances to time out.
   248  		ncalls := min(len(remain), prog.MaxCalls/2)
   249  		progStr := strings.Join(remain[:ncalls], "\n")
   250  		remain = remain[ncalls:]
   251  		p, err := ctx.target.Deserialize([]byte(progStr), mode)
   252  		if err != nil {
   253  			panic(fmt.Sprintf("failed to deserialize: %v\n%v", err, progStr))
   254  		}
   255  		req := &queue.Request{
   256  			Prog: p,
   257  			ExecOpts: ipc.ExecOpts{
   258  				EnvFlags:   sandbox,
   259  				ExecFlags:  0,
   260  				SandboxArg: ctx.cfg.SandboxArg,
   261  			},
   262  			Important: true,
   263  		}
   264  		ctx.executor.Submit(req)
   265  		res := req.Wait(ctx.ctx)
   266  		if res.Status == queue.Success {
   267  			info.Calls = append(info.Calls, res.Info.Calls...)
   268  		} else if res.Status == queue.Crashed {
   269  			// Pretend these calls were not executed.
   270  			info.Calls = append(info.Calls, ipc.EmptyProgInfo(ncalls).Calls...)
   271  		} else {
   272  			// The program must have been either executed or not due to a crash.
   273  			panic(fmt.Sprintf("got unexpected execution status (%d) for the prog %s",
   274  				res.Status, progStr))
   275  		}
   276  	}
   277  	if len(info.Calls) != len(calls) {
   278  		panic(fmt.Sprintf("got %v != %v results for program:\n%s",
   279  			len(info.Calls), len(calls), strings.Join(calls, "\n")))
   280  	}
   281  	return info
   282  }
   283  
   284  func (ctx *checkContext) readFile(name string) ([]byte, error) {
   285  	return ctx.fs.ReadFile(name)
   286  }
   287  
   288  func alwaysSupported(ctx *checkContext, call *prog.Syscall) string {
   289  	return ""
   290  }
   291  
   292  func extractStringConst(typ prog.Type) (string, bool) {
   293  	ptr, ok := typ.(*prog.PtrType)
   294  	if !ok {
   295  		panic("first open arg is not a pointer to string const")
   296  	}
   297  	str, ok := ptr.Elem.(*prog.BufferType)
   298  	if !ok || str.Kind != prog.BufferString || len(str.Values) == 0 {
   299  		return "", false
   300  	}
   301  	v := str.Values[0]
   302  	for v != "" && v[len(v)-1] == 0 {
   303  		v = v[:len(v)-1] // string terminating \x00
   304  	}
   305  	return v, true
   306  }