github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/ifaceprobe/ifaceprobe.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 ifaceprobe implements dynamic component of automatic kernel interface extraction.
     5  // Currently it discovers all /{dev,sys,proc} files, and collects coverage for open/read/write/mmap/ioctl
     6  // syscalls on these files. Later this allows to build file path <-> file_operations mapping.
     7  package ifaceprobe
     8  
     9  import (
    10  	"context"
    11  	"fmt"
    12  	"path/filepath"
    13  	"slices"
    14  	"strings"
    15  	"sync"
    16  
    17  	"github.com/google/syzkaller/pkg/csource"
    18  	"github.com/google/syzkaller/pkg/flatrpc"
    19  	"github.com/google/syzkaller/pkg/fuzzer/queue"
    20  	"github.com/google/syzkaller/pkg/log"
    21  	"github.com/google/syzkaller/pkg/mgrconfig"
    22  	"github.com/google/syzkaller/pkg/symbolizer"
    23  	"github.com/google/syzkaller/prog"
    24  )
    25  
    26  // Info represents information about dynamically extracted information.
    27  type Info struct {
    28  	Files []FileInfo
    29  	PCs   []PCInfo
    30  }
    31  
    32  type FileInfo struct {
    33  	Name  string // Full file name, e.g. /dev/null.
    34  	Cover []int  // Combined coverage for operations on the file.
    35  }
    36  
    37  type PCInfo struct {
    38  	Func string
    39  	File string
    40  }
    41  
    42  // Run does dynamic analysis and returns dynamic info.
    43  // As it runs it will submit some test program requests to the exec queue.
    44  func Run(ctx context.Context, cfg *mgrconfig.Config, features flatrpc.Feature, exec queue.Executor) (*Info, error) {
    45  	return (&prober{
    46  		ctx:      ctx,
    47  		cfg:      cfg,
    48  		features: features,
    49  		exec:     exec,
    50  		done:     make(chan *fileDesc, 100),
    51  		errc:     make(chan error, 1),
    52  	}).run()
    53  }
    54  
    55  type prober struct {
    56  	ctx      context.Context
    57  	cfg      *mgrconfig.Config
    58  	features flatrpc.Feature
    59  	exec     queue.Executor
    60  	wg       sync.WaitGroup
    61  	done     chan *fileDesc
    62  	errc     chan error
    63  }
    64  
    65  type fileDesc struct {
    66  	file    string
    67  	results []*queue.Result
    68  }
    69  
    70  func (pr *prober) run() (*Info, error) {
    71  	symb := symbolizer.Make(pr.cfg.SysTarget)
    72  	defer symb.Close()
    73  
    74  	for _, glob := range globList() {
    75  		pr.submitGlob(glob)
    76  	}
    77  
    78  	go func() {
    79  		pr.wg.Wait()
    80  		close(pr.done)
    81  	}()
    82  
    83  	info := &Info{}
    84  	pcIndexes := make(map[uint64]int)
    85  	kernelObj := filepath.Join(pr.cfg.KernelObj, pr.cfg.SysTarget.KernelObject)
    86  	sourceBase := filepath.Clean(pr.cfg.KernelSrc) + string(filepath.Separator)
    87  	i := 0
    88  	for desc := range pr.done {
    89  		i++
    90  		if i%500 == 0 {
    91  			log.Logf(0, "done file %v", i)
    92  		}
    93  		fi := FileInfo{
    94  			Name: desc.file,
    95  		}
    96  		fileDedup := make(map[uint64]bool)
    97  		for _, res := range desc.results {
    98  			cover := append(res.Info.Calls[0].Cover, res.Info.Calls[1].Cover...)
    99  			for _, pc := range cover {
   100  				if fileDedup[pc] {
   101  					continue
   102  				}
   103  				fileDedup[pc] = true
   104  				pcIndex, ok := pcIndexes[pc]
   105  				if !ok {
   106  					pcIndex = -1
   107  					frames, err := symb.Symbolize(kernelObj, pc)
   108  					if err != nil {
   109  						return nil, err
   110  					}
   111  					if len(frames) != 0 {
   112  						// Look only at the non-inline frame,
   113  						// callbacks we are looking for can't be inlined.
   114  						frame := frames[len(frames)-1]
   115  						pcIndex = len(info.PCs)
   116  						info.PCs = append(info.PCs, PCInfo{
   117  							Func: frame.Func,
   118  							File: strings.TrimPrefix(filepath.Clean(frame.File), sourceBase),
   119  						})
   120  					}
   121  					pcIndexes[pc] = pcIndex
   122  				}
   123  				if pcIndex >= 0 {
   124  					fi.Cover = append(fi.Cover, pcIndex)
   125  				}
   126  			}
   127  		}
   128  		slices.Sort(fi.Cover)
   129  		info.Files = append(info.Files, fi)
   130  	}
   131  	slices.SortFunc(info.Files, func(a, b FileInfo) int {
   132  		return strings.Compare(a.Name, b.Name)
   133  	})
   134  	select {
   135  	case err := <-pr.errc:
   136  		return nil, err
   137  	default:
   138  		return info, nil
   139  	}
   140  }
   141  
   142  func (pr *prober) noteError(err error) {
   143  	select {
   144  	case pr.errc <- err:
   145  	default:
   146  	}
   147  }
   148  
   149  func (pr *prober) submitGlob(glob string) {
   150  	pr.wg.Add(1)
   151  	req := &queue.Request{
   152  		Type:        flatrpc.RequestTypeGlob,
   153  		GlobPattern: glob,
   154  		ExecOpts: flatrpc.ExecOpts{
   155  			EnvFlags: flatrpc.ExecEnvSandboxNone | csource.FeaturesToFlags(pr.features, nil),
   156  		},
   157  		Important: true,
   158  	}
   159  	req.OnDone(pr.onGlobDone)
   160  	pr.exec.Submit(req)
   161  }
   162  
   163  func (pr *prober) onGlobDone(req *queue.Request, res *queue.Result) bool {
   164  	defer pr.wg.Done()
   165  	if res.Status != queue.Success {
   166  		pr.noteError(fmt.Errorf("failed to execute glob: %w (%v)\n%s\n%s",
   167  			res.Err, res.Status, req.GlobPattern, res.Output))
   168  	}
   169  	files := res.GlobFiles()
   170  	log.Logf(0, "glob %v expanded to %v files", req.GlobPattern, len(files))
   171  	for _, file := range files {
   172  		if extractFileFilter(file) {
   173  			pr.submitFile(file)
   174  		}
   175  	}
   176  	return true
   177  }
   178  
   179  func (pr *prober) submitFile(file string) {
   180  	pr.wg.Add(1)
   181  	var fops = []struct {
   182  		mode string
   183  		call string
   184  	}{
   185  		{mode: "O_RDONLY", call: "read(r0, &AUTO=' ', AUTO)"},
   186  		{mode: "O_WRONLY", call: "write(r0, &AUTO=' ', AUTO)"},
   187  		{mode: "O_RDONLY", call: "ioctl(r0, 0x0, 0x0)"},
   188  		{mode: "O_WRONLY", call: "ioctl(r0, 0x0, 0x0)"},
   189  		{mode: "O_RDONLY", call: "mmap(0x0, 0x1000, 0x1, 0x2, r0, 0)"},
   190  		{mode: "O_WRONLY", call: "mmap(0x0, 0x1000, 0x2, 0x2, r0, 0)"},
   191  	}
   192  	desc := &fileDesc{
   193  		file: file,
   194  	}
   195  	var reqs []*queue.Request
   196  	for _, desc := range fops {
   197  		text := fmt.Sprintf("r0 = openat(0x%x, &AUTO='%s', 0x%x, 0x0)\n%v",
   198  			pr.constVal("AT_FDCWD"), file, pr.constVal(desc.mode), desc.call)
   199  		p, err := pr.cfg.Target.Deserialize([]byte(text), prog.StrictUnsafe)
   200  		if err != nil {
   201  			panic(fmt.Sprintf("failed to deserialize: %v\n%v", err, text))
   202  		}
   203  		req := &queue.Request{
   204  			Prog: p,
   205  			ExecOpts: flatrpc.ExecOpts{
   206  				EnvFlags: flatrpc.ExecEnvSandboxNone | flatrpc.ExecEnvSignal |
   207  					csource.FeaturesToFlags(pr.features, nil),
   208  				ExecFlags: flatrpc.ExecFlagCollectCover,
   209  			},
   210  			Important: true,
   211  		}
   212  		reqs = append(reqs, req)
   213  		pr.exec.Submit(req)
   214  	}
   215  	go func() {
   216  		defer pr.wg.Done()
   217  		for _, req := range reqs {
   218  			res := req.Wait(pr.ctx)
   219  			if res.Status != queue.Success {
   220  				pr.noteError(fmt.Errorf("failed to execute prog: %w (%v)\n%s\n%s",
   221  					res.Err, res.Status, req.Prog.Serialize(), res.Output))
   222  				continue
   223  			}
   224  			desc.results = append(desc.results, res)
   225  		}
   226  		pr.done <- desc
   227  	}()
   228  }
   229  
   230  func (pr *prober) constVal(name string) uint64 {
   231  	val, ok := pr.cfg.Target.ConstMap[name]
   232  	if !ok {
   233  		panic(fmt.Sprintf("const %v is not present", name))
   234  	}
   235  	return val
   236  }
   237  
   238  // globList returns a list of glob's we are interested in.
   239  func globList() []string {
   240  	var globs []string
   241  	// /selinux is mounted by executor, we probably should mount it at the standard /sys/fs/selinux,
   242  	// but this is where it is now.
   243  	// Also query the test cwd, executor creates some links in there.
   244  	for _, path := range []string{"/dev", "/sys", "/proc", "/selinux", "."} {
   245  		// Our globs currently do not support recursion (#4906),
   246  		// so we append N "/*" parts manully. Some of the paths can be very deep, e.g. try:
   247  		// sudo find /sys -ls 2>/dev/null | sed "s#[^/]##g" | sort | uniq -c
   248  		for i := 1; i < 15; i++ {
   249  			globs = append(globs, path+strings.Repeat("/*", i))
   250  		}
   251  	}
   252  	return globs
   253  }
   254  
   255  func extractFileFilter(file string) bool {
   256  	if strings.HasPrefix(file, "/dev/") ||
   257  		strings.HasPrefix(file, "/selinux/") ||
   258  		strings.HasPrefix(file, "./") {
   259  		return true
   260  	}
   261  	if proc := "/proc/"; strings.HasPrefix(file, proc) {
   262  		// These won't be present in the test process.
   263  		if strings.HasPrefix(file, "/proc/self/fdinfo/") ||
   264  			strings.HasPrefix(file, "/proc/thread-self/fdinfo/") {
   265  			return false
   266  		}
   267  		// It contains actual pid number that will be different in the test.
   268  		if strings.HasPrefix(file, "/proc/self/task/") {
   269  			return false
   270  		}
   271  		// Ignore all actual processes.
   272  		c := file[len(proc)]
   273  		return c < '0' || c > '9'
   274  	}
   275  	if strings.HasPrefix(file, "/sys/") {
   276  		// There are too many tracing events, so leave just one of them.
   277  		if strings.HasPrefix(file, "/sys/kernel/tracing/events/") &&
   278  			!strings.HasPrefix(file, "/sys/kernel/tracing/events/vmalloc/") ||
   279  			strings.HasPrefix(file, "/sys/kernel/debug/tracing/events/") &&
   280  				!strings.HasPrefix(file, "/sys/kernel/debug/tracing/events/vmalloc/") {
   281  			return false
   282  		}
   283  		// There are too many slabs, so leave just one of them.
   284  		if strings.HasPrefix(file, "/sys/kernel/slab/") &&
   285  			!strings.HasPrefix(file, "/sys/kernel/slab/kmalloc-64") {
   286  			return false
   287  		}
   288  		// There are too many of these, leave just one of them.
   289  		if strings.HasPrefix(file, "/sys/fs/selinux/class/") &&
   290  			!strings.HasPrefix(file, "/sys/fs/selinux/class/file/") {
   291  			return false
   292  		}
   293  		return true
   294  	}
   295  	panic(fmt.Sprintf("unhandled file %q", file))
   296  }