Skip to content

Commit

Permalink
tools: add a syz-diff tool
Browse files Browse the repository at this point in the history
This is the prototype version of the patch series fuzzing functionality
based on the syzkaller fuzzing engine.

The tool takes two syzkaller configs -- one for the base kernel, one for
the patched kernel. Optionally the patch itself can be also provided.

syz-diff will consider a bug patched-only if:
1) It happened while fuzzing the patched kernel.
2) It was never observed on the base kernel.
3) The tool found a repro on the patched kernel.
4) The repro did not crash the base kernel.
  • Loading branch information
a-nogikh committed Oct 21, 2024
1 parent ade32c2 commit d9c9616
Show file tree
Hide file tree
Showing 12 changed files with 952 additions and 21 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ repro: descriptions
mutate: descriptions
GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-mutate github.com/google/syzkaller/tools/syz-mutate

diff: host target
GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-diff github.com/google/syzkaller/tools/syz-diff

prog2c: descriptions
GOOS=$(HOSTOS) GOARCH=$(HOSTARCH) $(HOSTGO) build $(GOHOSTFLAGS) -o ./bin/syz-prog2c github.com/google/syzkaller/tools/syz-prog2c

Expand Down
68 changes: 68 additions & 0 deletions pkg/fuzzer/queue/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"context"
"encoding/gob"
"fmt"
"math/rand"
"sync"
"sync/atomic"

Expand Down Expand Up @@ -451,3 +452,70 @@ func (do *defaultOpts) Next() *Request {
req.ExecOpts.SandboxArg = do.opts.SandboxArg
return req
}

// RandomQueue holds up to |size| elements.
// Next() evicts a random one.
// On Submit(), if the queue is full, a random element is replaced.
type RandomQueue struct {
mu sync.Mutex
queue []*Request
maxSize int
rnd *rand.Rand
}

func NewRandomQueue(size int, rnd *rand.Rand) *RandomQueue {
return &RandomQueue{
maxSize: size,
rnd: rnd,
}
}

func (rq *RandomQueue) Next() *Request {
rq.mu.Lock()
defer rq.mu.Unlock()
if len(rq.queue) == 0 {
return nil
}
pos := rq.rnd.Intn(len(rq.queue))
item := rq.queue[pos]

last := len(rq.queue) - 1
rq.queue[pos] = rq.queue[last]
rq.queue[last] = nil
rq.queue = rq.queue[0 : len(rq.queue)-1]
return item
}

func (rq *RandomQueue) Submit(req *Request) {
rq.mu.Lock()
defer rq.mu.Unlock()
if len(rq.queue) < rq.maxSize {
rq.queue = append(rq.queue, req)
} else {
pos := rq.rnd.Intn(rq.maxSize + 1)
if pos < len(rq.queue) {
rq.queue[pos].Done(&Result{Status: ExecFailure})
rq.queue[pos] = req
}
}
}

type tee struct {
queue Executor
src Source
}

func Tee(src Source, queue Executor) Source {
return &tee{src: src, queue: queue}
}

func (t *tee) Next() *Request {
req := t.src.Next()
if req == nil {
return nil
}
t.queue.Submit(&Request{
Prog: req.Prog.Clone(),
})
return req
}
13 changes: 7 additions & 6 deletions pkg/manager/covfilter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import (
"github.com/google/syzkaller/pkg/mgrconfig"
)

