github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/asset/storage_test.go (about) 1 // Copyright 2022 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 asset 5 6 import ( 7 "bytes" 8 "compress/gzip" 9 "errors" 10 "fmt" 11 "io" 12 "reflect" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/google/syzkaller/dashboard/dashapi" 18 "github.com/google/syzkaller/pkg/debugtracer" 19 "github.com/stretchr/testify/assert" 20 "github.com/ulikunitz/xz" 21 ) 22 23 type addBuildAssetCallback func(obj dashapi.NewAsset) error 24 25 type dashMock struct { 26 downloadURLs map[string]bool 27 addBuildAsset addBuildAssetCallback 28 } 29 30 func newDashMock() *dashMock { 31 return &dashMock{downloadURLs: map[string]bool{}} 32 } 33 34 func (dm *dashMock) AddBuildAssets(req *dashapi.AddBuildAssetsReq) error { 35 for _, obj := range req.Assets { 36 if dm.addBuildAsset != nil { 37 if err := dm.addBuildAsset(obj); err != nil { 38 return err 39 } 40 } 41 dm.downloadURLs[obj.DownloadURL] = true 42 } 43 return nil 44 } 45 46 func (dm *dashMock) NeededAssetsList() (*dashapi.NeededAssetsResp, error) { 47 resp := &dashapi.NeededAssetsResp{} 48 for url := range dm.downloadURLs { 49 resp.DownloadURLs = append(resp.DownloadURLs, url) 50 } 51 return resp, nil 52 } 53 54 func makeStorage(t *testing.T, dash Dashboard) (*Storage, *dummyStorageBackend) { 55 be := makeDummyStorageBackend() 56 cfg := &Config{ 57 UploadTo: "dummy://test", 58 } 59 return &Storage{ 60 dash: dash, 61 cfg: cfg, 62 backend: be, 63 tracer: &debugtracer.TestTracer{T: t}, 64 }, be 65 } 66 67 func validateGzip(res *uploadedFile, expected []byte) error { 68 if res == nil { 69 return fmt.Errorf("no file was uploaded") 70 } 71 reader, err := gzip.NewReader(bytes.NewReader(res.bytes)) 72 if err != nil { 73 return fmt.Errorf("gzip.NewReader failed: %w", err) 74 } 75 defer reader.Close() 76 body, err := io.ReadAll(reader) 77 if err != nil { 78 return fmt.Errorf("read of ungzipped content failed: %w", err) 79 } 80 if !reflect.DeepEqual(body, expected) { 81 return fmt.Errorf("decompressed: %#v, expected: %#v", body, expected) 82 } 83 return nil 84 } 85 86 func validateXz(res *uploadedFile, expected []byte) error { 87 if res == nil { 88 return fmt.Errorf("no file was uploaded") 89 } 90 xzUsed := strings.HasSuffix(res.req.savePath, ".xz") 91 if !xzUsed { 92 return fmt.Errorf("xz expected to be used") 93 } 94 xzReader, err := xz.NewReader(bytes.NewReader(res.bytes)) 95 if err != nil { 96 return fmt.Errorf("xz reader failed: %w", err) 97 } 98 out, err := io.ReadAll(xzReader) 99 if err != nil { 100 return fmt.Errorf("xz decompression failed: %w", err) 101 } 102 if !reflect.DeepEqual(out, expected) { 103 return fmt.Errorf("decompressed: %#v, expected: %#v", out, expected) 104 } 105 return nil 106 } 107 108 func (storage *Storage) sendBuildAsset(reader io.Reader, fileName string, assetType dashapi.AssetType, 109 build *dashapi.Build) error { 110 asset, err := storage.UploadBuildAsset(reader, fileName, assetType, build, nil) 111 if err != nil { 112 return err 113 } 114 return storage.ReportBuildAssets(build, asset) 115 } 116 117 func TestUploadBuildAsset(t *testing.T) { 118 dashMock := newDashMock() 119 storage, be := makeStorage(t, dashMock) 120 be.currentTime = time.Now().Add(-2 * deletionEmbargo) 121 build := &dashapi.Build{ID: "1234", KernelCommit: "abcdef2134"} 122 123 // Upload two assets using different means. 124 vmLinuxContent := []byte{0xDE, 0xAD, 0xBE, 0xEF} 125 dashMock.addBuildAsset = func(newAsset dashapi.NewAsset) error { 126 if newAsset.Type != dashapi.KernelObject { 127 t.Fatalf("expected KernelObject, got %v", newAsset.Type) 128 } 129 if !strings.Contains(newAsset.DownloadURL, "vmlinux") { 130 t.Fatalf("%#v was expected to mention vmlinux", newAsset.DownloadURL) 131 } 132 return nil 133 } 134 var file *uploadedFile 135 be.objectUpload = collectBytes(&file) 136 err := storage.sendBuildAsset(bytes.NewReader(vmLinuxContent), "vmlinux", 137 dashapi.KernelObject, build) 138 if err != nil { 139 t.Fatalf("file upload failed: %s", err) 140 } 141 if err := validateXz(file, vmLinuxContent); err != nil { 142 t.Fatalf("vmlinux validation failed: %s", err) 143 } 144 // Upload the same file the second time. 145 storage.sendBuildAsset(bytes.NewReader(vmLinuxContent), "vmlinux", dashapi.KernelObject, build) 146 // The currently expected behavior is that it will be uploaded twice and will have 147 // different names. 148 if len(dashMock.downloadURLs) < 2 { 149 t.Fatalf("same-file upload was expected to succeed, but it didn't; %#v", dashMock.downloadURLs) 150 } 151 152 diskImageContent := []byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8} 153 dashMock.addBuildAsset = func(newAsset dashapi.NewAsset) error { 154 if newAsset.Type != dashapi.KernelImage { 155 t.Fatalf("expected KernelImage, got %v", newAsset.Type) 156 } 157 if !strings.Contains(newAsset.DownloadURL, "disk") || 158 !strings.Contains(newAsset.DownloadURL, ".img") { 159 t.Fatalf("%#v was expected to mention disk.img", newAsset.DownloadURL) 160 } 161 if !strings.Contains(newAsset.DownloadURL, build.KernelCommit[:6]) { 162 t.Fatalf("%#v was expected to mention build commit", newAsset.DownloadURL) 163 } 164 return nil 165 } 166 file = nil 167 be.objectUpload = collectBytes(&file) 168 storage.sendBuildAsset(bytes.NewReader(diskImageContent), "disk.img", dashapi.KernelImage, build) 169 if err := validateXz(file, diskImageContent); err != nil { 170 t.Fatalf("disk.img validation failed: %s", err) 171 } 172 173 allUrls := []string{} 174 for url := range dashMock.downloadURLs { 175 allUrls = append(allUrls, url) 176 } 177 if len(allUrls) != 3 { 178 t.Fatalf("invalid dashMock state: expected 3 assets, got %d", len(allUrls)) 179 } 180 // First try to remove two assets. 181 dashMock.downloadURLs = map[string]bool{allUrls[2]: true, "http://download/unrelated.txt": true} 182 183 // Pretend there's an asset deletion error. 184 be.objectRemove = func(string) error { return fmt.Errorf("not now") } 185 err = storage.DeprecateAssets() 186 if err == nil { 187 t.Fatalf("DeprecateAssets should have failed") 188 } 189 190 // Let the deletion be successful. 191 be.objectRemove = nil 192 err = storage.DeprecateAssets() 193 if err != nil { 194 t.Fatalf("DeprecateAssets was expected to be successful, got %s", err) 195 } 196 path, err := be.getPath(allUrls[2]) 197 if err != nil { 198 t.Fatalf("getPath failed: %s", err) 199 } 200 err = be.hasOnly([]string{path}) 201 if err != nil { 202 t.Fatalf("after first DeprecateAssets: %s", err) 203 } 204 205 // Delete the rest. 206 dashMock.downloadURLs = map[string]bool{} 207 err = storage.DeprecateAssets() 208 if err != nil || len(be.objects) != 0 { 209 t.Fatalf("second DeprecateAssets failed: %s, len %d", 210 err, len(be.objects)) 211 } 212 } 213 214 type uploadedFile struct { 215 req uploadRequest 216 bytes []byte 217 } 218 219 func collectBytes(saveTo **uploadedFile) objectUploadCallback { 220 return func(req *uploadRequest) (*uploadResponse, error) { 221 buf := &bytes.Buffer{} 222 wwc := &wrappedWriteCloser{ 223 writer: buf, 224 closeCallback: func() error { 225 *saveTo = &uploadedFile{req: *req, bytes: buf.Bytes()} 226 return nil 227 }, 228 } 229 return &uploadResponse{path: req.savePath, writer: wwc}, nil 230 } 231 } 232 233 func TestUploadHtmlAsset(t *testing.T) { 234 dashMock := newDashMock() 235 storage, be := makeStorage(t, dashMock) 236 build := &dashapi.Build{ID: "1234", KernelCommit: "abcdef2134"} 237 htmlContent := []byte("<html><head><title>Hi!</title></head></html>") 238 dashMock.addBuildAsset = func(newAsset dashapi.NewAsset) error { 239 if newAsset.Type != dashapi.HTMLCoverageReport { 240 t.Fatalf("expected HtmlCoverageReport, got %v", newAsset.Type) 241 } 242 if !strings.Contains(newAsset.DownloadURL, "cover_report") { 243 t.Fatalf("%#v was expected to mention cover_report", newAsset.DownloadURL) 244 } 245 if !strings.HasSuffix(newAsset.DownloadURL, ".html") { 246 t.Fatalf("%#v was expected to have .html extension", newAsset.DownloadURL) 247 } 248 return nil 249 } 250 var file *uploadedFile 251 be.objectUpload = collectBytes(&file) 252 storage.sendBuildAsset(bytes.NewReader(htmlContent), "cover_report.html", 253 dashapi.HTMLCoverageReport, build) 254 if err := validateGzip(file, htmlContent); err != nil { 255 t.Fatalf("cover_report.html validation failed: %s", err) 256 } 257 } 258 259 func TestRecentAssetDeletionProtection(t *testing.T) { 260 dashMock := newDashMock() 261 storage, be := makeStorage(t, dashMock) 262 build := &dashapi.Build{ID: "1234", KernelCommit: "abcdef2134"} 263 htmlContent := []byte("<html><head><title>Hi!</title></head></html>") 264 be.currentTime = time.Now().Add(-time.Hour * 24 * 6) 265 err := storage.sendBuildAsset(bytes.NewReader(htmlContent), "cover_report.html", 266 dashapi.HTMLCoverageReport, build) 267 if err != nil { 268 t.Fatalf("failed to upload a file: %v", err) 269 } 270 271 // Try to delete a recent file. 272 dashMock.downloadURLs = map[string]bool{} 273 err = storage.DeprecateAssets() 274 if err != nil { 275 t.Fatalf("DeprecateAssets failed: %v", err) 276 } else if len(be.objects) == 0 { 277 t.Fatalf("a recent object was deleted: %v", err) 278 } 279 } 280 281 func TestAssetStorageConfiguration(t *testing.T) { 282 dashMock := newDashMock() 283 cfg := &Config{ 284 UploadTo: "dummy://", 285 Assets: map[dashapi.AssetType]TypeConfig{ 286 dashapi.HTMLCoverageReport: {Never: true}, 287 dashapi.KernelObject: {}, 288 }, 289 } 290 storage, err := StorageFromConfig(cfg, dashMock) 291 if err != nil { 292 t.Fatalf("unexpected error from StorageFromConfig: %s", err) 293 } 294 build := &dashapi.Build{ID: "1234", KernelCommit: "abcdef2134"} 295 296 // Uploading a file of a disabled asset type. 297 htmlContent := []byte("<html><head><title>Hi!</title></head></html>") 298 err = storage.sendBuildAsset(bytes.NewReader(htmlContent), "cover_report.html", 299 dashapi.HTMLCoverageReport, build) 300 if !errors.Is(err, ErrAssetTypeDisabled) { 301 t.Fatalf("UploadBuildAssetStream expected to fail with ErrAssetTypeDisabled, but got %v", err) 302 } 303 304 // Uploading a file of an unspecified asset type. 305 testContent := []byte{0x1, 0x2, 0x3, 0x4} 306 err = storage.sendBuildAsset(bytes.NewReader(testContent), "disk.raw", dashapi.BootableDisk, build) 307 if err != nil { 308 t.Fatalf("UploadBuildAssetStream of BootableDisk expected to succeed, got %v", err) 309 } 310 311 // Uploading a file of a specified asset type. 312 err = storage.sendBuildAsset(bytes.NewReader(testContent), "vmlinux", dashapi.KernelObject, build) 313 if err != nil { 314 t.Fatalf("UploadBuildAssetStream of BootableDisk expected to succeed, got %v", err) 315 } 316 } 317 318 func TestUploadSameContent(t *testing.T) { 319 dashMock := newDashMock() 320 storage, be := makeStorage(t, dashMock) 321 be.currentTime = time.Now().Add(-2 * deletionEmbargo) 322 323 build := &dashapi.Build{ID: "1234", KernelCommit: "abcdef2134"} 324 extra := &ExtraUploadArg{UniqueTag: "uniquetag", SkipIfExists: true} 325 testContent := []byte{0x1, 0x2, 0x3, 0x4} 326 asset, err := storage.UploadBuildAsset(bytes.NewReader(testContent), "disk.raw", 327 dashapi.BootableDisk, build, extra) 328 if err != nil { 329 t.Fatalf("UploadBuildAssetexpected to succeed, got %v", err) 330 } 331 if !strings.Contains(asset.DownloadURL, extra.UniqueTag) { 332 t.Fatalf("%#v was expected to contain %#v", asset.DownloadURL, extra.UniqueTag) 333 } 334 // Upload the same asset again. 335 be.objectUpload = func(req *uploadRequest) (*uploadResponse, error) { 336 return nil, &FileExistsError{req.savePath} 337 } 338 assetTwo, err := storage.UploadBuildAsset(bytes.NewReader(testContent), "disk.raw", 339 dashapi.BootableDisk, build, extra) 340 if err != nil { 341 t.Fatalf("UploadBuildAssetexpected to succeed, got %v", err) 342 } 343 if asset.DownloadURL != assetTwo.DownloadURL { 344 t.Fatalf("assets were expected to have same download URL, got %#v %#v", 345 asset.DownloadURL, assetTwo.DownloadURL) 346 } 347 } 348 349 // Test that we adequately handle the case when several syz-cis with separate buckets 350 // are connected to a single dashboard. 351 // nolint: dupl 352 func TestTwoBucketDeprecation(t *testing.T) { 353 dash := newDashMock() 354 storage, dummy := makeStorage(t, dash) 355 356 // "Upload" an asset from this instance. 357 resp, _ := dummy.upload(&uploadRequest{ 358 savePath: `folder/file.txt`, 359 }) 360 url, _ := dummy.downloadURL(resp.path, true) 361 362 // Dashboard returns two asset URLs. 363 dash.downloadURLs = map[string]bool{ 364 "http://unknown-bucket/other-folder/other-file.txt": true, // will cause ErrUnknownBucket 365 url: true, 366 } 367 dummy.objectRemove = func(url string) error { 368 t.Fatalf("unexpected removal") 369 return nil 370 } 371 err := storage.DeprecateAssets() 372 assert.NoError(t, err) 373 } 374 375 // nolint: dupl 376 func TestInvalidAssetURLs(t *testing.T) { 377 dash := newDashMock() 378 storage, dummy := makeStorage(t, dash) 379 380 // "Upload" an asset from this instance. 381 resp, _ := dummy.upload(&uploadRequest{ 382 savePath: `folder/file.txt`, 383 }) 384 url, _ := dummy.downloadURL(resp.path, true) 385 386 // Dashboard returns two asset URLs. 387 dash.downloadURLs = map[string]bool{ 388 "http://totally-unknown-bucket/other-folder/other-file.txt": true, 389 url: true, 390 } 391 dummy.objectRemove = func(url string) error { 392 t.Fatalf("unexpected removal") 393 return nil 394 } 395 err := storage.DeprecateAssets() 396 assert.Error(t, err) 397 }