aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorFlorent Revest <revest@chromium.org>2024-11-28 01:50:23 +0100
committerAleksandr Nogikh <nogikh@google.com>2024-12-09 18:35:48 +0000
commitdeb728774249ce479316c219f77530e2af52e3bd (patch)
tree0c40542088d8ffebf4ee5ddb56a61a94ed57afaf /pkg
parent07e46fbc2bd7ff8782c975596672e4e3d3891865 (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.
Diffstat (limited to 'pkg')
-rw-r--r--pkg/compiler/testdata/all.txt4
-rw-r--r--pkg/compiler/testdata/errors.txt2
-rw-r--r--pkg/image/fsck.go59
-rw-r--r--pkg/image/fsck_test.go93
-rw-r--r--pkg/manager/crash.go2
-rw-r--r--pkg/mgrconfig/config.go7
6 files changed, 166 insertions, 1 deletions
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.