func CreateCoverageFilter(source *ReportGeneratorWrapper, covCfg mgrconfig.CovFilterCfg) ([]uint64,
map[uint64]struct{}, error) {
func CreateCoverageFilter(source *ReportGeneratorWrapper, covCfg mgrconfig.CovFilterCfg,
strict bool) ([]uint64, map[uint64]struct{}, error) {
if covCfg.Empty() {
return nil, nil, nil
}
Expand All @@ -31,15 +31,15 @@ func CreateCoverageFilter(source *ReportGeneratorWrapper, covCfg mgrconfig.CovFi
apply(&sym.ObjectUnit)
}
}
if err := covFilterAddFilter(pcs, covCfg.Functions, foreachSymbol); err != nil {
if err := covFilterAddFilter(pcs, covCfg.Functions, foreachSymbol, strict); err != nil {
return nil, nil, err
}
foreachUnit := func(apply func(*backend.ObjectUnit)) {
for _, unit := range rg.Units {
apply(&unit.ObjectUnit)
}
}
if err := covFilterAddFilter(pcs, covCfg.Files, foreachUnit); err != nil {
if err := covFilterAddFilter(pcs, covCfg.Files, foreachUnit, strict); err != nil {
return nil, nil, err
}
if err := covFilterAddRawPCs(pcs, covCfg.RawPCs); err != nil {
Expand All @@ -59,7 +59,8 @@ func CreateCoverageFilter(source *ReportGeneratorWrapper, covCfg mgrconfig.CovFi
return execPCs, pcs, nil
}

func covFilterAddFilter(pcs map[uint64]struct{}, filters []string, foreach func(func(*backend.ObjectUnit))) error {
func covFilterAddFilter(pcs map[uint64]struct{}, filters []string, foreach func(func(*backend.ObjectUnit)),
strict bool) error {
res, err := compileRegexps(filters)
if err != nil {
return err
Expand All @@ -85,7 +86,7 @@ func covFilterAddFilter(pcs map[uint64]struct{}, filters []string, foreach func(
sort.Strings(used[re])
log.Logf(0, "coverage filter: %v: %v", re, used[re])
}
if len(res) != len(used) {
if strict && len(res) != len(used) {
return fmt.Errorf("some filters don't match anything")
}
return nil
Expand Down
4 changes: 2 additions & 2 deletions pkg/manager/crash.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,11 @@ func (cs *CrashStore) BugList() ([]*BugInfo, error) {
return ret, nil
}

func (cs *CrashStore) titleToID(title string) string {
func crashHash(title string) string {
sig := hash.Hash([]byte(title))
return sig.String()
}

func (cs *CrashStore) path(title string) string {
return filepath.Join(cs.BaseDir, "crashes", cs.titleToID(title))
return filepath.Join(cs.BaseDir, "crashes", crashHash(title))
}
4 changes: 2 additions & 2 deletions pkg/manager/crash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func TestMaxCrashLogs(t *testing.T) {
assert.NoError(t, err)
}

info, err := crashStore.BugInfo(crashStore.titleToID("Title A"), false)
info, err := crashStore.BugInfo(crashHash("Title A"), false)
assert.NoError(t, err)
assert.Len(t, info.Crashes, 5)
}
Expand Down Expand Up @@ -96,7 +96,7 @@ func TestCrashRepro(t *testing.T) {
}, []byte("prog text"), []byte("c prog text"))
assert.NoError(t, err)

report, err := crashStore.Report(crashStore.titleToID("Some title"))
report, err := crashStore.Report(crashHash("Some title"))
assert.NoError(t, err)
assert.Equal(t, "Some title", report.Title)
assert.Equal(t, "abcd", report.Tag)
Expand Down
135 changes: 135 additions & 0 deletions pkg/manager/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// 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 manager

import (
"fmt"
"path/filepath"
"sync"
"time"

"github.com/google/syzkaller/pkg/log"
"github.com/google/syzkaller/pkg/osutil"
)

type DiffBug struct {
Title string
Base DiffBugInfo
Patched DiffBugInfo
}

func (bug DiffBug) PatchedOnly() bool {
return bug.Base.NotCrashed && bug.Patched.Crashes > 0
}

func (bug DiffBug) AffectsBoth() bool {
return bug.Base.Crashes > 0 && bug.Patched.Crashes > 0
}

type DiffBugInfo struct {
Crashes int // Count of detected crashes.
NotCrashed bool // If were proven not to crash by running a repro.

// File paths.
Report string
Repro string
ReproLog string
}

// DiffFuzzerStore provides the functionality of a database of the patch fuzzing.
type DiffFuzzerStore struct {
BasePath string

mu sync.Mutex
bugs map[string]*DiffBug
}

func (s *DiffFuzzerStore) BaseCrashed(title string, report []byte) {
s.patch(title, func(obj *DiffBug) {
obj.Base.Crashes++
if len(report) > 0 {
obj.Base.Report = s.saveFile(title, "base_report", report)
}
})
}

func (s *DiffFuzzerStore) EverCrashedBase(title string) bool {
s.mu.Lock()
defer s.mu.Unlock()
obj := s.bugs[title]
return obj != nil && obj.Base.Crashes > 0
}

func (s *DiffFuzzerStore) BaseNotCrashed(title string) {
s.patch(title, func(obj *DiffBug) {
if obj.Base.Crashes == 0 {
obj.Base.NotCrashed = true
}
})
}

func (s *DiffFuzzerStore) PatchedCrashed(title string, report []byte) {
s.patch(title, func(obj *DiffBug) {
obj.Patched.Crashes++
if len(report) > 0 {
obj.Patched.Report = s.saveFile(title, "patched_report", report)
}
})
}

func (s *DiffFuzzerStore) SaveRepro(result *ReproResult) {
title := result.Crash.Report.Title
if result.Repro != nil {
// If there's a repro, save under the new title.
title = result.Repro.Report.Title
}

now := time.Now().Unix()
crashLog := fmt.Sprintf("%v.crash.log", now)
s.saveFile(title, crashLog, result.Crash.Output)
log.Logf(0, "%q: saved crash log into %s", title, crashLog)

s.patch(title, func(obj *DiffBug) {
if result.Repro != nil {
obj.Patched.Repro = s.saveFile(title, reproFileName, result.Repro.Prog.Serialize())
}
if result.Stats != nil {
reproLog := fmt.Sprintf("%v.repro.log", now)
obj.Patched.ReproLog = s.saveFile(title, reproLog, result.Stats.FullLog())
log.Logf(0, "%q: saved repro log into %s", title, reproLog)
}
})
}

func (s *DiffFuzzerStore) List() []DiffBug {
s.mu.Lock()
defer s.mu.Unlock()
var list []DiffBug
for _, obj := range s.bugs {
list = append(list, *obj)
}
return list
}

func (s *DiffFuzzerStore) saveFile(title, name string, data []byte) string {
hash := crashHash(title)
path := filepath.Join(s.BasePath, "crashes", hash)
osutil.MkdirAll(path)
osutil.WriteFile(filepath.Join(path, name), data)
return filepath.Join("crashes", hash, name)
}

func (s *DiffFuzzerStore) patch(title string, cb func(*DiffBug)) {
s.mu.Lock()
defer s.mu.Unlock()
if s.bugs == nil {
s.bugs = map[string]*DiffBug{}
}
obj, ok := s.bugs[title]
if !ok {
obj = &DiffBug{Title: title}
s.bugs[title] = obj
}
cb(obj)
}
Loading

0 comments on commit d9c9616

Please sign in to comment.