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 @@
{{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