Skip to content

Commit 9120da8

Browse files
committed
prog: annotate image assets with fsck logs
Syscall attributes are extended with a fsck command field which lets file system mount definitions specify a fsck-like command to run. This is required because all file systems have a custom fsck command invokation style. When uploading a compressed image asset to the dashboard, syz-manager also runs the fsck command and logs its output over the dashapi. The dashboard logs these fsck logs into the database. This has been requested by fs maintainer Ted Tso who would like to quickly understand whether a filesystem is corrupted or not before looking at a reproducer in more details. Ultimately, this could be used as an early triage sign to determine whether a bug is obviously critical.
1 parent 2d7edeb commit 9120da8

File tree

15 files changed

+206
-33
lines changed

15 files changed

+206
-33
lines changed

dashboard/app/api.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build
491491
*Build, bool, error) {
492492
newAssets := []Asset{}
493493
for i, toAdd := range req.Assets {
494-
newAsset, err := parseIncomingAsset(c, toAdd)
494+
newAsset, err := parseIncomingAsset(c, toAdd, ns)
495495
if err != nil {
496496
return nil, false, fmt.Errorf("failed to parse asset #%d: %w", i, err)
497497
}
@@ -783,7 +783,8 @@ func apiReportCrash(c context.Context, ns string, r *http.Request, payload []byt
783783

784784
// nolint: gocyclo
785785
func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, error) {
786-
assets, err := parseCrashAssets(c, req)
786+
ns := build.Namespace
787+
assets, err := parseCrashAssets(c, req, ns)
787788
if err != nil {
788789
return nil, err
789790
}
@@ -798,7 +799,6 @@ func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, err
798799
}
799800
req.Maintainers = email.MergeEmailLists(req.Maintainers)
800801

801-
ns := build.Namespace
802802
bug, err := findBugForCrash(c, ns, req.AltTitles)
803803
if err != nil {
804804
return nil, fmt.Errorf("failed to find bug for the crash: %w", err)
@@ -895,10 +895,10 @@ func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, err
895895
return bug, nil
896896
}
897897

898-
func parseCrashAssets(c context.Context, req *dashapi.Crash) ([]Asset, error) {
898+
func parseCrashAssets(c context.Context, req *dashapi.Crash, ns string) ([]Asset, error) {
899899
assets := []Asset{}
900900
for i, toAdd := range req.Assets {
901-
newAsset, err := parseIncomingAsset(c, toAdd)
901+
newAsset, err := parseIncomingAsset(c, toAdd, ns)
902902
if err != nil {
903903
return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err)
904904
}
@@ -1309,7 +1309,7 @@ func apiAddBuildAssets(c context.Context, ns string, r *http.Request, payload []
13091309
}
13101310
assets := []Asset{}
13111311
for i, toAdd := range req.Assets {
1312-
asset, err := parseIncomingAsset(c, toAdd)
1312+
asset, err := parseIncomingAsset(c, toAdd, ns)
13131313
if err != nil {
13141314
return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err)
13151315
}
@@ -1322,7 +1322,7 @@ func apiAddBuildAssets(c context.Context, ns string, r *http.Request, payload []
13221322
return nil, nil
13231323
}
13241324

1325-
func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, error) {
1325+
func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset, ns string) (Asset, error) {
13261326
typeInfo := asset.GetTypeDescription(newAsset.Type)
13271327
if typeInfo == nil {
13281328
return Asset{}, fmt.Errorf("unknown asset type")
@@ -1331,10 +1331,18 @@ func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, er
13311331
if err != nil {
13321332
return Asset{}, fmt.Errorf("invalid URL: %w", err)
13331333
}
1334+
fsckLog := int64(0)
1335+
if len(newAsset.FsckLog) > 0 {
1336+
fsckLog, err = putText(c, ns, textFsckLog, newAsset.FsckLog)
1337+
if err != nil {
1338+
return Asset{}, err
1339+
}
1340+
}
13341341
return Asset{
13351342
Type: newAsset.Type,
13361343
DownloadURL: newAsset.DownloadURL,
13371344
CreateDate: timeNow(c),
1345+
FsckLog: fsckLog,
13381346
}, nil
13391347
}
13401348

dashboard/app/asset_storage.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ func queryLatestManagerAssets(c context.Context, ns string, assetType dashapi.As
496496
return ret, nil
497497
}
498498

499-
func createAssetList(build *Build, crash *Crash, forReport bool) []dashapi.Asset {
499+
func createAssetList(c context.Context, build *Build, crash *Crash, forReport bool) []dashapi.Asset {
500500
var crashAssets []Asset
501501
if crash != nil {
502502
crashAssets = crash.Assets
@@ -512,6 +512,13 @@ func createAssetList(build *Build, crash *Crash, forReport bool) []dashapi.Asset
512512
DownloadURL: reportAsset.DownloadURL,
513513
Type: reportAsset.Type,
514514
})
515+
if reportAsset.FsckLog != 0 {
516+
assetList = append(assetList, dashapi.Asset{
517+
Title: "fsck log",
518+
DownloadURL: externalLink(c, textFsckLog, reportAsset.FsckLog),
519+
Type: dashapi.FsckLog,
520+
})
521+
}
515522
}
516523
sort.SliceStable(assetList, func(i, j int) bool {
517524
return asset.GetTypeDescription(assetList[i].Type).ReportingPrio <

dashboard/app/entities_datastore.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type Asset struct {
5959
Type dashapi.AssetType
6060
DownloadURL string
6161
CreateDate time.Time
62+
FsckLog int64 // references to fsck logstext entity
6263
}
6364

6465
type Build struct {
@@ -666,6 +667,7 @@ const (
666667
textLog = "Log"
667668
textError = "Error"
668669
textReproLog = "ReproLog"
670+
textFsckLog = "FsckLog"
669671
)
670672

671673
const (

dashboard/app/main.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,9 +2047,9 @@ func linkifyReport(report []byte, repo, commit string) template.HTML {
20472047

20482048
var sourceFileRe = regexp.MustCompile("( |\t|\n)([a-zA-Z0-9/_.-]+\\.(?:h|c|cc|cpp|s|S|go|rs)):([0-9]+)( |!|\\)|\t|\n)")
20492049

2050-
func makeUIAssets(build *Build, crash *Crash, forReport bool) []*uiAsset {
2050+
func makeUIAssets(c context.Context, build *Build, crash *Crash, forReport bool) []*uiAsset {
20512051
var uiAssets []*uiAsset
2052-
for _, asset := range createAssetList(build, crash, forReport) {
2052+
for _, asset := range createAssetList(c, build, crash, forReport) {
20532053
uiAssets = append(uiAssets, &uiAsset{
20542054
Title: asset.Title,
20552055
DownloadURL: asset.DownloadURL,
@@ -2072,7 +2072,7 @@ func makeUICrash(c context.Context, crash *Crash, build *Build) *uiCrash {
20722072
ReproLogLink: textLink(textReproLog, crash.ReproLog),
20732073
ReproIsRevoked: crash.ReproIsRevoked,
20742074
MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo),
2075-
Assets: makeUIAssets(build, crash, true),
2075+
Assets: makeUIAssets(c, build, crash, true),
20762076
}
20772077
if build != nil {
20782078
ui.uiBuild = makeUIBuild(c, build, true)
@@ -2094,7 +2094,7 @@ func makeUIBuild(c context.Context, build *Build, forReport bool) *uiBuild {
20942094
KernelCommitTitle: build.KernelCommitTitle,
20952095
KernelCommitDate: build.KernelCommitDate,
20962096
KernelConfigLink: textLink(textKernelConfig, build.KernelConfig),
2097-
Assets: makeUIAssets(build, nil, forReport),
2097+
Assets: makeUIAssets(c, build, nil, forReport),
20982098
}
20992099
}
21002100

dashboard/app/reporting.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ func crashBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key,
549549
if !bugReporting.Reported.IsZero() {
550550
typ = dashapi.ReportRepro
551551
}
552-
assetList := createAssetList(build, crash, true)
552+
assetList := createAssetList(c, build, crash, true)
553553
kernelRepo := kernelRepoInfo(c, build)
554554
rep := &dashapi.BugReport{
555555
Type: typ,

dashboard/dashapi/dashapi.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ const (
510510
KernelImage AssetType = "kernel_image"
511511
HTMLCoverageReport AssetType = "html_coverage_report"
512512
MountInRepro AssetType = "mount_in_repro"
513+
FsckLog AssetType = "fsck_log"
513514
)
514515

515516
type BisectResult struct {
@@ -802,6 +803,7 @@ func (dash *Dashboard) UploadManagerStats(req *ManagerStatsReq) error {
802803
type NewAsset struct {
803804
DownloadURL string
804805
Type AssetType
806+
FsckLog []byte
805807
}
806808

807809
type AddBuildAssetsReq struct {

docs/syscall_descriptions_syntax.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ rest of the type-options are type-specific:
6767
value range start, how many values per process, underlying type
6868
"compressed_image": zlib-compressed disk image
6969
syscalls accepting compressed images must be marked with `no_generate`
70-
and `no_minimize` call attributes.
70+
and `no_minimize` call attributes. if the content of the decompressed image
71+
can be checked by a `fsck`-like command, use the `fsck` syscall attribute
7172
"text": machine code of the specified type, type-options:
7273
text type (x86_real, x86_16, x86_32, x86_64, arm64)
7374
"void": type with static size 0
@@ -101,6 +102,8 @@ Call attributes are:
101102
"breaks_returns": ignore return values of all subsequent calls in the program in fallback feedback (can't be trusted).
102103
"no_generate": do not try to generate this syscall, i.e. use only seed descriptions to produce it.
103104
"no_minimize": do not modify instances of this syscall when trying to minimize a crashing program.
105+
"fsck": the content of the compressed buffer argument for this syscall is a file system and the
106+
string argument is a fsck-like command that will be called to verify the filesystem
104107
"remote_cover": wait longer to collect remote coverage for this call.
105108
```
106109

pkg/manager/crash.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (cs *CrashStore) SaveRepro(res *ReproResult, progText, cProgText []byte) er
148148
osutil.WriteFile(filepath.Join(dir, cReproFileName), cProgText)
149149
}
150150
var assetErr error
151-
repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader) {
151+
repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader, c *prog.Call) {
152152
fileName := filepath.Join(dir, name+".gz")
153153
if err := osutil.WriteGzipStream(fileName, r); err != nil {
154154
assetErr = fmt.Errorf("failed to write crash asset: type %d, %w", typ, err)

prog/analysis.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,7 @@ const (
383383
MountInRepro AssetType = iota
384384
)
385385

386-
func (p *Prog) ForEachAsset(cb func(name string, typ AssetType, r io.Reader)) {
386+
func (p *Prog) ForEachAsset(cb func(name string, typ AssetType, r io.Reader, c *Call)) {
387387
for id, c := range p.Calls {
388388
ForeachArg(c, func(arg Arg, _ *ArgCtx) {
389389
a, ok := arg.(*DataArg)
@@ -395,7 +395,7 @@ func (p *Prog) ForEachAsset(cb func(name string, typ AssetType, r io.Reader)) {
395395
if len(data) == 0 {
396396
return
397397
}
398-
cb(fmt.Sprintf("mount_%v", id), MountInRepro, bytes.NewReader(data))
398+
cb(fmt.Sprintf("mount_%v", id), MountInRepro, bytes.NewReader(data), c)
399399
})
400400
}
401401
}

prog/fsck.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2024 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 prog
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"io"
10+
"os"
11+
"os/exec"
12+
"strconv"
13+
"strings"
14+
15+
"github.com/google/syzkaller/pkg/osutil"
16+
)
17+
18+
// Fsck returns whether a filesystem is clean or not.
19+
func Fsck(r io.Reader, fsckCmd string) ([]byte, error) {
20+
// Write the image to a temporary file.
21+
tempFile, err := os.CreateTemp("", "*.img")
22+
if err != nil {
23+
return []byte{}, fmt.Errorf("failed to create temporary file: %w", err)
24+
}
25+
defer os.Remove(tempFile.Name())
26+
27+
_, err = io.Copy(tempFile, r)
28+
if err != nil {
29+
return []byte{}, fmt.Errorf("failed to write data to temporary file: %w", err)
30+
}
31+
32+
if err := tempFile.Close(); err != nil {
33+
return []byte{}, fmt.Errorf("failed to close temporary file: %w", err)
34+
}
35+
36+
// And run the provided fsck command on it.
37+
fsck := append(strings.Fields(fsckCmd), tempFile.Name())
38+
cmd := osutil.Command(fsck[0], fsck[1:]...)
39+
if err := osutil.Sandbox(cmd, true, true); err != nil {
40+
return []byte{}, err
41+
}
42+
43+
output, err := cmd.CombinedOutput()
44+
var exitError (*exec.ExitError)
45+
ok := errors.As(err, &exitError)
46+
if !ok {
47+
return []byte{}, err
48+
}
49+
50+
prefix := fsckCmd + " exited with status code " + strconv.Itoa(exitError.ExitCode()) + "\n"
51+
return append([]byte(prefix), output...), nil
52+
}

0 commit comments

Comments
 (0)