diff options
Diffstat (limited to 'pkg/declextract/fileops.go')
| -rw-r--r-- | pkg/declextract/fileops.go | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/pkg/declextract/fileops.go b/pkg/declextract/fileops.go new file mode 100644 index 000000000..a18566647 --- /dev/null +++ b/pkg/declextract/fileops.go @@ -0,0 +1,256 @@ +// 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 declextract + +import ( + "fmt" + "slices" + "strings" +) + +// TODO: also emit interface entry for file_operations. + +func (ctx *context) serializeFileOps() { + fopsToFiles := ctx.mapFopsToFiles() + for _, fops := range ctx.FileOps { + files := fopsToFiles[fops] + if len(files) == 0 { + continue // each unmapped entry means some code we don't know how to cover yet + } + ctx.createFops(fops, files) + } +} + +func (ctx *context) createFops(fops *FileOps, files []string) { + // If it has only open, then emit only openat that returns generic fd. + fdt := "fd" + if len(fops.ops()) > 1 || fops.Open == "" { + fdt = fmt.Sprintf("fd_%v", fops.Name) + ctx.fmt("resource %v[fd]\n", fdt) + } + suffix := autoSuffix + "_" + fops.Name + if len(files) == 1 { + ctx.fmt("openat%v(fd const[AT_FDCWD], file ptr[in, string[\"%s\"]],"+ + " flags flags[open_flags], mode const[0]) %v\n", suffix, files[0], fdt) + } else { + // If there are too many files, split them into parts. + // First, compiler currently sets limit of 2000 values for string flags; + // second, it should provide additional prioritization signal for the fuzzer + // (if there are multiple openat calls, it should call them more often than a single call). + const partSize = 100 + singlePart := len(files) <= partSize + for partID := 0; len(files) != 0; partID++ { + part := files[:min(100, len(files))] + files = files[len(part):] + partSuffix := "" + if !singlePart { + partSuffix = fmt.Sprint(partID) + } + fileFlags := fmt.Sprintf("%v_files%v", fops.Name, partSuffix) + ctx.fmt("%v = ", fileFlags) + for i, file := range part { + ctx.fmt("%v \"%v\"", comma(i), file) + } + ctx.fmt("\n") + ctx.fmt("openat%v%v(fd const[AT_FDCWD], file ptr[in, string[%v]],"+ + " flags flags[open_flags], mode const[0]) %v\n", + suffix, partSuffix, fileFlags, fdt) + } + } + if fops.Read != "" { + ctx.fmt("read%v(fd %v, buf ptr[out, array[int8]], len bytesize[buf])\n", suffix, fdt) + } + if fops.Write != "" { + ctx.fmt("write%v(fd %v, buf ptr[in, array[int8]], len bytesize[buf])\n", suffix, fdt) + } + if fops.Mmap != "" { + ctx.fmt("mmap%v(addr vma, len len[addr], prot flags[mmap_prot],"+ + " flags flags[mmap_flags], fd %v, offset fileoff)\n", suffix, fdt) + } + if fops.Ioctl != "" { + if len(fops.IoctlCmds) == 0 { + ctx.fmt("ioctl%v(fd %v, cmd intptr, arg ptr[in, array[int8]])\n", suffix, fdt) + } else { + for _, cmd := range sortAndDedupSlice(fops.IoctlCmds) { + name := ctx.uniqualize("ioctl cmd", cmd.Name) + f := &Field{ + Name: strings.ToLower(cmd.Name), + Type: cmd.Type, + } + typ := ctx.fieldType(f, nil, "", false) + ctx.fmt("ioctl%v_%v(fd %v, cmd const[%v], arg %v)\n", + autoSuffix, name, fdt, cmd.Name, typ) + } + } + } + ctx.fmt("\n") +} + +// mapFopsToFiles maps file_operations to actual file names. +func (ctx *context) mapFopsToFiles() map[*FileOps][]string { + // Mapping turns out to be more of an art than science because + // (1) there are lots of common callback functions that present in lots of file_operations + // in different combinations, (2) some file operations are updated at runtime, + // (3) some file operations are chained at runtime and we see callbacks from several + // of them at the same time, (4) some callbacks are not reached (e.g. debugfs files + // always have write callback, but can be installed without write permission). + + // uniqueFuncs hold callback functions that are present in only 1 file_operations, + // if such a callback is matched, it's a stronger prioritization signal for that file_operations. + uniqueFuncs := make(map[string]int) + funcToFops := make(map[string][]*FileOps) + for _, fops := range ctx.FileOps { + for _, fn := range fops.ops() { + funcToFops[fn] = append(funcToFops[fn], fops) + uniqueFuncs[fn]++ + } + } + pcToFunc := make(map[uint64]string) + for _, pc := range ctx.probe.PCs { + pcToFunc[pc.PC] = pc.Func + } + // matchedFuncs holds functions are present in any file_operations callbacks + // (lots of coverage is not related to any file_operations at all). + matchedFuncs := make(map[string]bool) + // Maps file names to set of all callbacks that operations on the file has reached. + fileToFuncs := make(map[string]map[string]bool) + for _, file := range ctx.probe.Files { + funcs := make(map[string]bool) + fileToFuncs[file.Name] = funcs + for _, pc := range file.Cover { + fn := pcToFunc[pc] + if len(funcToFops[fn]) != 0 { + funcs[fn] = true + matchedFuncs[fn] = true + } + } + } + // This is a special entry for files that has only open callback + // (it does not make sense to differentiate them further). + generic := &FileOps{ + Name: "generic", + Open: "only_open", + } + ctx.FileOps = append(ctx.FileOps, generic) + fopsToFiles := make(map[*FileOps][]string) + for _, file := range ctx.probe.Files { + // For each file figure out the potential file_operations that match this file best. + funcs := fileToFuncs[file.Name] + // First collect all candidates (all file_operations for which at least 1 callback was triggered). + candidates := make(map[*FileOps]int) + for fn := range funcs { + for _, fops := range funcToFops[fn] { + if fops.Open != "" && len(fops.ops()) == 1 { + // If it has only open, it's not very interesting + // (we will use generic for it below). + continue + } + hasUnique := false + for _, fn := range fops.ops() { + if uniqueFuncs[fn] == 1 { + hasUnique = true + } + } + // If we've triggered at least one unique callback, we take this + // file_operations in any case. Otherwise check if file_operations + // has open/ioctl that we haven't triggered. + // Note that it may have open/ioctl, and this is the right file_operations + // for the file, yet we haven't triggered them for reasons described + // in the beginning of the function. + if !hasUnique { + if fops.Open != "" && !funcs[fops.Open] { + continue + } + if fops.Ioctl != "" && !funcs[fops.Ioctl] { + continue + } + } + candidates[fops] = 0 + } + } + if len(candidates) == 0 { + candidates[generic] = 0 + } + // Now find the best set of candidates. + // There are lots of false positives due to common callback functions. + maxScore := 0 + for fops := range candidates { + ops := fops.ops() + // All else being equal prefer file_operations with more callbacks defined. + score := len(ops) + for _, fn := range ops { + if !funcs[fn] { + continue + } + // Matched callbacks increase the score. + score += 10 + // If we matched ioctl, bump score by a lot. + // We do want to emit ioctl's b/c they the only non-trivial + // operations we emit at the moment. + if fn == fops.Ioctl { + score += 100 + } + // Unique callbacks are the strongest prioritization signal. + // Besides some corner cases there is no way we can reach a unique callback + // from a wrong file (a corner case would be in one callback calls another + // callback directly). + if uniqueFuncs[fn] == 1 { + score += 1000 + } + } + candidates[fops] = score + maxScore = max(maxScore, score) + } + // Now, take the candidates with the highest score (there still may be several of them). + var best []*FileOps + for fops, score := range candidates { + if score == maxScore { + best = append(best, fops) + } + } + best = sortAndDedupSlice(best) + // Now, filter out some excessive file_operations. + // An example of an excessive case is if we have 2 file_operations with just read+write, + // currently we emit generic read/write operations, so we would emit completly equal + // descriptions for both. Ioctl commands is the only non-generic descriptions we emit now, + // so if a file_operations has any commands, it won't be considered excessive. + // Note that if we generate specialized descriptions for read/write/mmap in future, + // then these won't be considered excessive as well. + excessive := make(map[*FileOps]bool) + for i := 0; i < len(best); i++ { + for j := i + 1; j < len(best); j++ { + a, b := best[i], best[j] + if (a.Ioctl == b.Ioctl || len(a.IoctlCmds)+len(b.IoctlCmds) == 0) && + (a.Read == "") == (b.Read == "") && + (a.Write == "") == (b.Write == "") && + (a.Mmap == "") == (b.Mmap == "") && + (a.Ioctl == "") == (b.Ioctl == "") { + excessive[b] = true + } + } + } + // Finally record the file for the best non-excessive file_operations + // (there are still can be several of them). + for _, fops := range best { + if !excessive[fops] { + fopsToFiles[fops] = append(fopsToFiles[fops], file.Name) + } + } + } + for fops, files := range fopsToFiles { + slices.Sort(files) + fopsToFiles[fops] = files + } + return fopsToFiles +} + +func (fops *FileOps) ops() []string { + var ops []string + for _, op := range []string{fops.Open, fops.Read, fops.Write, fops.Mmap, fops.Ioctl} { + if op != "" { + ops = append(ops, op) + } + } + return ops +} |
