1// Copyright 2017 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 4package dash 5 6import ( 7 "encoding/json" 8 "fmt" 9 "regexp" 10 "time" 11 12 "github.com/google/syzkaller/pkg/email" 13) 14 15// There are multiple configurable aspects of the app (namespaces, reporting, API clients, etc). 16// The exact config is stored in a global config variable and is read-only. 17// Also see config_stub.go. 18type GlobalConfig struct { 19 // Min access levels specified hierarchically throughout the config. 20 AccessLevel AccessLevel 21 // Email suffix of authorized users (e.g. "@foobar.com"). 22 AuthDomain string 23 // Google Analytics Tracking ID. 24 AnalyticsTrackingID string 25 // Global API clients that work across namespaces (e.g. external reporting). 26 Clients map[string]string 27 // List of emails blacklisted from issuing test requests. 28 EmailBlacklist []string 29 // Per-namespace config. 30 // Namespaces are a mechanism to separate groups of different kernels. 31 // E.g. Debian 4.4 kernels and Ubuntu 4.9 kernels. 32 // Each namespace has own reporting config, own API clients 33 // and bugs are not merged across namespaces. 34 Namespaces map[string]*Config 35 // Maps full repository address/branch to description of this repo. 36 KernelRepos map[string]KernelRepo 37} 38 39// Per-namespace config. 40type Config struct { 41 // See GlobalConfig.AccessLevel. 42 AccessLevel AccessLevel 43 // Name used in UI. 44 DisplayTitle string 45 // URL of a source coverage report for this namespace 46 // (uploading/updating the report is out of scope of the system for now). 47 CoverLink string 48 // Per-namespace clients that act only on a particular namespace. 49 Clients map[string]string 50 // A unique key for hashing, can be anything. 51 Key string 52 // Mail bugs without reports (e.g. "no output"). 53 MailWithoutReport bool 54 // How long should we wait before reporting a bug. 55 ReportingDelay time.Duration 56 // How long should we wait for a C repro before reporting a bug. 57 WaitForRepro time.Duration 58 // Managers contains some special additional info about syz-manager instances. 59 Managers map[string]ConfigManager 60 // Reporting config. 61 Reporting []Reporting 62} 63 64// ConfigManager describes a single syz-manager instance. 65// Dashboard does not generally need to know about all of them, 66// but in some special cases it needs to know some additional information. 67type ConfigManager struct { 68 Decommissioned bool // The instance is no longer active. 69 DelegatedTo string // If Decommissioned, test requests should go to this instance instead. 70 // Normally instances can test patches on any tree. 71 // However, some (e.g. non-upstreamed KMSAN) can test only on a fixed tree. 72 // RestrictedTestingRepo contains the repo for such instances 73 // and RestrictedTestingReason contains a human readable reason for the restriction. 74 RestrictedTestingRepo string 75 RestrictedTestingReason string 76} 77 78// One reporting stage. 79type Reporting struct { 80 // See GlobalConfig.AccessLevel. 81 AccessLevel AccessLevel 82 // A unique name (the app does not care about exact contents). 83 Name string 84 // Name used in UI. 85 DisplayTitle string 86 // Filter can be used to conditionally skip this reporting or hold off reporting. 87 Filter ReportingFilter 88 // How many new bugs report per day. 89 DailyLimit int 90 // Type of reporting and its configuration. 91 // The app has one built-in type, EmailConfig, which reports bugs by email. 92 // And ExternalConfig which can be used to attach any external reporting system (e.g. Bugzilla). 93 Config ReportingType 94} 95 96type ReportingType interface { 97 // Type returns a unique string that identifies this reporting type (e.g. "email"). 98 Type() string 99 // NeedMaintainers says if this reporting requires non-empty maintainers list. 100 NeedMaintainers() bool 101 // Validate validates the current object, this is called only during init. 102 Validate() error 103} 104 105type KernelRepo struct { 106 // Alias is a short, readable name of a kernel repository. 107 Alias string 108 // ReportingPriority says if we need to prefer to report crashes in this 109 // repo over crashes in repos with lower value. Must be in [0-9] range. 110 ReportingPriority int 111} 112 113var ( 114 clientNameRe = regexp.MustCompile("^[a-zA-Z0-9-_]{4,100}$") 115 clientKeyRe = regexp.MustCompile("^[a-zA-Z0-9]{16,128}$") 116) 117 118type ( 119 FilterResult int 120 ReportingFilter func(bug *Bug) FilterResult 121) 122 123const ( 124 FilterReport FilterResult = iota // Report bug in this reporting (default). 125 FilterSkip // Skip this reporting and proceed to the next one. 126 FilterHold // Hold off with reporting this bug. 127) 128 129func (cfg *Config) ReportingByName(name string) *Reporting { 130 for i := range cfg.Reporting { 131 reporting := &cfg.Reporting[i] 132 if reporting.Name == name { 133 return reporting 134 } 135 } 136 return nil 137} 138 139// config is populated by installConfig which should be called either from tests 140// or from a separate file that provides actual production config. 141var config *GlobalConfig 142 143func init() { 144 // Prevents gometalinter from considering everything as dead code. 145 if false { 146 installConfig(nil) 147 } 148} 149 150func installConfig(cfg *GlobalConfig) { 151 if config != nil { 152 panic("another config is already installed") 153 } 154 // Validate the global cfg. 155 if len(cfg.Namespaces) == 0 { 156 panic("no namespaces found") 157 } 158 for i := range cfg.EmailBlacklist { 159 cfg.EmailBlacklist[i] = email.CanonicalEmail(cfg.EmailBlacklist[i]) 160 } 161 namespaces := make(map[string]bool) 162 clientNames := make(map[string]bool) 163 checkClients(clientNames, cfg.Clients) 164 checkConfigAccessLevel(&cfg.AccessLevel, AccessPublic, "global") 165 for ns, cfg := range cfg.Namespaces { 166 checkNamespace(ns, cfg, namespaces, clientNames) 167 } 168 for repo, info := range cfg.KernelRepos { 169 if info.Alias == "" { 170 panic(fmt.Sprintf("empty kernel repo alias for %q", repo)) 171 } 172 if prio := info.ReportingPriority; prio < 0 || prio > 9 { 173 panic(fmt.Sprintf("bad kernel repo reporting priority %v for %q", prio, repo)) 174 } 175 } 176 config = cfg 177 initEmailReporting() 178 initHTTPHandlers() 179 initAPIHandlers() 180} 181 182func checkNamespace(ns string, cfg *Config, namespaces, clientNames map[string]bool) { 183 if ns == "" { 184 panic("empty namespace name") 185 } 186 if namespaces[ns] { 187 panic(fmt.Sprintf("duplicate namespace %q", ns)) 188 } 189 namespaces[ns] = true 190 if cfg.DisplayTitle == "" { 191 cfg.DisplayTitle = ns 192 } 193 checkClients(clientNames, cfg.Clients) 194 for name, mgr := range cfg.Managers { 195 checkManager(ns, name, mgr) 196 } 197 if !clientKeyRe.MatchString(cfg.Key) { 198 panic(fmt.Sprintf("bad namespace %q key: %q", ns, cfg.Key)) 199 } 200 if len(cfg.Reporting) == 0 { 201 panic(fmt.Sprintf("no reporting in namespace %q", ns)) 202 } 203 checkConfigAccessLevel(&cfg.AccessLevel, cfg.AccessLevel, fmt.Sprintf("namespace %q", ns)) 204 parentAccessLevel := cfg.AccessLevel 205 reportingNames := make(map[string]bool) 206 // Go backwards because access levels get stricter backwards. 207 for ri := len(cfg.Reporting) - 1; ri >= 0; ri-- { 208 reporting := &cfg.Reporting[ri] 209 if reporting.Name == "" { 210 panic(fmt.Sprintf("empty reporting name in namespace %q", ns)) 211 } 212 if reportingNames[reporting.Name] { 213 panic(fmt.Sprintf("duplicate reporting name %q", reporting.Name)) 214 } 215 if reporting.DisplayTitle == "" { 216 reporting.DisplayTitle = reporting.Name 217 } 218 checkConfigAccessLevel(&reporting.AccessLevel, parentAccessLevel, 219 fmt.Sprintf("reporting %q/%q", ns, reporting.Name)) 220 parentAccessLevel = reporting.AccessLevel 221 if reporting.Filter == nil { 222 reporting.Filter = func(bug *Bug) FilterResult { return FilterReport } 223 } 224 reportingNames[reporting.Name] = true 225 if reporting.Config.Type() == "" { 226 panic(fmt.Sprintf("empty reporting type for %q", reporting.Name)) 227 } 228 if err := reporting.Config.Validate(); err != nil { 229 panic(err) 230 } 231 if _, err := json.Marshal(reporting.Config); err != nil { 232 panic(fmt.Sprintf("failed to json marshal %q config: %v", 233 reporting.Name, err)) 234 } 235 } 236} 237 238func checkManager(ns, name string, mgr ConfigManager) { 239 if mgr.Decommissioned && mgr.DelegatedTo == "" { 240 panic(fmt.Sprintf("decommissioned manager %v/%v does not have delegate", ns, name)) 241 } 242 if !mgr.Decommissioned && mgr.DelegatedTo != "" { 243 panic(fmt.Sprintf("non-decommissioned manager %v/%v has delegate", ns, name)) 244 } 245 if mgr.RestrictedTestingRepo != "" && mgr.RestrictedTestingReason == "" { 246 panic(fmt.Sprintf("restricted manager %v/%v does not have restriction reason", ns, name)) 247 } 248 if mgr.RestrictedTestingRepo == "" && mgr.RestrictedTestingReason != "" { 249 panic(fmt.Sprintf("unrestricted manager %v/%v has restriction reason", ns, name)) 250 } 251} 252 253func checkConfigAccessLevel(current *AccessLevel, parent AccessLevel, what string) { 254 verifyAccessLevel(parent) 255 if *current == 0 { 256 *current = parent 257 } 258 verifyAccessLevel(*current) 259 if *current < parent { 260 panic(fmt.Sprintf("bad %v access level %v", what, *current)) 261 } 262} 263 264func checkClients(clientNames map[string]bool, clients map[string]string) { 265 for name, key := range clients { 266 if !clientNameRe.MatchString(name) { 267 panic(fmt.Sprintf("bad client name: %v", name)) 268 } 269 if !clientKeyRe.MatchString(key) { 270 panic(fmt.Sprintf("bad client key: %v", key)) 271 } 272 if clientNames[name] { 273 panic(fmt.Sprintf("duplicate client name: %v", name)) 274 } 275 clientNames[name] = true 276 } 277} 278