github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/syz-manager/http.go (about)

     1  // Copyright 2015 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 main
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/json"
     9  	"fmt"
    10  	"html/template"
    11  	"io"
    12  	"net/http"
    13  	_ "net/http/pprof"
    14  	"os"
    15  	"path/filepath"
    16  	"regexp"
    17  	"runtime/debug"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"github.com/google/syzkaller/pkg/cover"
    24  	"github.com/google/syzkaller/pkg/html/pages"
    25  	"github.com/google/syzkaller/pkg/log"
    26  	"github.com/google/syzkaller/pkg/osutil"
    27  	"github.com/google/syzkaller/pkg/stats"
    28  	"github.com/google/syzkaller/pkg/vcs"
    29  	"github.com/google/syzkaller/prog"
    30  	"github.com/gorilla/handlers"
    31  	"github.com/prometheus/client_golang/prometheus"
    32  	"github.com/prometheus/client_golang/prometheus/promhttp"
    33  )
    34  
    35  func (mgr *Manager) initHTTP() {
    36  	handle := func(pattern string, handler func(http.ResponseWriter, *http.Request)) {
    37  		http.Handle(pattern, handlers.CompressHandler(http.HandlerFunc(handler)))
    38  	}
    39  	handle("/", mgr.httpSummary)
    40  	handle("/config", mgr.httpConfig)
    41  	handle("/expert_mode", mgr.httpExpertMode)
    42  	handle("/stats", mgr.httpStats)
    43  	handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).ServeHTTP)
    44  	handle("/syscalls", mgr.httpSyscalls)
    45  	handle("/corpus", mgr.httpCorpus)
    46  	handle("/corpus.db", mgr.httpDownloadCorpus)
    47  	handle("/crash", mgr.httpCrash)
    48  	handle("/cover", mgr.httpCover)
    49  	handle("/subsystemcover", mgr.httpSubsystemCover)
    50  	handle("/modulecover", mgr.httpModuleCover)
    51  	handle("/prio", mgr.httpPrio)
    52  	handle("/file", mgr.httpFile)
    53  	handle("/report", mgr.httpReport)
    54  	handle("/rawcover", mgr.httpRawCover)
    55  	handle("/rawcoverfiles", mgr.httpRawCoverFiles)
    56  	handle("/filterpcs", mgr.httpFilterPCs)
    57  	handle("/funccover", mgr.httpFuncCover)
    58  	handle("/filecover", mgr.httpFileCover)
    59  	handle("/input", mgr.httpInput)
    60  	handle("/debuginput", mgr.httpDebugInput)
    61  	handle("/modules", mgr.modulesInfo)
    62  	// Browsers like to request this, without special handler this goes to / handler.
    63  	handle("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {})
    64  
    65  	log.Logf(0, "serving http on http://%v", mgr.cfg.HTTP)
    66  	go func() {
    67  		err := http.ListenAndServe(mgr.cfg.HTTP, nil)
    68  		if err != nil {
    69  			log.Fatalf("failed to listen on %v: %v", mgr.cfg.HTTP, err)
    70  		}
    71  	}()
    72  }
    73  
    74  func (mgr *Manager) httpSummary(w http.ResponseWriter, r *http.Request) {
    75  	data := &UISummaryData{
    76  		Name:         mgr.cfg.Name,
    77  		Revision:     prog.GitRevisionBase[:8],
    78  		RevisionLink: vcs.LogLink(vcs.SyzkallerRepo, prog.GitRevisionBase),
    79  		Expert:       mgr.expertMode,
    80  		Log:          log.CachedLogOutput(),
    81  	}
    82  
    83  	level := stats.Simple
    84  	if mgr.expertMode {
    85  		level = stats.All
    86  	}
    87  	for _, stat := range stats.Collect(level) {
    88  		data.Stats = append(data.Stats, UIStat{
    89  			Name:  stat.Name,
    90  			Value: stat.Value,
    91  			Hint:  stat.Desc,
    92  			Link:  stat.Link,
    93  		})
    94  	}
    95  
    96  	var err error
    97  	if data.Crashes, err = mgr.collectCrashes(mgr.cfg.Workdir); err != nil {
    98  		http.Error(w, fmt.Sprintf("failed to collect crashes: %v", err), http.StatusInternalServerError)
    99  		return
   100  	}
   101  	executeTemplate(w, summaryTemplate, data)
   102  }
   103  
   104  func (mgr *Manager) httpConfig(w http.ResponseWriter, r *http.Request) {
   105  	data, err := json.MarshalIndent(mgr.cfg, "", "\t")
   106  	if err != nil {
   107  		http.Error(w, fmt.Sprintf("failed to encode json: %v", err),
   108  			http.StatusInternalServerError)
   109  		return
   110  	}
   111  	w.Write(data)
   112  }
   113  
   114  func (mgr *Manager) httpExpertMode(w http.ResponseWriter, r *http.Request) {
   115  	mgr.expertMode = !mgr.expertMode
   116  	http.Redirect(w, r, "/", http.StatusFound)
   117  }
   118  
   119  func (mgr *Manager) httpSyscalls(w http.ResponseWriter, r *http.Request) {
   120  	data := &UISyscallsData{
   121  		Name: mgr.cfg.Name,
   122  	}
   123  	for c, cc := range mgr.collectSyscallInfo() {
   124  		var syscallID *int
   125  		if syscall, ok := mgr.target.SyscallMap[c]; ok {
   126  			syscallID = &syscall.ID
   127  		}
   128  		data.Calls = append(data.Calls, UICallType{
   129  			Name:   c,
   130  			ID:     syscallID,
   131  			Inputs: cc.Count,
   132  			Cover:  len(cc.Cover),
   133  		})
   134  	}
   135  	sort.Slice(data.Calls, func(i, j int) bool {
   136  		return data.Calls[i].Name < data.Calls[j].Name
   137  	})
   138  	executeTemplate(w, syscallsTemplate, data)
   139  }
   140  
   141  func (mgr *Manager) httpStats(w http.ResponseWriter, r *http.Request) {
   142  	data, err := stats.RenderHTML()
   143  	if err != nil {
   144  		log.Logf(0, "failed to execute template: %v", err)
   145  		http.Error(w, err.Error(), http.StatusInternalServerError)
   146  		return
   147  	}
   148  	w.Write(data)
   149  }
   150  
   151  func (mgr *Manager) httpCrash(w http.ResponseWriter, r *http.Request) {
   152  	crashID := r.FormValue("id")
   153  	crash := readCrash(mgr.cfg.Workdir, crashID, nil, mgr.firstConnect.Load(), true)
   154  	if crash == nil {
   155  		http.Error(w, "failed to read crash info", http.StatusInternalServerError)
   156  		return
   157  	}
   158  	executeTemplate(w, crashTemplate, crash)
   159  }
   160  
   161  func (mgr *Manager) httpCorpus(w http.ResponseWriter, r *http.Request) {
   162  	mgr.mu.Lock()
   163  	defer mgr.mu.Unlock()
   164  
   165  	data := UICorpus{
   166  		Call:     r.FormValue("call"),
   167  		RawCover: mgr.cfg.RawCover,
   168  	}
   169  	for _, inp := range mgr.corpus.Items() {
   170  		if data.Call != "" && data.Call != inp.StringCall() {
   171  			continue
   172  		}
   173  		data.Inputs = append(data.Inputs, &UIInput{
   174  			Sig:   inp.Sig,
   175  			Short: inp.Prog.String(),
   176  			Cover: len(inp.Cover),
   177  		})
   178  	}
   179  	sort.Slice(data.Inputs, func(i, j int) bool {
   180  		a, b := data.Inputs[i], data.Inputs[j]
   181  		if a.Cover != b.Cover {
   182  			return a.Cover > b.Cover
   183  		}
   184  		return a.Short < b.Short
   185  	})
   186  	executeTemplate(w, corpusTemplate, data)
   187  }
   188  
   189  func (mgr *Manager) httpDownloadCorpus(w http.ResponseWriter, r *http.Request) {
   190  	corpus := filepath.Join(mgr.cfg.Workdir, "corpus.db")
   191  	file, err := os.Open(corpus)
   192  	if err != nil {
   193  		http.Error(w, fmt.Sprintf("failed to open corpus : %v", err), http.StatusInternalServerError)
   194  		return
   195  	}
   196  	defer file.Close()
   197  	buf, err := io.ReadAll(file)
   198  	if err != nil {
   199  		http.Error(w, fmt.Sprintf("failed to read corpus : %v", err), http.StatusInternalServerError)
   200  		return
   201  	}
   202  	w.Write(buf)
   203  }
   204  
   205  const (
   206  	DoHTML int = iota
   207  	DoHTMLTable
   208  	DoModuleCover
   209  	DoCSV
   210  	DoCSVFiles
   211  	DoRawCoverFiles
   212  	DoRawCover
   213  	DoFilterPCs
   214  	DoCoverJSONL
   215  )
   216  
   217  func (mgr *Manager) httpCover(w http.ResponseWriter, r *http.Request) {
   218  	if !mgr.cfg.Cover {
   219  		mgr.httpCoverFallback(w, r)
   220  		return
   221  	}
   222  	if r.FormValue("jsonl") == "1" {
   223  		mgr.httpCoverCover(w, r, DoCoverJSONL)
   224  		return
   225  	}
   226  	mgr.httpCoverCover(w, r, DoHTML)
   227  }
   228  
   229  func (mgr *Manager) httpSubsystemCover(w http.ResponseWriter, r *http.Request) {
   230  	if !mgr.cfg.Cover {
   231  		mgr.httpCoverFallback(w, r)
   232  		return
   233  	}
   234  	mgr.httpCoverCover(w, r, DoHTMLTable)
   235  }
   236  
   237  func (mgr *Manager) httpModuleCover(w http.ResponseWriter, r *http.Request) {
   238  	if !mgr.cfg.Cover {
   239  		mgr.httpCoverFallback(w, r)
   240  		return
   241  	}
   242  	mgr.httpCoverCover(w, r, DoModuleCover)
   243  }
   244  
   245  const ctTextPlain = "text/plain; charset=utf-8"
   246  const ctApplicationJSON = "application/json"
   247  
   248  func (mgr *Manager) httpCoverCover(w http.ResponseWriter, r *http.Request, funcFlag int) {
   249  	if !mgr.cfg.Cover {
   250  		http.Error(w, "coverage is not enabled", http.StatusInternalServerError)
   251  		return
   252  	}
   253  
   254  	// Don't hold the mutex while creating report generator and generating the report,
   255  	// these operations take lots of time.
   256  	if !mgr.serv.checkDone.Load() {
   257  		http.Error(w, "coverage is not ready, please try again later after fuzzer started", http.StatusInternalServerError)
   258  		return
   259  	}
   260  
   261  	rg, err := getReportGenerator(mgr.cfg, mgr.serv.modules)
   262  	if err != nil {
   263  		http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError)
   264  		return
   265  	}
   266  
   267  	if r.FormValue("flush") != "" {
   268  		defer func() {
   269  			resetReportGenerator()
   270  			debug.FreeOSMemory()
   271  		}()
   272  	}
   273  
   274  	mgr.mu.Lock()
   275  	var progs []cover.Prog
   276  	if sig := r.FormValue("input"); sig != "" {
   277  		inp := mgr.corpus.Item(sig)
   278  		if inp == nil {
   279  			http.Error(w, "unknown input hash", http.StatusInternalServerError)
   280  			return
   281  		}
   282  		if r.FormValue("update_id") != "" {
   283  			updateID, err := strconv.Atoi(r.FormValue("update_id"))
   284  			if err != nil || updateID < 0 || updateID >= len(inp.Updates) {
   285  				http.Error(w, "bad call_id", http.StatusBadRequest)
   286  				return
   287  			}
   288  			progs = append(progs, cover.Prog{
   289  				Sig:  sig,
   290  				Data: string(inp.ProgData),
   291  				PCs:  coverToPCs(rg, inp.Updates[updateID].RawCover),
   292  			})
   293  		} else {
   294  			progs = append(progs, cover.Prog{
   295  				Sig:  sig,
   296  				Data: string(inp.ProgData),
   297  				PCs:  coverToPCs(rg, inp.Cover),
   298  			})
   299  		}
   300  	} else {
   301  		call := r.FormValue("call")
   302  		for _, inp := range mgr.corpus.Items() {
   303  			if call != "" && call != inp.StringCall() {
   304  				continue
   305  			}
   306  			progs = append(progs, cover.Prog{
   307  				Sig:  inp.Sig,
   308  				Data: string(inp.ProgData),
   309  				PCs:  coverToPCs(rg, inp.Cover),
   310  			})
   311  		}
   312  	}
   313  	mgr.mu.Unlock()
   314  
   315  	var coverFilter map[uint32]uint32
   316  	if r.FormValue("filter") != "" || funcFlag == DoFilterPCs {
   317  		if mgr.serv.coverFilter == nil {
   318  			http.Error(w, "cover is not filtered in config", http.StatusInternalServerError)
   319  			return
   320  		}
   321  		coverFilter = mgr.serv.coverFilter
   322  	}
   323  
   324  	params := cover.CoverHandlerParams{
   325  		Progs:       progs,
   326  		CoverFilter: coverFilter,
   327  		Debug:       r.FormValue("debug") != "",
   328  		Force:       r.FormValue("force") != "",
   329  	}
   330  
   331  	type handlerFuncType func(w io.Writer, params cover.CoverHandlerParams) error
   332  	flagToFunc := map[int]struct {
   333  		Do          handlerFuncType
   334  		contentType string
   335  	}{
   336  		DoHTML:          {rg.DoHTML, ""},
   337  		DoHTMLTable:     {rg.DoHTMLTable, ""},
   338  		DoModuleCover:   {rg.DoModuleCover, ""},
   339  		DoCSV:           {rg.DoCSV, ctTextPlain},
   340  		DoCSVFiles:      {rg.DoCSVFiles, ctTextPlain},
   341  		DoRawCoverFiles: {rg.DoRawCoverFiles, ctTextPlain},
   342  		DoRawCover:      {rg.DoRawCover, ctTextPlain},
   343  		DoFilterPCs:     {rg.DoFilterPCs, ctTextPlain},
   344  		DoCoverJSONL:    {rg.DoCoverJSONL, ctApplicationJSON},
   345  	}
   346  
   347  	if ct := flagToFunc[funcFlag].contentType; ct != "" {
   348  		w.Header().Set("Content-Type", ct)
   349  	}
   350  
   351  	if err := flagToFunc[funcFlag].Do(w, params); err != nil {
   352  		http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError)
   353  		return
   354  	}
   355  }
   356  
   357  func (mgr *Manager) httpCoverFallback(w http.ResponseWriter, r *http.Request) {
   358  	mgr.mu.Lock()
   359  	defer mgr.mu.Unlock()
   360  	calls := make(map[int][]int)
   361  	for s := range mgr.corpus.Signal() {
   362  		id, errno := prog.DecodeFallbackSignal(uint32(s))
   363  		calls[id] = append(calls[id], errno)
   364  	}
   365  	data := &UIFallbackCoverData{}
   366  	for call := range mgr.targetEnabledSyscalls {
   367  		errnos := calls[call.ID]
   368  		sort.Ints(errnos)
   369  		successful := 0
   370  		for len(errnos) != 0 && errnos[0] == 0 {
   371  			successful++
   372  			errnos = errnos[1:]
   373  		}
   374  		data.Calls = append(data.Calls, UIFallbackCall{
   375  			Name:       call.Name,
   376  			Successful: successful,
   377  			Errnos:     errnos,
   378  		})
   379  	}
   380  	sort.Slice(data.Calls, func(i, j int) bool {
   381  		return data.Calls[i].Name < data.Calls[j].Name
   382  	})
   383  	executeTemplate(w, fallbackCoverTemplate, data)
   384  }
   385  
   386  func (mgr *Manager) httpFuncCover(w http.ResponseWriter, r *http.Request) {
   387  	mgr.httpCoverCover(w, r, DoCSV)
   388  }
   389  
   390  func (mgr *Manager) httpFileCover(w http.ResponseWriter, r *http.Request) {
   391  	mgr.httpCoverCover(w, r, DoCSVFiles)
   392  }
   393  
   394  func (mgr *Manager) httpPrio(w http.ResponseWriter, r *http.Request) {
   395  	mgr.mu.Lock()
   396  	defer mgr.mu.Unlock()
   397  
   398  	callName := r.FormValue("call")
   399  	call := mgr.target.SyscallMap[callName]
   400  	if call == nil {
   401  		http.Error(w, fmt.Sprintf("unknown call: %v", callName), http.StatusInternalServerError)
   402  		return
   403  	}
   404  
   405  	var corpus []*prog.Prog
   406  	for _, inp := range mgr.corpus.Items() {
   407  		corpus = append(corpus, inp.Prog)
   408  	}
   409  	prios := mgr.target.CalculatePriorities(corpus)
   410  
   411  	data := &UIPrioData{Call: callName}
   412  	for i, p := range prios[call.ID] {
   413  		data.Prios = append(data.Prios, UIPrio{mgr.target.Syscalls[i].Name, p})
   414  	}
   415  	sort.Slice(data.Prios, func(i, j int) bool {
   416  		return data.Prios[i].Prio > data.Prios[j].Prio
   417  	})
   418  	executeTemplate(w, prioTemplate, data)
   419  }
   420  
   421  func (mgr *Manager) httpFile(w http.ResponseWriter, r *http.Request) {
   422  	file := filepath.Clean(r.FormValue("name"))
   423  	if !strings.HasPrefix(file, "crashes/") && !strings.HasPrefix(file, "corpus/") {
   424  		http.Error(w, "oh, oh, oh!", http.StatusInternalServerError)
   425  		return
   426  	}
   427  	file = filepath.Join(mgr.cfg.Workdir, file)
   428  	f, err := os.Open(file)
   429  	if err != nil {
   430  		http.Error(w, "failed to open the file", http.StatusInternalServerError)
   431  		return
   432  	}
   433  	defer f.Close()
   434  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   435  	io.Copy(w, f)
   436  }
   437  
   438  func (mgr *Manager) httpInput(w http.ResponseWriter, r *http.Request) {
   439  	mgr.mu.Lock()
   440  	defer mgr.mu.Unlock()
   441  	inp := mgr.corpus.Item(r.FormValue("sig"))
   442  	if inp == nil {
   443  		http.Error(w, "can't find the input", http.StatusInternalServerError)
   444  		return
   445  	}
   446  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   447  	w.Write(inp.ProgData)
   448  }
   449  
   450  func (mgr *Manager) httpDebugInput(w http.ResponseWriter, r *http.Request) {
   451  	mgr.mu.Lock()
   452  	defer mgr.mu.Unlock()
   453  	inp := mgr.corpus.Item(r.FormValue("sig"))
   454  	if inp == nil {
   455  		http.Error(w, "can't find the input", http.StatusInternalServerError)
   456  		return
   457  	}
   458  	getIDs := func(callID int) []int {
   459  		ret := []int{}
   460  		for id, update := range inp.Updates {
   461  			if update.Call == callID {
   462  				ret = append(ret, id)
   463  			}
   464  		}
   465  		return ret
   466  	}
   467  	data := []UIRawCallCover{}
   468  	for pos, line := range strings.Split(string(inp.ProgData), "\n") {
   469  		line = strings.TrimSpace(line)
   470  		if line == "" {
   471  			continue
   472  		}
   473  		data = append(data, UIRawCallCover{
   474  			Sig:       r.FormValue("sig"),
   475  			Call:      line,
   476  			UpdateIDs: getIDs(pos),
   477  		})
   478  	}
   479  	extraIDs := getIDs(-1)
   480  	if len(extraIDs) > 0 {
   481  		data = append(data, UIRawCallCover{
   482  			Sig:       r.FormValue("sig"),
   483  			Call:      ".extra",
   484  			UpdateIDs: extraIDs,
   485  		})
   486  	}
   487  	executeTemplate(w, rawCoverTemplate, data)
   488  }
   489  
   490  func (mgr *Manager) modulesInfo(w http.ResponseWriter, r *http.Request) {
   491  	if mgr.serv.canonicalModules == nil {
   492  		fmt.Fprintf(w, "module information not retrieved yet, please retry after fuzzing starts\n")
   493  		return
   494  	}
   495  	// NewCanonicalizer() is initialized with serv.modules.
   496  	modules, err := json.MarshalIndent(mgr.serv.modules, "", "\t")
   497  	if err != nil {
   498  		fmt.Fprintf(w, "unable to create JSON modules info: %v", err)
   499  		return
   500  	}
   501  	w.Header().Set("Content-Type", "application/json")
   502  	w.Write(modules)
   503  }
   504  
   505  var alphaNumRegExp = regexp.MustCompile(`^[a-zA-Z0-9]*$`)
   506  
   507  func isAlphanumeric(s string) bool {
   508  	return alphaNumRegExp.MatchString(s)
   509  }
   510  
   511  func (mgr *Manager) httpReport(w http.ResponseWriter, r *http.Request) {
   512  	mgr.mu.Lock()
   513  	defer mgr.mu.Unlock()
   514  
   515  	crashID := r.FormValue("id")
   516  	if !isAlphanumeric(crashID) {
   517  		http.Error(w, "wrong id", http.StatusBadRequest)
   518  		return
   519  	}
   520  
   521  	desc, err := os.ReadFile(filepath.Join(mgr.crashdir, crashID, "description"))
   522  	if err != nil {
   523  		http.Error(w, "failed to read description file", http.StatusInternalServerError)
   524  		return
   525  	}
   526  	tag, _ := os.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.tag"))
   527  	prog, _ := os.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.prog"))
   528  	cprog, _ := os.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.cprog"))
   529  	rep, _ := os.ReadFile(filepath.Join(mgr.crashdir, crashID, "repro.report"))
   530  
   531  	commitDesc := ""
   532  	if len(tag) != 0 {
   533  		commitDesc = fmt.Sprintf(" on commit %s.", trimNewLines(tag))
   534  	}
   535  	fmt.Fprintf(w, "Syzkaller hit '%s' bug%s.\n\n", trimNewLines(desc), commitDesc)
   536  	if len(rep) != 0 {
   537  		fmt.Fprintf(w, "%s\n\n", rep)
   538  	}
   539  	if len(prog) == 0 && len(cprog) == 0 {
   540  		fmt.Fprintf(w, "The bug is not reproducible.\n")
   541  	} else {
   542  		fmt.Fprintf(w, "Syzkaller reproducer:\n%s\n\n", prog)
   543  		if len(cprog) != 0 {
   544  			fmt.Fprintf(w, "C reproducer:\n%s\n\n", cprog)
   545  		}
   546  	}
   547  }
   548  
   549  func (mgr *Manager) httpRawCover(w http.ResponseWriter, r *http.Request) {
   550  	mgr.httpCoverCover(w, r, DoRawCover)
   551  }
   552  
   553  func (mgr *Manager) httpRawCoverFiles(w http.ResponseWriter, r *http.Request) {
   554  	mgr.httpCoverCover(w, r, DoRawCoverFiles)
   555  }
   556  
   557  func (mgr *Manager) httpFilterPCs(w http.ResponseWriter, r *http.Request) {
   558  	mgr.httpCoverCover(w, r, DoFilterPCs)
   559  }
   560  
   561  func (mgr *Manager) collectCrashes(workdir string) ([]*UICrashType, error) {
   562  	// Note: mu is not locked here.
   563  	reproReply := make(chan map[string]bool)
   564  	mgr.reproRequest <- reproReply
   565  	repros := <-reproReply
   566  
   567  	crashdir := filepath.Join(workdir, "crashes")
   568  	dirs, err := osutil.ListDir(crashdir)
   569  	if err != nil {
   570  		return nil, err
   571  	}
   572  	var crashTypes []*UICrashType
   573  	for _, dir := range dirs {
   574  		crash := readCrash(workdir, dir, repros, mgr.firstConnect.Load(), false)
   575  		if crash != nil {
   576  			crashTypes = append(crashTypes, crash)
   577  		}
   578  	}
   579  	sort.Slice(crashTypes, func(i, j int) bool {
   580  		return strings.ToLower(crashTypes[i].Description) < strings.ToLower(crashTypes[j].Description)
   581  	})
   582  	return crashTypes, nil
   583  }
   584  
   585  func readCrash(workdir, dir string, repros map[string]bool, start int64, full bool) *UICrashType {
   586  	if len(dir) != 40 {
   587  		return nil
   588  	}
   589  	crashdir := filepath.Join(workdir, "crashes")
   590  	descFile, err := os.Open(filepath.Join(crashdir, dir, "description"))
   591  	if err != nil {
   592  		return nil
   593  	}
   594  	defer descFile.Close()
   595  	descBytes, err := io.ReadAll(descFile)
   596  	if err != nil || len(descBytes) == 0 {
   597  		return nil
   598  	}
   599  	desc := string(trimNewLines(descBytes))
   600  	stat, err := descFile.Stat()
   601  	if err != nil {
   602  		return nil
   603  	}
   604  	modTime := stat.ModTime()
   605  	descFile.Close()
   606  
   607  	files, err := osutil.ListDir(filepath.Join(crashdir, dir))
   608  	if err != nil {
   609  		return nil
   610  	}
   611  	var crashes []*UICrash
   612  	reproAttempts := 0
   613  	hasRepro, hasCRepro := false, false
   614  	strace := ""
   615  	reports := make(map[string]bool)
   616  	for _, f := range files {
   617  		if strings.HasPrefix(f, "log") {
   618  			index, err := strconv.ParseUint(f[3:], 10, 64)
   619  			if err == nil {
   620  				crashes = append(crashes, &UICrash{
   621  					Index: int(index),
   622  				})
   623  			}
   624  		} else if strings.HasPrefix(f, "report") {
   625  			reports[f] = true
   626  		} else if f == "repro.prog" {
   627  			hasRepro = true
   628  		} else if f == "repro.cprog" {
   629  			hasCRepro = true
   630  		} else if f == "repro.report" {
   631  		} else if f == "repro0" || f == "repro1" || f == "repro2" {
   632  			reproAttempts++
   633  		} else if f == "strace.log" {
   634  			strace = filepath.Join("crashes", dir, f)
   635  		}
   636  	}
   637  
   638  	if full {
   639  		for _, crash := range crashes {
   640  			index := strconv.Itoa(crash.Index)
   641  			crash.Log = filepath.Join("crashes", dir, "log"+index)
   642  			if stat, err := os.Stat(filepath.Join(workdir, crash.Log)); err == nil {
   643  				crash.Time = stat.ModTime()
   644  				crash.Active = start != 0 && crash.Time.Unix() >= start
   645  			}
   646  			tag, _ := os.ReadFile(filepath.Join(crashdir, dir, "tag"+index))
   647  			crash.Tag = string(tag)
   648  			reportFile := filepath.Join("crashes", dir, "report"+index)
   649  			if osutil.IsExist(filepath.Join(workdir, reportFile)) {
   650  				crash.Report = reportFile
   651  			}
   652  		}
   653  		sort.Slice(crashes, func(i, j int) bool {
   654  			return crashes[i].Time.After(crashes[j].Time)
   655  		})
   656  	}
   657  
   658  	triaged := reproStatus(hasRepro, hasCRepro, repros[desc], reproAttempts >= maxReproAttempts)
   659  	return &UICrashType{
   660  		Description: desc,
   661  		LastTime:    modTime,
   662  		Active:      start != 0 && modTime.Unix() >= start,
   663  		ID:          dir,
   664  		Count:       len(crashes),
   665  		Triaged:     triaged,
   666  		Strace:      strace,
   667  		Crashes:     crashes,
   668  	}
   669  }
   670  
   671  func reproStatus(hasRepro, hasCRepro, reproducing, nonReproducible bool) string {
   672  	status := ""
   673  	if hasRepro {
   674  		status = "has repro"
   675  		if hasCRepro {
   676  			status = "has C repro"
   677  		}
   678  	} else if reproducing {
   679  		status = "reproducing"
   680  	} else if nonReproducible {
   681  		status = "non-reproducible"
   682  	}
   683  	return status
   684  }
   685  
   686  func executeTemplate(w http.ResponseWriter, templ *template.Template, data interface{}) {
   687  	buf := new(bytes.Buffer)
   688  	if err := templ.Execute(buf, data); err != nil {
   689  		log.Logf(0, "failed to execute template: %v", err)
   690  		http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
   691  		return
   692  	}
   693  	w.Write(buf.Bytes())
   694  }
   695  
   696  func trimNewLines(data []byte) []byte {
   697  	for len(data) > 0 && data[len(data)-1] == '\n' {
   698  		data = data[:len(data)-1]
   699  	}
   700  	return data
   701  }
   702  
   703  type UISummaryData struct {
   704  	Name         string
   705  	Revision     string
   706  	RevisionLink string
   707  	Expert       bool
   708  	Stats        []UIStat
   709  	Crashes      []*UICrashType
   710  	Log          string
   711  }
   712  
   713  type UISyscallsData struct {
   714  	Name  string
   715  	Calls []UICallType
   716  }
   717  
   718  type UICrashType struct {
   719  	Description string
   720  	LastTime    time.Time
   721  	Active      bool
   722  	ID          string
   723  	Count       int
   724  	Triaged     string
   725  	Strace      string
   726  	Crashes     []*UICrash
   727  }
   728  
   729  type UICrash struct {
   730  	Index  int
   731  	Time   time.Time
   732  	Active bool
   733  	Log    string
   734  	Report string
   735  	Tag    string
   736  }
   737  
   738  type UIStat struct {
   739  	Name  string
   740  	Value string
   741  	Hint  string
   742  	Link  string
   743  }
   744  
   745  type UICallType struct {
   746  	Name   string
   747  	ID     *int
   748  	Inputs int
   749  	Cover  int
   750  }
   751  
   752  type UICorpus struct {
   753  	Call     string
   754  	RawCover bool
   755  	Inputs   []*UIInput
   756  }
   757  
   758  type UIInput struct {
   759  	Sig   string
   760  	Short string
   761  	Cover int
   762  }
   763  
   764  var summaryTemplate = pages.Create(`
   765  <!doctype html>
   766  <html>
   767  <head>
   768  	<title>{{.Name}} syzkaller</title>
   769  	{{HEAD}}
   770  </head>
   771  <body>
   772  <b>{{.Name }} syzkaller</b>
   773  <a href='/config'>[config]</a>
   774  <a href='{{.RevisionLink}}'>{{.Revision}}</a>
   775  <a class="navigation_tab" href='expert_mode'>{{if .Expert}}disable{{else}}enable{{end}} expert mode</a>
   776  <br>
   777  
   778  <table class="list_table">
   779  	<caption><a href='/stats'>Stats 📈</a></caption>
   780  	{{range $s := $.Stats}}
   781  	<tr>
   782  		<td class="stat_name" title="{{$s.Hint}}">{{$s.Name}}</td>
   783  		<td class="stat_value">
   784  			{{if $s.Link}}
   785  				<a href="{{$s.Link}}">{{$s.Value}}</a>
   786  			{{else}}
   787  				{{$s.Value}}
   788  			{{end}}
   789  		</td>
   790  	</tr>
   791  	{{end}}
   792  </table>
   793  
   794  <table class="list_table">
   795  	<caption>Crashes:</caption>
   796  	<tr>
   797  		<th><a onclick="return sortTable(this, 'Description', textSort)" href="#">Description</a></th>
   798  		<th><a onclick="return sortTable(this, 'Count', numSort)" href="#">Count</a></th>
   799  		<th><a onclick="return sortTable(this, 'Last Time', textSort, true)" href="#">Last Time</a></th>
   800  		<th><a onclick="return sortTable(this, 'Report', textSort)" href="#">Report</a></th>
   801  	</tr>
   802  	{{range $c := $.Crashes}}
   803  	<tr>
   804  		<td class="title"><a href="/crash?id={{$c.ID}}">{{$c.Description}}</a></td>
   805  		<td class="stat {{if not $c.Active}}inactive{{end}}">{{$c.Count}}</td>
   806  		<td class="time {{if not $c.Active}}inactive{{end}}">{{formatTime $c.LastTime}}</td>
   807  		<td>
   808  			{{if $c.Triaged}}
   809  				<a href="/report?id={{$c.ID}}">{{$c.Triaged}}</a>
   810  			{{end}}
   811  			{{if $c.Strace}}
   812  				<a href="/file?name={{$c.Strace}}">Strace</a>
   813  			{{end}}
   814  		</td>
   815  	</tr>
   816  	{{end}}
   817  </table>
   818  
   819  <b>Log:</b>
   820  <br>
   821  <textarea id="log_textarea" readonly rows="20" wrap=off>
   822  {{.Log}}
   823  </textarea>
   824  <script>
   825  	var textarea = document.getElementById("log_textarea");
   826  	textarea.scrollTop = textarea.scrollHeight;
   827  </script>
   828  </body></html>
   829  `)
   830  
   831  var syscallsTemplate = pages.Create(`
   832  <!doctype html>
   833  <html>
   834  <head>
   835  	<title>{{.Name }} syzkaller</title>
   836  	{{HEAD}}
   837  </head>
   838  <body>
   839  
   840  <table class="list_table">
   841  	<caption>Per-syscall coverage:</caption>
   842  	<tr>
   843  		<th><a onclick="return sortTable(this, 'Syscall', textSort)" href="#">Syscall</a></th>
   844  		<th><a onclick="return sortTable(this, 'Inputs', numSort)" href="#">Inputs</a></th>
   845  		<th><a onclick="return sortTable(this, 'Coverage', numSort)" href="#">Coverage</a></th>
   846  		<th>Prio</th>
   847  	</tr>
   848  	{{range $c := $.Calls}}
   849  	<tr>
   850  		<td>{{$c.Name}}{{if $c.ID }} [{{$c.ID}}]{{end}}</td>
   851  		<td><a href='/corpus?call={{$c.Name}}'>{{$c.Inputs}}</a></td>
   852  		<td><a href='/cover?call={{$c.Name}}'>{{$c.Cover}}</a></td>
   853  		<td><a href='/prio?call={{$c.Name}}'>prio</a></td>
   854  	</tr>
   855  	{{end}}
   856  </table>
   857  </body></html>
   858  `)
   859  
   860  var crashTemplate = pages.Create(`
   861  <!doctype html>
   862  <html>
   863  <head>
   864  	<title>{{.Description}}</title>
   865  	{{HEAD}}
   866  </head>
   867  <body>
   868  <b>{{.Description}}</b>
   869  
   870  {{if .Triaged}}
   871  Report: <a href="/report?id={{.ID}}">{{.Triaged}}</a>
   872  {{end}}
   873  
   874  <table class="list_table">
   875  	<tr>
   876  		<th>#</th>
   877  		<th>Log</th>
   878  		<th>Report</th>
   879  		<th>Time</th>
   880  		<th>Tag</th>
   881  	</tr>
   882  	{{range $c := $.Crashes}}
   883  	<tr>
   884  		<td>{{$c.Index}}</td>
   885  		<td><a href="/file?name={{$c.Log}}">log</a></td>
   886  		<td>
   887  			{{if $c.Report}}
   888  				<a href="/file?name={{$c.Report}}">report</a></td>
   889  			{{end}}
   890  		</td>
   891  		<td class="time {{if not $c.Active}}inactive{{end}}">{{formatTime $c.Time}}</td>
   892  		<td class="tag {{if not $c.Active}}inactive{{end}}" title="{{$c.Tag}}">{{formatTagHash $c.Tag}}</td>
   893  	</tr>
   894  	{{end}}
   895  </table>
   896  </body></html>
   897  `)
   898  
   899  var corpusTemplate = pages.Create(`
   900  <!doctype html>
   901  <html>
   902  <head>
   903  	<title>syzkaller corpus</title>
   904  	{{HEAD}}
   905  </head>
   906  <body>
   907  
   908  <table class="list_table">
   909  	<caption>Corpus{{if $.Call}} for {{$.Call}}{{end}}:</caption>
   910  	<tr>
   911  		<th>Coverage</th>
   912  		<th>Program</th>
   913  	</tr>
   914  	{{range $inp := $.Inputs}}
   915  	<tr>
   916  		<td>
   917  			<a href='/cover?input={{$inp.Sig}}'>{{$inp.Cover}}</a>
   918  	{{if $.RawCover}}
   919  		/ <a href="/debuginput?sig={{$inp.Sig}}">[raw]</a>
   920  	{{end}}
   921  		</td>
   922  		<td><a href="/input?sig={{$inp.Sig}}">{{$inp.Short}}</a></td>
   923  	</tr>
   924  	{{end}}
   925  </table>
   926  </body></html>
   927  `)
   928  
   929  type UIPrioData struct {
   930  	Call  string
   931  	Prios []UIPrio
   932  }
   933  
   934  type UIPrio struct {
   935  	Call string
   936  	Prio int32
   937  }
   938  
   939  var prioTemplate = pages.Create(`
   940  <!doctype html>
   941  <html>
   942  <head>
   943  	<title>syzkaller priorities</title>
   944  	{{HEAD}}
   945  </head>
   946  <body>
   947  <table class="list_table">
   948  	<caption>Priorities for {{$.Call}}:</caption>
   949  	<tr>
   950  		<th><a onclick="return sortTable(this, 'Prio', floatSort)" href="#">Prio</a></th>
   951  		<th><a onclick="return sortTable(this, 'Call', textSort)" href="#">Call</a></th>
   952  	</tr>
   953  	{{range $p := $.Prios}}
   954  	<tr>
   955  		<td>{{printf "%5v" $p.Prio}}</td>
   956  		<td><a href='/prio?call={{$p.Call}}'>{{$p.Call}}</a></td>
   957  	</tr>
   958  	{{end}}
   959  </table>
   960  </body></html>
   961  `)
   962  
   963  type UIFallbackCoverData struct {
   964  	Calls []UIFallbackCall
   965  }
   966  
   967  type UIFallbackCall struct {
   968  	Name       string
   969  	Successful int
   970  	Errnos     []int
   971  }
   972  
   973  var fallbackCoverTemplate = pages.Create(`
   974  <!doctype html>
   975  <html>
   976  <head>
   977  	<title>syzkaller coverage</title>
   978  	{{HEAD}}
   979  </head>
   980  <body>
   981  <table class="list_table">
   982  	<tr>
   983  		<th>Call</th>
   984  		<th>Successful</th>
   985  		<th>Errnos</th>
   986  	</tr>
   987  	{{range $c := $.Calls}}
   988  	<tr>
   989  		<td>{{$c.Name}}</td>
   990  		<td>{{if $c.Successful}}{{$c.Successful}}{{end}}</td>
   991  		<td>{{range $e := $c.Errnos}}{{$e}}&nbsp;{{end}}</td>
   992  	</tr>
   993  	{{end}}
   994  </table>
   995  </body></html>
   996  `)
   997  
   998  type UIRawCallCover struct {
   999  	Sig       string
  1000  	Call      string
  1001  	UpdateIDs []int
  1002  }
  1003  
  1004  var rawCoverTemplate = pages.Create(`
  1005  <!doctype html>
  1006  <html>
  1007  <head>
  1008  	<title>syzkaller raw cover</title>
  1009  	{{HEAD}}
  1010  </head>
  1011  <body>
  1012  
  1013  <table class="list_table">
  1014  	<caption>Raw cover</caption>
  1015  	<tr>
  1016  		<th>Line</th>
  1017  		<th>Links</th>
  1018  	</tr>
  1019  	{{range $line := .}}
  1020  	<tr>
  1021  		<td>{{$line.Call}}</td>
  1022  		<td>
  1023  		{{range $id := $line.UpdateIDs}}
  1024  		<a href="/rawcover?input={{$line.Sig}}&update_id={{$id}}">[{{$id}}]</a>
  1025  		{{end}}
  1026  </td>
  1027  	</tr>
  1028  	{{end}}
  1029  </table>
  1030  </body></html>
  1031  `)