aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHrutvik Kanabar <hrutvik@google.com>2022-09-20 14:43:25 +0000
committerMarco Elver <me@marcoelver.com>2022-09-22 16:42:04 +0200
commit3fddc7194573e00eabde07dbb8ff17b025eb5c75 (patch)
tree666f974b48e810491fc10070d8176acbf881c3ac
parent5088f152247b1ec7659f72a05309254ca1b2b1d7 (diff)
pkg/mgrconfig, prog, syz-fuzzer: manager-configurable syscall mutation
Allow manager configuration to specify that certain syscalls should not be mutated. This is expected to be useful when mutating certain syscalls is unlikely to produce interesting executions. For example, mutating a `syz_mount_image` call will likely produce a corrupt image. Some implementation details: - Add a `no_mutate_syscalls` manager config entry, with the same format as `enable_syscalls`. Ensure this is parsed and stored in the config as a set of syscall IDs. - Send this set to fuzzers when they connect to their managers via RPC. Ensure each fuzzer stores a copy of the set. - When mutating arguments of a syscall, check first whether it has been specified as non-mutatable. - For all mutations not managed by a `syz-manager`, retain previous behaviour by ensuring that no syscalls are considered non-mutable.
-rw-r--r--pkg/mgrconfig/config.go2
-rw-r--r--pkg/mgrconfig/load.go28
-rw-r--r--pkg/rpctype/rpctype.go1
-rw-r--r--prog/checksum_test.go2
-rw-r--r--prog/mutation.go41
-rw-r--r--prog/mutation_test.go8
-rw-r--r--prog/prog_test.go2
-rw-r--r--prog/rand_test.go6
-rw-r--r--prog/size_test.go2
-rw-r--r--prog/test/fuzz.go2
-rw-r--r--syz-fuzzer/fuzzer.go2
-rw-r--r--syz-fuzzer/proc.go4
-rw-r--r--syz-manager/rpc.go1
-rw-r--r--tools/syz-mutate/mutate.go2
-rw-r--r--tools/syz-stress/stress.go6
15 files changed, 74 insertions, 35 deletions
diff --git a/pkg/mgrconfig/config.go b/pkg/mgrconfig/config.go
index 9073c482c..46270ab0c 100644
--- a/pkg/mgrconfig/config.go
+++ b/pkg/mgrconfig/config.go
@@ -166,6 +166,8 @@ type Config struct {
EnabledSyscalls []string `json:"enable_syscalls,omitempty"`
// List of system calls that should be treated as disabled (optional).
DisabledSyscalls []string `json:"disable_syscalls,omitempty"`
+ // List of syscalls that should not be mutated by the fuzzer (optional).
+ NoMutateSyscalls []string `json:"no_mutate_syscalls,omitempty"`
// List of regexps for known bugs.
// Don't save reports matching these regexps, but reboot VM after them,
// matched against whole report output.
diff --git a/pkg/mgrconfig/load.go b/pkg/mgrconfig/load.go
index 2b49d0cbd..f1063ed17 100644
--- a/pkg/mgrconfig/load.go
+++ b/pkg/mgrconfig/load.go
@@ -34,8 +34,9 @@ type Derived struct {
ExecprogBin string
ExecutorBin string
- Syscalls []int
- Timeouts targets.Timeouts
+ Syscalls []int
+ NoMutateCalls map[int]bool // Set of IDs of syscalls which should not be mutated.
+ Timeouts targets.Timeouts
}
func LoadData(data []byte) (*Config, error) {
@@ -177,6 +178,10 @@ func Complete(cfg *Config) error {
if err != nil {
return err
}
+ cfg.NoMutateCalls, err = ParseNoMutateSyscalls(cfg.Target, cfg.NoMutateSyscalls)
+ if err != nil {
+ return err
+ }
cfg.initTimeouts()
return nil
}
@@ -329,6 +334,25 @@ func ParseEnabledSyscalls(target *prog.Target, enabled, disabled []string) ([]in
return arr, nil
}
+func ParseNoMutateSyscalls(target *prog.Target, syscalls []string) (map[int]bool, error) {
+ var result = make(map[int]bool)
+
+ for _, c := range syscalls {
+ n := 0
+ for _, call := range target.Syscalls {
+ if MatchSyscall(call.Name, c) {
+ result[call.ID] = true
+ n++
+ }
+ }
+ if n == 0 {
+ return nil, fmt.Errorf("unknown no_mutate syscall: %v", c)
+ }
+ }
+
+ return result, nil
+}
+
func MatchSyscall(name, pattern string) bool {
if pattern == name || strings.HasPrefix(name, pattern+"$") {
return true
diff --git a/pkg/rpctype/rpctype.go b/pkg/rpctype/rpctype.go
index f1d889a20..fc582e576 100644
--- a/pkg/rpctype/rpctype.go
+++ b/pkg/rpctype/rpctype.go
@@ -41,6 +41,7 @@ type ConnectArgs struct {
type ConnectRes struct {
EnabledCalls []int
+ NoMutateCalls map[int]bool
GitRevision string
TargetRevision string
AllSandboxes bool
diff --git a/prog/checksum_test.go b/prog/checksum_test.go
index 46fdce6b7..abed9803c 100644
--- a/prog/checksum_test.go
+++ b/prog/checksum_test.go
@@ -18,7 +18,7 @@ func TestChecksumCalcRandom(t *testing.T) {
for _, call := range p.Calls {
CalcChecksumsCall(call)
}
- p.Mutate(rs, 10, ct, nil)
+ p.Mutate(rs, 10, ct, nil, nil)
for _, call := range p.Calls {
CalcChecksumsCall(call)
}
diff --git a/prog/mutation.go b/prog/mutation.go
index b03987671..ca53b1c7b 100644
--- a/prog/mutation.go
+++ b/prog/mutation.go
@@ -16,22 +16,24 @@ const maxBlobLen = uint64(100 << 10)
// Mutate program p.
//
-// p: The program to mutate.
-// rs: Random source.
-// ncalls: The allowed maximum calls in mutated program.
-// ct: ChoiceTable for syscalls.
-// corpus: The entire corpus, including original program p.
-func (p *Prog) Mutate(rs rand.Source, ncalls int, ct *ChoiceTable, corpus []*Prog) {
+// p: The program to mutate.
+// rs: Random source.
+// ncalls: The allowed maximum calls in mutated program.
+// ct: ChoiceTable for syscalls.
+// noMutate: Set of IDs of syscalls which should not be mutated.
+// corpus: The entire corpus, including original program p.
+func (p *Prog) Mutate(rs rand.Source, ncalls int, ct *ChoiceTable, noMutate map[int]bool, corpus []*Prog) {
r := newRand(p.Target, rs)
if ncalls < len(p.Calls) {
ncalls = len(p.Calls)
}
ctx := &mutator{
- p: p,
- r: r,
- ncalls: ncalls,
- ct: ct,
- corpus: corpus,
+ p: p,
+ r: r,
+ ncalls: ncalls,
+ ct: ct,
+ noMutate: noMutate,
+ corpus: corpus,
}
for stop, ok := false, false; !stop; stop = ok && len(p.Calls) != 0 && r.oneOf(3) {
switch {
@@ -59,11 +61,12 @@ func (p *Prog) Mutate(rs rand.Source, ncalls int, ct *ChoiceTable, corpus []*Pro
// Internal state required for performing mutations -- currently this matches
// the arguments passed to Mutate().
type mutator struct {
- p *Prog // The program to mutate.
- r *randGen // The randGen instance.
- ncalls int // The allowed maximum calls in mutated program.
- ct *ChoiceTable // ChoiceTable for syscalls.
- corpus []*Prog // The entire corpus, including original program p.
+ p *Prog // The program to mutate.
+ r *randGen // The randGen instance.
+ ncalls int // The allowed maximum calls in mutated program.
+ ct *ChoiceTable // ChoiceTable for syscalls.
+ noMutate map[int]bool // Set of IDs of syscalls which should not be mutated.
+ corpus []*Prog // The entire corpus, including original program p.
}
// This function selects a random other program p0 out of the corpus, and
@@ -93,6 +96,9 @@ func (ctx *mutator) squashAny() bool {
return false
}
ptr := complexPtrs[r.Intn(len(complexPtrs))]
+ if ctx.noMutate[ptr.call.Meta.ID] {
+ return false
+ }
if !p.Target.isAnyPtr(ptr.arg.Type()) {
p.Target.squashPtr(ptr.arg)
}
@@ -172,6 +178,9 @@ func (ctx *mutator) mutateArg() bool {
return false
}
c := p.Calls[idx]
+ if ctx.noMutate[c.Meta.ID] {
+ return false
+ }
updateSizes := true
for stop, ok := false, false; !stop; stop = ok && r.oneOf(3) {
ok = true
diff --git a/prog/mutation_test.go b/prog/mutation_test.go
index 5edba991c..0c084d1fe 100644
--- a/prog/mutation_test.go
+++ b/prog/mutation_test.go
@@ -221,7 +221,7 @@ func TestMutateRandom(t *testing.T) {
// There is a chance that mutation will produce the same program.
// So we check that at least 1 out of 20 mutations actually change the program.
for try := 0; try < 20; try++ {
- p1.Mutate(rs, 10, ct, nil)
+ p1.Mutate(rs, 10, ct, nil, nil)
data := p.Serialize()
if !bytes.Equal(data0, data) {
t.Fatalf("program changed after mutate\noriginal:\n%s\n\nnew:\n%s\n",
@@ -251,7 +251,7 @@ func TestMutateCorpus(t *testing.T) {
}
for i := 0; i < iters; i++ {
p1 := target.Generate(rs, 10, ct)
- p1.Mutate(rs, 10, ct, corpus)
+ p1.Mutate(rs, 10, ct, nil, corpus)
}
}
@@ -383,7 +383,7 @@ func BenchmarkMutate(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
rs := rand.NewSource(0)
for pb.Next() {
- p.Clone().Mutate(rs, progLen, ct, nil)
+ p.Clone().Mutate(rs, progLen, ct, nil, nil)
}
})
}
@@ -422,7 +422,7 @@ func runMutationTests(t *testing.T, tests [][2]string, valid bool) {
}
for i := 0; i < iters; i++ {
p1 := p.Clone()
- p1.Mutate(rs, len(goal.Calls), ct, nil)
+ p1.Mutate(rs, len(goal.Calls), ct, nil, nil)
data1 := p1.Serialize()
if bytes.Equal(want, data1) {
if !valid {
diff --git a/prog/prog_test.go b/prog/prog_test.go
index 2247694a1..9e347b4e8 100644
--- a/prog/prog_test.go
+++ b/prog/prog_test.go
@@ -193,7 +193,7 @@ func testCrossTarget(t *testing.T, target *Target, crossTargets []*Target) {
t.Fatal(err)
}
testCrossArchProg(t, p, crossTargets)
- p.Mutate(rs, 20, ct, nil)
+ p.Mutate(rs, 20, ct, nil, nil)
testCrossArchProg(t, p, crossTargets)
p, _ = Minimize(p, -1, false, func(*Prog, int) bool {
return rs.Int63()%2 == 0
diff --git a/prog/rand_test.go b/prog/rand_test.go
index cc663bf8b..ffe9bd822 100644
--- a/prog/rand_test.go
+++ b/prog/rand_test.go
@@ -56,7 +56,7 @@ func TestDeterminism(t *testing.T) {
func generateProg(t *testing.T, target *Target, rs rand.Source, ct *ChoiceTable, corpus []*Prog) *Prog {
p := target.Generate(rs, 5, ct)
- p.Mutate(rs, 10, ct, corpus)
+ p.Mutate(rs, 10, ct, nil, corpus)
for i, c := range p.Calls {
comps := make(CompMap)
for v := range extractValues(c) {
@@ -101,7 +101,7 @@ func TestEnabledCalls(t *testing.T) {
for i := 0; i < tries; i++ {
p := target.Generate(rs, 50, ct)
for it := 0; it < iters/tries; it++ {
- p.Mutate(rs, 50, ct, nil)
+ p.Mutate(rs, 50, ct, nil, nil)
}
for _, c := range p.Calls {
if _, ok := enabledCalls[c.Meta.Name]; !ok {
@@ -225,7 +225,7 @@ func TestNoGenerate(t *testing.T) {
for i := 0; i < tries; i++ {
p := target.Generate(rs, 50, ct)
for it := 0; it < iters/tries; it++ {
- p.Mutate(rs, 50, ct, nil)
+ p.Mutate(rs, 50, ct, nil, nil)
}
for _, c := range p.Calls {
if c.Meta.Attrs.NoGenerate {
diff --git a/prog/size_test.go b/prog/size_test.go
index 6e8a28c4d..ea8b7a58a 100644
--- a/prog/size_test.go
+++ b/prog/size_test.go
@@ -20,7 +20,7 @@ func TestAssignSizeRandom(t *testing.T) {
if data1 := p.Serialize(); !bytes.Equal(data0, data1) {
t.Fatalf("different lens assigned, initial:\n%s\nnew:\n%s\n", data0, data1)
}
- p.Mutate(rs, 10, ct, nil)
+ p.Mutate(rs, 10, ct, nil, nil)
p.Serialize()
for _, call := range p.Calls {
target.assignSizesCall(call)
diff --git a/prog/test/fuzz.go b/prog/test/fuzz.go
index 0d2807f7f..5857ff98d 100644
--- a/prog/test/fuzz.go
+++ b/prog/test/fuzz.go
@@ -53,7 +53,7 @@ func FuzzDeserialize(data []byte) int {
panic(err)
}
}
- p3.Mutate(rand.NewSource(0), 3, fuzzChoiceTable, nil)
+ p3.Mutate(rand.NewSource(0), 3, fuzzChoiceTable, nil, nil)
return 0
}
diff --git a/syz-fuzzer/fuzzer.go b/syz-fuzzer/fuzzer.go
index d281c38c6..cce2b6630 100644
--- a/syz-fuzzer/fuzzer.go
+++ b/syz-fuzzer/fuzzer.go
@@ -40,6 +40,7 @@ type Fuzzer struct {
workQueue *WorkQueue
needPoll chan struct{}
choiceTable *prog.ChoiceTable
+ noMutate map[int]bool
stats [StatCount]uint64
manager *rpctype.RPCClient
target *prog.Target
@@ -275,6 +276,7 @@ func main() {
corpusHashes: make(map[hash.Sig]struct{}),
checkResult: r.CheckResult,
fetchRawCover: *flagRawCover,
+ noMutate: r.NoMutateCalls,
}
gateCallback := fuzzer.useBugFrames(r, *flagProcs)
fuzzer.gate = ipc.NewGate(2**flagProcs, gateCallback)
diff --git a/syz-fuzzer/proc.go b/syz-fuzzer/proc.go
index d19490fc8..a215d81c8 100644
--- a/syz-fuzzer/proc.go
+++ b/syz-fuzzer/proc.go
@@ -92,7 +92,7 @@ func (proc *Proc) loop() {
} else {
// Mutate an existing prog.
p := fuzzerSnapshot.chooseProgram(proc.rnd).Clone()
- p.Mutate(proc.rnd, prog.RecommendedCalls, ct, fuzzerSnapshot.corpus)
+ p.Mutate(proc.rnd, prog.RecommendedCalls, ct, proc.fuzzer.noMutate, fuzzerSnapshot.corpus)
log.Logf(1, "#%v: mutated", proc.pid)
proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatFuzz)
}
@@ -216,7 +216,7 @@ func (proc *Proc) smashInput(item *WorkSmash) {
fuzzerSnapshot := proc.fuzzer.snapshot()
for i := 0; i < 100; i++ {
p := item.p.Clone()
- p.Mutate(proc.rnd, prog.RecommendedCalls, proc.fuzzer.choiceTable, fuzzerSnapshot.corpus)
+ p.Mutate(proc.rnd, prog.RecommendedCalls, proc.fuzzer.choiceTable, proc.fuzzer.noMutate, fuzzerSnapshot.corpus)
log.Logf(1, "#%v: smash mutated", proc.pid)
proc.executeAndCollide(proc.execOpts, p, ProgNormal, StatSmash)
}
diff --git a/syz-manager/rpc.go b/syz-manager/rpc.go
index 397484783..ea91e601c 100644
--- a/syz-manager/rpc.go
+++ b/syz-manager/rpc.go
@@ -109,6 +109,7 @@ func (serv *RPCServer) Connect(a *rpctype.ConnectArgs, r *rpctype.ConnectRes) er
r.DataRaceFrames = bugFrames.dataRaces
r.CoverFilterBitmap = coverBitmap
r.EnabledCalls = serv.cfg.Syscalls
+ r.NoMutateCalls = serv.cfg.NoMutateCalls
r.GitRevision = prog.GitRevision
r.TargetRevision = serv.cfg.Target.Revision
if serv.mgr.rotateCorpus() && serv.rnd.Intn(5) == 0 {
diff --git a/tools/syz-mutate/mutate.go b/tools/syz-mutate/mutate.go
index b6b43f48f..7619acbd9 100644
--- a/tools/syz-mutate/mutate.go
+++ b/tools/syz-mutate/mutate.go
@@ -79,7 +79,7 @@ func main() {
fmt.Fprintf(os.Stderr, "failed to deserialize the program: %v\n", err)
os.Exit(1)
}
- p.Mutate(rs, *flagLen, ct, corpus)
+ p.Mutate(rs, *flagLen, ct, nil, corpus)
}
fmt.Printf("%s\n", p.Serialize())
}
diff --git a/tools/syz-stress/stress.go b/tools/syz-stress/stress.go
index 15847df16..26d17844b 100644
--- a/tools/syz-stress/stress.go
+++ b/tools/syz-stress/stress.go
@@ -104,13 +104,13 @@ func main() {
if *flagGenerate && len(corpus) == 0 || i%4 != 0 {
p = target.Generate(rs, prog.RecommendedCalls, ct)
execute(pid, env, execOpts, p)
- p.Mutate(rs, prog.RecommendedCalls, ct, corpus)
+ p.Mutate(rs, prog.RecommendedCalls, ct, nil, corpus)
execute(pid, env, execOpts, p)
} else {
p = corpus[rnd.Intn(len(corpus))].Clone()
- p.Mutate(rs, prog.RecommendedCalls, ct, corpus)
+ p.Mutate(rs, prog.RecommendedCalls, ct, nil, corpus)
execute(pid, env, execOpts, p)
- p.Mutate(rs, prog.RecommendedCalls, ct, corpus)
+ p.Mutate(rs, prog.RecommendedCalls, ct, nil, corpus)
execute(pid, env, execOpts, p)
}
}