From 4f0d9df0078216bcadfc7d4a744a160a1d3659be Mon Sep 17 00:00:00 2001 From: Dmitry Vyukov Date: Tue, 26 Nov 2024 15:25:16 +0100 Subject: pkg/ifaceprobe: add package Package ifaceprobe implements dynamic component of automatic kernel interface extraction. Currently it discovers all /{dev,sys,proc} files, and collects coverage for open/read/write/mmap/ioctl syscalls on these files. Later this allows to build file path <-> file_operations mapping. I've tried 2 other approaches: 1. Immediately map file to file_operations callbacks similar to tools/fops_probe, and export only that. This required lots of hardcoding of kernel function/file names, did not work well in all cases, and presumably would produce more maintanance in future. 2. Automatically infer what kernel functions are common, and which correspond to file_operations callbacks by first collecting coverage for all files/programs, and then counting how many times wach PC is encountered in combined coverage. Presumably common functions (SYS_read, vfs_read) will be present in most/all traces, while the actual file_operations callback will be present in only one/few traces. This also did not work well and produced lots of bugs where common functions were somehow called in few programs, or common file_operations callbacks were called in too many traces. --- pkg/ifaceprobe/ifaceprobe.go | 238 +++++++++++++++++++++++++++++++++++++++++++ syz-manager/manager.go | 30 ++++++ 2 files changed, 268 insertions(+) create mode 100644 pkg/ifaceprobe/ifaceprobe.go diff --git a/pkg/ifaceprobe/ifaceprobe.go b/pkg/ifaceprobe/ifaceprobe.go new file mode 100644 index 000000000..f3ab8ba4a --- /dev/null +++ b/pkg/ifaceprobe/ifaceprobe.go @@ -0,0 +1,238 @@ +// Copyright 2024 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 ifaceprobe implements dynamic component of automatic kernel interface extraction. +// Currently it discovers all /{dev,sys,proc} files, and collects coverage for open/read/write/mmap/ioctl +// syscalls on these files. Later this allows to build file path <-> file_operations mapping. +package ifaceprobe + +import ( + "context" + "fmt" + "path/filepath" + "slices" + "strings" + + "github.com/google/syzkaller/pkg/flatrpc" + "github.com/google/syzkaller/pkg/fuzzer/queue" + "github.com/google/syzkaller/pkg/log" + "github.com/google/syzkaller/pkg/mgrconfig" + "github.com/google/syzkaller/pkg/symbolizer" + "github.com/google/syzkaller/prog" +) + +// Info represents information about dynamically extracted information. +type Info struct { + Files []FileInfo + PCs []PCInfo +} + +type FileInfo struct { + Name string // Full file name, e.g. /dev/null. + Cover []uint64 // Combined coverage for operations on the file. +} + +type PCInfo struct { + PC uint64 + Func string + File string +} + +// Globs returns a list of glob's that should be requested from the target machine. +// Result of querying these globs should be later passed to Run in info. +func Globs() []string { + var globs []string + for _, path := range []string{"/dev", "/sys", "/proc"} { + // Our globs currently do not support recursion (#4906), + // so we append N "/*" parts manully. Some of the paths can be very deep, e.g. try: + // sudo find /sys -ls 2>/dev/null | sed "s#[^/]##g" | sort | uniq -c + for i := 1; i < 15; i++ { + globs = append(globs, path+strings.Repeat("/*", i)) + } + } + return globs +} + +// Run finishes dynamic analysis and returns dynamic info. +// As it runs it will submit some test program requests to the exec queue. +// Info is used to extract results of glob querying, see Globs function. +func Run(ctx context.Context, cfg *mgrconfig.Config, exec queue.Executor, info *flatrpc.InfoRequest) (*Info, error) { + return (&prober{ + ctx: ctx, + cfg: cfg, + exec: exec, + info: info, + }).run() +} + +type prober struct { + ctx context.Context + cfg *mgrconfig.Config + exec queue.Executor + info *flatrpc.InfoRequest +} + +func (pr *prober) run() (*Info, error) { + symb := symbolizer.NewSymbolizer(pr.cfg.SysTarget) + defer symb.Close() + + files := extractFiles(pr.info) + var reqs [][]*queue.Request + for _, file := range extractFiles(pr.info) { + reqs1, err := pr.submitFile(file) + if err != nil { + return nil, err + } + reqs = append(reqs, reqs1) + } + + info := &Info{} + dedup := make(map[uint64]bool) + kernelObj := filepath.Join(pr.cfg.KernelObj, pr.cfg.SysTarget.KernelObject) + sourceBase := filepath.Clean(pr.cfg.KernelSrc) + string(filepath.Separator) + for i, file := range files { + if i%500 == 0 { + log.Logf(0, "processing file %v/%v", i, len(files)) + } + fi := FileInfo{ + Name: file, + } + fileDedup := make(map[uint64]bool) + for _, req := range reqs[i] { + res := req.Wait(pr.ctx) + if res.Status != queue.Success { + return nil, fmt.Errorf("failed to execute prog: %w (%v)", res.Err, res.Status) + } + cover := append(res.Info.Calls[0].Cover, res.Info.Calls[1].Cover...) + for _, pc := range cover { + if fileDedup[pc] { + continue + } + fileDedup[pc] = true + fi.Cover = append(fi.Cover, pc) + if dedup[pc] { + continue + } + dedup[pc] = true + frames, err := symb.Symbolize(kernelObj, pc) + if err != nil { + return nil, err + } + if len(frames) == 0 { + continue + } + // Look only at the non-inline frame, callbacks we are looking for can't be inlined. + frame := frames[len(frames)-1] + info.PCs = append(info.PCs, PCInfo{ + PC: pc, + Func: frame.Func, + File: strings.TrimPrefix(filepath.Clean(frame.File), sourceBase), + }) + } + } + slices.Sort(fi.Cover) + info.Files = append(info.Files, fi) + } + slices.SortFunc(info.PCs, func(a, b PCInfo) int { + return int(a.PC - b.PC) + }) + return info, nil +} + +func (pr *prober) submitFile(file string) ([]*queue.Request, error) { + var fops = []struct { + mode string + call string + }{ + {mode: "O_RDONLY", call: "read(r0, &AUTO=' ', AUTO)"}, + {mode: "O_WRONLY", call: "write(r0, &AUTO=' ', AUTO)"}, + {mode: "O_RDONLY", call: "ioctl(r0, 0x0, 0x0)"}, + {mode: "O_WRONLY", call: "ioctl(r0, 0x0, 0x0)"}, + {mode: "O_RDONLY", call: "mmap(0x0, 0x1000, 0x1, 0x2, r0, 0)"}, + {mode: "O_WRONLY", call: "mmap(0x0, 0x1000, 0x2, 0x2, r0, 0)"}, + } + var reqs []*queue.Request + for _, desc := range fops { + text := fmt.Sprintf("r0 = openat(0x%x, &AUTO='%s', 0x%x, 0x0)\n%v", + pr.constVal("AT_FDCWD"), file, pr.constVal(desc.mode), desc.call) + p, err := pr.cfg.Target.Deserialize([]byte(text), prog.StrictUnsafe) + if err != nil { + return nil, fmt.Errorf("failed to deserialize: %w\n%v", err, text) + } + req := &queue.Request{ + Prog: p, + ExecOpts: flatrpc.ExecOpts{ + EnvFlags: flatrpc.ExecEnvSandboxNone | flatrpc.ExecEnvSignal, + ExecFlags: flatrpc.ExecFlagCollectCover, + }, + Important: true, + } + reqs = append(reqs, req) + pr.exec.Submit(req) + } + return reqs, nil +} + +func (pr *prober) constVal(name string) uint64 { + val, ok := pr.cfg.Target.ConstMap[name] + if !ok { + panic(fmt.Sprintf("const %v is not present", name)) + } + return val +} + +func extractFiles(info *flatrpc.InfoRequestRawT) []string { + var files []string + dedup := make(map[string]bool) + for _, glob := range info.Globs { + for _, file := range glob.Files { + if dedup[file] || !extractFileFilter(file) { + continue + } + dedup[file] = true + files = append(files, file) + } + } + return files +} + +func extractFileFilter(file string) bool { + if strings.HasPrefix(file, "/dev/") { + return true + } + if proc := "/proc/"; strings.HasPrefix(file, proc) { + // These won't be present in the test process. + if strings.HasPrefix(file, "/proc/self/fdinfo/") || + strings.HasPrefix(file, "/proc/thread-self/fdinfo/") { + return false + } + // It contains actual pid number that will be different in the test. + if strings.HasPrefix(file, "/proc/self/task/") { + return false + } + // Ignore all actual processes. + c := file[len(proc)] + return c < '0' || c > '9' + } + if strings.HasPrefix(file, "/sys/") { + // There are too many tracing events, so leave just one of them. + if strings.HasPrefix(file, "/sys/kernel/tracing/events/") && + !strings.HasPrefix(file, "/sys/kernel/tracing/events/vmalloc/") || + strings.HasPrefix(file, "/sys/kernel/debug/tracing/events/") && + !strings.HasPrefix(file, "/sys/kernel/debug/tracing/events/vmalloc/") { + return false + } + // There are too many slabs, so leave just one of them. + if strings.HasPrefix(file, "/sys/kernel/slab/") && + !strings.HasPrefix(file, "/sys/kernel/slab/kmalloc-64") { + return false + } + // There are too many of these, leave just one of them. + if strings.HasPrefix(file, "/sys/fs/selinux/class/") && + !strings.HasPrefix(file, "/sys/fs/selinux/class/file/") { + return false + } + return true + } + return false +} diff --git a/syz-manager/manager.go b/syz-manager/manager.go index aed721a79..3b6072629 100644 --- a/syz-manager/manager.go +++ b/syz-manager/manager.go @@ -30,6 +30,7 @@ import ( "github.com/google/syzkaller/pkg/fuzzer" "github.com/google/syzkaller/pkg/fuzzer/queue" "github.com/google/syzkaller/pkg/gce" + "github.com/google/syzkaller/pkg/ifaceprobe" "github.com/google/syzkaller/pkg/log" "github.com/google/syzkaller/pkg/manager" "github.com/google/syzkaller/pkg/mgrconfig" @@ -154,6 +155,20 @@ var ( Description: `run unit tests Run sys/os/test/* tests in various modes and print results.`, } + ModeIfaceProbe = &Mode{ + Name: "iface-probe", + Description: `run dynamic part of kernel interface auto-extraction + When the probe is finished, manager writes the result to workdir/interfaces.json file and exits.`, + CheckConfig: func(cfg *mgrconfig.Config) error { + if cfg.Snapshot { + return fmt.Errorf("snapshot mode is not supported") + } + if cfg.Sandbox != "none" { + return fmt.Errorf("sandbox \"%v\" is not supported (only \"none\")", cfg.Sandbox) + } + return nil + }, + } modes = []*Mode{ ModeFuzzing, @@ -161,6 +176,7 @@ var ( ModeCorpusTriage, ModeCorpusRun, ModeRunTests, + ModeIfaceProbe, } ) @@ -285,6 +301,9 @@ func RunManager(mode *Mode, cfg *mgrconfig.Config) { Stats: mgr.servStats, Debug: *flagDebug, } + if mode == ModeIfaceProbe { + rpcCfg.CheckGlobs = ifaceprobe.Globs() + } mgr.serv, err = rpcserver.New(rpcCfg) if err != nil { log.Fatalf("failed to create rpc server: %v", err) @@ -1164,6 +1183,17 @@ func (mgr *Manager) MachineChecked(info *flatrpc.InfoRequest, features flatrpc.F mgr.exit("tests") }() return ctx + } else if mgr.mode == ModeIfaceProbe { + exec := queue.Plain() + go func() { + res, err := ifaceprobe.Run(vm.ShutdownCtx(), mgr.cfg, exec, info) + if err != nil { + log.Fatalf("interface probing failed: %v", err) + } + mgr.saveJSON("interfaces.json", res) + mgr.exit("interface probe") + }() + return exec } panic(fmt.Sprintf("unexpected mode %q", mgr.mode.Name)) } -- cgit mrf-deployment