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 }