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