github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/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 manager
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"embed"
    10  	"encoding/json"
    11  	"fmt"
    12  	"html/template"
    13  	"io"
    14  	"net/http"
    15  	_ "net/http/pprof"
    16  	"os"
    17  	"path/filepath"
    18  	"regexp"
    19  	"runtime/debug"
    20  	"sort"
    21  	"strconv"
    22  	"strings"
    23  	"sync/atomic"
    24  	"time"
    25  
    26  	"github.com/google/syzkaller/pkg/corpus"
    27  	"github.com/google/syzkaller/pkg/cover"
    28  	"github.com/google/syzkaller/pkg/fuzzer"
    29  	"github.com/google/syzkaller/pkg/html/pages"
    30  	"github.com/google/syzkaller/pkg/log"
    31  	"github.com/google/syzkaller/pkg/mgrconfig"
    32  	"github.com/google/syzkaller/pkg/report"
    33  	"github.com/google/syzkaller/pkg/stat"
    34  	"github.com/google/syzkaller/pkg/vcs"
    35  	"github.com/google/syzkaller/pkg/vminfo"
    36  	"github.com/google/syzkaller/prog"
    37  	"github.com/google/syzkaller/vm"
    38  	"github.com/google/syzkaller/vm/dispatcher"
    39  	"github.com/gorilla/handlers"
    40  	"github.com/prometheus/client_golang/prometheus"
    41  	"github.com/prometheus/client_golang/prometheus/promhttp"
    42  )
    43  
    44  type CoverageInfo struct {
    45  	Modules         []*vminfo.KernelModule
    46  	ReportGenerator *ReportGeneratorWrapper
    47  	CoverFilter     map[uint64]struct{}
    48  }
    49  
    50  type HTTPServer struct {
    51  	// To be set before calling Serve.
    52  	Cfg         *mgrconfig.Config
    53  	StartTime   time.Time
    54  	CrashStore  *CrashStore
    55  	DiffStore   *DiffFuzzerStore
    56  	ReproLoop   *ReproLoop
    57  	Pool        *vm.Dispatcher
    58  	Pools       map[string]*vm.Dispatcher
    59  	TogglePause func(paused bool)
    60  
    61  	// Can be set dynamically after calling Serve.
    62  	Corpus          atomic.Pointer[corpus.Corpus]
    63  	Fuzzer          atomic.Pointer[fuzzer.Fuzzer]
    64  	Cover           atomic.Pointer[CoverageInfo]
    65  	EnabledSyscalls atomic.Value // map[*prog.Syscall]bool
    66  
    67  	// Internal state.
    68  	expertMode bool
    69  	paused     bool
    70  }
    71  
    72  func (serv *HTTPServer) Serve(ctx context.Context) error {
    73  	if serv.Cfg.HTTP == "" {
    74  		return fmt.Errorf("starting a disabled HTTP server")
    75  	}
    76  	if serv.Pool != nil {
    77  		serv.Pools = map[string]*vm.Dispatcher{"": serv.Pool}
    78  	}
    79  	handle := func(pattern string, handler func(http.ResponseWriter, *http.Request)) {
    80  		http.Handle(pattern, handlers.CompressHandler(http.HandlerFunc(handler)))
    81  	}
    82  	// keep-sorted start
    83  	handle("/", serv.httpMain)
    84  	handle("/action", serv.httpAction)
    85  	handle("/addcandidate", serv.httpAddCandidate)
    86  	handle("/config", serv.httpConfig)
    87  	handle("/corpus", serv.httpCorpus)
    88  	handle("/corpus.db", serv.httpDownloadCorpus)
    89  	handle("/cover", serv.httpCover)
    90  	handle("/coverprogs", serv.httpPrograms)
    91  	handle("/debuginput", serv.httpDebugInput)
    92  	handle("/file", serv.httpFile)
    93  	handle("/filecover", serv.httpFileCover)
    94  	handle("/filterpcs", serv.httpFilterPCs)
    95  	handle("/funccover", serv.httpFuncCover)
    96  	handle("/input", serv.httpInput)
    97  	handle("/jobs", serv.httpJobs)
    98  	handle("/metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).ServeHTTP)
    99  	handle("/modulecover", serv.httpModuleCover)
   100  	handle("/modules", serv.modulesInfo)
   101  	handle("/prio", serv.httpPrio)
   102  	handle("/rawcover", serv.httpRawCover)
   103  	handle("/rawcoverfiles", serv.httpRawCoverFiles)
   104  	handle("/stats", serv.httpStats)
   105  	handle("/subsystemcover", serv.httpSubsystemCover)
   106  	handle("/syscalls", serv.httpSyscalls)
   107  	handle("/vm", serv.httpVM)
   108  	handle("/vms", serv.httpVMs)
   109  	// keep-sorted end
   110  	if serv.CrashStore != nil {
   111  		handle("/crash", serv.httpCrash)
   112  		handle("/report", serv.httpReport)
   113  	}
   114  	// Browsers like to request this, without special handler this goes to / handler.
   115  	handle("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {})
   116  
   117  	log.Logf(0, "serving http on http://%v", serv.Cfg.HTTP)
   118  	server := &http.Server{Addr: serv.Cfg.HTTP}
   119  	go func() {
   120  		// The http server package unfortunately does not natively take a context.Context.
   121  		// Let's emulate it via server.Shutdown()
   122  		<-ctx.Done()
   123  		server.Close()
   124  	}()
   125  
   126  	err := server.ListenAndServe()
   127  	if err != http.ErrServerClosed {
   128  		return err
   129  	}
   130  	return nil
   131  }
   132  
   133  func (serv *HTTPServer) httpAction(w http.ResponseWriter, r *http.Request) {
   134  	switch r.FormValue("toggle") {
   135  	case "expert":
   136  		serv.expertMode = !serv.expertMode
   137  	case "pause":
   138  		if serv.TogglePause == nil {
   139  			http.Error(w, "pause is not implemented", http.StatusNotImplemented)
   140  			return
   141  		}
   142  		serv.paused = !serv.paused
   143  		serv.TogglePause(serv.paused)
   144  	}
   145  	http.Redirect(w, r, r.FormValue("url"), http.StatusFound)
   146  }
   147  
   148  func (serv *HTTPServer) httpMain(w http.ResponseWriter, r *http.Request) {
   149  	data := &UISummaryData{
   150  		UIPageHeader: serv.pageHeader(r, "syzkaller"),
   151  		Log:          log.CachedLogOutput(),
   152  	}
   153  
   154  	level := stat.Simple
   155  	if serv.expertMode {
   156  		level = stat.All
   157  	}
   158  	for _, stat := range stat.Collect(level) {
   159  		data.Stats = append(data.Stats, UIStat{
   160  			Name:  stat.Name,
   161  			Value: stat.Value,
   162  			Hint:  stat.Desc,
   163  			Link:  stat.Link,
   164  		})
   165  	}
   166  	if serv.CrashStore != nil {
   167  		var err error
   168  		if data.Crashes, err = serv.collectCrashes(serv.Cfg.Workdir); err != nil {
   169  			http.Error(w, fmt.Sprintf("failed to collect crashes: %v", err), http.StatusInternalServerError)
   170  			return
   171  		}
   172  	}
   173  	if serv.DiffStore != nil {
   174  		data.PatchedOnly, data.AffectsBoth, data.InProgress = serv.collectDiffCrashes()
   175  	}
   176  	executeTemplate(w, mainTemplate, data)
   177  }
   178  
   179  func (serv *HTTPServer) httpConfig(w http.ResponseWriter, r *http.Request) {
   180  	serv.jsonPage(w, r, "config", serv.Cfg)
   181  }
   182  
   183  func (serv *HTTPServer) jsonPage(w http.ResponseWriter, r *http.Request, title string, data any) {
   184  	text, err := json.MarshalIndent(data, "", "\t")
   185  	if err != nil {
   186  		http.Error(w, fmt.Sprintf("failed to encode json: %v", err), http.StatusInternalServerError)
   187  		return
   188  	}
   189  	serv.textPage(w, r, title, text)
   190  }
   191  
   192  func (serv *HTTPServer) textPage(w http.ResponseWriter, r *http.Request, title string, text []byte) {
   193  	if r.FormValue("raw") != "" {
   194  		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   195  		w.Write(text)
   196  		return
   197  	}
   198  	data := &UITextPage{
   199  		UIPageHeader: serv.pageHeader(r, title),
   200  		Text:         text,
   201  	}
   202  	executeTemplate(w, textTemplate, data)
   203  }
   204  
   205  func (serv *HTTPServer) httpSyscalls(w http.ResponseWriter, r *http.Request) {
   206  	var calls map[string]*corpus.CallCov
   207  	total := make(map[string]int)
   208  	fuzzerObj := serv.Fuzzer.Load()
   209  	syscallsObj := serv.EnabledSyscalls.Load()
   210  	corpusObj := serv.Corpus.Load()
   211  	if corpusObj != nil && syscallsObj != nil {
   212  		calls = corpusObj.CallCover()
   213  		// Add enabled, but not yet covered calls.
   214  		for call := range syscallsObj.(map[*prog.Syscall]bool) {
   215  			if calls[call.Name] == nil {
   216  				calls[call.Name] = new(corpus.CallCov)
   217  			}
   218  		}
   219  		// Count number of programs that include each call.
   220  		last := make(map[string]*prog.Prog)
   221  		for _, inp := range corpusObj.Items() {
   222  			for _, call := range inp.Prog.Calls {
   223  				name := call.Meta.Name
   224  				if last[name] != inp.Prog {
   225  					total[name]++
   226  				}
   227  				last[name] = inp.Prog
   228  			}
   229  		}
   230  	}
   231  	data := &UISyscallsData{
   232  		UIPageHeader: serv.pageHeader(r, "syscalls"),
   233  	}
   234  	for c, cc := range calls {
   235  		var syscallID *int
   236  		if syscall, ok := serv.Cfg.Target.SyscallMap[c]; ok {
   237  			syscallID = &syscall.ID
   238  		}
   239  		coverOverflows, compsOverflows := 0, 0
   240  		if fuzzerObj != nil {
   241  			idx := len(serv.Cfg.Target.Syscalls)
   242  			if c != prog.ExtraCallName {
   243  				idx = serv.Cfg.Target.SyscallMap[c].ID
   244  			}
   245  			coverOverflows = int(fuzzerObj.Syscalls[idx].CoverOverflows.Load())
   246  			compsOverflows = int(fuzzerObj.Syscalls[idx].CompsOverflows.Load())
   247  		}
   248  		data.Calls = append(data.Calls, UICallType{
   249  			Name:           c,
   250  			ID:             syscallID,
   251  			Inputs:         cc.Count,
   252  			Total:          total[c],
   253  			Cover:          len(cc.Cover),
   254  			CoverOverflows: coverOverflows,
   255  			CompsOverflows: compsOverflows,
   256  		})
   257  	}
   258  	sort.Slice(data.Calls, func(i, j int) bool {
   259  		return data.Calls[i].Name < data.Calls[j].Name
   260  	})
   261  	executeTemplate(w, syscallsTemplate, data)
   262  }
   263  
   264  func (serv *HTTPServer) httpStats(w http.ResponseWriter, r *http.Request) {
   265  	html, err := pages.StatsHTML()
   266  	if err != nil {
   267  		http.Error(w, err.Error(), http.StatusInternalServerError)
   268  		return
   269  	}
   270  	data := &UITextPage{
   271  		UIPageHeader: serv.pageHeader(r, "stats"),
   272  		HTML:         html,
   273  	}
   274  	executeTemplate(w, textTemplate, data)
   275  }
   276  
   277  func (serv *HTTPServer) httpVMs(w http.ResponseWriter, r *http.Request) {
   278  	pool := serv.Pools[r.FormValue("pool")]
   279  	if pool == nil {
   280  		http.Error(w, "no such VM pool is known (yet)", http.StatusInternalServerError)
   281  		return
   282  	}
   283  	data := &UIVMData{
   284  		UIPageHeader: serv.pageHeader(r, "VMs"),
   285  	}
   286  	// TODO: we could also query vmLoop for VMs that are idle (waiting to start reproducing),
   287  	// and query the exact bug that is being reproduced by a VM.
   288  	for id, state := range pool.State() {
   289  		name := fmt.Sprintf("#%d", id)
   290  		info := UIVMInfo{
   291  			Name:  name,
   292  			State: "unknown",
   293  			Since: time.Since(state.LastUpdate),
   294  		}
   295  		switch state.State {
   296  		case dispatcher.StateOffline:
   297  			info.State = "offline"
   298  		case dispatcher.StateBooting:
   299  			info.State = "booting"
   300  		case dispatcher.StateWaiting:
   301  			info.State = "waiting"
   302  		case dispatcher.StateRunning:
   303  			info.State = "running: " + state.Status
   304  		}
   305  		if state.Reserved {
   306  			info.State = "[reserved] " + info.State
   307  		}
   308  		if state.MachineInfo != nil {
   309  			info.MachineInfo = fmt.Sprintf("/vm?type=machine-info&id=%d", id)
   310  		}
   311  		if state.DetailedStatus != nil {
   312  			info.DetailedStatus = fmt.Sprintf("/vm?type=detailed-status&id=%v", id)
   313  		}
   314  		data.VMs = append(data.VMs, info)
   315  	}
   316  	executeTemplate(w, vmsTemplate, data)
   317  }
   318  
   319  func (serv *HTTPServer) httpVM(w http.ResponseWriter, r *http.Request) {
   320  	pool := serv.Pools[r.FormValue("pool")]
   321  	if pool == nil {
   322  		http.Error(w, "no such VM pool is known (yet)", http.StatusInternalServerError)
   323  		return
   324  	}
   325  
   326  	w.Header().Set("Content-Type", ctTextPlain)
   327  	id, err := strconv.Atoi(r.FormValue("id"))
   328  	infos := pool.State()
   329  	if err != nil || id < 0 || id >= len(infos) {
   330  		http.Error(w, "invalid instance id", http.StatusBadRequest)
   331  		return
   332  	}
   333  	info := infos[id]
   334  	switch r.FormValue("type") {
   335  	case "machine-info":
   336  		if info.MachineInfo != nil {
   337  			w.Write(info.MachineInfo())
   338  		}
   339  	case "detailed-status":
   340  		if info.DetailedStatus != nil {
   341  			w.Write(info.DetailedStatus())
   342  		}
   343  	default:
   344  		w.Write([]byte("unknown info type"))
   345  	}
   346  }
   347  
   348  func makeUICrashType(info *BugInfo, startTime time.Time, repros map[string]bool) UICrashType {
   349  	var crashes []UICrash
   350  	for _, crash := range info.Crashes {
   351  		crashes = append(crashes, UICrash{
   352  			CrashInfo: *crash,
   353  			Active:    crash.Time.After(startTime),
   354  		})
   355  	}
   356  	triaged := reproStatus(info.HasRepro, info.HasCRepro, repros[info.Title],
   357  		info.ReproAttempts >= MaxReproAttempts)
   358  	return UICrashType{
   359  		BugInfo:     *info,
   360  		RankTooltip: higherRankTooltip(info.Title, info.TailTitles),
   361  		New:         info.FirstTime.After(startTime),
   362  		Active:      info.LastTime.After(startTime),
   363  		Triaged:     triaged,
   364  		Crashes:     crashes,
   365  	}
   366  }
   367  
   368  // higherRankTooltip generates the prioritized list of the titles with higher Rank
   369  // than the firstTitle has.
   370  func higherRankTooltip(firstTitle string, titlesInfo []*report.TitleFreqRank) string {
   371  	baseRank := report.TitlesToImpact(firstTitle)
   372  	res := ""
   373  	for _, ti := range titlesInfo {
   374  		if ti.Rank <= baseRank {
   375  			continue
   376  		}
   377  		res += fmt.Sprintf("[rank %2v, freq %5.1f%%] %s\n",
   378  			ti.Rank,
   379  			100*float32(ti.Count)/float32(ti.Total),
   380  			ti.Title)
   381  	}
   382  	if res != "" {
   383  		return fmt.Sprintf("[rank %2v,  originally] %s\n%s", baseRank, firstTitle, res)
   384  	}
   385  	return res
   386  }
   387  
   388  var crashIDRe = regexp.MustCompile(`^\w+$`)
   389  
   390  func (serv *HTTPServer) httpCrash(w http.ResponseWriter, r *http.Request) {
   391  	crashID := r.FormValue("id")
   392  	if !crashIDRe.MatchString(crashID) {
   393  		http.Error(w, "invalid crash ID", http.StatusBadRequest)
   394  		return
   395  	}
   396  	info, err := serv.CrashStore.BugInfo(crashID, true)
   397  	if err != nil {
   398  		http.Error(w, "failed to read crash info", http.StatusInternalServerError)
   399  		return
   400  	}
   401  	data := UICrashPage{
   402  		UIPageHeader: serv.pageHeader(r, info.Title),
   403  		UICrashType:  makeUICrashType(info, serv.StartTime, nil),
   404  	}
   405  	executeTemplate(w, crashTemplate, data)
   406  }
   407  
   408  func (serv *HTTPServer) httpCorpus(w http.ResponseWriter, r *http.Request) {
   409  	corpus := serv.Corpus.Load()
   410  	if corpus == nil {
   411  		http.Error(w, "the corpus information is not yet available", http.StatusInternalServerError)
   412  		return
   413  	}
   414  	data := UICorpusPage{
   415  		UIPageHeader: serv.pageHeader(r, "corpus"),
   416  		Call:         r.FormValue("call"),
   417  		RawCover:     serv.Cfg.RawCover,
   418  	}
   419  	for _, inp := range corpus.Items() {
   420  		if data.Call != "" && data.Call != inp.StringCall() {
   421  			continue
   422  		}
   423  		data.Inputs = append(data.Inputs, UIInput{
   424  			Sig:   inp.Sig,
   425  			Short: inp.Prog.String(),
   426  			Cover: len(inp.Cover),
   427  		})
   428  	}
   429  	sort.Slice(data.Inputs, func(i, j int) bool {
   430  		a, b := data.Inputs[i], data.Inputs[j]
   431  		if a.Cover != b.Cover {
   432  			return a.Cover > b.Cover
   433  		}
   434  		return a.Short < b.Short
   435  	})
   436  	executeTemplate(w, corpusTemplate, data)
   437  }
   438  
   439  func (serv *HTTPServer) httpDownloadCorpus(w http.ResponseWriter, r *http.Request) {
   440  	corpus := filepath.Join(serv.Cfg.Workdir, "corpus.db")
   441  	file, err := os.Open(corpus)
   442  	if err != nil {
   443  		http.Error(w, fmt.Sprintf("failed to open corpus : %v", err), http.StatusInternalServerError)
   444  		return
   445  	}
   446  	defer file.Close()
   447  	buf, err := io.ReadAll(file)
   448  	if err != nil {
   449  		http.Error(w, fmt.Sprintf("failed to read corpus : %v", err), http.StatusInternalServerError)
   450  		return
   451  	}
   452  	w.Write(buf)
   453  }
   454  
   455  const (
   456  	DoHTML int = iota
   457  	DoSubsystemCover
   458  	DoModuleCover
   459  	DoFuncCover
   460  	DoFileCover
   461  	DoRawCoverFiles
   462  	DoRawCover
   463  	DoFilterPCs
   464  	DoCoverJSONL
   465  	DoCoverPrograms
   466  )
   467  
   468  func (serv *HTTPServer) httpCover(w http.ResponseWriter, r *http.Request) {
   469  	if !serv.Cfg.Cover {
   470  		serv.httpCoverFallback(w, r)
   471  		return
   472  	}
   473  	if r.FormValue("jsonl") == "1" {
   474  		serv.httpCoverCover(w, r, DoCoverJSONL)
   475  		return
   476  	}
   477  	serv.httpCoverCover(w, r, DoHTML)
   478  }
   479  
   480  func (serv *HTTPServer) httpPrograms(w http.ResponseWriter, r *http.Request) {
   481  	if !serv.Cfg.Cover {
   482  		http.Error(w, "coverage is not enabled", http.StatusInternalServerError)
   483  		return
   484  	}
   485  	if r.FormValue("jsonl") != "1" {
   486  		http.Error(w, "only ?jsonl=1 param is supported", http.StatusBadRequest)
   487  		return
   488  	}
   489  	serv.httpCoverCover(w, r, DoCoverPrograms)
   490  }
   491  
   492  func (serv *HTTPServer) httpSubsystemCover(w http.ResponseWriter, r *http.Request) {
   493  	if !serv.Cfg.Cover {
   494  		serv.httpCoverFallback(w, r)
   495  		return
   496  	}
   497  	serv.httpCoverCover(w, r, DoSubsystemCover)
   498  }
   499  
   500  func (serv *HTTPServer) httpModuleCover(w http.ResponseWriter, r *http.Request) {
   501  	if !serv.Cfg.Cover {
   502  		serv.httpCoverFallback(w, r)
   503  		return
   504  	}
   505  	serv.httpCoverCover(w, r, DoModuleCover)
   506  }
   507  
   508  const ctTextPlain = "text/plain; charset=utf-8"
   509  const ctApplicationJSON = "application/json"
   510  
   511  func (serv *HTTPServer) httpCoverCover(w http.ResponseWriter, r *http.Request, funcFlag int) {
   512  	if !serv.Cfg.Cover {
   513  		http.Error(w, "coverage is not enabled", http.StatusInternalServerError)
   514  		return
   515  	}
   516  
   517  	coverInfo := serv.Cover.Load()
   518  	if coverInfo == nil {
   519  		http.Error(w, "coverage is not ready, please try again later after fuzzer started", http.StatusInternalServerError)
   520  		return
   521  	}
   522  
   523  	corpus := serv.Corpus.Load()
   524  	if corpus == nil {
   525  		http.Error(w, "the corpus information is not yet available", http.StatusInternalServerError)
   526  		return
   527  	}
   528  
   529  	rg, err := coverInfo.ReportGenerator.Get()
   530  	if err != nil {
   531  		http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError)
   532  		return
   533  	}
   534  
   535  	if r.FormValue("flush") != "" {
   536  		defer func() {
   537  			coverInfo.ReportGenerator.Reset()
   538  			debug.FreeOSMemory()
   539  		}()
   540  	}
   541  
   542  	var progs []coverProgRaw
   543  	if sig := r.FormValue("input"); sig != "" {
   544  		inp := corpus.Item(sig)
   545  		if inp == nil {
   546  			http.Error(w, "unknown input hash", http.StatusInternalServerError)
   547  			return
   548  		}
   549  		if r.FormValue("update_id") != "" {
   550  			updateID, err := strconv.Atoi(r.FormValue("update_id"))
   551  			if err != nil || updateID < 0 || updateID >= len(inp.Updates) {
   552  				http.Error(w, "bad call_id", http.StatusBadRequest)
   553  				return
   554  			}
   555  			progs = append(progs, coverProgRaw{
   556  				sig:  sig,
   557  				prog: inp.Prog,
   558  				pcs:  CoverToPCs(serv.Cfg, inp.Updates[updateID].RawCover),
   559  			})
   560  		} else {
   561  			progs = append(progs, coverProgRaw{
   562  				sig:  sig,
   563  				prog: inp.Prog,
   564  				pcs:  CoverToPCs(serv.Cfg, inp.Cover),
   565  			})
   566  		}
   567  	} else {
   568  		call := r.FormValue("call")
   569  		for _, inp := range corpus.Items() {
   570  			if call != "" && call != inp.StringCall() {
   571  				continue
   572  			}
   573  			progs = append(progs, coverProgRaw{
   574  				sig:  inp.Sig,
   575  				prog: inp.Prog,
   576  				pcs:  CoverToPCs(serv.Cfg, inp.Cover),
   577  			})
   578  		}
   579  	}
   580  
   581  	var coverFilter map[uint64]struct{}
   582  	if r.FormValue("filter") != "" || funcFlag == DoFilterPCs {
   583  		if coverInfo.CoverFilter == nil {
   584  			http.Error(w, "cover is not filtered in config", http.StatusInternalServerError)
   585  			return
   586  		}
   587  		coverFilter = coverInfo.CoverFilter
   588  	}
   589  
   590  	params := cover.HandlerParams{
   591  		Progs:  serv.serializeCoverProgs(progs),
   592  		Filter: coverFilter,
   593  		Debug:  r.FormValue("debug") != "",
   594  		Force:  r.FormValue("force") != "",
   595  	}
   596  
   597  	type handlerFuncType func(w io.Writer, params cover.HandlerParams) error
   598  	flagToFunc := map[int]struct {
   599  		Do          handlerFuncType
   600  		contentType string
   601  	}{
   602  		DoHTML:           {rg.DoHTML, ""},
   603  		DoSubsystemCover: {rg.DoSubsystemCover, ""},
   604  		DoModuleCover:    {rg.DoModuleCover, ""},
   605  		DoFuncCover:      {rg.DoFuncCover, ctTextPlain},
   606  		DoFileCover:      {rg.DoFileCover, ctTextPlain},
   607  		DoRawCoverFiles:  {rg.DoRawCoverFiles, ctTextPlain},
   608  		DoRawCover:       {rg.DoRawCover, ctTextPlain},
   609  		DoFilterPCs:      {rg.DoFilterPCs, ctTextPlain},
   610  		DoCoverJSONL:     {rg.DoCoverJSONL, ctApplicationJSON},
   611  		DoCoverPrograms:  {rg.DoCoverPrograms, ctApplicationJSON},
   612  	}
   613  
   614  	if ct := flagToFunc[funcFlag].contentType; ct != "" {
   615  		w.Header().Set("Content-Type", ct)
   616  	}
   617  
   618  	if err := flagToFunc[funcFlag].Do(w, params); err != nil {
   619  		http.Error(w, fmt.Sprintf("failed to generate coverage profile: %v", err), http.StatusInternalServerError)
   620  		return
   621  	}
   622  }
   623  
   624  type coverProgRaw struct {
   625  	sig  string
   626  	prog *prog.Prog
   627  	pcs  []uint64
   628  }
   629  
   630  // Once the total size of corpus programs exceeds 100MB, skip fs images from it.
   631  const compactProgsCutOff = 100 * 1000 * 1000
   632  
   633  func (serv *HTTPServer) serializeCoverProgs(rawProgs []coverProgRaw) []cover.Prog {
   634  	skipImages := false
   635  outerLoop:
   636  	for {
   637  		var flags []prog.SerializeFlag
   638  		if skipImages {
   639  			flags = append(flags, prog.SkipImages)
   640  		}
   641  		totalSize := 0
   642  		var ret []cover.Prog
   643  		for _, item := range rawProgs {
   644  			prog := cover.Prog{
   645  				Sig:  item.sig,
   646  				Data: string(item.prog.Serialize(flags...)),
   647  				PCs:  item.pcs,
   648  			}
   649  			totalSize += len(prog.Data)
   650  			if totalSize > compactProgsCutOff && !skipImages {
   651  				log.Logf(0, "total size of corpus programs is too big, "+
   652  					"full fs image won't be included in the cover reports")
   653  				skipImages = true
   654  				continue outerLoop
   655  			}
   656  			ret = append(ret, prog)
   657  		}
   658  		return ret
   659  	}
   660  }
   661  
   662  func (serv *HTTPServer) httpCoverFallback(w http.ResponseWriter, r *http.Request) {
   663  	corpus := serv.Corpus.Load()
   664  	if corpus == nil {
   665  		http.Error(w, "the corpus information is not yet available", http.StatusInternalServerError)
   666  		return
   667  	}
   668  	calls := make(map[int][]int)
   669  	for s := range corpus.Signal() {
   670  		id, errno := prog.DecodeFallbackSignal(uint64(s))
   671  		calls[id] = append(calls[id], errno)
   672  	}
   673  	data := &UIFallbackCoverData{
   674  		UIPageHeader: serv.pageHeader(r, "fallback coverage"),
   675  	}
   676  	if obj := serv.EnabledSyscalls.Load(); obj != nil {
   677  		for call := range obj.(map[*prog.Syscall]bool) {
   678  			errnos := calls[call.ID]
   679  			sort.Ints(errnos)
   680  			successful := 0
   681  			for len(errnos) != 0 && errnos[0] == 0 {
   682  				successful++
   683  				errnos = errnos[1:]
   684  			}
   685  			data.Calls = append(data.Calls, UIFallbackCall{
   686  				Name:       call.Name,
   687  				Successful: successful,
   688  				Errnos:     errnos,
   689  			})
   690  		}
   691  	}
   692  	sort.Slice(data.Calls, func(i, j int) bool {
   693  		return data.Calls[i].Name < data.Calls[j].Name
   694  	})
   695  	executeTemplate(w, fallbackCoverTemplate, data)
   696  }
   697  
   698  func (serv *HTTPServer) httpFuncCover(w http.ResponseWriter, r *http.Request) {
   699  	serv.httpCoverCover(w, r, DoFuncCover)
   700  }
   701  
   702  func (serv *HTTPServer) httpFileCover(w http.ResponseWriter, r *http.Request) {
   703  	serv.httpCoverCover(w, r, DoFileCover)
   704  }
   705  
   706  func (serv *HTTPServer) httpPrio(w http.ResponseWriter, r *http.Request) {
   707  	corpus := serv.Corpus.Load()
   708  	if corpus == nil {
   709  		http.Error(w, "the corpus information is not yet available", http.StatusInternalServerError)
   710  		return
   711  	}
   712  
   713  	callName := r.FormValue("call")
   714  	call := serv.Cfg.Target.SyscallMap[callName]
   715  	if call == nil {
   716  		http.Error(w, fmt.Sprintf("unknown call: %v", callName), http.StatusInternalServerError)
   717  		return
   718  	}
   719  
   720  	var progs []*prog.Prog
   721  	for _, inp := range corpus.Items() {
   722  		progs = append(progs, inp.Prog)
   723  	}
   724  
   725  	var enabled map[*prog.Syscall]bool
   726  	if obj := serv.EnabledSyscalls.Load(); obj != nil {
   727  		enabled = obj.(map[*prog.Syscall]bool)
   728  	}
   729  	prios, generatable := serv.Cfg.Target.CalculatePriorities(progs, enabled)
   730  
   731  	data := &UIPrioData{
   732  		UIPageHeader: serv.pageHeader(r, "syscall priorities"),
   733  		Call:         callName,
   734  	}
   735  	for i, p := range prios[call.ID] {
   736  		syscall := serv.Cfg.Target.Syscalls[i]
   737  		if !generatable[syscall] {
   738  			continue
   739  		}
   740  		data.Prios = append(data.Prios, UIPrio{syscall.Name, p})
   741  	}
   742  	sort.Slice(data.Prios, func(i, j int) bool {
   743  		return data.Prios[i].Prio > data.Prios[j].Prio
   744  	})
   745  	executeTemplate(w, prioTemplate, data)
   746  }
   747  
   748  func (serv *HTTPServer) httpFile(w http.ResponseWriter, r *http.Request) {
   749  	file := filepath.Clean(r.FormValue("name"))
   750  	if !strings.HasPrefix(file, "crashes/") && !strings.HasPrefix(file, "corpus/") {
   751  		http.Error(w, "oh, oh, oh!", http.StatusInternalServerError)
   752  		return
   753  	}
   754  	data, err := os.ReadFile(filepath.Join(serv.Cfg.Workdir, file))
   755  	if err != nil {
   756  		http.Error(w, "failed to read the file", http.StatusInternalServerError)
   757  		return
   758  	}
   759  	serv.textPage(w, r, "file", data)
   760  }
   761  
   762  func (serv *HTTPServer) httpInput(w http.ResponseWriter, r *http.Request) {
   763  	corpus := serv.Corpus.Load()
   764  	if corpus == nil {
   765  		http.Error(w, "the corpus information is not yet available", http.StatusInternalServerError)
   766  		return
   767  	}
   768  	inp := corpus.Item(r.FormValue("sig"))
   769  	if inp == nil {
   770  		http.Error(w, "can't find the input", http.StatusInternalServerError)
   771  		return
   772  	}
   773  	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
   774  	w.Write(inp.Prog.Serialize())
   775  }
   776  
   777  func (serv *HTTPServer) httpDebugInput(w http.ResponseWriter, r *http.Request) {
   778  	corpus := serv.Corpus.Load()
   779  	if corpus == nil {
   780  		http.Error(w, "the corpus information is not yet available", http.StatusInternalServerError)
   781  		return
   782  	}
   783  	inp := corpus.Item(r.FormValue("sig"))
   784  	if inp == nil {
   785  		http.Error(w, "can't find the input", http.StatusInternalServerError)
   786  		return
   787  	}
   788  	getIDs := func(callID int) []int {
   789  		ret := []int{}
   790  		for id, update := range inp.Updates {
   791  			if update.Call == callID {
   792  				ret = append(ret, id)
   793  			}
   794  		}
   795  		return ret
   796  	}
   797  	var calls []UIRawCallCover
   798  	for pos, line := range strings.Split(string(inp.Prog.Serialize()), "\n") {
   799  		line = strings.TrimSpace(line)
   800  		if line == "" {
   801  			continue
   802  		}
   803  		calls = append(calls, UIRawCallCover{
   804  			Sig:       r.FormValue("sig"),
   805  			Call:      line,
   806  			UpdateIDs: getIDs(pos),
   807  		})
   808  	}
   809  	extraIDs := getIDs(-1)
   810  	if len(extraIDs) > 0 {
   811  		calls = append(calls, UIRawCallCover{
   812  			Sig:       r.FormValue("sig"),
   813  			Call:      prog.ExtraCallName,
   814  			UpdateIDs: extraIDs,
   815  		})
   816  	}
   817  	data := UIRawCoverPage{
   818  		UIPageHeader: serv.pageHeader(r, "raw coverage"),
   819  		Calls:        calls,
   820  	}
   821  	executeTemplate(w, rawCoverTemplate, data)
   822  }
   823  
   824  func (serv *HTTPServer) modulesInfo(w http.ResponseWriter, r *http.Request) {
   825  	cover := serv.Cover.Load()
   826  	if cover == nil {
   827  		http.Error(w, "info is not ready, please try again later after fuzzer started", http.StatusInternalServerError)
   828  		return
   829  	}
   830  	serv.jsonPage(w, r, "modules", cover.Modules)
   831  }
   832  
   833  func (serv *HTTPServer) httpAddCandidate(w http.ResponseWriter, r *http.Request) {
   834  	if r.Method != http.MethodPost {
   835  		http.Error(w, "only POST method supported", http.StatusMethodNotAllowed)
   836  		return
   837  	}
   838  	err := r.ParseMultipartForm(20 << 20)
   839  	if err != nil {
   840  		http.Error(w, fmt.Sprintf("failed to parse form: %v", err), http.StatusBadRequest)
   841  		return
   842  	}
   843  	file, _, err := r.FormFile("file")
   844  	if err != nil {
   845  		http.Error(w, fmt.Sprintf("failed to retrieve file from form-data: %v", err), http.StatusBadRequest)
   846  		return
   847  	}
   848  	defer file.Close()
   849  	data, err := io.ReadAll(file)
   850  	if err != nil {
   851  		http.Error(w, fmt.Sprintf("failed to read file: %v", err), http.StatusBadRequest)
   852  		return
   853  	}
   854  	prog, err := ParseSeed(serv.Cfg.Target, data)
   855  	if err != nil {
   856  		http.Error(w, fmt.Sprintf("failed to parse seed: %v", err), http.StatusBadRequest)
   857  		return
   858  	}
   859  	if !prog.OnlyContains(serv.Fuzzer.Load().Config.EnabledCalls) {
   860  		http.Error(w, "contains disabled syscall", http.StatusBadRequest)
   861  		return
   862  	}
   863  	var flags fuzzer.ProgFlags
   864  	flags |= fuzzer.ProgMinimized
   865  	flags |= fuzzer.ProgSmashed
   866  	candidates := []fuzzer.Candidate{{
   867  		Prog:  prog,
   868  		Flags: flags,
   869  	}}
   870  	serv.Fuzzer.Load().AddCandidates(candidates)
   871  }
   872  
   873  var alphaNumRegExp = regexp.MustCompile(`^[a-zA-Z0-9]*$`)
   874  
   875  func isAlphanumeric(s string) bool {
   876  	return alphaNumRegExp.MatchString(s)
   877  }
   878  
   879  func (serv *HTTPServer) httpReport(w http.ResponseWriter, r *http.Request) {
   880  	crashID := r.FormValue("id")
   881  	if !isAlphanumeric(crashID) {
   882  		http.Error(w, "wrong id", http.StatusBadRequest)
   883  		return
   884  	}
   885  
   886  	info, err := serv.CrashStore.Report(crashID)
   887  	if err != nil {
   888  		http.Error(w, fmt.Sprintf("%v", err), http.StatusBadRequest)
   889  		return
   890  	}
   891  
   892  	commitDesc := ""
   893  	if info.Tag != "" {
   894  		commitDesc = fmt.Sprintf(" on commit %s.", info.Tag)
   895  	}
   896  	fmt.Fprintf(w, "Syzkaller hit '%s' bug%s.\n\n", info.Title, commitDesc)
   897  	if len(info.Report) != 0 {
   898  		fmt.Fprintf(w, "%s\n\n", info.Report)
   899  	}
   900  	if len(info.Prog) == 0 && len(info.CProg) == 0 {
   901  		fmt.Fprintf(w, "The bug is not reproducible.\n")
   902  	} else {
   903  		fmt.Fprintf(w, "Syzkaller reproducer:\n%s\n\n", info.Prog)
   904  		if len(info.CProg) != 0 {
   905  			fmt.Fprintf(w, "C reproducer:\n%s\n\n", info.CProg)
   906  		}
   907  	}
   908  }
   909  
   910  func (serv *HTTPServer) httpRawCover(w http.ResponseWriter, r *http.Request) {
   911  	serv.httpCoverCover(w, r, DoRawCover)
   912  }
   913  
   914  func (serv *HTTPServer) httpRawCoverFiles(w http.ResponseWriter, r *http.Request) {
   915  	serv.httpCoverCover(w, r, DoRawCoverFiles)
   916  }
   917  
   918  func (serv *HTTPServer) httpFilterPCs(w http.ResponseWriter, r *http.Request) {
   919  	serv.httpCoverCover(w, r, DoFilterPCs)
   920  }
   921  
   922  func (serv *HTTPServer) collectDiffCrashes() (patchedOnly, both, inProgress *UIDiffTable) {
   923  	for _, item := range serv.allDiffCrashes() {
   924  		if item.PatchedOnly() {
   925  			if patchedOnly == nil {
   926  				patchedOnly = &UIDiffTable{Title: "Patched-only"}
   927  			}
   928  			patchedOnly.List = append(patchedOnly.List, item)
   929  		} else if item.AffectsBoth() {
   930  			if both == nil {
   931  				both = &UIDiffTable{Title: "Affects both"}
   932  			}
   933  			both.List = append(both.List, item)
   934  		} else {
   935  			if inProgress == nil {
   936  				inProgress = &UIDiffTable{Title: "In Progress"}
   937  			}
   938  			inProgress.List = append(inProgress.List, item)
   939  		}
   940  	}
   941  	return
   942  }
   943  
   944  func (serv *HTTPServer) allDiffCrashes() []UIDiffBug {
   945  	repros := serv.ReproLoop.Reproducing()
   946  	var list []UIDiffBug
   947  	for _, bug := range serv.DiffStore.List() {
   948  		list = append(list, UIDiffBug{
   949  			DiffBug:     bug,
   950  			Reproducing: repros[bug.Title],
   951  		})
   952  	}
   953  	sort.Slice(list, func(i, j int) bool {
   954  		first, second := list[i], list[j]
   955  		firstPatched, secondPatched := first.PatchedOnly(), second.PatchedOnly()
   956  		if firstPatched != secondPatched {
   957  			return firstPatched
   958  		}
   959  		return first.Title < second.Title
   960  	})
   961  	return list
   962  }
   963  
   964  func (serv *HTTPServer) collectCrashes(workdir string) ([]UICrashType, error) {
   965  	list, err := serv.CrashStore.BugList()
   966  	if err != nil {
   967  		return nil, err
   968  	}
   969  	repros := serv.ReproLoop.Reproducing()
   970  	var ret []UICrashType
   971  	for _, info := range list {
   972  		ret = append(ret, makeUICrashType(info, serv.StartTime, repros))
   973  	}
   974  	return ret, nil
   975  }
   976  
   977  func (serv *HTTPServer) httpJobs(w http.ResponseWriter, r *http.Request) {
   978  	var list []*fuzzer.JobInfo
   979  	if fuzzer := serv.Fuzzer.Load(); fuzzer != nil {
   980  		list = fuzzer.RunningJobs()
   981  	}
   982  	if key := r.FormValue("id"); key != "" {
   983  		for _, item := range list {
   984  			if item.ID() == key {
   985  				w.Write(item.Bytes())
   986  				return
   987  			}
   988  		}
   989  		http.Error(w, "invalid job id (the job has likely already finished)", http.StatusBadRequest)
   990  		return
   991  	}
   992  	jobType := r.FormValue("type")
   993  	data := UIJobList{
   994  		UIPageHeader: serv.pageHeader(r, fmt.Sprintf("%s jobs", jobType)),
   995  	}
   996  	switch jobType {
   997  	case "triage":
   998  	case "smash":
   999  	case "hints":
  1000  	default:
  1001  		http.Error(w, "unknown job type", http.StatusBadRequest)
  1002  		return
  1003  	}
  1004  	for _, item := range list {
  1005  		if item.Type != jobType {
  1006  			continue
  1007  		}
  1008  		data.Jobs = append(data.Jobs, UIJobInfo{
  1009  			ID:    item.ID(),
  1010  			Short: item.Name,
  1011  			Execs: item.Execs.Load(),
  1012  			Calls: strings.Join(item.Calls, ", "),
  1013  		})
  1014  	}
  1015  	sort.Slice(data.Jobs, func(i, j int) bool {
  1016  		a, b := data.Jobs[i], data.Jobs[j]
  1017  		return a.Short < b.Short
  1018  	})
  1019  	executeTemplate(w, jobListTemplate, data)
  1020  }
  1021  
  1022  func reproStatus(hasRepro, hasCRepro, reproducing, nonReproducible bool) string {
  1023  	status := ""
  1024  	if hasRepro {
  1025  		status = "has repro"
  1026  		if hasCRepro {
  1027  			status = "has C repro"
  1028  		}
  1029  	} else if reproducing {
  1030  		status = "reproducing"
  1031  	} else if nonReproducible {
  1032  		status = "non-reproducible"
  1033  	}
  1034  	return status
  1035  }
  1036  
  1037  func executeTemplate(w http.ResponseWriter, templ *template.Template, data interface{}) {
  1038  	buf := new(bytes.Buffer)
  1039  	if err := templ.Execute(buf, data); err != nil {
  1040  		log.Logf(0, "failed to execute template: %v", err)
  1041  		http.Error(w, fmt.Sprintf("failed to execute template: %v", err), http.StatusInternalServerError)
  1042  		return
  1043  	}
  1044  	w.Write(buf.Bytes())
  1045  }
  1046  
  1047  type UISummaryData struct {
  1048  	UIPageHeader
  1049  	Stats       []UIStat
  1050  	Crashes     []UICrashType
  1051  	PatchedOnly *UIDiffTable
  1052  	AffectsBoth *UIDiffTable
  1053  	InProgress  *UIDiffTable
  1054  	Log         string
  1055  }
  1056  
  1057  type UIDiffTable struct {
  1058  	Title string
  1059  	List  []UIDiffBug
  1060  }
  1061  
  1062  type UIVMData struct {
  1063  	UIPageHeader
  1064  	VMs []UIVMInfo
  1065  }
  1066  
  1067  type UIVMInfo struct {
  1068  	Name           string
  1069  	State          string
  1070  	Since          time.Duration
  1071  	MachineInfo    string
  1072  	DetailedStatus string
  1073  }
  1074  
  1075  type UISyscallsData struct {
  1076  	UIPageHeader
  1077  	Calls []UICallType
  1078  }
  1079  
  1080  type UICrashPage struct {
  1081  	UIPageHeader
  1082  	UICrashType
  1083  }
  1084  
  1085  type UICrashType struct {
  1086  	BugInfo
  1087  	RankTooltip string
  1088  	New         bool // was first found in the current run
  1089  	Active      bool // was found in the current run
  1090  	Triaged     string
  1091  	Crashes     []UICrash
  1092  }
  1093  
  1094  type UICrash struct {
  1095  	CrashInfo
  1096  	Active bool
  1097  }
  1098  
  1099  type UIDiffBug struct {
  1100  	DiffBug
  1101  	Reproducing bool
  1102  }
  1103  
  1104  type UIStat struct {
  1105  	Name  string
  1106  	Value string
  1107  	Hint  string
  1108  	Link  string
  1109  }
  1110  
  1111  type UICallType struct {
  1112  	Name           string
  1113  	ID             *int
  1114  	Inputs         int
  1115  	Total          int
  1116  	Cover          int
  1117  	CoverOverflows int
  1118  	CompsOverflows int
  1119  }
  1120  
  1121  type UICorpusPage struct {
  1122  	UIPageHeader
  1123  	Call     string
  1124  	RawCover bool
  1125  	Inputs   []UIInput
  1126  }
  1127  
  1128  type UIInput struct {
  1129  	Sig   string
  1130  	Short string
  1131  	Cover int
  1132  }
  1133  
  1134  type UIPageHeader struct {
  1135  	Name      string
  1136  	PageTitle string
  1137  	// Relative page URL w/o GET parameters (e.g. "/stats").
  1138  	URLPath string
  1139  	// Relative page URL with GET parameters/fragment/etc (e.g. "/stats?foo=1#bar").
  1140  	CurrentURL string
  1141  	// syzkaller build git revision and link.
  1142  	GitRevision     string
  1143  	GitRevisionLink string
  1144  	ExpertMode      bool
  1145  	Paused          bool
  1146  }
  1147  
  1148  func (serv *HTTPServer) pageHeader(r *http.Request, title string) UIPageHeader {
  1149  	revision, revisionLink := prog.GitRevisionBase, ""
  1150  	if len(revision) > 8 {
  1151  		revisionLink = vcs.LogLink(vcs.SyzkallerRepo, revision)
  1152  		revision = revision[:8]
  1153  	}
  1154  	url := r.URL
  1155  	url.Scheme = ""
  1156  	url.Host = ""
  1157  	url.User = nil
  1158  	return UIPageHeader{
  1159  		Name:            serv.Cfg.Name,
  1160  		PageTitle:       title,
  1161  		URLPath:         r.URL.Path,
  1162  		CurrentURL:      url.String(),
  1163  		GitRevision:     revision,
  1164  		GitRevisionLink: revisionLink,
  1165  		ExpertMode:      serv.expertMode,
  1166  		Paused:          serv.paused,
  1167  	}
  1168  }
  1169  
  1170  func createPage(name string, data any) *template.Template {
  1171  	templ := pages.Create(fmt.Sprintf(string(mustReadHTML("common")), mustReadHTML(name)))
  1172  	templTypes = append(templTypes, templType{
  1173  		templ: templ,
  1174  		data:  data,
  1175  	})
  1176  	return templ
  1177  }
  1178  
  1179  type templType struct {
  1180  	templ *template.Template
  1181  	data  any
  1182  }
  1183  
  1184  var templTypes []templType
  1185  
  1186  type UIPrioData struct {
  1187  	UIPageHeader
  1188  	Call  string
  1189  	Prios []UIPrio
  1190  }
  1191  
  1192  type UIPrio struct {
  1193  	Call string
  1194  	Prio int32
  1195  }
  1196  
  1197  type UIFallbackCoverData struct {
  1198  	UIPageHeader
  1199  	Calls []UIFallbackCall
  1200  }
  1201  
  1202  type UIFallbackCall struct {
  1203  	Name       string
  1204  	Successful int
  1205  	Errnos     []int
  1206  }
  1207  
  1208  type UIRawCoverPage struct {
  1209  	UIPageHeader
  1210  	Calls []UIRawCallCover
  1211  }
  1212  
  1213  type UIRawCallCover struct {
  1214  	Sig       string
  1215  	Call      string
  1216  	UpdateIDs []int
  1217  }
  1218  
  1219  type UIJobList struct {
  1220  	UIPageHeader
  1221  	Jobs []UIJobInfo
  1222  }
  1223  
  1224  type UIJobInfo struct {
  1225  	ID    string
  1226  	Short string
  1227  	Calls string
  1228  	Execs int32
  1229  }
  1230  
  1231  type UITextPage struct {
  1232  	UIPageHeader
  1233  	Text []byte
  1234  	HTML template.HTML
  1235  }
  1236  
  1237  var (
  1238  	mainTemplate          = createPage("main", UISummaryData{})
  1239  	syscallsTemplate      = createPage("syscalls", UISyscallsData{})
  1240  	vmsTemplate           = createPage("vms", UIVMData{})
  1241  	crashTemplate         = createPage("crash", UICrashPage{})
  1242  	corpusTemplate        = createPage("corpus", UICorpusPage{})
  1243  	prioTemplate          = createPage("prio", UIPrioData{})
  1244  	fallbackCoverTemplate = createPage("fallback_cover", UIFallbackCoverData{})
  1245  	rawCoverTemplate      = createPage("raw_cover", UIRawCoverPage{})
  1246  	jobListTemplate       = createPage("job_list", UIJobList{})
  1247  	textTemplate          = createPage("text", UITextPage{})
  1248  )
  1249  
  1250  //go:embed html/*.html
  1251  var htmlFiles embed.FS
  1252  
  1253  func mustReadHTML(name string) []byte {
  1254  	data, err := htmlFiles.ReadFile("html/" + name + ".html")
  1255  	if err != nil {
  1256  		panic(err)
  1257  	}
  1258  	return data
  1259  }