diff options
| author | Florent Revest <revest@chromium.org> | 2024-11-28 01:50:23 +0100 |
|---|---|---|
| committer | Aleksandr Nogikh <nogikh@google.com> | 2024-12-09 18:35:48 +0000 |
| commit | deb728774249ce479316c219f77530e2af52e3bd (patch) | |
| tree | 0c40542088d8ffebf4ee5ddb56a61a94ed57afaf | |
| parent | 07e46fbc2bd7ff8782c975596672e4e3d3891865 (diff) | |
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.
| -rw-r--r-- | dashboard/app/api.go | 23 | ||||
| -rw-r--r-- | dashboard/app/asset_storage.go | 11 | ||||
| -rw-r--r-- | dashboard/app/entities_datastore.go | 3 | ||||
| -rw-r--r-- | dashboard/app/main.go | 12 | ||||
| -rw-r--r-- | dashboard/app/reporting.go | 2 | ||||
| -rw-r--r-- | dashboard/app/templates/templates.html | 2 | ||||
| -rw-r--r-- | dashboard/dashapi/dashapi.go | 4 | ||||
| -rw-r--r-- | docs/syscall_descriptions_syntax.md | 5 | ||||
| -rw-r--r-- | pkg/compiler/testdata/all.txt | 4 | ||||
| -rw-r--r-- | pkg/compiler/testdata/errors.txt | 2 | ||||
| -rw-r--r-- | pkg/image/fsck.go | 59 | ||||
| -rw-r--r-- | pkg/image/fsck_test.go | 93 | ||||
| -rw-r--r-- | pkg/manager/crash.go | 2 | ||||
| -rw-r--r-- | pkg/mgrconfig/config.go | 7 | ||||
| -rw-r--r-- | prog/analysis.go | 4 | ||||
| -rw-r--r-- | prog/images_test.go | 5 | ||||
| -rw-r--r-- | prog/prog_test.go | 16 | ||||
| -rw-r--r-- | prog/types.go | 3 | ||||
| -rw-r--r-- | sys/linux/filesystem.txt | 28 | ||||
| -rw-r--r-- | sys/test/test.txt | 2 | ||||
| -rw-r--r-- | syz-manager/manager.go | 13 |
21 files changed, 263 insertions, 37 deletions
diff --git a/dashboard/app/api.go b/dashboard/app/api.go index 107add5bd..bfd3add1b 100644 --- a/dashboard/app/api.go +++ b/dashboard/app/api.go @@ -491,7 +491,7 @@ func uploadBuild(c context.Context, now time.Time, ns string, req *dashapi.Build *Build, bool, error) { newAssets := []Asset{} for i, toAdd := range req.Assets { - newAsset, err := parseIncomingAsset(c, toAdd) + newAsset, err := parseIncomingAsset(c, toAdd, ns) if err != nil { return nil, false, fmt.Errorf("failed to parse asset #%d: %w", i, err) } @@ -783,7 +783,8 @@ func apiReportCrash(c context.Context, ns string, r *http.Request, payload []byt // nolint: gocyclo func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, error) { - assets, err := parseCrashAssets(c, req) + ns := build.Namespace + assets, err := parseCrashAssets(c, req, ns) if err != nil { return nil, err } @@ -798,7 +799,6 @@ func reportCrash(c context.Context, build *Build, req *dashapi.Crash) (*Bug, err } req.Maintainers = email.MergeEmailLists(req.Maintainers) - ns := build.Namespace bug, err := findBugForCrash(c, ns, req.AltTitles) if err != nil { 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 return bug, nil } -func parseCrashAssets(c context.Context, req *dashapi.Crash) ([]Asset, error) { +func parseCrashAssets(c context.Context, req *dashapi.Crash, ns string) ([]Asset, error) { assets := []Asset{} for i, toAdd := range req.Assets { - newAsset, err := parseIncomingAsset(c, toAdd) + newAsset, err := parseIncomingAsset(c, toAdd, ns) if err != nil { return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err) } @@ -1309,7 +1309,7 @@ func apiAddBuildAssets(c context.Context, ns string, r *http.Request, payload [] } assets := []Asset{} for i, toAdd := range req.Assets { - asset, err := parseIncomingAsset(c, toAdd) + asset, err := parseIncomingAsset(c, toAdd, ns) if err != nil { return nil, fmt.Errorf("failed to parse asset #%d: %w", i, err) } @@ -1322,7 +1322,7 @@ func apiAddBuildAssets(c context.Context, ns string, r *http.Request, payload [] return nil, nil } -func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, error) { +func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset, ns string) (Asset, error) { typeInfo := asset.GetTypeDescription(newAsset.Type) if typeInfo == nil { return Asset{}, fmt.Errorf("unknown asset type") @@ -1331,10 +1331,19 @@ func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, er if err != nil { return Asset{}, fmt.Errorf("invalid URL: %w", err) } + fsckLog := int64(0) + if len(newAsset.FsckLog) > 0 { + fsckLog, err = putText(c, ns, textFsckLog, newAsset.FsckLog) + if err != nil { + return Asset{}, err + } + } return Asset{ Type: newAsset.Type, DownloadURL: newAsset.DownloadURL, CreateDate: timeNow(c), + FsckLog: fsckLog, + FsIsClean: newAsset.FsIsClean, }, nil } diff --git a/dashboard/app/asset_storage.go b/dashboard/app/asset_storage.go index 2eb042ea1..ccb1dda37 100644 --- a/dashboard/app/asset_storage.go +++ b/dashboard/app/asset_storage.go @@ -496,7 +496,7 @@ func queryLatestManagerAssets(c context.Context, ns string, assetType dashapi.As return ret, nil } -func createAssetList(build *Build, crash *Crash, forReport bool) []dashapi.Asset { +func createAssetList(c context.Context, build *Build, crash *Crash, forReport bool) []dashapi.Asset { var crashAssets []Asset if crash != nil { crashAssets = crash.Assets @@ -507,11 +507,16 @@ func createAssetList(build *Build, crash *Crash, forReport bool) []dashapi.Asset if typeDescr == nil || forReport && typeDescr.NoReporting { continue } - assetList = append(assetList, dashapi.Asset{ + newAsset := dashapi.Asset{ Title: typeDescr.GetTitle(targets.Get(build.OS, build.Arch)), DownloadURL: reportAsset.DownloadURL, Type: reportAsset.Type, - }) + } + if reportAsset.FsckLog != 0 { + newAsset.FsckLogURL = externalLink(c, textFsckLog, reportAsset.FsckLog) + newAsset.FsIsClean = reportAsset.FsIsClean + } + assetList = append(assetList, newAsset) } sort.SliceStable(assetList, func(i, j int) bool { return asset.GetTypeDescription(assetList[i].Type).ReportingPrio < diff --git a/dashboard/app/entities_datastore.go b/dashboard/app/entities_datastore.go index 43adf9356..e424172f4 100644 --- a/dashboard/app/entities_datastore.go +++ b/dashboard/app/entities_datastore.go @@ -59,6 +59,8 @@ type Asset struct { Type dashapi.AssetType DownloadURL string CreateDate time.Time + FsckLog int64 // references to fsck logstext entity - 0 if fsck wasn't run + FsIsClean bool // undefined value if FsckLog is 0 } type Build struct { @@ -666,6 +668,7 @@ const ( textLog = "Log" textError = "Error" textReproLog = "ReproLog" + textFsckLog = "FsckLog" ) const ( diff --git a/dashboard/app/main.go b/dashboard/app/main.go index 24046de68..328253139 100644 --- a/dashboard/app/main.go +++ b/dashboard/app/main.go @@ -405,6 +405,8 @@ type uiCrash struct { type uiAsset struct { Title string DownloadURL string + FsckLogURL string + FsIsClean bool } type uiCrashTable struct { @@ -2047,12 +2049,14 @@ func linkifyReport(report []byte, repo, commit string) template.HTML { var sourceFileRe = regexp.MustCompile("( |\t|\n)([a-zA-Z0-9/_.-]+\\.(?:h|c|cc|cpp|s|S|go|rs)):([0-9]+)( |!|\\)|\t|\n)") -func makeUIAssets(build *Build, crash *Crash, forReport bool) []*uiAsset { +func makeUIAssets(c context.Context, build *Build, crash *Crash, forReport bool) []*uiAsset { var uiAssets []*uiAsset - for _, asset := range createAssetList(build, crash, forReport) { + for _, asset := range createAssetList(c, build, crash, forReport) { uiAssets = append(uiAssets, &uiAsset{ Title: asset.Title, DownloadURL: asset.DownloadURL, + FsckLogURL: asset.FsckLogURL, + FsIsClean: asset.FsIsClean, }) } return uiAssets @@ -2072,7 +2076,7 @@ func makeUICrash(c context.Context, crash *Crash, build *Build) *uiCrash { ReproLogLink: textLink(textReproLog, crash.ReproLog), ReproIsRevoked: crash.ReproIsRevoked, MachineInfoLink: textLink(textMachineInfo, crash.MachineInfo), - Assets: makeUIAssets(build, crash, true), + Assets: makeUIAssets(c, build, crash, true), } if build != nil { ui.uiBuild = makeUIBuild(c, build, true) @@ -2094,7 +2098,7 @@ func makeUIBuild(c context.Context, build *Build, forReport bool) *uiBuild { KernelCommitTitle: build.KernelCommitTitle, KernelCommitDate: build.KernelCommitDate, KernelConfigLink: textLink(textKernelConfig, build.KernelConfig), - Assets: makeUIAssets(build, nil, forReport), + Assets: makeUIAssets(c, build, nil, forReport), } } diff --git a/dashboard/app/reporting.go b/dashboard/app/reporting.go index cf292fc12..e8355acab 100644 --- a/dashboard/app/reporting.go +++ b/dashboard/app/reporting.go @@ -549,7 +549,7 @@ func crashBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *db.Key, if !bugReporting.Reported.IsZero() { typ = dashapi.ReportRepro } - assetList := createAssetList(build, crash, true) + assetList := createAssetList(c, build, crash, true) kernelRepo := kernelRepoInfo(c, build) rep := &dashapi.BugReport{ Type: typ, diff --git a/dashboard/app/templates/templates.html b/dashboard/app/templates/templates.html index 7ead75f58..0f859f715 100644 --- a/dashboard/app/templates/templates.html +++ b/dashboard/app/templates/templates.html @@ -460,7 +460,7 @@ Use of this source code is governed by Apache 2 LICENSE that can be found in the <td class="repro{{if $b.ReproIsRevoked}} stale_repro{{end}}">{{if $b.ReproCLink}}<a href="{{$b.ReproCLink}}">C</a>{{end}}</td> <td class="repro">{{if $b.MachineInfoLink}}<a href="{{$b.MachineInfoLink}}">info</a>{{end}}</td> <td class="assets">{{range $i, $asset := .Assets}} - <span class="no-break">[<a href="{{$asset.DownloadURL}}">{{$asset.Title}}</a>]</span> + <span class="no-break">[<a href="{{$asset.DownloadURL}}">{{$asset.Title}}</a>{{if $asset.FsckLogURL}} (<a href="{{$asset.FsckLogURL}}">{{if $asset.FsIsClean}}clean{{else}}corrupt{{end}} fs</a>){{end}}]</span> {{end}}</td> <td class="manager">{{$b.Manager}}</td> <td class="manager">{{$b.Title}}</td> diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go index 6396f5df7..ab59f4500 100644 --- a/dashboard/dashapi/dashapi.go +++ b/dashboard/dashapi/dashapi.go @@ -497,6 +497,8 @@ type Asset struct { Title string DownloadURL string Type AssetType + FsckLogURL string + FsIsClean bool } type AssetType string @@ -802,6 +804,8 @@ func (dash *Dashboard) UploadManagerStats(req *ManagerStatsReq) error { type NewAsset struct { DownloadURL string Type AssetType + FsckLog []byte + FsIsClean bool } type AddBuildAssetsReq struct { diff --git a/docs/syscall_descriptions_syntax.md b/docs/syscall_descriptions_syntax.md index 63c86cc93..2bb790da1 100644 --- a/docs/syscall_descriptions_syntax.md +++ b/docs/syscall_descriptions_syntax.md @@ -67,7 +67,8 @@ rest of the type-options are type-specific: value range start, how many values per process, underlying type "compressed_image": zlib-compressed disk image syscalls accepting compressed images must be marked with `no_generate` - and `no_minimize` call attributes. + and `no_minimize` call attributes. if the content of the decompressed image + can be checked by a `fsck`-like command, use the `fsck` syscall attribute "text": machine code of the specified type, type-options: text type (x86_real, x86_16, x86_32, x86_64, arm64) "void": type with static size 0 @@ -101,6 +102,8 @@ Call attributes are: "breaks_returns": ignore return values of all subsequent calls in the program in fallback feedback (can't be trusted). "no_generate": do not try to generate this syscall, i.e. use only seed descriptions to produce it. "no_minimize": do not modify instances of this syscall when trying to minimize a crashing program. +"fsck": the content of the compressed buffer argument for this syscall is a file system and the + string argument is a fsck-like command that will be called to verify the filesystem "remote_cover": wait longer to collect remote coverage for this call. ``` diff --git a/pkg/compiler/testdata/all.txt b/pkg/compiler/testdata/all.txt index b19b85980..1202e7511 100644 --- a/pkg/compiler/testdata/all.txt +++ b/pkg/compiler/testdata/all.txt @@ -336,6 +336,10 @@ struct$fmt0 { flags_with_one_value = 0 +# Syscall attributes. + +fsck_test() (fsck["fsck.test -n"]) + # Compressed images. struct_compressed { diff --git a/pkg/compiler/testdata/errors.txt b/pkg/compiler/testdata/errors.txt index 4f3186698..dc9170315 100644 --- a/pkg/compiler/testdata/errors.txt +++ b/pkg/compiler/testdata/errors.txt @@ -507,3 +507,5 @@ conditional_fields_union2 [ u2 int32 ### either no fields have conditions or all except the last u3 int32 ] + +invalid_string_attr() (invalid["string"]) ### unknown syscall invalid_string_attr attribute invalid
\ No newline at end of file diff --git a/pkg/image/fsck.go b/pkg/image/fsck.go new file mode 100644 index 000000000..4c619b828 --- /dev/null +++ b/pkg/image/fsck.go @@ -0,0 +1,59 @@ +// Copyright 2024 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package image + +import ( + "errors" + "fmt" + "io" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/google/syzkaller/pkg/osutil" +) + +// Fsck runs fsckCmd against a file system image provided in r. It returns the +// fsck logs, whether the file system is clean and an error in case fsck could +// not be run. +func Fsck(r io.Reader, fsckCmd string) ([]byte, bool, error) { + // Write the image to a temporary file. + tempFile, err := os.CreateTemp("", "*.img") + if err != nil { + return nil, false, fmt.Errorf("failed to create temporary file: %w", err) + } + defer os.Remove(tempFile.Name()) + + _, err = io.Copy(tempFile, r) + if err != nil { + return nil, false, fmt.Errorf("failed to write data to temporary file: %w", err) + } + + if err := tempFile.Close(); err != nil { + return nil, false, fmt.Errorf("failed to close temporary file: %w", err) + } + + // And run the provided fsck command on it. + fsck := append(strings.Fields(fsckCmd), tempFile.Name()) + cmd := osutil.Command(fsck[0], fsck[1:]...) + if err := osutil.Sandbox(cmd, true, true); err != nil { + return nil, false, err + } + + exitCode := 0 + output, err := cmd.CombinedOutput() + if err != nil { + var exitError (*exec.ExitError) + ok := errors.As(err, &exitError) + if ok { + exitCode = exitError.ExitCode() + } else { + return nil, false, err + } + } + + prefix := fsckCmd + " exited with status code " + strconv.Itoa(exitCode) + "\n" + return append([]byte(prefix), output...), exitCode == 0, nil +} diff --git a/pkg/image/fsck_test.go b/pkg/image/fsck_test.go new file mode 100644 index 000000000..aae102ec7 --- /dev/null +++ b/pkg/image/fsck_test.go @@ -0,0 +1,93 @@ +// Copyright 2024 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package image_test + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + . "github.com/google/syzkaller/pkg/image" + "github.com/google/syzkaller/prog" + "github.com/google/syzkaller/sys/targets" +) + +// To get maximum test coverage here, install the following Debian packages: +// dosfstools e2fsprogs btrfs-progs util-linux f2fs-tools jfsutils util-linux +// dosfstools ocfs2-tools reiserfsprogs xfsprogs erofs-utils exfatprogs +// gfs2-utils. + +const corruptedFs = "IAmACorruptedFs" + +func fsckAvailable(cmd string) bool { + _, err := exec.LookPath(strings.Fields(cmd)[0]) + return err == nil +} + +func TestFsck(t *testing.T) { + target, err := prog.GetTarget(targets.Linux, targets.AMD64) + if err != nil { + t.Fatal(err) + } + + // Use the images generated by syz-imagegen as a collection of clean file systems. + cleanFsProgs, err := filepath.Glob(filepath.Join("..", "sys", "linux", "test", "syz_mount_image_*_0")) + if err != nil { + t.Fatalf("directory read failed: %v", err) + } + + for _, file := range cleanFsProgs { + sourceProg, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + p, err := target.Deserialize(sourceProg, prog.NonStrict) + if err != nil { + t.Fatalf("failed to deserialize %s: %s", file, err) + } + p.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader, c *prog.Call) { + if c.Meta.Attrs.Fsck == "" { + return + } + fsckCmd := c.Meta.Attrs.Fsck + // Tolerate missing fsck commands except during CI runs. + skip := !fsckAvailable(fsckCmd) && os.Getenv("CI") == "" + + fsName := strings.TrimPrefix(c.Meta.Name, "syz_mount_image$") + // Check that the file system in the image is detected as clean. + t.Run(fmt.Sprintf("clean %s", fsName), func(t *testing.T) { + if skip { + t.Skipf("%s not available", fsckCmd) + } + + logs, isClean, err := Fsck(r, fsckCmd) + if err != nil { + t.Fatalf("failed to run fsck %s", err) + } + if !isClean { + t.Fatalf("%s should exit 0 on a clean file system %s", fsckCmd, string(logs)) + } + }) + + // And use the same fsck command on a dummy fs to make sure that fails. + t.Run(fmt.Sprintf("corrupt %s", fsName), func(t *testing.T) { + if skip { + t.Skipf("%s not available", fsckCmd) + } + + logs, isClean, err := Fsck(strings.NewReader(corruptedFs), fsckCmd) + if err != nil { + t.Fatalf("failed to run fsck %s", err) + } + if isClean { + t.Fatalf("%s shouldn't exit 0 on a corrupt file system %s", fsckCmd, string(logs)) + } + }) + }) + } +} diff --git a/pkg/manager/crash.go b/pkg/manager/crash.go index 56142695a..d995b5633 100644 --- a/pkg/manager/crash.go +++ b/pkg/manager/crash.go @@ -148,7 +148,7 @@ func (cs *CrashStore) SaveRepro(res *ReproResult, progText, cProgText []byte) er osutil.WriteFile(filepath.Join(dir, cReproFileName), cProgText) } var assetErr error - repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader) { + repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader, c *prog.Call) { fileName := filepath.Join(dir, name+".gz") if err := osutil.WriteGzipStream(fileName, r); err != nil { assetErr = fmt.Errorf("failed to write crash asset: type %d, %w", typ, err) diff --git a/pkg/mgrconfig/config.go b/pkg/mgrconfig/config.go index b475c4eed..1e499412a 100644 --- a/pkg/mgrconfig/config.go +++ b/pkg/mgrconfig/config.go @@ -192,6 +192,13 @@ type Config struct { // the output. StraceBin string `json:"strace_bin"` + // Whether to run fsck commands on file system images found in new crash + // reproducers. The fsck logs get reported as assets in the dashboard. + // Note: you may need to install 3rd-party dependencies for this to work. + // fsck commands that can be run by syz-manager are specified in mount + // syscall descriptions - typically in sys/linux/filesystem.txt. + RunFsck bool `json:"run_fsck"` + // Type of virtual machine to use, e.g. "qemu", "gce", "android", "isolated", etc. Type string `json:"type"` // VM-type-specific parameters. diff --git a/prog/analysis.go b/prog/analysis.go index 087a2b3dc..a6ad97c08 100644 --- a/prog/analysis.go +++ b/prog/analysis.go @@ -383,7 +383,7 @@ const ( MountInRepro AssetType = iota ) -func (p *Prog) ForEachAsset(cb func(name string, typ AssetType, r io.Reader)) { +func (p *Prog) ForEachAsset(cb func(name string, typ AssetType, r io.Reader, c *Call)) { for id, c := range p.Calls { ForeachArg(c, func(arg Arg, _ *ArgCtx) { a, ok := arg.(*DataArg) @@ -395,7 +395,7 @@ func (p *Prog) ForEachAsset(cb func(name string, typ AssetType, r io.Reader)) { if len(data) == 0 { return } - cb(fmt.Sprintf("mount_%v", id), MountInRepro, bytes.NewReader(data)) + cb(fmt.Sprintf("mount_%v", id), MountInRepro, bytes.NewReader(data), c) }) } } diff --git a/prog/images_test.go b/prog/images_test.go index 86914105f..1ae7008f9 100644 --- a/prog/images_test.go +++ b/prog/images_test.go @@ -46,10 +46,13 @@ func TestForEachAsset(t *testing.T) { t.Fatalf("failed to deserialize %s: %s", file, err) } base := strings.TrimSuffix(file, ".in") - p.ForEachAsset(func(name string, typ AssetType, r io.Reader) { + p.ForEachAsset(func(name string, typ AssetType, r io.Reader, c *Call) { if typ != MountInRepro { t.Fatalf("unknown asset type %v", typ) } + if !strings.HasPrefix(c.Meta.Name, "syz_mount_image$") { + t.Fatalf("unexpected syscall name %v", c.Meta.Name) + } testResult, err := io.ReadAll(r) if err != nil { t.Fatal(err) diff --git a/prog/prog_test.go b/prog/prog_test.go index 29f2aee5b..96280b3e8 100644 --- a/prog/prog_test.go +++ b/prog/prog_test.go @@ -140,6 +140,22 @@ func TestVmaType(t *testing.T) { } } +func TestFsckAttr(t *testing.T) { + target, err := GetTarget("test", "64") + if err != nil { + t.Fatal(err) + } + + syscall := target.SyscallMap["test$fsck_attr"] + if syscall == nil { + t.Fatal("could not find test$fsck_attr in sys/test") + } + + if syscall.Attrs.Fsck != "fsck.test -n" { + t.Fatalf("unexpected fsck command %s", syscall.Attrs.Fsck) + } +} + // TestCrossTarget ensures that a program serialized for one arch can be // deserialized for another arch. This happens when managers exchange // programs via hub. diff --git a/prog/types.go b/prog/types.go index 5f2360946..02bbdffdf 100644 --- a/prog/types.go +++ b/prog/types.go @@ -33,7 +33,7 @@ type Syscall struct { // pkg/compiler uses this structure to parse descriptions. // syz-sysgen uses this structure to generate code for executor. // -// Only `bool`s and `uint64`s are currently supported. +// Only `bool`s, `string`s and `uint64`s are currently supported. // // See docs/syscall_descriptions_syntax.md for description of individual attributes. type SyscallAttrs struct { @@ -47,6 +47,7 @@ type SyscallAttrs struct { RemoteCover bool Automatic bool AutomaticHelper bool + Fsck string } // MaxArgs is maximum number of syscall arguments. diff --git a/sys/linux/filesystem.txt b/sys/linux/filesystem.txt index c4c788043..85282d01a 100644 --- a/sys/linux/filesystem.txt +++ b/sys/linux/filesystem.txt @@ -109,26 +109,26 @@ syz_read_part_table(size len[img], img ptr[in, compressed_image]) (timeout[200], define SYZ_MOUNT_IMAGE_TIMEOUT 4000 -syz_mount_image$vfat(fs ptr[in, string["vfat"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[vfat_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$msdos(fs ptr[in, string["msdos"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[msdos_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) +syz_mount_image$vfat(fs ptr[in, string["vfat"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[vfat_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.vfat -n"]) +syz_mount_image$msdos(fs ptr[in, string["msdos"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[msdos_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.msdos -n"]) syz_mount_image$bfs(fs ptr[in, string["bfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[stringnoz]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$xfs(fs ptr[in, string["xfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[xfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$minix(fs ptr[in, string["minix"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[stringnoz]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$reiserfs(fs ptr[in, string["reiserfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[reiserfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) +syz_mount_image$xfs(fs ptr[in, string["xfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[xfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["xfs_repair -n"]) +syz_mount_image$minix(fs ptr[in, string["minix"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[stringnoz]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.minix"]) +syz_mount_image$reiserfs(fs ptr[in, string["reiserfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[reiserfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.reiserfs -n"]) syz_mount_image$hfs(fs ptr[in, string["hfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[hfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) syz_mount_image$hfsplus(fs ptr[in, string["hfsplus"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[hfsplus_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) syz_mount_image$iso9660(fs ptr[in, string["iso9660"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[iso9660_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$gfs2(fs ptr[in, string["gfs2"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[gfs2_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$jfs(fs ptr[in, string["jfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[jfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$btrfs(fs ptr[in, string["btrfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[btrfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) +syz_mount_image$gfs2(fs ptr[in, string["gfs2"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[gfs2_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.gfs2 -n"]) +syz_mount_image$jfs(fs ptr[in, string["jfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[jfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.jfs -n"]) +syz_mount_image$btrfs(fs ptr[in, string["btrfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[btrfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["btrfsck --readonly"]) syz_mount_image$ntfs(fs ptr[in, string["ntfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[ntfs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) syz_mount_image$ntfs3(fs ptr[in, string["ntfs3"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[ntfs3_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$ext4(fs ptr[in, string[ext4_types]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[ext4_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$f2fs(fs ptr[in, string["f2fs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[f2fs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$ocfs2(fs ptr[in, string["ocfs2"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[ocfs2_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$erofs(fs ptr[in, string["erofs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[erofs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$exfat(fs ptr[in, string["exfat"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[exfat_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) -syz_mount_image$cramfs(fs ptr[in, string["cramfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[stringnoz]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) +syz_mount_image$ext4(fs ptr[in, string[ext4_types]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[ext4_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.ext4 -n"]) +syz_mount_image$f2fs(fs ptr[in, string["f2fs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[f2fs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.f2fs"]) +syz_mount_image$ocfs2(fs ptr[in, string["ocfs2"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[ocfs2_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.ocfs2 -n"]) +syz_mount_image$erofs(fs ptr[in, string["erofs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[erofs_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.erofs"]) +syz_mount_image$exfat(fs ptr[in, string["exfat"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[exfat_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.exfat -n"]) +syz_mount_image$cramfs(fs ptr[in, string["cramfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[stringnoz]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize, fsck["fsck.cramfs"]) syz_mount_image$romfs(fs ptr[in, string["romfs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[stringnoz]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) syz_mount_image$efs(fs ptr[in, string["efs"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[stringnoz]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) syz_mount_image$jffs2(fs ptr[in, string["jffs2"]], dir ptr[in, filename], flags flags[mount_flags], opts ptr[in, fs_options[jffs2_options]], chdir bool8, size len[img], img ptr[in, compressed_image]) fd_dir (timeout[SYZ_MOUNT_IMAGE_TIMEOUT], no_generate, no_minimize) diff --git a/sys/test/test.txt b/sys/test/test.txt index b08b9bcf8..71ec2a011 100644 --- a/sys/test/test.txt +++ b/sys/test/test.txt @@ -991,3 +991,5 @@ test$produce_subtype_of_common() subtype_of_common test$consume_common(val common) test$consume_subtype_of_common(val subtype_of_common) + +test$fsck_attr() (fsck["fsck.test -n"]) diff --git a/syz-manager/manager.go b/syz-manager/manager.go index 3b6072629..98c7b4dbd 100644 --- a/syz-manager/manager.go +++ b/syz-manager/manager.go @@ -31,6 +31,7 @@ import ( "github.com/google/syzkaller/pkg/fuzzer/queue" "github.com/google/syzkaller/pkg/gce" "github.com/google/syzkaller/pkg/ifaceprobe" + "github.com/google/syzkaller/pkg/image" "github.com/google/syzkaller/pkg/log" "github.com/google/syzkaller/pkg/manager" "github.com/google/syzkaller/pkg/mgrconfig" @@ -921,7 +922,7 @@ func (mgr *Manager) uploadReproAssets(repro *repro.Result) []dashapi.NewAsset { } ret := []dashapi.NewAsset{} - repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader) { + repro.Prog.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader, c *prog.Call) { dashTyp, ok := map[prog.AssetType]dashapi.AssetType{ prog.MountInRepro: dashapi.MountInRepro, }[typ] @@ -933,6 +934,16 @@ func (mgr *Manager) uploadReproAssets(repro *repro.Result) []dashapi.NewAsset { log.Logf(1, "processing of the asset %v (%v) failed: %v", name, typ, err) return } + // Report file systems that fail fsck with a separate tag. + if mgr.cfg.RunFsck && dashTyp == dashapi.MountInRepro && c.Meta.Attrs.Fsck != "" { + logs, isClean, err := image.Fsck(r, c.Meta.Attrs.Fsck) + if err != nil { + log.Logf(1, "fsck of the asset %v failed: %v", name, err) + } else { + asset.FsckLog = logs + asset.FsIsClean = isClean + } + } ret = append(ret, asset) }) return ret |
