From 334b7c2855d1d62b1eda7dfac4f5a3c6df0bac11 Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Mon, 24 Jun 2024 17:00:09 +0200 Subject: dashboard: support manual reproduction requests As it is problematic to set up automatic bidirectional sharing of reproducer files between namespaces, let's support the ability to manually request a reproduction attempt on a specific syz-manager instance. That should help in the time being. --- dashboard/app/api.go | 53 +++++++++++++++++++++++++++++++++++++ dashboard/app/entities_datastore.go | 9 +++++++ dashboard/app/index.yaml | 6 +++++ dashboard/app/main.go | 20 +++++++++++--- dashboard/app/main_test.go | 20 ++++++++++++++ dashboard/app/manager.html | 23 +++++++++++++++- dashboard/app/repro_test.go | 29 ++++++++++++++++++++ dashboard/app/util_test.go | 23 +++++++++++++--- pkg/html/pages/style.css | 5 ++++ 9 files changed, 180 insertions(+), 8 deletions(-) diff --git a/dashboard/app/api.go b/dashboard/app/api.go index 409fb023c..f9b6816db 100644 --- a/dashboard/app/api.go +++ b/dashboard/app/api.go @@ -1740,6 +1740,17 @@ func apiLogToReproduce(c context.Context, ns string, r *http.Request, payload [] if err != nil { return nil, err } + // First check if there have been any manual requests. + log, err := takeReproTask(c, ns, build.Manager) + if err != nil { + return nil, err + } + if log != nil { + return &dashapi.LogToReproResp{ + CrashLog: log, + }, nil + } + bugs, _, err := loadAllBugs(c, func(query *db.Query) *db.Query { return query.Filter("Namespace=", ns). Filter("HappenedOn=", build.Manager). @@ -1796,3 +1807,45 @@ func logToReproForBug(c context.Context, bug *Bug, manager string) (*dashapi.Log } return nil, nil } + +func saveReproTask(c context.Context, ns, manager string, repro []byte) error { + log, err := putText(c, ns, textCrashLog, repro, false) + if err != nil { + return err + } + // We don't control the status of each attempt, so let's just try twice. + const attempts = 2 + obj := &ReproTask{ + Namespace: ns, + Manager: manager, + Log: log, + AttemptsLeft: attempts, + } + key := db.NewIncompleteKey(c, "ReproTask", nil) + _, err = db.Put(c, key, obj) + return err +} + +func takeReproTask(c context.Context, ns, manager string) ([]byte, error) { + var tasks []*ReproTask + keys, err := db.NewQuery("ReproTask"). + Filter("Namespace=", ns). + Filter("Manager=", manager). + Filter("AttemptsLeft>", 0). + GetAll(c, &tasks) + if err != nil || len(keys) == 0 { + return nil, err + } + + // Yes, it's possible that the entity will be modified simultaneously, and we + // ideall need a transaction, but let's just ignore this possibility -- in the + // worst case we'd just try to reproduce it once more. + key, task := keys[0], tasks[0] + task.AttemptsLeft-- + task.LastAttempt = timeNow(c) + if _, err := db.Put(c, key, task); err != nil { + return nil, err + } + log, _, err := getText(c, textCrashLog, task.Log) + return log, err +} diff --git a/dashboard/app/entities_datastore.go b/dashboard/app/entities_datastore.go index 3d81722fc..87c8bd643 100644 --- a/dashboard/app/entities_datastore.go +++ b/dashboard/app/entities_datastore.go @@ -722,6 +722,15 @@ func (status BisectStatus) String() string { } } +// ReproTask is a manually requested reproduction attempt. +type ReproTask struct { + Namespace string + Manager string + Log int64 // Reference to CrashLog text entity. + AttemptsLeft int64 + LastAttempt time.Time +} + func mgrKey(c context.Context, ns, name string) *db.Key { return db.NewKey(c, "Manager", fmt.Sprintf("%v-%v", ns, name), 0, nil) } diff --git a/dashboard/app/index.yaml b/dashboard/app/index.yaml index f56095bcd..eae3d3c13 100644 --- a/dashboard/app/index.yaml +++ b/dashboard/app/index.yaml @@ -256,3 +256,9 @@ indexes: properties: - name: Namespace - name: Type + +- kind: ReproTask + properties: + - name: Namespace + - name: Manager + - name: AttemptsLeft diff --git a/dashboard/app/main.go b/dashboard/app/main.go index 42a0a9da3..4d68b14cd 100644 --- a/dashboard/app/main.go +++ b/dashboard/app/main.go @@ -211,9 +211,11 @@ type uiAdminPage struct { } type uiManagerPage struct { - Header *uiHeader - Manager *uiManager - Builds []*uiBuild + Header *uiHeader + Manager *uiManager + Message string + ShowReproForm bool + Builds []*uiBuild } type uiManager struct { @@ -583,6 +585,18 @@ func handleManagerPage(c context.Context, w http.ResponseWriter, r *http.Request return fmt.Errorf("failed to query builds: %w", err) } managerPage := &uiManagerPage{Manager: manager, Header: hdr} + accessLevel := accessLevel(c, r) + if accessLevel >= AccessUser { + managerPage.ShowReproForm = true + if repro := r.FormValue("send-repro"); repro != "" { + err := saveReproTask(c, hdr.Namespace, manager.Name, []byte(repro)) + if err != nil { + return fmt.Errorf("failed to request reproduction: %w", err) + } + managerPage.Message = "Repro request was saved!" + } + } + for _, build := range builds { managerPage.Builds = append(managerPage.Builds, makeUIBuild(c, build, false)) } diff --git a/dashboard/app/main_test.go b/dashboard/app/main_test.go index f8d35ff56..bbca35dae 100644 --- a/dashboard/app/main_test.go +++ b/dashboard/app/main_test.go @@ -465,3 +465,23 @@ func TestManagerPage(t *testing.T) { c.expectTrue(errors.As(err, &httpErr)) c.expectEQ(httpErr.Code, http.StatusBadRequest) } + +func TestReproSubmitAccess(t *testing.T) { + c := NewCtx(t) + defer c.Close() + client := c.makeClient(clientPublic, keyPublic, true) + + build := testBuild(1) + build.Manager = "test-manager" + client.UploadBuild(build) + + reply, err := c.AuthGET(AccessPublic, "/access-public/manager/test-manager") + c.expectOK(err) + assert.NotContains(t, string(reply), "Send a reproducer") + + for _, access := range []AccessLevel{AccessUser, AccessAdmin} { + reply, err := c.AuthGET(access, "/access-public/manager/test-manager") + c.expectOK(err) + assert.Contains(t, string(reply), "Send a reproducer") + } +} diff --git a/dashboard/app/manager.html b/dashboard/app/manager.html index 98b865172..65c3d9cb7 100644 --- a/dashboard/app/manager.html +++ b/dashboard/app/manager.html @@ -11,7 +11,28 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the {{template "header" .Header}} - {{optlink .Manager.Link "[Syz-Manager]"}} +
{{optlink .Manager.Link "[Syz-Manager]"}}

