• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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