Skip to content

Commit 913008b

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

File tree

12 files changed

+184
-29
lines changed

12 files changed

+184
-29
lines changed

dashboard/app/api.go

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

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

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

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

1325-
func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, error) {
1325+
func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset, ns string) (Asset, error) {
13261326
typeInfo := asset.GetTypeDescription(newAsset.Type)
13271327
if typeInfo == nil {
13281328
return Asset{}, fmt.Errorf("unknown asset type")
@@ -1331,10 +1331,15 @@ func parseIncomingAsset(c context.Context, newAsset dashapi.NewAsset) (Asset, er
13311331
if err != nil {
13321332
return Asset{}, fmt.Errorf("invalid URL: %w", err)
13331333
}
1334+
fsckLog, err := putText(c, ns, textFsckLog, newAsset.FsckLog)
1335+
if err != nil {
1336+
return Asset{}, err
1337+
}
13341338
return Asset{
13351339
Type: newAsset.Type,
13361340
DownloadURL: newAsset.DownloadURL,
13371341
CreateDate: timeNow(c),
1342+
FsckLog: fsckLog,
13381343
}, nil
13391344
}
13401345

dashboard/app/entities_datastore.go

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

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

671673
const (

dashboard/dashapi/dashapi.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ func (dash *Dashboard) UploadManagerStats(req *ManagerStatsReq) error {
802802
type NewAsset struct {
803803
DownloadURL string
804804
Type AssetType
805+
FsckLog []byte
805806
}
806807

807808
type AddBuildAssetsReq struct {

docs/syscall_descriptions_syntax.md

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

pkg/manager/crash.go

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

prog/analysis.go

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

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

prog/fsck.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2024 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package prog
5+
6+
import (
7+
"fmt"
8+
"io"
9+
"os"
10+
"strconv"
11+
"strings"
12+
13+
"github.com/google/syzkaller/pkg/osutil"
14+
)
15+
16+
// Fsck returns whether a filesystem is clean or not
17+
func Fsck(r io.Reader, fsckCmd string) ([]byte, error) {
18+
// Write the image to a temporary file
19+
tempFile, err := os.CreateTemp("", "*.img")
20+
if err != nil {
21+
return []byte{}, fmt.Errorf("failed to create temporary file: %v", err)
22+
}
23+
defer os.Remove(tempFile.Name())
24+
25+
_, err = io.Copy(tempFile, r)
26+
if err != nil {
27+
return []byte{}, fmt.Errorf("failed to write data to temporary file: %v", err)
28+
}
29+
30+
if err := tempFile.Close(); err != nil {
31+
return []byte{}, fmt.Errorf("failed to close temporary file: %v", err)
32+
}
33+
34+
// And run the provided fsck command on it
35+
fsck := append(strings.Fields(fsckCmd), tempFile.Name())
36+
cmd := osutil.Command(fsck[0], fsck[1:]...)
37+
if err := osutil.Sandbox(cmd, true, true); err != nil {
38+
return []byte{}, err
39+
}
40+
41+
output, err := cmd.CombinedOutput()
42+
prefix := fsckCmd + " exited with status code " + strconv.Itoa(cmd.ProcessState.ExitCode()) + "\n"
43+
return append([]byte(prefix), output...), nil
44+
}

prog/fsck_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright 2024 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package prog_test
5+
6+
import (
7+
"fmt"
8+
"io"
9+
"os"
10+
"os/exec"
11+
"path/filepath"
12+
"strings"
13+
"testing"
14+
15+
"github.com/google/syzkaller/prog"
16+
. "github.com/google/syzkaller/prog"
17+
"github.com/google/syzkaller/sys/targets"
18+
)
19+
20+
// To get maximum test coverage here, install the following Debian packages:
21+
// dosfstools e2fsprogs btrfs-progs util-linux f2fs-tools hfsprogs jfsutils
22+
// util-linux dosfstools ocfs2-tools reiserfsprogs xfsprogs erofs-utils
23+
// exfatprogs gfs2-utils ntfs-3g-dev
24+
25+
const corruptedFs = "IAmACorruptedFs"
26+
27+
func fsckAvailable(cmd string) bool {
28+
_, err := exec.LookPath(strings.Fields(cmd)[0])
29+
return err == nil
30+
}
31+
32+
func TestFsck(t *testing.T) {
33+
target, err := GetTarget(targets.Linux, targets.AMD64)
34+
if err != nil {
35+
t.Fatal(err)
36+
}
37+
38+
// Use the images generated by syz-imagegen as a collection of clean file systems
39+
cleanFsProgs, err := filepath.Glob(filepath.Join("..", "sys", "linux", "test", "syz_mount_image_*_0"))
40+
if err != nil {
41+
t.Fatalf("directory read failed: %v", err)
42+
}
43+
44+
for _, file := range cleanFsProgs {
45+
sourceProg, err := os.ReadFile(file)
46+
if err != nil {
47+
t.Fatal(err)
48+
}
49+
p, err := target.Deserialize(sourceProg, NonStrict)
50+
if err != nil {
51+
t.Fatalf("failed to deserialize %s: %s", file, err)
52+
}
53+
p.ForEachAsset(func(name string, typ AssetType, r io.Reader, c *prog.Call) {
54+
if c.Meta.Attrs.Fsck == "" {
55+
return
56+
}
57+
fsckCmd := c.Meta.Attrs.Fsck
58+
if !fsckAvailable(fsckCmd) {
59+
t.Skipf("%s not available", fsckCmd)
60+
}
61+
62+
fsName := strings.TrimPrefix(c.Meta.Name, "syz_mount_image$")
63+
// Check that the file system in the image is detected as clean
64+
t.Run(fmt.Sprintf("clean %s", fsName), func(t *testing.T) {
65+
logs, err := Fsck(r, fsckCmd)
66+
if err != nil {
67+
t.Fatalf("failed to run fsck %s", err)
68+
}
69+
if !strings.Contains(string(logs), " exited with status code 0") {
70+
t.Fatalf("%s should exit 0 on a clean file system", fsckCmd)
71+
}
72+
})
73+
74+
// And use the same fsck command on a dummy fs to make sure that fails
75+
t.Run(fmt.Sprintf("corrupt %s", fsName), func(t *testing.T) {
76+
logs, err := Fsck(strings.NewReader(corruptedFs), fsckCmd)
77+
if err != nil {
78+
t.Fatalf("failed to run fsck %s", err)
79+
}
80+
if strings.Contains(string(logs), " exited with status code 0") {
81+
t.Fatalf("%s shouldn't exit 0 on a corrupt file system", fsckCmd)
82+
}
83+
})
84+
})
85+
}
86+
}

prog/images_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/google/go-cmp/cmp"
1818
"github.com/google/syzkaller/pkg/osutil"
19+
"github.com/google/syzkaller/prog"
1920
. "github.com/google/syzkaller/prog"
2021
"github.com/google/syzkaller/sys/targets"
2122
)
@@ -46,10 +47,13 @@ func TestForEachAsset(t *testing.T) {
4647
t.Fatalf("failed to deserialize %s: %s", file, err)
4748
}
4849
base := strings.TrimSuffix(file, ".in")
49-
p.ForEachAsset(func(name string, typ AssetType, r io.Reader) {
50+
p.ForEachAsset(func(name string, typ AssetType, r io.Reader, c *prog.Call) {
5051
if typ != MountInRepro {
5152
t.Fatalf("unknown asset type %v", typ)
5253
}
54+
if !strings.HasPrefix(c.Meta.Name, "syz_mount_image$") {
55+
t.Fatalf("unexpected syscall name %v", c.Meta.Name)
56+
}
5357
testResult, err := io.ReadAll(r)
5458
if err != nil {
5559
t.Fatal(err)

prog/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type SyscallAttrs struct {
4747
RemoteCover bool
4848
Automatic bool
4949
AutomaticHelper bool
50+
Fsck string
5051
}
5152

5253
// MaxArgs is maximum number of syscall arguments.

0 commit comments

Comments
 (0)