• 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
4// Package dashapi defines data structures used in dashboard communication
5// and provides client interface.
6package dashapi
7
8import (
9	"bytes"
10	"compress/gzip"
11	"encoding/json"
12	"fmt"
13	"io"
14	"io/ioutil"
15	"net/http"
16	"net/url"
17	"reflect"
18	"strings"
19	"time"
20)
21
22type Dashboard struct {
23	Client       string
24	Addr         string
25	Key          string
26	ctor         RequestCtor
27	doer         RequestDoer
28	logger       RequestLogger
29	errorHandler func(error)
30}
31
32func New(client, addr, key string) *Dashboard {
33	return NewCustom(client, addr, key, http.NewRequest, http.DefaultClient.Do, nil, nil)
34}
35
36type (
37	RequestCtor   func(method, url string, body io.Reader) (*http.Request, error)
38	RequestDoer   func(req *http.Request) (*http.Response, error)
39	RequestLogger func(msg string, args ...interface{})
40)
41
42func NewCustom(client, addr, key string, ctor RequestCtor, doer RequestDoer,
43	logger RequestLogger, errorHandler func(error)) *Dashboard {
44	return &Dashboard{
45		Client:       client,
46		Addr:         addr,
47		Key:          key,
48		ctor:         ctor,
49		doer:         doer,
50		logger:       logger,
51		errorHandler: errorHandler,
52	}
53}
54
55// Build describes all aspects of a kernel build.
56type Build struct {
57	Manager           string
58	ID                string
59	OS                string
60	Arch              string
61	VMArch            string
62	SyzkallerCommit   string
63	CompilerID        string
64	KernelRepo        string
65	KernelBranch      string
66	KernelCommit      string
67	KernelCommitTitle string
68	KernelCommitDate  time.Time
69	KernelConfig      []byte
70	Commits           []string // see BuilderPoll
71	FixCommits        []FixCommit
72}
73
74type FixCommit struct {
75	Title string
76	BugID string
77}
78
79func (dash *Dashboard) UploadBuild(build *Build) error {
80	return dash.Query("upload_build", build, nil)
81}
82
83// BuilderPoll request is done by kernel builder before uploading a new build
84// with UploadBuild request. Response contains list of commit titles that
85// dashboard is interested in (i.e. commits that fix open bugs) and email that
86// appears in Reported-by tags for bug ID extraction. When uploading a new build
87// builder will pass subset of the commit titles that are present in the build
88// in Build.Commits field and list of {bug ID, commit title} pairs extracted
89// from git log.
90
91type BuilderPollReq struct {
92	Manager string
93}
94
95type BuilderPollResp struct {
96	PendingCommits []string
97	ReportEmail    string
98}
99
100func (dash *Dashboard) BuilderPoll(manager string) (*BuilderPollResp, error) {
101	req := &BuilderPollReq{
102		Manager: manager,
103	}
104	resp := new(BuilderPollResp)
105	err := dash.Query("builder_poll", req, resp)
106	return resp, err
107}
108
109// Jobs workflow:
110//   - syz-ci sends JobPollReq periodically to check for new jobs,
111//     request contains list of managers that this syz-ci runs.
112//   - dashboard replies with JobPollResp that contains job details,
113//     if no new jobs available ID is set to empty string.
114//   - when syz-ci finishes the job, it sends JobDoneReq which contains
115//     job execution result (Build, Crash or Error details),
116//     ID must match JobPollResp.ID.
117
118type JobPollReq struct {
119	Managers []string
120}
121
122type JobPollResp struct {
123	ID              string
124	Manager         string
125	KernelRepo      string
126	KernelBranch    string
127	KernelConfig    []byte
128	SyzkallerCommit string
129	Patch           []byte
130	ReproOpts       []byte
131	ReproSyz        []byte
132	ReproC          []byte
133}
134
135type JobDoneReq struct {
136	ID          string
137	Build       Build
138	Error       []byte
139	CrashTitle  string
140	CrashLog    []byte
141	CrashReport []byte
142}
143
144func (dash *Dashboard) JobPoll(managers []string) (*JobPollResp, error) {
145	req := &JobPollReq{Managers: managers}
146	resp := new(JobPollResp)
147	err := dash.Query("job_poll", req, resp)
148	return resp, err
149}
150
151func (dash *Dashboard) JobDone(req *JobDoneReq) error {
152	return dash.Query("job_done", req, nil)
153}
154
155type BuildErrorReq struct {
156	Build Build
157	Crash Crash
158}
159
160func (dash *Dashboard) ReportBuildError(req *BuildErrorReq) error {
161	return dash.Query("report_build_error", req, nil)
162}
163
164// Crash describes a single kernel crash (potentially with repro).
165type Crash struct {
166	BuildID     string // refers to Build.ID
167	Title       string
168	Corrupted   bool // report is corrupted (corrupted title, no stacks, etc)
169	Maintainers []string
170	Log         []byte
171	Report      []byte
172	// The following is optional and is filled only after repro.
173	ReproOpts []byte
174	ReproSyz  []byte
175	ReproC    []byte
176}
177
178type ReportCrashResp struct {
179	NeedRepro bool
180}
181
182func (dash *Dashboard) ReportCrash(crash *Crash) (*ReportCrashResp, error) {
183	resp := new(ReportCrashResp)
184	err := dash.Query("report_crash", crash, resp)
185	return resp, err
186}
187
188// CrashID is a short summary of a crash for repro queries.
189type CrashID struct {
190	BuildID   string
191	Title     string
192	Corrupted bool
193}
194
195type NeedReproResp struct {
196	NeedRepro bool
197}
198
199// NeedRepro checks if dashboard needs a repro for this crash or not.
200func (dash *Dashboard) NeedRepro(crash *CrashID) (bool, error) {
201	resp := new(NeedReproResp)
202	err := dash.Query("need_repro", crash, resp)
203	return resp.NeedRepro, err
204}
205
206// ReportFailedRepro notifies dashboard about a failed repro attempt for the crash.
207func (dash *Dashboard) ReportFailedRepro(crash *CrashID) error {
208	return dash.Query("report_failed_repro", crash, nil)
209}
210
211type LogEntry struct {
212	Name string
213	Text string
214}
215
216// Centralized logging on dashboard.
217func (dash *Dashboard) LogError(name, msg string, args ...interface{}) {
218	req := &LogEntry{
219		Name: name,
220		Text: fmt.Sprintf(msg, args...),
221	}
222	dash.Query("log_error", req, nil)
223}
224
225// BugReport describes a single bug.
226// Used by dashboard external reporting.
227type BugReport struct {
228	Namespace         string
229	Config            []byte
230	ID                string
231	JobID             string
232	ExtID             string // arbitrary reporting ID forwarded from BugUpdate.ExtID
233	First             bool   // Set for first report for this bug.
234	Title             string
235	Maintainers       []string
236	CC                []string // additional CC emails
237	OS                string
238	Arch              string
239	VMArch            string
240	CompilerID        string
241	KernelRepo        string
242	KernelRepoAlias   string
243	KernelBranch      string
244	KernelCommit      string
245	KernelCommitTitle string
246	KernelCommitDate  time.Time
247	KernelConfig      []byte
248	KernelConfigLink  string
249	Log               []byte
250	LogLink           string
251	Report            []byte
252	ReportLink        string
253	ReproC            []byte
254	ReproCLink        string
255	ReproSyz          []byte
256	ReproSyzLink      string
257	CrashID           int64 // returned back in BugUpdate
258	NumCrashes        int64
259	HappenedOn        []string // list of kernel repo aliases
260
261	CrashTitle string // job execution crash title
262	Error      []byte // job execution error
263	ErrorLink  string
264	Patch      []byte // testing job patch
265	PatchLink  string
266}
267
268type BugUpdate struct {
269	ID         string
270	ExtID      string
271	Link       string
272	Status     BugStatus
273	ReproLevel ReproLevel
274	DupOf      string
275	FixCommits []string // Titles of commits that fix this bug.
276	CC         []string // Additional emails to add to CC list in future emails.
277	CrashID    int64
278}
279
280type BugUpdateReply struct {
281	// Bug update can fail for 2 reason:
282	//  - update does not pass logical validataion, in this case OK=false
283	//  - internal/datastore error, in this case Error=true
284	OK    bool
285	Error bool
286	Text  string
287}
288
289type PollBugsRequest struct {
290	Type string
291}
292
293type PollBugsResponse struct {
294	Reports []*BugReport
295}
296
297type PollClosedRequest struct {
298	IDs []string
299}
300
301type PollClosedResponse struct {
302	IDs []string
303}
304
305func (dash *Dashboard) ReportingPollBugs(typ string) (*PollBugsResponse, error) {
306	req := &PollBugsRequest{
307		Type: typ,
308	}
309	resp := new(PollBugsResponse)
310	if err := dash.Query("reporting_poll_bugs", req, resp); err != nil {
311		return nil, err
312	}
313	return resp, nil
314}
315
316func (dash *Dashboard) ReportingPollClosed(ids []string) ([]string, error) {
317	req := &PollClosedRequest{
318		IDs: ids,
319	}
320	resp := new(PollClosedResponse)
321	if err := dash.Query("reporting_poll_closed", req, resp); err != nil {
322		return nil, err
323	}
324	return resp.IDs, nil
325}
326
327func (dash *Dashboard) ReportingUpdate(upd *BugUpdate) (*BugUpdateReply, error) {
328	resp := new(BugUpdateReply)
329	if err := dash.Query("reporting_update", upd, resp); err != nil {
330		return nil, err
331	}
332	return resp, nil
333}
334
335type ManagerStatsReq struct {
336	Name string
337	Addr string
338
339	// Current level:
340	UpTime time.Duration
341	Corpus uint64
342	Cover  uint64
343
344	// Delta since last sync:
345	FuzzingTime time.Duration
346	Crashes     uint64
347	Execs       uint64
348}
349
350func (dash *Dashboard) UploadManagerStats(req *ManagerStatsReq) error {
351	return dash.Query("manager_stats", req, nil)
352}
353
354type (
355	BugStatus  int
356	ReproLevel int
357)
358
359const (
360	BugStatusOpen BugStatus = iota
361	BugStatusUpstream
362	BugStatusInvalid
363	BugStatusDup
364	BugStatusUpdate // aux info update (i.e. ExtID/Link/CC)
365)
366
367const (
368	ReproLevelNone ReproLevel = iota
369	ReproLevelSyz
370	ReproLevelC
371)
372
373func (dash *Dashboard) Query(method string, req, reply interface{}) error {
374	if dash.logger != nil {
375		dash.logger("API(%v): %#v", method, req)
376	}
377	err := dash.queryImpl(method, req, reply)
378	if err != nil {
379		if dash.logger != nil {
380			dash.logger("API(%v): ERROR: %v", method, err)
381		}
382		if dash.errorHandler != nil {
383			dash.errorHandler(err)
384		}
385		return err
386	}
387	if dash.logger != nil {
388		dash.logger("API(%v): REPLY: %#v", method, reply)
389	}
390	return nil
391}
392
393func (dash *Dashboard) queryImpl(method string, req, reply interface{}) error {
394	if reply != nil {
395		// json decoding behavior is somewhat surprising
396		// (see // https://github.com/golang/go/issues/21092).
397		// To avoid any surprises, we zero the reply.
398		typ := reflect.TypeOf(reply)
399		if typ.Kind() != reflect.Ptr {
400			return fmt.Errorf("resp must be a pointer")
401		}
402		reflect.ValueOf(reply).Elem().Set(reflect.New(typ.Elem()).Elem())
403	}
404	values := make(url.Values)
405	values.Add("client", dash.Client)
406	values.Add("key", dash.Key)
407	values.Add("method", method)
408	if req != nil {
409		data, err := json.Marshal(req)
410		if err != nil {
411			return fmt.Errorf("failed to marshal request: %v", err)
412		}
413		buf := new(bytes.Buffer)
414		gz := gzip.NewWriter(buf)
415		if _, err := gz.Write(data); err != nil {
416			return err
417		}
418		if err := gz.Close(); err != nil {
419			return err
420		}
421		values.Add("payload", buf.String())
422	}
423	r, err := dash.ctor("POST", fmt.Sprintf("%v/api", dash.Addr), strings.NewReader(values.Encode()))
424	if err != nil {
425		return err
426	}
427	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
428	resp, err := dash.doer(r)
429	if err != nil {
430		return fmt.Errorf("http request failed: %v", err)
431	}
432	defer resp.Body.Close()
433	if resp.StatusCode != http.StatusOK {
434		data, _ := ioutil.ReadAll(resp.Body)
435		return fmt.Errorf("request failed with %v: %s", resp.Status, data)
436	}
437	if reply != nil {
438		if err := json.NewDecoder(resp.Body).Decode(reply); err != nil {
439			return fmt.Errorf("failed to unmarshal response: %v", err)
440		}
441	}
442	return nil
443}
444