diff --git a/dashboard/app/api.go b/dashboard/app/api.go index 107add5bd319..bfd3add1bcbb 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 2eb042ea1cef..ccb1dda37a85 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 43adf93560d9..e424172f4cce 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 24046de681aa..3282531395af 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 cf292fc12369..e8355acab8d5 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 7ead75f5884f..0f859f7150f7 100644 --- a/dashboard/app/templates/templates.html +++ b/dashboard/app/templates/templates.html @@ -460,7 +460,7 @@

syzbot

{{if $b.ReproCLink}}C{{end}} {{if $b.MachineInfoLink}}info{{end}} {{range $i, $asset := .Assets}} - [{{$asset.Title}}] + [{{$asset.Title}}{{if $asset.FsckLogURL}} ({{if $asset.FsIsClean}}clean{{else}}corrupt{{end}} fs){{end}}] {{end}} {{$b.Manager}} {{$b.Title}} diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go index 6396f5df76ac..ab59f45000c2 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 63c86cc93c92..2bb790da12ef 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/attrs.go b/pkg/compiler/attrs.go index 197093b50429..ba877caf6219 100644 --- a/pkg/compiler/attrs.go +++ b/pkg/compiler/attrs.go @@ -18,6 +18,7 @@ const ( // This will facilitate const expressions in e.g. size[] or align[]. intAttr exprAttr + stringAttr ) type attrDesc struct { @@ -80,6 +81,8 @@ func initCallAttrs() { case reflect.Bool: case reflect.Uint64: desc.Type = intAttr + case reflect.String: + desc.Type = stringAttr default: panic("unsupported syscall attribute type") } diff --git a/pkg/compiler/check.go b/pkg/compiler/check.go index d693fa790a53..92e3e5e032ac 100644 --- a/pkg/compiler/check.go +++ b/pkg/compiler/check.go @@ -209,7 +209,7 @@ func (comp *compiler) checkStructFields(n *ast.Struct, typ, name string) { prevFieldHadIf := false for fieldIdx, f := range n.Fields { if n.IsUnion { - _, exprs := comp.parseAttrs(unionFieldAttrs, f, f.Attrs) + _, exprs, _ := comp.parseAttrs(unionFieldAttrs, f, f.Attrs) if fieldIdx > 0 && fieldIdx+1 < len(n.Fields) && prevFieldHadIf && exprs[attrIf] == nil { comp.error(f.Pos, "either no fields have conditions or all except the last") @@ -220,7 +220,7 @@ func (comp *compiler) checkStructFields(n *ast.Struct, typ, name string) { } continue } - attrs, _ := comp.parseAttrs(structFieldAttrs, f, f.Attrs) + attrs, _, _ := comp.parseAttrs(structFieldAttrs, f, f.Attrs) dirCount := attrs[attrIn] + attrs[attrOut] + attrs[attrInOut] if dirCount != 0 { hasDirections = true @@ -1458,7 +1458,7 @@ func (comp *compiler) checkVarlen(n *ast.Struct) { } } for i, f := range n.Fields { - _, exprs := comp.parseAttrs(structOrUnionFieldAttrs(n), f, f.Attrs) + _, exprs, _ := comp.parseAttrs(structOrUnionFieldAttrs(n), f, f.Attrs) if !n.IsUnion && i == len(n.Fields)-1 { break } diff --git a/pkg/compiler/compiler.go b/pkg/compiler/compiler.go index 978255219b71..fa23ce6bf2f5 100644 --- a/pkg/compiler/compiler.go +++ b/pkg/compiler/compiler.go @@ -210,34 +210,36 @@ func (comp *compiler) structIsVarlen(name string) bool { func (comp *compiler) parseIntAttrs(descs map[string]*attrDesc, parent ast.Node, attrs []*ast.Type) map[*attrDesc]uint64 { - intAttrs, _ := comp.parseAttrs(descs, parent, attrs) + intAttrs, _, _ := comp.parseAttrs(descs, parent, attrs) return intAttrs } func (comp *compiler) parseAttrs(descs map[string]*attrDesc, parent ast.Node, attrs []*ast.Type) ( - map[*attrDesc]uint64, map[*attrDesc]prog.Expression) { + map[*attrDesc]uint64, map[*attrDesc]prog.Expression, map[*attrDesc]string) { _, parentType, parentName := parent.Info() resInt := make(map[*attrDesc]uint64) resExpr := make(map[*attrDesc]prog.Expression) + resString := make(map[*attrDesc]string) for _, attr := range attrs { if unexpected, _, ok := checkTypeKind(attr, kindIdent); !ok { comp.error(attr.Pos, "unexpected %v, expect attribute", unexpected) - return resInt, resExpr + return resInt, resExpr, resString } if len(attr.Colon) != 0 { comp.error(attr.Colon[0].Pos, "unexpected ':'") - return resInt, resExpr + return resInt, resExpr, resString } desc := descs[attr.Ident] if desc == nil { comp.error(attr.Pos, "unknown %v %v attribute %v", parentType, parentName, attr.Ident) - return resInt, resExpr + return resInt, resExpr, resString } _, dupInt := resInt[desc] _, dupExpr := resExpr[desc] - if dupInt || dupExpr { + _, dupString := resString[desc] + if dupInt || dupExpr || dupString { comp.error(attr.Pos, "duplicate %v %v attribute %v", parentType, parentName, attr.Ident) - return resInt, resExpr + return resInt, resExpr, resString } switch desc.Type { @@ -245,18 +247,20 @@ func (comp *compiler) parseAttrs(descs map[string]*attrDesc, parent ast.Node, at resInt[desc] = 1 if len(attr.Args) != 0 { comp.error(attr.Pos, "%v attribute has args", attr.Ident) - return nil, nil + return nil, nil, nil } case intAttr: resInt[desc] = comp.parseAttrIntArg(attr) case exprAttr: resExpr[desc] = comp.parseAttrExprArg(attr) + case stringAttr: + resString[desc] = comp.parseAttrStringArg(attr) default: comp.error(attr.Pos, "attribute %v has unknown type", attr.Ident) - return nil, nil + return nil, nil, nil } } - return resInt, resExpr + return resInt, resExpr, resString } func (comp *compiler) parseAttrExprArg(attr *ast.Type) prog.Expression { @@ -289,6 +293,19 @@ func (comp *compiler) parseAttrIntArg(attr *ast.Type) uint64 { return sz.Value } +func (comp *compiler) parseAttrStringArg(attr *ast.Type) string { + if len(attr.Args) != 1 { + comp.error(attr.Pos, "%v attribute is expected to have 1 argument", attr.Ident) + return "" + } + arg := attr.Args[0] + if !arg.HasString { + comp.error(attr.Pos, "%v argument must be a string", attr.Ident) + return "" + } + return arg.String +} + func (comp *compiler) getTypeDesc(t *ast.Type) *typeDesc { if desc := builtinTypes[t.Ident]; desc != nil { return desc diff --git a/pkg/compiler/gen.go b/pkg/compiler/gen.go index a6fd938a9ac2..fb803875ae57 100644 --- a/pkg/compiler/gen.go +++ b/pkg/compiler/gen.go @@ -132,8 +132,8 @@ func (comp *compiler) genSyscall(n *ast.Call, argSizes []uint64) *prog.Syscall { ret = comp.genType(n.Ret, comp.ptrSize) } var attrs prog.SyscallAttrs - descAttrs := comp.parseIntAttrs(callAttrs, n, n.Attrs) - for desc, val := range descAttrs { + intAttrs, _, stringAttrs := comp.parseAttrs(callAttrs, n, n.Attrs) + for desc, val := range intAttrs { fld := reflect.ValueOf(&attrs).Elem().FieldByName(desc.Name) switch desc.Type { case intAttr: @@ -144,6 +144,15 @@ func (comp *compiler) genSyscall(n *ast.Call, argSizes []uint64) *prog.Syscall { panic(fmt.Sprintf("unexpected attrDesc type: %q", desc.Type)) } } + for desc, val := range stringAttrs { + fld := reflect.ValueOf(&attrs).Elem().FieldByName(desc.Name) + switch desc.Type { + case stringAttr: + fld.SetString(val) + default: + panic(fmt.Sprintf("unexpected attrDesc type: %q", desc.Type)) + } + } fields, _ := comp.genFieldArray(n.Args, argSizes) return &prog.Syscall{ Name: n.Name.Name, @@ -513,7 +522,7 @@ func (comp *compiler) genFieldDir(attrs map[*attrDesc]uint64) (prog.Dir, bool) { } func (comp *compiler) genField(f *ast.Field, argSize uint64, overlayDir prog.Dir) prog.Field { - intAttrs, exprAttrs := comp.parseAttrs(structFieldAttrs, f, f.Attrs) + intAttrs, exprAttrs, _ := comp.parseAttrs(structFieldAttrs, f, f.Attrs) dir, hasDir := overlayDir, true if overlayDir == prog.DirInOut { dir, hasDir = comp.genFieldDir(intAttrs) diff --git a/pkg/compiler/testdata/all.txt b/pkg/compiler/testdata/all.txt index b19b85980303..1202e7511b8c 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 4f3186698faa..dc9170315a6b 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 000000000000..4c619b828420 --- /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 000000000000..aae102ec7e77 --- /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 56142695ad03..d995b5633244 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 b475c4eed3b2..1e499412ae74 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 087a2b3dcaaf..a6ad97c08a3d 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 86914105f489..1ae7008f9ac9 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 29f2aee5b500..96280b3e8830 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 5f2360946ec5..02bbdffdfc22 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 c4c788043fcc..85282d01a543 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/syz-sysgen/sysgen.go b/sys/syz-sysgen/sysgen.go index f03972a9fa5f..79475e81975a 100644 --- a/sys/syz-sysgen/sysgen.go +++ b/sys/syz-sysgen/sysgen.go @@ -312,6 +312,8 @@ func generateExecutorSyscalls(target *targets.Target, syscalls []*prog.Syscall, } case reflect.Uint64: val = attr.Uint() + case reflect.String: + continue default: panic("unsupported syscall attribute type") } diff --git a/sys/test/test.txt b/sys/test/test.txt index b08b9bcf8de2..71ec2a0111c2 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 3b60726290b8..98c7b4dbdb74 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