| // Copyright 2017 syzkaller project authors. All rights reserved. |
| // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. |
| |
| package dash |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "regexp" |
| "time" |
| |
| "github.com/google/syzkaller/pkg/email" |
| ) |
| |
| // There are multiple configurable aspects of the app (namespaces, reporting, API clients, etc). |
| // The exact config is stored in a global config variable and is read-only. |
| // Also see config_stub.go. |
| type GlobalConfig struct { |
| // Min access levels specified hierarchically throughout the config. |
| AccessLevel AccessLevel |
| // Email suffix of authorized users (e.g. "@foobar.com"). |
| AuthDomain string |
| // Google Analytics Tracking ID. |
| AnalyticsTrackingID string |
| // Global API clients that work across namespaces (e.g. external reporting). |
| Clients map[string]string |
| // List of emails blacklisted from issuing test requests. |
| EmailBlacklist []string |
| // Per-namespace config. |
| // Namespaces are a mechanism to separate groups of different kernels. |
| // E.g. Debian 4.4 kernels and Ubuntu 4.9 kernels. |
| // Each namespace has own reporting config, own API clients |
| // and bugs are not merged across namespaces. |
| Namespaces map[string]*Config |
| // Maps full repository address/branch to description of this repo. |
| KernelRepos map[string]KernelRepo |
| } |
| |
| // Per-namespace config. |
| type Config struct { |
| // See GlobalConfig.AccessLevel. |
| AccessLevel AccessLevel |
| // Name used in UI. |
| DisplayTitle string |
| // URL of a source coverage report for this namespace |
| // (uploading/updating the report is out of scope of the system for now). |
| CoverLink string |
| // Per-namespace clients that act only on a particular namespace. |
| Clients map[string]string |
| // A unique key for hashing, can be anything. |
| Key string |
| // Mail bugs without reports (e.g. "no output"). |
| MailWithoutReport bool |
| // How long should we wait before reporting a bug. |
| ReportingDelay time.Duration |
| // How long should we wait for a C repro before reporting a bug. |
| WaitForRepro time.Duration |
| // Managers contains some special additional info about syz-manager instances. |
| Managers map[string]ConfigManager |
| // Reporting config. |
| Reporting []Reporting |
| } |
| |
| // ConfigManager describes a single syz-manager instance. |
| // Dashboard does not generally need to know about all of them, |
| // but in some special cases it needs to know some additional information. |
| type ConfigManager struct { |
| Decommissioned bool // The instance is no longer active. |
| DelegatedTo string // If Decommissioned, test requests should go to this instance instead. |
| // Normally instances can test patches on any tree. |
| // However, some (e.g. non-upstreamed KMSAN) can test only on a fixed tree. |
| // RestrictedTestingRepo contains the repo for such instances |
| // and RestrictedTestingReason contains a human readable reason for the restriction. |
| RestrictedTestingRepo string |
| RestrictedTestingReason string |
| } |
| |
| // One reporting stage. |
| type Reporting struct { |
| // See GlobalConfig.AccessLevel. |
| AccessLevel AccessLevel |
| // A unique name (the app does not care about exact contents). |
| Name string |
| // Name used in UI. |
| DisplayTitle string |
| // Filter can be used to conditionally skip this reporting or hold off reporting. |
| Filter ReportingFilter |
| // How many new bugs report per day. |
| DailyLimit int |
| // Type of reporting and its configuration. |
| // The app has one built-in type, EmailConfig, which reports bugs by email. |
| // And ExternalConfig which can be used to attach any external reporting system (e.g. Bugzilla). |
| Config ReportingType |
| } |
| |
| type ReportingType interface { |
| // Type returns a unique string that identifies this reporting type (e.g. "email"). |
| Type() string |
| // NeedMaintainers says if this reporting requires non-empty maintainers list. |
| NeedMaintainers() bool |
| // Validate validates the current object, this is called only during init. |
| Validate() error |
| } |
| |
| type KernelRepo struct { |
| // Alias is a short, readable name of a kernel repository. |
| Alias string |
| // ReportingPriority says if we need to prefer to report crashes in this |
| // repo over crashes in repos with lower value. Must be in [0-9] range. |
| ReportingPriority int |
| } |
| |
| var ( |
| clientNameRe = regexp.MustCompile("^[a-zA-Z0-9-_]{4,100}$") |
| clientKeyRe = regexp.MustCompile("^[a-zA-Z0-9]{16,128}$") |
| ) |
| |
| type ( |
| FilterResult int |
| ReportingFilter func(bug *Bug) FilterResult |
| ) |
| |
| const ( |
| FilterReport FilterResult = iota // Report bug in this reporting (default). |
| FilterSkip // Skip this reporting and proceed to the next one. |
| FilterHold // Hold off with reporting this bug. |
| ) |
| |
| func (cfg *Config) ReportingByName(name string) *Reporting { |
| for i := range cfg.Reporting { |
| reporting := &cfg.Reporting[i] |
| if reporting.Name == name { |
| return reporting |
| } |
| } |
| return nil |
| } |
| |
| // config is populated by installConfig which should be called either from tests |
| // or from a separate file that provides actual production config. |
| var config *GlobalConfig |
| |
| func init() { |
| // Prevents gometalinter from considering everything as dead code. |
| if false { |
| installConfig(nil) |
| } |
| } |
| |
| func installConfig(cfg *GlobalConfig) { |
| if config != nil { |
| panic("another config is already installed") |
| } |
| // Validate the global cfg. |
| if len(cfg.Namespaces) == 0 { |
| panic("no namespaces found") |
| } |
| for i := range cfg.EmailBlacklist { |
| cfg.EmailBlacklist[i] = email.CanonicalEmail(cfg.EmailBlacklist[i]) |
| } |
| namespaces := make(map[string]bool) |
| clientNames := make(map[string]bool) |
| checkClients(clientNames, cfg.Clients) |
| checkConfigAccessLevel(&cfg.AccessLevel, AccessPublic, "global") |
| for ns, cfg := range cfg.Namespaces { |
| checkNamespace(ns, cfg, namespaces, clientNames) |
| } |
| for repo, info := range cfg.KernelRepos { |
| if info.Alias == "" { |
| panic(fmt.Sprintf("empty kernel repo alias for %q", repo)) |
| } |
| if prio := info.ReportingPriority; prio < 0 || prio > 9 { |
| panic(fmt.Sprintf("bad kernel repo reporting priority %v for %q", prio, repo)) |
| } |
| } |
| config = cfg |
| initEmailReporting() |
| initHTTPHandlers() |
| initAPIHandlers() |
| } |
| |
| func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]bool) { |
| if ns == "" { |
| panic("empty namespace name") |
| } |
| if namespaces[ns] { |
| panic(fmt.Sprintf("duplicate namespace %q", ns)) |
| } |
| namespaces[ns] = true |
| if cfg.DisplayTitle == "" { |
| cfg.DisplayTitle = ns |
| } |
| checkClients(clientNames, cfg.Clients) |
| for name, mgr := range cfg.Managers { |
| checkManager(ns, name, mgr) |
| } |
| if !clientKeyRe.MatchString(cfg.Key) { |
| panic(fmt.Sprintf("bad namespace %q key: %q", ns, cfg.Key)) |
| } |
| if len(cfg.Reporting) == 0 { |
| panic(fmt.Sprintf("no reporting in namespace %q", ns)) |
| } |
| checkConfigAccessLevel(&cfg.AccessLevel, cfg.AccessLevel, fmt.Sprintf("namespace %q", ns)) |
| parentAccessLevel := cfg.AccessLevel |
| reportingNames := make(map[string]bool) |
| // Go backwards because access levels get stricter backwards. |
| for ri := len(cfg.Reporting) - 1; ri >= 0; ri-- { |
| reporting := &cfg.Reporting[ri] |
| if reporting.Name == "" { |
| panic(fmt.Sprintf("empty reporting name in namespace %q", ns)) |
| } |
| if reportingNames[reporting.Name] { |
| panic(fmt.Sprintf("duplicate reporting name %q", reporting.Name)) |
| } |
| if reporting.DisplayTitle == "" { |
| reporting.DisplayTitle = reporting.Name |
| } |
| checkConfigAccessLevel(&reporting.AccessLevel, parentAccessLevel, |
| fmt.Sprintf("reporting %q/%q", ns, reporting.Name)) |
| parentAccessLevel = reporting.AccessLevel |
| if reporting.Filter == nil { |
| reporting.Filter = func(bug *Bug) FilterResult { return FilterReport } |
| } |
| reportingNames[reporting.Name] = true |
| if reporting.Config.Type() == "" { |
| panic(fmt.Sprintf("empty reporting type for %q", reporting.Name)) |
| } |
| if err := reporting.Config.Validate(); err != nil { |
| panic(err) |
| } |
| if _, err := json.Marshal(reporting.Config); err != nil { |
| panic(fmt.Sprintf("failed to json marshal %q config: %v", |
| reporting.Name, err)) |
| } |
| } |
| } |
| |
| func checkManager(ns, name string, mgr ConfigManager) { |
| if mgr.Decommissioned && mgr.DelegatedTo == "" { |
| panic(fmt.Sprintf("decommissioned manager %v/%v does not have delegate", ns, name)) |
| } |
| if !mgr.Decommissioned && mgr.DelegatedTo != "" { |
| panic(fmt.Sprintf("non-decommissioned manager %v/%v has delegate", ns, name)) |
| } |
| if mgr.RestrictedTestingRepo != "" && mgr.RestrictedTestingReason == "" { |
| panic(fmt.Sprintf("restricted manager %v/%v does not have restriction reason", ns, name)) |
| } |
| if mgr.RestrictedTestingRepo == "" && mgr.RestrictedTestingReason != "" { |
| panic(fmt.Sprintf("unrestricted manager %v/%v has restriction reason", ns, name)) |
| } |
| } |
| |
| func checkConfigAccessLevel(current *AccessLevel, parent AccessLevel, what string) { |
| verifyAccessLevel(parent) |
| if *current == 0 { |
| *current = parent |
| } |
| verifyAccessLevel(*current) |
| if *current < parent { |
| panic(fmt.Sprintf("bad %v access level %v", what, *current)) |
| } |
| } |
| |
| func checkClients(clientNames map[string]bool, clients map[string]string) { |
| for name, key := range clients { |
| if !clientNameRe.MatchString(name) { |
| panic(fmt.Sprintf("bad client name: %v", name)) |
| } |
| if !clientKeyRe.MatchString(key) { |
| panic(fmt.Sprintf("bad client key: %v", key)) |
| } |
| if clientNames[name] { |
| panic(fmt.Sprintf("duplicate client name: %v", name)) |
| } |
| clientNames[name] = true |
| } |
| } |