+ {{if .Message}}{{.Message}}
{{end}} + {{if .ShowReproForm}} +
+
+ + + Send a reproducer to {{.Manager.Name}} +
+
+
+
+ + + +
+ +
+
+ + {{end}} +
Kernel images history:
diff --git a/dashboard/app/repro_test.go b/dashboard/app/repro_test.go index db37046d6..51b9d1049 100644 --- a/dashboard/app/repro_test.go +++ b/dashboard/app/repro_test.go @@ -6,6 +6,7 @@ package main import ( "fmt" "net/http" + "net/url" "testing" "time" @@ -485,3 +486,31 @@ func TestReproForDifferentCrash(t *testing.T) { dbBug, _, _ := c.loadBug(oldBug.ID) c.expectEQ(len(dbBug.ReproAttempts), 1) } + +func TestReproTask(t *testing.T) { + c := NewCtx(t) + defer c.Close() + client := c.client + + build := testBuild(1) + build.Manager = "test-manager" + client.UploadBuild(build) + + form := url.Values{} + const reproValue = "Some repro text" + form.Add("send-repro", reproValue) + + c.POSTForm("/test1/manager/test-manager", form) + + // We run the reproducer request 2 times. + for i := 0; i < 2; i++ { + resp, err := client.LogToRepro(&dashapi.LogToReproReq{BuildID: build.ID}) + c.expectOK(err) + c.expectEQ(string(resp.CrashLog), reproValue) + } + + // But no more. + resp, err := client.LogToRepro(&dashapi.LogToReproReq{BuildID: build.ID}) + c.expectOK(err) + c.expectEQ(resp.CrashLog, []byte(nil)) +} diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go index ff74187a9..c2f64ff67 100644 --- a/dashboard/app/util_test.go +++ b/dashboard/app/util_test.go @@ -14,6 +14,7 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "os" "os/exec" "path/filepath" @@ -292,7 +293,7 @@ func (c *Ctx) GET(url string) ([]byte, error) { // AuthGET sends HTTP GET request to the app with the specified authorization. func (c *Ctx) AuthGET(access AccessLevel, url string) ([]byte, error) { - w, err := c.httpRequest("GET", url, "", access) + w, err := c.httpRequest("GET", url, "", "", access) if err != nil { return nil, err } @@ -301,7 +302,17 @@ func (c *Ctx) AuthGET(access AccessLevel, url string) ([]byte, error) { // POST sends admin-authorized HTTP POST requestd to the app. func (c *Ctx) POST(url, body string) ([]byte, error) { - w, err := c.httpRequest("POST", url, body, AccessAdmin) + w, err := c.httpRequest("POST", url, body, "", AccessAdmin) + if err != nil { + return nil, err + } + return w.Body.Bytes(), nil +} + +// POST sends an admin-authorized HTTP POST form to the app. +func (c *Ctx) POSTForm(url string, form url.Values) ([]byte, error) { + w, err := c.httpRequest("POST", url, form.Encode(), + "application/x-www-form-urlencoded", AccessAdmin) if err != nil { return nil, err } @@ -310,7 +321,7 @@ func (c *Ctx) POST(url, body string) ([]byte, error) { // ContentType returns the response Content-Type header value. func (c *Ctx) ContentType(url string) (string, error) { - w, err := c.httpRequest("HEAD", url, "", AccessAdmin) + w, err := c.httpRequest("HEAD", url, "", "", AccessAdmin) if err != nil { return "", err } @@ -321,13 +332,17 @@ func (c *Ctx) ContentType(url string) (string, error) { return values[0], nil } -func (c *Ctx) httpRequest(method, url, body string, access AccessLevel) (*httptest.ResponseRecorder, error) { +func (c *Ctx) httpRequest(method, url, body, contentType string, + access AccessLevel) (*httptest.ResponseRecorder, error) { c.t.Logf("%v: %v", method, url) r, err := c.inst.NewRequest(method, url, strings.NewReader(body)) if err != nil { c.t.Fatal(err) } r.Header.Add("X-Appengine-User-IP", "127.0.0.1") + if contentType != "" { + r.Header.Add("Content-Type", contentType) + } r = registerRequest(r, c) r = r.WithContext(c.transformContext(r.Context())) if access == AccessAdmin || access == AccessUser { diff --git a/pkg/html/pages/style.css b/pkg/html/pages/style.css index 9ec565616..60e523ffa 100644 --- a/pkg/html/pages/style.css +++ b/pkg/html/pages/style.css @@ -354,6 +354,11 @@ aside { margin-bottom: 7px; } +.input-values textarea { + width: 750pt; + height: 250pt; +} + .input-group { margin-top: 7px; margin-bottom: 7px; -- cgit mrf-deployment
Time