github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/manager/crash.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 manager
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/google/syzkaller/pkg/hash"
    17  	"github.com/google/syzkaller/pkg/log"
    18  	"github.com/google/syzkaller/pkg/mgrconfig"
    19  	"github.com/google/syzkaller/pkg/osutil"
    20  	"github.com/google/syzkaller/pkg/report"
    21  	"github.com/google/syzkaller/prog"
    22  )
    23  
    24  type CrashStore struct {
    25  	Tag          string
    26  	BaseDir      string
    27  	MaxCrashLogs int
    28  	MaxReproLogs int
    29  }
    30  
    31  const reproFileName = "repro.prog"
    32  const cReproFileName = "repro.cprog"
    33  const straceFileName = "strace.log"
    34  
    35  const MaxReproAttempts = 3
    36  
    37  func NewCrashStore(cfg *mgrconfig.Config) *CrashStore {
    38  	return &CrashStore{
    39  		Tag:          cfg.Tag,
    40  		BaseDir:      cfg.Workdir,
    41  		MaxCrashLogs: cfg.MaxCrashLogs,
    42  		MaxReproLogs: MaxReproAttempts,
    43  	}
    44  }
    45  
    46  func ReadCrashStore(workdir string) *CrashStore {
    47  	return &CrashStore{
    48  		BaseDir: workdir,
    49  	}
    50  }
    51  
    52  // Returns whether it was the first crash of a kind.
    53  func (cs *CrashStore) SaveCrash(crash *Crash) (bool, error) {
    54  	dir := cs.path(crash.Title)
    55  	osutil.MkdirAll(dir)
    56  
    57  	err := osutil.WriteFile(filepath.Join(dir, "description"), []byte(crash.Title+"\n"))
    58  	if err != nil {
    59  		return false, fmt.Errorf("failed to write crash: %w", err)
    60  	}
    61  
    62  	// Save up to cs.cfg.MaxCrashLogs reports, overwrite the oldest once we've reached that number.
    63  	// Newer reports are generally more useful. Overwriting is also needed
    64  	// to be able to understand if a particular bug still happens or already fixed.
    65  	oldestI, first := 0, false
    66  	var oldestTime time.Time
    67  	for i := 0; i < cs.MaxCrashLogs; i++ {
    68  		info, err := os.Stat(filepath.Join(dir, fmt.Sprintf("log%v", i)))
    69  		if err != nil {
    70  			oldestI = i
    71  			if i == 0 {
    72  				first = true
    73  			}
    74  			break
    75  		}
    76  		if oldestTime.IsZero() || info.ModTime().Before(oldestTime) {
    77  			oldestI = i
    78  			oldestTime = info.ModTime()
    79  		}
    80  	}
    81  	writeOrRemove := func(name string, data []byte) {
    82  		filename := filepath.Join(dir, name+fmt.Sprint(oldestI))
    83  		if len(data) == 0 {
    84  			os.Remove(filename)
    85  			return
    86  		}
    87  		osutil.WriteFile(filename, data)
    88  	}
    89  	reps := append([]*report.Report{crash.Report}, crash.TailReports...)
    90  	writeOrRemove("log", crash.Output)
    91  	writeOrRemove("tag", []byte(cs.Tag))
    92  	writeOrRemove("report", report.MergeReportBytes(reps))
    93  	writeOrRemove("machineInfo", crash.MachineInfo)
    94  	if err := report.AddTitleStat(filepath.Join(dir, "title-stat"), reps); err != nil {
    95  		return false, fmt.Errorf("report.AddTitleStat: %w", err)
    96  	}
    97  
    98  	return first, nil
    99  }
   100  
   101  func (cs *CrashStore) HasRepro(title string) bool {
   102  	return osutil.IsExist(filepath.Join(cs.path(title), reproFileName))
   103  }
   104  
   105  func (cs *CrashStore) MoreReproAttempts(title string) bool {
   106  	dir := cs.path(title)
   107  	for i := 0; i < cs.MaxReproLogs; i++ {
   108  		if !osutil.IsExist(filepath.Join(dir, fmt.Sprintf("repro%v", i))) {
   109  			return true
   110  		}
   111  	}
   112  	return false
   113  }
   114  
   115  func (cs *CrashStore) SaveFailedRepro(title string, log []byte) error {
   116  	dir := cs.path(title)
   117  	osutil.MkdirAll(dir)
   118  	for i := 0; i < cs.MaxReproLogs; i++ {
   119  		name := filepath.Join(dir, fmt.Sprintf("repro%v", i))
   120  		if !osutil.IsExist(name) && len(log) > 0 {
   121  			err := osutil.WriteFile(name, log)
   122  			if err != nil {
   123  				return err
   124  			}
   125  			break
   126  		}
   127  	}
   128  	return nil
   129  }
   130  
   131  func (cs *CrashStore) SaveRepro(res *ReproResult, progText, cProgText []byte) error {
   132  	repro := res.Repro
   133  	rep := repro.Report
   134  	dir := cs.path(rep.Title)
   135  	osutil.MkdirAll(dir)
   136  
   137  	err := osutil.WriteFile(filepath.Join(dir, "description"), []byte(rep.Title+"\n"))
   138  	if err != nil {
   139  		return fmt.Errorf("failed to write crash: %w", err)
   140  	}
   141  	// TODO: detect and handle errors below as well.
   142  	osutil.WriteFile(filepath.Join(dir, reproFileName), progText)
   143  	if cs.Tag != "" {
   144  		osutil.WriteFile(filepath.Join(dir, "repro.tag"), []byte(cs.Tag))
   145  	}
   146  	if len(rep.Output) > 0 {
   147  		osutil.WriteFile(filepath.Join(dir, "repro.log"), rep.Output)
   148  	}
   149  	if len(rep.Report) > 0 {
   150  		osutil.WriteFile(filepath.Join(dir, "repro.report"), rep.Report)
   151  	}
   152  	if len(cProgText) > 0 {
   153  		osutil.WriteFile(filepath.Join(dir, cReproFileName), cProgText)
   154  	}
   155  	var assetErr error
   156  	repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader, c *prog.Call) {
   157  		fileName := filepath.Join(dir, name+".gz")
   158  		if err := osutil.WriteGzipStream(fileName, r); err != nil {
   159  			assetErr = fmt.Errorf("failed to write crash asset: type %d, %w", typ, err)
   160  		}
   161  	})
   162  	if assetErr != nil {
   163  		return assetErr
   164  	}
   165  	if res.Strace != nil {
   166  		// Unlike dashboard reporting, we save strace output separately from the original log.
   167  		if res.Strace.Error != nil {
   168  			osutil.WriteFile(filepath.Join(dir, "strace.error"),
   169  				[]byte(fmt.Sprintf("%v", res.Strace.Error)))
   170  		}
   171  		if len(res.Strace.Output) > 0 {
   172  			osutil.WriteFile(filepath.Join(dir, straceFileName), res.Strace.Output)
   173  		}
   174  	}
   175  	if reproLog := res.Stats.FullLog(); len(reproLog) > 0 {
   176  		osutil.WriteFile(filepath.Join(dir, "repro.stats"), reproLog)
   177  	}
   178  	return nil
   179  }
   180  
   181  type BugReport struct {
   182  	Title  string
   183  	Tag    string
   184  	Prog   []byte
   185  	CProg  []byte
   186  	Report []byte
   187  }
   188  
   189  func (cs *CrashStore) Report(id string) (*BugReport, error) {
   190  	dir := filepath.Join(cs.BaseDir, "crashes", id)
   191  	desc, err := os.ReadFile(filepath.Join(dir, "description"))
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	tag, _ := os.ReadFile(filepath.Join(dir, "repro.tag"))
   196  	ret := &BugReport{
   197  		Title: strings.TrimSpace(string(desc)),
   198  		Tag:   strings.TrimSpace(string(tag)),
   199  	}
   200  	ret.Prog, _ = os.ReadFile(filepath.Join(dir, reproFileName))
   201  	ret.CProg, _ = os.ReadFile(filepath.Join(dir, cReproFileName))
   202  	ret.Report, _ = os.ReadFile(filepath.Join(dir, "repro.report"))
   203  	return ret, nil
   204  }
   205  
   206  type CrashInfo struct {
   207  	Index int
   208  	Log   string // filename relative to the workdir
   209  
   210  	// These fields are only set if full=true.
   211  	Tag    string
   212  	Report string // filename relative to workdir
   213  	Time   time.Time
   214  }
   215  
   216  type BugInfo struct {
   217  	ID            string
   218  	Title         string
   219  	TailTitles    []*report.TitleFreqRank
   220  	FirstTime     time.Time
   221  	LastTime      time.Time
   222  	HasRepro      bool
   223  	HasCRepro     bool
   224  	StraceFile    string // relative to the workdir
   225  	ReproAttempts int
   226  	Crashes       []*CrashInfo
   227  	Rank          int
   228  }
   229  
   230  func (cs *CrashStore) BugInfo(id string, full bool) (*BugInfo, error) {
   231  	dir := filepath.Join(cs.BaseDir, "crashes", id)
   232  
   233  	ret := &BugInfo{ID: id}
   234  	desc, err := os.ReadFile(filepath.Join(dir, "description"))
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  	stat, err := os.Stat(filepath.Join(dir, "description"))
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	ret.Title = strings.TrimSpace(string(desc))
   243  
   244  	// Bug rank may go up over time if we observe higher ranked bugs as a consequence of the first failure.
   245  	ret.Rank = report.TitlesToImpact(ret.Title)
   246  	if titleStat, err := report.ReadStatFile(filepath.Join(dir, "title-stat")); err == nil {
   247  		ret.TailTitles = report.ExplainTitleStat(titleStat)
   248  		for _, ti := range ret.TailTitles {
   249  			ret.Rank = max(ret.Rank, ti.Rank)
   250  		}
   251  	}
   252  
   253  	ret.FirstTime = osutil.CreationTime(stat)
   254  	ret.LastTime = stat.ModTime()
   255  	files, err := osutil.ListDir(dir)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	for _, f := range files {
   260  		if strings.HasPrefix(f, "log") {
   261  			index, err := strconv.ParseUint(f[3:], 10, 64)
   262  			if err == nil {
   263  				ret.Crashes = append(ret.Crashes, &CrashInfo{
   264  					Index: int(index),
   265  					Log:   filepath.Join("crashes", id, f),
   266  				})
   267  			}
   268  		} else if f == reproFileName {
   269  			ret.HasRepro = true
   270  		} else if f == cReproFileName {
   271  			ret.HasCRepro = true
   272  		} else if f == straceFileName {
   273  			ret.StraceFile = filepath.Join(dir, f)
   274  		} else if strings.HasPrefix(f, "repro") {
   275  			ret.ReproAttempts++
   276  		}
   277  	}
   278  	if !full {
   279  		return ret, nil
   280  	}
   281  	for _, crash := range ret.Crashes {
   282  		if stat, err := os.Stat(filepath.Join(cs.BaseDir, crash.Log)); err == nil {
   283  			crash.Time = stat.ModTime()
   284  		}
   285  		tag, _ := os.ReadFile(filepath.Join(dir, fmt.Sprintf("tag%d", crash.Index)))
   286  		crash.Tag = string(tag)
   287  		reportFile := filepath.Join("crashes", id, fmt.Sprintf("report%d", crash.Index))
   288  		if osutil.IsExist(filepath.Join(cs.BaseDir, reportFile)) {
   289  			crash.Report = reportFile
   290  		}
   291  	}
   292  	sort.Slice(ret.Crashes, func(i, j int) bool {
   293  		return ret.Crashes[i].Time.After(ret.Crashes[j].Time)
   294  	})
   295  	return ret, nil
   296  }
   297  
   298  func (cs *CrashStore) BugList() ([]*BugInfo, error) {
   299  	dirs, err := osutil.ListDir(filepath.Join(cs.BaseDir, "crashes"))
   300  	if err != nil {
   301  		if os.IsNotExist(err) {
   302  			// If there were no crashes, it's okay that there's no such folder.
   303  			return nil, nil
   304  		}
   305  		return nil, err
   306  	}
   307  	var ret []*BugInfo
   308  	var lastErr error
   309  	errCount := 0
   310  	for _, dir := range dirs {
   311  		info, err := cs.BugInfo(dir, false)
   312  		if err != nil {
   313  			errCount++
   314  			lastErr = err
   315  			continue
   316  		}
   317  		ret = append(ret, info)
   318  	}
   319  	sort.Slice(ret, func(i, j int) bool {
   320  		return strings.ToLower(ret[i].Title) < strings.ToLower(ret[j].Title)
   321  	})
   322  	if lastErr != nil {
   323  		log.Logf(0, "some stored crashes are inconsistent: %d skipped, last error %v", errCount, lastErr)
   324  	}
   325  	return ret, nil
   326  }
   327  
   328  func crashHash(title string) string {
   329  	sig := hash.Hash([]byte(title))
   330  	return sig.String()
   331  }
   332  
   333  func (cs *CrashStore) path(title string) string {
   334  	return filepath.Join(cs.BaseDir, "crashes", crashHash(title))
   335  }