github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/prog/minimization.go (about)

     1  // Copyright 2018 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 prog
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"reflect"
    10  
    11  	"github.com/google/syzkaller/pkg/hash"
    12  	"github.com/google/syzkaller/pkg/stat"
    13  )
    14  
    15  var (
    16  	statMinRemoveCall = stat.New("minimize: call",
    17  		"Total number of remove call attempts during minimization", stat.StackedGraph("minimize"))
    18  	statMinRemoveProps = stat.New("minimize: props",
    19  		"Total number of remove properties attempts during minimization", stat.StackedGraph("minimize"))
    20  	statMinPtr = stat.New("minimize: pointer",
    21  		"Total number of pointer minimization attempts", stat.StackedGraph("minimize"))
    22  	statMinArray = stat.New("minimize: array",
    23  		"Total number of array minimization attempts", stat.StackedGraph("minimize"))
    24  	statMinInt = stat.New("minimize: integer",
    25  		"Total number of integer minimization attempts", stat.StackedGraph("minimize"))
    26  	statMinResource = stat.New("minimize: resource",
    27  		"Total number of resource minimization attempts", stat.StackedGraph("minimize"))
    28  	statMinBuffer = stat.New("minimize: buffer",
    29  		"Total number of buffer minimization attempts", stat.StackedGraph("minimize"))
    30  	statMinFilename = stat.New("minimize: filename",
    31  		"Total number of filename minimization attempts", stat.StackedGraph("minimize"))
    32  )
    33  
    34  type MinimizeMode int
    35  
    36  const (
    37  	// Minimize for inclusion into corpus.
    38  	// This generally tries to reduce number of arguments for future mutation.
    39  	MinimizeCorpus MinimizeMode = iota
    40  	// Minimize crash reproducer.
    41  	// This mode assumes each test is expensive (need to reboot), so tries fewer things.
    42  	MinimizeCrash
    43  	// Minimize crash reproducer in snapshot mode.
    44  	// This mode does not assume that tests are expensive, and tries to minimize for reproducer readability.
    45  	MinimizeCrashSnapshot
    46  	// Only try to remove calls.
    47  	MinimizeCallsOnly
    48  )
    49  
    50  // Minimize minimizes program p into an equivalent program using the equivalence
    51  // predicate pred. It iteratively generates simpler programs and asks pred
    52  // whether it is equal to the original program or not. If it is equivalent then
    53  // the simplification attempt is committed and the process continues.
    54  func Minimize(p0 *Prog, callIndex0 int, mode MinimizeMode, pred0 func(*Prog, int) bool) (*Prog, int) {
    55  	// Generally we try to avoid generating duplicates, but in some cases they are hard to avoid.
    56  	// For example, if we have an array with several equal elements, removing them leads to the same program.
    57  	dedup := make(map[string]bool)
    58  	pred := func(p *Prog, callIndex int, what *stat.Val, path string) bool {
    59  		// Note: path is unused, but is useful for manual debugging.
    60  		what.Add(1)
    61  		p.sanitizeFix()
    62  		p.debugValidate()
    63  		id := hash.String(p.Serialize())
    64  		if _, ok := dedup[id]; !ok {
    65  			dedup[id] = pred0(p, callIndex)
    66  		}
    67  		return dedup[id]
    68  	}
    69  	name0 := ""
    70  	if callIndex0 != -1 {
    71  		if callIndex0 < 0 || callIndex0 >= len(p0.Calls) {
    72  			panic("bad call index")
    73  		}
    74  		name0 = p0.Calls[callIndex0].Meta.Name
    75  	}
    76  
    77  	// Try to remove all calls except the last one one-by-one.
    78  	p0, callIndex0 = removeCalls(p0, callIndex0, pred)
    79  
    80  	if mode != MinimizeCallsOnly {
    81  		// Try to reset all call props to their default values.
    82  		p0 = resetCallProps(p0, callIndex0, pred)
    83  
    84  		// Try to minimize individual calls.
    85  		for i := 0; i < len(p0.Calls); i++ {
    86  			if p0.Calls[i].Meta.Attrs.NoMinimize {
    87  				continue
    88  			}
    89  			ctx := &minimizeArgsCtx{
    90  				target:     p0.Target,
    91  				p0:         &p0,
    92  				callIndex0: callIndex0,
    93  				mode:       mode,
    94  				pred:       pred,
    95  				triedPaths: make(map[string]bool),
    96  			}
    97  		again:
    98  			ctx.p = p0.Clone()
    99  			ctx.call = ctx.p.Calls[i]
   100  			for j, field := range ctx.call.Meta.Args {
   101  				if ctx.do(ctx.call.Args[j], field.Name, fmt.Sprintf("call%v", i)) {
   102  					goto again
   103  				}
   104  			}
   105  			p0 = minimizeCallProps(p0, i, callIndex0, pred)
   106  		}
   107  	}
   108  
   109  	if callIndex0 != -1 {
   110  		if callIndex0 < 0 || callIndex0 >= len(p0.Calls) || name0 != p0.Calls[callIndex0].Meta.Name {
   111  			panic(fmt.Sprintf("bad call index after minimization: ncalls=%v index=%v call=%v/%v",
   112  				len(p0.Calls), callIndex0, name0, p0.Calls[callIndex0].Meta.Name))
   113  		}
   114  	}
   115  	return p0, callIndex0
   116  }
   117  
   118  type minimizePred func(*Prog, int, *stat.Val, string) bool
   119  
   120  func removeCalls(p0 *Prog, callIndex0 int, pred minimizePred) (*Prog, int) {
   121  	if callIndex0 >= 0 && callIndex0+2 < len(p0.Calls) {
   122  		// It's frequently the case that all subsequent calls were not necessary.
   123  		// Try to drop them all at once.
   124  		p := p0.Clone()
   125  		for i := len(p0.Calls) - 1; i > callIndex0; i-- {
   126  			p.RemoveCall(i)
   127  		}
   128  		if pred(p, callIndex0, statMinRemoveCall, "trailing calls") {
   129  			p0 = p
   130  		}
   131  	}
   132  
   133  	if callIndex0 != -1 {
   134  		p0, callIndex0 = removeUnrelatedCalls(p0, callIndex0, pred)
   135  	}
   136  
   137  	for i := len(p0.Calls) - 1; i >= 0; i-- {
   138  		if i == callIndex0 {
   139  			continue
   140  		}
   141  		callIndex := callIndex0
   142  		if i < callIndex {
   143  			callIndex--
   144  		}
   145  		p := p0.Clone()
   146  		p.RemoveCall(i)
   147  		if !pred(p, callIndex, statMinRemoveCall, fmt.Sprintf("call %v", i)) {
   148  			continue
   149  		}
   150  		p0 = p
   151  		callIndex0 = callIndex
   152  	}
   153  	return p0, callIndex0
   154  }
   155  
   156  // removeUnrelatedCalls tries to remove all "unrelated" calls at once.
   157  // Unrelated calls are the calls that don't use any resources/files from
   158  // the transitive closure of the resources/files used by the target call.
   159  // This may significantly reduce large generated programs in a single step.
   160  func removeUnrelatedCalls(p0 *Prog, callIndex0 int, pred minimizePred) (*Prog, int) {
   161  	keepCalls := relatedCalls(p0, callIndex0)
   162  	if len(p0.Calls)-len(keepCalls) < 3 {
   163  		return p0, callIndex0
   164  	}
   165  	p, callIndex := p0.Clone(), callIndex0
   166  	for i := len(p0.Calls) - 1; i >= 0; i-- {
   167  		if keepCalls[i] {
   168  			continue
   169  		}
   170  		p.RemoveCall(i)
   171  		if i < callIndex {
   172  			callIndex--
   173  		}
   174  	}
   175  	if !pred(p, callIndex, statMinRemoveCall, "unrelated calls") {
   176  		return p0, callIndex0
   177  	}
   178  	return p, callIndex
   179  }
   180  
   181  func relatedCalls(p0 *Prog, callIndex0 int) map[int]bool {
   182  	keepCalls := map[int]bool{callIndex0: true}
   183  	used := uses(p0.Calls[callIndex0])
   184  	for {
   185  		n := len(used)
   186  		for i, call := range p0.Calls {
   187  			if keepCalls[i] {
   188  				continue
   189  			}
   190  			used1 := uses(call)
   191  			if intersects(used, used1) {
   192  				keepCalls[i] = true
   193  				for what := range used1 {
   194  					used[what] = true
   195  				}
   196  			}
   197  		}
   198  		if n == len(used) {
   199  			return keepCalls
   200  		}
   201  	}
   202  }
   203  
   204  func uses(call *Call) map[any]bool {
   205  	used := make(map[any]bool)
   206  	ForeachArg(call, func(arg Arg, _ *ArgCtx) {
   207  		switch typ := arg.Type().(type) {
   208  		case *ResourceType:
   209  			a := arg.(*ResultArg)
   210  			used[a] = true
   211  			if a.Res != nil {
   212  				used[a.Res] = true
   213  			}
   214  			for use := range a.uses {
   215  				used[use] = true
   216  			}
   217  		case *BufferType:
   218  			a := arg.(*DataArg)
   219  			if a.Dir() != DirOut && typ.Kind == BufferFilename {
   220  				val := string(bytes.TrimRight(a.Data(), "\x00"))
   221  				used[val] = true
   222  			}
   223  		}
   224  	})
   225  	return used
   226  }
   227  
   228  func intersects(list, list1 map[any]bool) bool {
   229  	for what := range list1 {
   230  		if list[what] {
   231  			return true
   232  		}
   233  	}
   234  	return false
   235  }
   236  
   237  func resetCallProps(p0 *Prog, callIndex0 int, pred minimizePred) *Prog {
   238  	// Try to reset all call props to their default values.
   239  	// This should be reasonable for many progs.
   240  	p := p0.Clone()
   241  	anyDifferent := false
   242  	for idx := range p.Calls {
   243  		if !reflect.DeepEqual(p.Calls[idx].Props, CallProps{}) {
   244  			p.Calls[idx].Props = CallProps{}
   245  			anyDifferent = true
   246  		}
   247  	}
   248  	if anyDifferent && pred(p, callIndex0, statMinRemoveProps, "props") {
   249  		return p
   250  	}
   251  	return p0
   252  }
   253  
   254  func minimizeCallProps(p0 *Prog, callIndex, callIndex0 int, pred minimizePred) *Prog {
   255  	props := p0.Calls[callIndex].Props
   256  
   257  	// Try to drop fault injection.
   258  	if props.FailNth > 0 {
   259  		p := p0.Clone()
   260  		p.Calls[callIndex].Props.FailNth = 0
   261  		if pred(p, callIndex0, statMinRemoveProps, "props") {
   262  			p0 = p
   263  		}
   264  	}
   265  
   266  	// Try to drop async.
   267  	if props.Async {
   268  		p := p0.Clone()
   269  		p.Calls[callIndex].Props.Async = false
   270  		if pred(p, callIndex0, statMinRemoveProps, "props") {
   271  			p0 = p
   272  		}
   273  	}
   274  
   275  	// Try to drop rerun.
   276  	if props.Rerun > 0 {
   277  		p := p0.Clone()
   278  		p.Calls[callIndex].Props.Rerun = 0
   279  		if pred(p, callIndex0, statMinRemoveProps, "props") {
   280  			p0 = p
   281  		}
   282  	}
   283  
   284  	return p0
   285  }
   286  
   287  type minimizeArgsCtx struct {
   288  	target     *Target
   289  	p0         **Prog
   290  	p          *Prog
   291  	call       *Call
   292  	callIndex0 int
   293  	mode       MinimizeMode
   294  	pred       minimizePred
   295  	triedPaths map[string]bool
   296  }
   297  
   298  func (ctx *minimizeArgsCtx) do(arg Arg, field, path string) bool {
   299  	path += fmt.Sprintf("-%v", field)
   300  	if ctx.triedPaths[path] {
   301  		return false
   302  	}
   303  	p0 := *ctx.p0
   304  	if arg.Type().minimize(ctx, arg, path) {
   305  		return true
   306  	}
   307  	if *ctx.p0 == ctx.p {
   308  		// If minimize committed a new program, it must return true.
   309  		// Otherwise *ctx.p0 and ctx.p will point to the same program
   310  		// and any temp mutations to ctx.p will unintentionally affect ctx.p0.
   311  		panic("shared program committed")
   312  	}
   313  	if *ctx.p0 != p0 {
   314  		// New program was committed, but we did not start iteration anew.
   315  		// This means we are iterating over a stale tree and any changes won't be visible.
   316  		panic("iterating over stale program")
   317  	}
   318  	ctx.triedPaths[path] = true
   319  	return false
   320  }
   321  
   322  func (typ *TypeCommon) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   323  	return false
   324  }
   325  
   326  func (typ *StructType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   327  	a := arg.(*GroupArg)
   328  	for i, innerArg := range a.Inner {
   329  		if ctx.do(innerArg, typ.Fields[i].Name, path) {
   330  			return true
   331  		}
   332  	}
   333  	return false
   334  }
   335  
   336  func (typ *UnionType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   337  	a := arg.(*UnionArg)
   338  	return ctx.do(a.Option, typ.Fields[a.Index].Name, path)
   339  }
   340  
   341  func (typ *PtrType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   342  	a := arg.(*PointerArg)
   343  	if a.Res == nil {
   344  		return false
   345  	}
   346  	if path1 := path + ">"; !ctx.triedPaths[path1] {
   347  		removeArg(a.Res)
   348  		replaceArg(a, MakeSpecialPointerArg(a.Type(), a.Dir(), 0))
   349  		ctx.target.assignSizesCall(ctx.call)
   350  		if ctx.pred(ctx.p, ctx.callIndex0, statMinPtr, path1) {
   351  			*ctx.p0 = ctx.p
   352  		}
   353  		ctx.triedPaths[path1] = true
   354  		return true
   355  	}
   356  	return ctx.do(a.Res, "", path)
   357  }
   358  
   359  func (typ *ArrayType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   360  	a := arg.(*GroupArg)
   361  	// If there are at least 3 elements, try to remove all at once first.
   362  	// If will be faster than removing them one-by-one if all of them are not needed.
   363  	if allPath := path + "-all"; len(a.Inner) >= 3 && typ.RangeBegin == 0 && !ctx.triedPaths[allPath] {
   364  		ctx.triedPaths[allPath] = true
   365  		for _, elem := range a.Inner {
   366  			removeArg(elem)
   367  		}
   368  		a.Inner = nil
   369  		ctx.target.assignSizesCall(ctx.call)
   370  		if ctx.pred(ctx.p, ctx.callIndex0, statMinArray, allPath) {
   371  			*ctx.p0 = ctx.p
   372  		}
   373  		return true
   374  	}
   375  	// Try to remove individual elements one-by-one.
   376  	for i := len(a.Inner) - 1; i >= 0; i-- {
   377  		elem := a.Inner[i]
   378  		elemPath := fmt.Sprintf("%v-%v", path, i)
   379  		if ctx.mode != MinimizeCrash && !ctx.triedPaths[elemPath] &&
   380  			(typ.Kind == ArrayRandLen ||
   381  				typ.Kind == ArrayRangeLen && uint64(len(a.Inner)) > typ.RangeBegin) {
   382  			ctx.triedPaths[elemPath] = true
   383  			copy(a.Inner[i:], a.Inner[i+1:])
   384  			a.Inner = a.Inner[:len(a.Inner)-1]
   385  			removeArg(elem)
   386  			ctx.target.assignSizesCall(ctx.call)
   387  			if ctx.pred(ctx.p, ctx.callIndex0, statMinArray, elemPath) {
   388  				*ctx.p0 = ctx.p
   389  			}
   390  			return true
   391  		}
   392  		if ctx.do(elem, "", elemPath) {
   393  			return true
   394  		}
   395  	}
   396  	return false
   397  }
   398  
   399  func (typ *IntType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   400  	return minimizeInt(ctx, arg, path)
   401  }
   402  
   403  func (typ *FlagsType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   404  	return minimizeInt(ctx, arg, path)
   405  }
   406  
   407  func (typ *ProcType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   408  	if !typ.Optional() {
   409  		// Default value for ProcType is 0 (same for all PID's).
   410  		// Usually 0 either does not make sense at all or make different PIDs collide
   411  		// (since we use ProcType to separate value ranges for different PIDs).
   412  		// So don't change ProcType to 0 unless the type is explicitly marked as opt
   413  		// (in that case we will also generate 0 anyway).
   414  		return false
   415  	}
   416  	return minimizeInt(ctx, arg, path)
   417  }
   418  
   419  func minimizeInt(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   420  	if ctx.mode != MinimizeCrashSnapshot {
   421  		return false
   422  	}
   423  	a := arg.(*ConstArg)
   424  	def := arg.Type().DefaultArg(arg.Dir()).(*ConstArg)
   425  	if a.Val == def.Val {
   426  		return false
   427  	}
   428  	v0 := a.Val
   429  	a.Val = def.Val
   430  
   431  	// By mutating an integer, we risk violating conditional fields.
   432  	// If the fields are patched, the minimization process must be restarted.
   433  	patched := ctx.call.setDefaultConditions(ctx.p.Target, false)
   434  	if ctx.pred(ctx.p, ctx.callIndex0, statMinInt, path) {
   435  		*ctx.p0 = ctx.p
   436  		ctx.triedPaths[path] = true
   437  		return true
   438  	}
   439  	a.Val = v0
   440  	if patched {
   441  		// No sense to return here.
   442  		ctx.triedPaths[path] = true
   443  	}
   444  	return patched
   445  }
   446  
   447  func (typ *ResourceType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   448  	if ctx.mode != MinimizeCrashSnapshot {
   449  		return false
   450  	}
   451  	a := arg.(*ResultArg)
   452  	if a.Res == nil {
   453  		return false
   454  	}
   455  	r0 := a.Res
   456  	delete(a.Res.uses, a)
   457  	a.Res, a.Val = nil, typ.Default()
   458  	if ctx.pred(ctx.p, ctx.callIndex0, statMinResource, path) {
   459  		*ctx.p0 = ctx.p
   460  	} else {
   461  		a.Res, a.Val = r0, 0
   462  		a.Res.uses[a] = true
   463  	}
   464  	ctx.triedPaths[path] = true
   465  	return true
   466  }
   467  
   468  func (typ *BufferType) minimize(ctx *minimizeArgsCtx, arg Arg, path string) bool {
   469  	if arg.Dir() == DirOut {
   470  		return false
   471  	}
   472  	if typ.IsCompressed() {
   473  		panic(fmt.Sprintf("minimizing `no_minimize` call %v", ctx.call.Meta.Name))
   474  	}
   475  	a := arg.(*DataArg)
   476  	switch typ.Kind {
   477  	case BufferBlobRand, BufferBlobRange:
   478  		len0 := len(a.Data())
   479  		minLen := int(typ.RangeBegin)
   480  		for step := len(a.Data()) - minLen; len(a.Data()) > minLen && step > 0; {
   481  			if len(a.Data())-step >= minLen {
   482  				a.data = a.Data()[:len(a.Data())-step]
   483  				ctx.target.assignSizesCall(ctx.call)
   484  				if ctx.pred(ctx.p, ctx.callIndex0, statMinBuffer, path) {
   485  					step /= 2
   486  					continue
   487  				}
   488  				a.data = a.Data()[:len(a.Data())+step]
   489  				ctx.target.assignSizesCall(ctx.call)
   490  			}
   491  			step /= 2
   492  			if ctx.mode == MinimizeCrash {
   493  				break
   494  			}
   495  		}
   496  		if len(a.Data()) != len0 {
   497  			*ctx.p0 = ctx.p
   498  			ctx.triedPaths[path] = true
   499  			return true
   500  		}
   501  	case BufferFilename:
   502  		if ctx.mode == MinimizeCorpus {
   503  			return false
   504  		}
   505  		// Try to undo target.SpecialFileLenghts mutation
   506  		// and reduce file name length.
   507  		if !typ.Varlen() {
   508  			return false
   509  		}
   510  		data0 := append([]byte{}, a.Data()...)
   511  		a.data = bytes.TrimRight(a.Data(), specialFileLenPad+"\x00")
   512  		if !typ.NoZ {
   513  			a.data = append(a.data, 0)
   514  		}
   515  		if bytes.Equal(a.data, data0) {
   516  			return false
   517  		}
   518  		ctx.target.assignSizesCall(ctx.call)
   519  		if ctx.pred(ctx.p, ctx.callIndex0, statMinFilename, path) {
   520  			*ctx.p0 = ctx.p
   521  		}
   522  		ctx.triedPaths[path] = true
   523  		return true
   524  	}
   525  	return false
   526  }