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  }