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