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  }