github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/declextract/fileops.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 declextract 5 6 import ( 7 "fmt" 8 "slices" 9 "strings" 10 11 "github.com/google/syzkaller/pkg/ast" 12 "github.com/google/syzkaller/pkg/clangtool" 13 ) 14 15 const ( 16 ioctlCmdArg = 1 17 ioctlArgArg = 2 18 ) 19 20 func (ctx *context) serializeFileOps() { 21 for _, ioctl := range ctx.Ioctls { 22 ctx.ioctls[ioctl.Name] = ioctl.Type 23 } 24 uniqueFuncs := ctx.resolveFopsCallbacks() 25 fopsToFiles := ctx.mapFopsToFiles(uniqueFuncs) 26 for _, fops := range ctx.FileOps { 27 files := fopsToFiles[fops] 28 canGenerate := Tristate(len(files) != 0) 29 for _, op := range []*Function{fops.open, fops.read, fops.write, fops.mmap} { 30 if op == nil { 31 continue 32 } 33 if op == fops.open && (uniqueFuncs[fops.read] == 1 || uniqueFuncs[fops.write] == 1 || 34 uniqueFuncs[fops.mmap] == 1 || uniqueFuncs[fops.ioctl] == 1) { 35 continue 36 } 37 ctx.noteInterface(&Interface{ 38 Type: IfaceFileop, 39 Name: op.Name, 40 Func: op.Name, 41 Files: []string{op.File}, 42 AutoDescriptions: canGenerate, 43 }) 44 } 45 var ioctlCmds []string 46 if fops.ioctl != nil { 47 ioctlCmds = ctx.inferCommandVariants(fops.Ioctl, fops.SourceFile, ioctlCmdArg) 48 for _, cmd := range ioctlCmds { 49 ctx.noteInterface(&Interface{ 50 Type: IfaceIoctl, 51 Name: cmd, 52 IdentifyingConst: cmd, 53 Files: []string{fops.ioctl.File}, 54 Func: fops.Ioctl, 55 AutoDescriptions: canGenerate, 56 scopeArg: ioctlCmdArg, 57 scopeVal: cmd, 58 }) 59 } 60 if len(ioctlCmds) == 0 { 61 ctx.noteInterface(&Interface{ 62 Type: IfaceIoctl, 63 Name: fops.Ioctl, 64 Files: []string{fops.ioctl.File}, 65 Func: fops.Ioctl, 66 AutoDescriptions: canGenerate, 67 }) 68 } 69 } 70 if len(files) == 0 { 71 continue // each unmapped entry means some code we don't know how to cover yet 72 } 73 ctx.createFops(fops, files, ioctlCmds) 74 } 75 } 76 77 func (ctx *context) createFops(fops *FileOps, files, ioctlCmds []string) { 78 name := ctx.uniqualize("fops name", fops.Name) 79 // If it has only open, then emit only openat that returns generic fd. 80 fdt := "fd" 81 if len(fops.ops) > 1 || fops.Open == "" { 82 fdt = fmt.Sprintf("fd_%v", name) 83 ctx.fmt("resource %v[fd]\n", fdt) 84 } 85 suffix := autoSuffix + "_" + name 86 fileFlags := fmt.Sprintf("\"%s\"", files[0]) 87 if len(files) > 1 { 88 fileFlags = fmt.Sprintf("%v_files", name) 89 ctx.fmt("%v = ", fileFlags) 90 for i, file := range files { 91 ctx.fmt("%v \"%v\"", comma(i), file) 92 } 93 ctx.fmt("\n") 94 } 95 ctx.fmt("openat%v(fd const[AT_FDCWD], file ptr[in, string[%v]], flags flags[open_flags], mode const[0]) %v\n", 96 suffix, fileFlags, fdt) 97 if fops.Read != "" { 98 ctx.fmt("read%v(fd %v, buf ptr[out, array[int8]], len bytesize[buf])\n", suffix, fdt) 99 } 100 if fops.Write != "" { 101 ctx.fmt("write%v(fd %v, buf ptr[in, array[int8]], len bytesize[buf])\n", suffix, fdt) 102 } 103 if fops.Mmap != "" { 104 ctx.fmt("mmap%v(addr vma, len len[addr], prot flags[mmap_prot],"+ 105 " flags flags[mmap_flags], fd %v, offset fileoff)\n", suffix, fdt) 106 } 107 if fops.Ioctl != "" { 108 ctx.createIoctls(fops, ioctlCmds, suffix, fdt) 109 } 110 ctx.fmt("\n") 111 } 112 113 func (ctx *context) createIoctls(fops *FileOps, ioctlCmds []string, suffix, fdt string) { 114 const defaultArgType = "ptr[in, array[int8]]" 115 cmds := ctx.inferCommandVariants(fops.Ioctl, fops.SourceFile, ioctlCmdArg) 116 if len(cmds) == 0 { 117 retType := ctx.inferReturnType(fops.Ioctl, fops.SourceFile, -1, "") 118 argType := ctx.inferArgType(fops.Ioctl, fops.SourceFile, ioctlArgArg, -1, "") 119 if argType == "" { 120 argType = defaultArgType 121 } 122 ctx.fmt("ioctl%v(fd %v, cmd intptr, arg %v) %v\n", suffix, fdt, argType, retType) 123 return 124 } 125 for _, cmd := range cmds { 126 argType := defaultArgType 127 if typ := ctx.ioctls[cmd]; typ != nil { 128 f := &Field{ 129 Name: strings.ToLower(cmd), 130 Type: typ, 131 } 132 argType = ctx.fieldType(f, nil, "", false) 133 } else { 134 argType = ctx.inferArgType(fops.Ioctl, fops.SourceFile, ioctlArgArg, ioctlCmdArg, cmd) 135 if argType == "" { 136 argType = defaultArgType 137 } 138 } 139 retType := ctx.inferReturnType(fops.Ioctl, fops.SourceFile, ioctlCmdArg, cmd) 140 name := ctx.uniqualize("ioctl cmd", cmd) 141 ctx.fmt("ioctl%v_%v(fd %v, cmd const[%v], arg %v) %v\n", 142 autoSuffix, name, fdt, cmd, argType, retType) 143 } 144 } 145 146 // mapFopsToFiles maps file_operations to actual file names. 147 func (ctx *context) mapFopsToFiles(uniqueFuncs map[*Function]int) map[*FileOps][]string { 148 // Mapping turns out to be more of an art than science because 149 // (1) there are lots of common callback functions that present in lots of file_operations 150 // in different combinations, (2) some file operations are updated at runtime, 151 // (3) some file operations are chained at runtime and we see callbacks from several 152 // of them at the same time, (4) some callbacks are not reached (e.g. debugfs files 153 // always have write callback, but can be installed without write permission). 154 // If a callback that is present in only 1 file_operations is matched, 155 // it's a stronger prioritization signal for that file_operations. 156 157 funcToFops := make(map[*Function][]*FileOps) 158 for _, fops := range ctx.FileOps { 159 for _, fn := range fops.ops { 160 funcToFops[fn] = append(funcToFops[fn], fops) 161 } 162 } 163 // Maps file names to set of all callbacks that operations on the file has reached. 164 fileToFuncs := make(map[string]map[*Function]bool) 165 for _, file := range ctx.probe.Files { 166 funcs := make(map[*Function]bool) 167 fileToFuncs[file.Name] = funcs 168 for _, pc := range file.Cover { 169 fn := ctx.findFunc(ctx.probe.PCs[pc].Func, ctx.probe.PCs[pc].File) 170 if len(funcToFops[fn]) != 0 { 171 funcs[fn] = true 172 } 173 } 174 } 175 // This is a special entry for files that has only open callback 176 // (it does not make sense to differentiate them further). 177 generic := &FileOps{ 178 Name: "generic", 179 Open: "only_open", 180 fileOps: &fileOps{}, 181 } 182 ctx.FileOps = append(ctx.FileOps, generic) 183 fopsToFiles := make(map[*FileOps][]string) 184 for _, file := range ctx.probe.Files { 185 // There is a single non US-ASCII file in sysfs: "/sys/bus/pci/drivers/CAFÉ NAND". 186 // Ignore it for now as descriptions shouldn't contain non US-ASCII chars. 187 if ast.IsValidStringLit(file.Name) >= 0 { 188 continue 189 } 190 // For each file figure out the potential file_operations that match this file best. 191 best := ctx.mapFileToFops(fileToFuncs[file.Name], funcToFops, uniqueFuncs, generic) 192 for _, fops := range best { 193 fopsToFiles[fops] = append(fopsToFiles[fops], file.Name) 194 } 195 } 196 for fops, files := range fopsToFiles { 197 slices.Sort(files) 198 fopsToFiles[fops] = files 199 } 200 return fopsToFiles 201 } 202 203 func (ctx *context) mapFileToFops(funcs map[*Function]bool, funcToFops map[*Function][]*FileOps, 204 uniqueFuncs map[*Function]int, generic *FileOps) []*FileOps { 205 // First collect all candidates (all file_operations for which at least 1 callback was triggered). 206 candidates := ctx.fileCandidates(funcs, funcToFops, uniqueFuncs) 207 if len(candidates) == 0 { 208 candidates[generic] = 0 209 } 210 // Now find the best set of candidates. 211 // There are lots of false positives due to common callback functions. 212 maxScore := 0 213 for fops := range candidates { 214 ops := fops.ops 215 // All else being equal prefer file_operations with more callbacks defined. 216 score := len(ops) 217 for _, fn := range ops { 218 if !funcs[fn] { 219 continue 220 } 221 // Matched callbacks increase the score. 222 score += 10 223 // If we matched ioctl, bump score by a lot. 224 // We do want to emit ioctl's b/c they the only non-trivial 225 // operations we emit at the moment. 226 if fn == fops.ioctl { 227 score += 100 228 } 229 // Unique callbacks are the strongest prioritization signal. 230 // Besides some corner cases there is no way we can reach a unique callback 231 // from a wrong file (a corner case would be in one callback calls another 232 // callback directly). 233 if uniqueFuncs[fn] == 1 { 234 score += 1000 235 } 236 } 237 candidates[fops] = score 238 maxScore = max(maxScore, score) 239 } 240 // Now, take the candidates with the highest score (there still may be several of them). 241 var best []*FileOps 242 for fops, score := range candidates { 243 if score == maxScore { 244 best = append(best, fops) 245 } 246 } 247 best = clangtool.SortAndDedupSlice(best) 248 // Now, filter out some excessive file_operations. 249 // An example of an excessive case is if we have 2 file_operations with just read+write, 250 // currently we emit generic read/write operations, so we would emit completly equal 251 // descriptions for both. Ioctl commands is the only non-generic descriptions we emit now, 252 // so if a file_operations has an ioctl handler, it won't be considered excessive. 253 // Note that if we generate specialized descriptions for read/write/mmap in future, 254 // then these won't be considered excessive as well. 255 excessive := make(map[*FileOps]bool) 256 for i := 0; i < len(best); i++ { 257 for j := i + 1; j < len(best); j++ { 258 a, b := best[i], best[j] 259 if (a.Ioctl == b.Ioctl) && 260 (a.Read == "") == (b.Read == "") && 261 (a.Write == "") == (b.Write == "") && 262 (a.Mmap == "") == (b.Mmap == "") && 263 (a.Ioctl == "") == (b.Ioctl == "") { 264 excessive[b] = true 265 } 266 } 267 } 268 // Finally record the file for the best non-excessive file_operations 269 // (there are still can be several of them). 270 best = slices.DeleteFunc(best, func(fops *FileOps) bool { 271 return excessive[fops] 272 }) 273 return best 274 } 275 276 func (ctx *context) fileCandidates(funcs map[*Function]bool, funcToFops map[*Function][]*FileOps, 277 uniqueFuncs map[*Function]int) map[*FileOps]int { 278 candidates := make(map[*FileOps]int) 279 for fn := range funcs { 280 for _, fops := range funcToFops[fn] { 281 if fops.Open != "" && len(fops.ops) == 1 { 282 // If it has only open, it's not very interesting 283 // (we will use generic for it below). 284 continue 285 } 286 hasUnique := false 287 for _, fn := range fops.ops { 288 if uniqueFuncs[fn] == 1 { 289 hasUnique = true 290 } 291 } 292 // If we've triggered at least one unique callback, we take this 293 // file_operations in any case. Otherwise check if file_operations 294 // has open/ioctl that we haven't triggered. 295 // Note that it may have open/ioctl, and this is the right file_operations 296 // for the file, yet we haven't triggered them for reasons described 297 // in the beginning of the function. 298 if !hasUnique { 299 if fops.open != nil && !funcs[fops.open] { 300 continue 301 } 302 if fops.ioctl != nil && !funcs[fops.ioctl] { 303 continue 304 } 305 } 306 candidates[fops] = 0 307 } 308 } 309 return candidates 310 } 311 312 func (ctx *context) resolveFopsCallbacks() map[*Function]int { 313 uniqueFuncs := make(map[*Function]int) 314 for _, fops := range ctx.FileOps { 315 fops.fileOps = &fileOps{ 316 open: ctx.mustFindFunc(fops.Open, fops.SourceFile), 317 read: ctx.mustFindFunc(fops.Read, fops.SourceFile), 318 write: ctx.mustFindFunc(fops.Write, fops.SourceFile), 319 mmap: ctx.mustFindFunc(fops.Mmap, fops.SourceFile), 320 ioctl: ctx.mustFindFunc(fops.Ioctl, fops.SourceFile), 321 } 322 for _, op := range []*Function{fops.open, fops.read, fops.write, fops.mmap, fops.ioctl} { 323 if op == nil { 324 continue 325 } 326 fops.ops = append(fops.ops, op) 327 uniqueFuncs[op]++ 328 } 329 } 330 return uniqueFuncs 331 }