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 }