github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/pkg/kcidb/client.go (about) 1 // Copyright 2020 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 kcidb 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "net/http" 12 "os" 13 "os/exec" 14 "strings" 15 "time" 16 17 "github.com/google/syzkaller/dashboard/dashapi" 18 "github.com/google/syzkaller/sys/targets" 19 ) 20 21 type Client struct { 22 ctx context.Context 23 origin string 24 resturi string 25 token string 26 } 27 28 // NewClient creates a new client to send pubsub messages to Kcidb. 29 // Origin is how this system identified in Kcidb, e.g. "syzbot_foobar". 30 // Project is Kcidb GCE project name, e.g. "kernelci-production". 31 // Topic is pubsub topic to publish messages to, e.g. "playground_kernelci_new". 32 // Credentials is Google application credentials file contents to use for authorization. 33 func NewClient(ctx context.Context, origin, resturi, token string) (*Client, error) { 34 c := &Client{ 35 ctx: ctx, 36 origin: origin, 37 resturi: resturi, 38 token: token, 39 } 40 return c, nil 41 } 42 43 func (c *Client) Close() error { 44 return nil 45 } 46 47 func (c *Client) RESTSubmit(data []byte) error { 48 if c.resturi == "" { 49 return fmt.Errorf("resturi is not set") 50 } 51 req, err := http.NewRequest("POST", c.resturi, bytes.NewReader(data)) 52 if err != nil { 53 return fmt.Errorf("failed to create request: %w", err) 54 } 55 req.Header.Set("Content-Type", "application/json") 56 req.Header.Set("Authorization", "Bearer "+c.token) 57 client := &http.Client{} 58 resp, err := client.Do(req) 59 if err != nil { 60 return fmt.Errorf("failed to send request: %w", err) 61 } 62 defer resp.Body.Close() 63 if resp.StatusCode != http.StatusOK { 64 return fmt.Errorf("unexpected response status: %s", resp.Status) 65 } 66 return nil 67 } 68 69 func (c *Client) Publish(bug *dashapi.BugReport) error { 70 target := targets.List[bug.OS][bug.VMArch] 71 if target == nil { 72 return fmt.Errorf("unsupported OS/arch %v/%v", bug.OS, bug.VMArch) 73 } 74 data, err := json.MarshalIndent(c.convert(target, bug), "", " ") 75 if err != nil { 76 return fmt.Errorf("failed to marshal kcidb json: %w", err) 77 } 78 if err := kcidbValidate(data); err != nil { 79 return err 80 } 81 if err := c.RESTSubmit(data); err != nil { 82 return fmt.Errorf("failed to submit kcidb json: %w", err) 83 } 84 return err 85 } 86 87 func (c *Client) PublishToFile(bug *dashapi.BugReport, filename string) error { 88 target := targets.List[bug.OS][bug.VMArch] 89 if target == nil { 90 return fmt.Errorf("unsupported OS/arch %v/%v", bug.OS, bug.VMArch) 91 } 92 data, err := json.MarshalIndent(c.convert(target, bug), "", " ") 93 if err != nil { 94 return fmt.Errorf("failed to marshal kcidb json: %w", err) 95 } 96 if err := kcidbValidate(data); err != nil { 97 return err 98 } 99 if err := os.WriteFile(filename, data, 0644); err != nil { 100 return fmt.Errorf("failed to write kcidb json to file: %w", err) 101 } 102 return nil 103 } 104 105 var Validate bool 106 107 func kcidbValidate(data []byte) error { 108 if !Validate { 109 return nil 110 } 111 const bin = "kcidb-validate" 112 if _, err := exec.LookPath(bin); err != nil { 113 fmt.Fprintf(os.Stderr, "%v is not found\n", bin) 114 return nil 115 } 116 cmd := exec.Command(bin) 117 cmd.Stdin = bytes.NewReader(data) 118 output, err := cmd.CombinedOutput() 119 if err != nil { 120 return fmt.Errorf("%v failed (%w) on:\n%s\n\nerror: %s", 121 bin, err, data, output) 122 } 123 return nil 124 } 125 126 func (c *Client) convert(target *targets.Target, bug *dashapi.BugReport) *Kcidb { 127 res := &Kcidb{ 128 Version: &Version{ 129 Major: 5, 130 Minor: 3, 131 }, 132 Checkouts: []*Checkout{ 133 { 134 Origin: c.origin, 135 ID: c.extID(bug.KernelCommit), 136 GitRepositoryURL: normalizeRepo(bug.KernelRepo), 137 GitCommitHash: bug.KernelCommit, 138 GitRepositoryBranch: bug.KernelBranch, 139 Comment: bug.KernelCommitTitle, 140 StartTime: bug.BuildTime.Format(time.RFC3339), 141 Valid: true, 142 }, 143 }, 144 Builds: []*Build{ 145 { 146 Origin: c.origin, 147 ID: c.extID(bug.BuildID), 148 CheckoutID: c.extID(bug.KernelCommit), 149 Architecture: target.KernelArch, 150 Compiler: bug.CompilerID, 151 StartTime: bug.BuildTime.Format(time.RFC3339), 152 ConfigURL: bug.KernelConfigLink, 153 Status: "PASS", 154 }, 155 }, 156 } 157 if strings.Contains(bug.Title, "build error") { 158 build := res.Builds[0] 159 build.Status = "FAIL" 160 build.LogURL = bug.LogLink 161 build.Misc = &BuildMisc{ 162 OriginURL: bug.Link, 163 ReportedBy: bug.CreditEmail, 164 } 165 } else { 166 var outputFiles []*Resource 167 if bug.ReportLink != "" { 168 outputFiles = append(outputFiles, &Resource{Name: "report.txt", URL: bug.ReportLink}) 169 } 170 if bug.LogLink != "" { 171 outputFiles = append(outputFiles, &Resource{Name: "log.txt", URL: bug.LogLink}) 172 } 173 if bug.ReproCLink != "" { 174 outputFiles = append(outputFiles, &Resource{Name: "repro.c", URL: bug.ReproCLink}) 175 } 176 if bug.ReproSyzLink != "" { 177 outputFiles = append(outputFiles, &Resource{Name: "repro.syz.txt", URL: bug.ReproSyzLink}) 178 } 179 if bug.MachineInfoLink != "" { 180 outputFiles = append(outputFiles, &Resource{Name: "machine_info.txt", URL: bug.MachineInfoLink}) 181 } 182 causeRevisionID := "" 183 if bug.BisectCause != nil && bug.BisectCause.Commit != nil { 184 causeRevisionID = bug.BisectCause.Commit.Hash 185 } 186 res.Tests = []*Test{ 187 { 188 Origin: c.origin, 189 ID: c.extID(bug.ID), 190 BuildID: c.extID(bug.BuildID), 191 Path: "syzkaller", 192 StartTime: bug.CrashTime.Format(time.RFC3339), 193 OutputFiles: outputFiles, 194 Comment: bug.Title, 195 Status: "FAIL", 196 Misc: &TestMisc{ 197 OriginURL: bug.Link, 198 ReportedBy: bug.CreditEmail, 199 UserSpaceArch: bug.UserSpaceArch, 200 CauseRevisionID: causeRevisionID, 201 }, 202 }, 203 } 204 } 205 return res 206 } 207 208 func normalizeRepo(repo string) string { 209 // Kcidb needs normalized repo addresses to match reports from different 210 // origins and with subscriptions. "https:" is always preferred over "git:" 211 // where available. Unfortunately we don't know where it's available 212 // and where it isn't. We know that "https:" is supported on kernel.org, 213 // and that's the main case we need to fix up. "https:" is always used 214 // for github.com and googlesource.com. 215 return strings.ReplaceAll(repo, "git://git.kernel.org", "https://git.kernel.org") 216 } 217 218 func (c *Client) extID(id string) string { 219 return c.origin + ":" + id 220 }