Skip to content

Commit d448004

Browse files
syz-manager: pkg: skip seeds with mismatching requirements in fuzzing mode
When running in the test mode, syz-manager already ignores tests that have arch requirements mismatching the target arch. Because the same tests are also used as seeds in the fuzzing mode, skip them likewise, instead of reporting errors if they contain arch-specific syscalls. The code and tests for parsing the requirements is moved from pkg/runtest to pkg/manager.
1 parent 330db27 commit d448004

File tree

4 files changed

+124
-89
lines changed

4 files changed

+124
-89
lines changed

pkg/manager/seeds.go

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
package manager
55

66
import (
7+
"bufio"
8+
"bytes"
9+
"errors"
710
"fmt"
811
"math/rand"
912
"os"
1013
"path/filepath"
1114
"runtime"
15+
"strings"
1216
"sync"
1317
"time"
1418

@@ -94,13 +98,19 @@ func LoadSeeds(cfg *mgrconfig.Config, immutable bool) Seeds {
9498
close(inputs)
9599
}()
96100
brokenSeeds := 0
101+
skippedSeeds := 0
97102
var brokenCorpus []string
98103
var candidates []fuzzer.Candidate
99104
for inp := range outputs {
100105
if inp.Prog == nil {
101106
if inp.IsSeed {
102-
brokenSeeds++
103-
log.Logf(0, "seed %s is broken: %s", inp.Path, inp.Err)
107+
if errors.Is(inp.Err, ErrSkippedTest) {
108+
skippedSeeds++
109+
log.Logf(2, "seed %s is skipped: %s", inp.Path, inp.Err)
110+
} else {
111+
brokenSeeds++
112+
log.Logf(0, "seed %s is broken: %s", inp.Path, inp.Err)
113+
}
104114
} else {
105115
brokenCorpus = append(brokenCorpus, inp.Key)
106116
}
@@ -123,6 +133,9 @@ func LoadSeeds(cfg *mgrconfig.Config, immutable bool) Seeds {
123133
if len(brokenCorpus)+brokenSeeds != 0 {
124134
log.Logf(0, "broken programs in the corpus: %v, broken seeds: %v", len(brokenCorpus), brokenSeeds)
125135
}
136+
if skippedSeeds != 0 {
137+
log.Logf(0, "skipped %v seeds", skippedSeeds)
138+
}
126139
if !immutable {
127140
// This needs to be done outside of the loop above to not race with corpusDB reads.
128141
for _, sig := range brokenCorpus {
@@ -179,28 +192,96 @@ func versionToFlags(version uint64) fuzzer.ProgFlags {
179192
}
180193

181194
func ParseSeed(target *prog.Target, data []byte) (*prog.Prog, error) {
182-
return parseProg(target, data, prog.NonStrict)
195+
p, _, err := parseProg(target, data, prog.NonStrict, nil)
196+
return p, err
197+
}
198+
199+
func ParseSeedWithRequirements(target *prog.Target, data []byte, reqs map[string]bool) (
200+
*prog.Prog, map[string]bool, error) {
201+
return parseProg(target, data, prog.Strict, reqs)
183202
}
184203

185-
func ParseSeedStrict(target *prog.Target, data []byte) (*prog.Prog, error) {
186-
return parseProg(target, data, prog.Strict)
204+
func parseRequires(data []byte) map[string]bool {
205+
requires := make(map[string]bool)
206+
for s := bufio.NewScanner(bytes.NewReader(data)); s.Scan(); {
207+
const prefix = "# requires:"
208+
line := s.Text()
209+
if !strings.HasPrefix(line, prefix) {
210+
continue
211+
}
212+
for _, req := range strings.Fields(line[len(prefix):]) {
213+
positive := true
214+
if req[0] == '-' {
215+
positive = false
216+
req = req[1:]
217+
}
218+
requires[req] = positive
219+
}
220+
}
221+
return requires
222+
}
223+
224+
func checkArch(requires map[string]bool, arch string) bool {
225+
for req, positive := range requires {
226+
const prefix = "arch="
227+
if strings.HasPrefix(req, prefix) &&
228+
arch != req[len(prefix):] == positive {
229+
return false
230+
}
231+
}
232+
return true
187233
}
188234

189-
func parseProg(target *prog.Target, data []byte, mode prog.DeserializeMode) (*prog.Prog, error) {
235+
func MatchRequirements(props, requires map[string]bool) bool {
236+
for req, positive := range requires {
237+
if positive {
238+
if !props[req] {
239+
return false
240+
}
241+
continue
242+
}
243+
matched := true
244+
for _, req1 := range strings.Split(req, ",") {
245+
if !props[req1] {
246+
matched = false
247+
}
248+
}
249+
if matched {
250+
return false
251+
}
252+
}
253+
return true
254+
}
255+
256+
var ErrSkippedTest = errors.New("skipped test based on constraints")
257+
258+
func parseProg(target *prog.Target, data []byte, mode prog.DeserializeMode, reqs map[string]bool) (
259+
*prog.Prog, map[string]bool, error) {
260+
properties := parseRequires(data)
261+
// Need to check requirements early, as some programs may fail to deserialize
262+
// on some arches due to missing syscalls. We also do not want to parse tests
263+
// that are marked as 'manual'.
264+
if !checkArch(properties, target.Arch) || !MatchRequirements(properties, reqs) {
265+
var pairs []string
266+
for k, v := range properties {
267+
pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
268+
}
269+
return nil, properties, fmt.Errorf("%w: %s", ErrSkippedTest, strings.Join(pairs, ", "))
270+
}
190271
p, err := target.Deserialize(data, mode)
191272
if err != nil {
192-
return nil, err
273+
return nil, nil, err
193274
}
194275
if len(p.Calls) > prog.MaxCalls {
195-
return nil, fmt.Errorf("longer than %d calls (%d)", prog.MaxCalls, len(p.Calls))
276+
return nil, nil, fmt.Errorf("longer than %d calls (%d)", prog.MaxCalls, len(p.Calls))
196277
}
197278
// For some yet unknown reasons, programs with fail_nth > 0 may sneak in. Ignore them.
198279
for _, call := range p.Calls {
199280
if call.Props.FailNth > 0 {
200-
return nil, fmt.Errorf("input has fail_nth > 0")
281+
return nil, nil, fmt.Errorf("input has fail_nth > 0")
201282
}
202283
}
203-
return p, nil
284+
return p, properties, nil
204285
}
205286

206287
type FilteredCandidates struct {

pkg/manager/seeds_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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 manager
5+
6+
import (
7+
"testing"
8+
)
9+
10+
func TestRequires(t *testing.T) {
11+
{
12+
requires := parseRequires([]byte("# requires: manual arch=amd64"))
13+
if !checkArch(requires, "amd64") {
14+
t.Fatalf("amd64 does not pass check")
15+
}
16+
if checkArch(requires, "riscv64") {
17+
t.Fatalf("riscv64 passes check")
18+
}
19+
}
20+
{
21+
requires := parseRequires([]byte("# requires: -arch=arm64 manual -arch=riscv64"))
22+
if !checkArch(requires, "amd64") {
23+
t.Fatalf("amd64 does not pass check")
24+
}
25+
if checkArch(requires, "riscv64") {
26+
t.Fatalf("riscv64 passes check")
27+
}
28+
}
29+
}

pkg/runtest/run.go

Lines changed: 4 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"bufio"
1515
"bytes"
1616
"context"
17+
"errors"
1718
"fmt"
1819
"os"
1920
"path/filepath"
@@ -305,13 +306,10 @@ func parseProg(target *prog.Target, dir, filename string, requires map[string]bo
305306
if err != nil {
306307
return nil, nil, nil, fmt.Errorf("failed to read %v: %w", filename, err)
307308
}
308-
properties := parseRequires(data)
309-
// Need to check arch requirement early as some programs
310-
// may fail to deserialize on some arches due to missing syscalls.
311-
if !checkArch(properties, target.Arch) || !match(properties, requires) {
309+
p, properties, err := manager.ParseSeedWithRequirements(target, data, requires)
310+
if errors.Is(err, manager.ErrSkippedTest) {
312311
return nil, nil, nil, nil
313312
}
314-
p, err := manager.ParseSeedStrict(target, data)
315313
if err != nil {
316314
return nil, nil, nil, fmt.Errorf("failed to deserialize %v: %w", filename, err)
317315
}
@@ -366,42 +364,11 @@ func parseProg(target *prog.Target, dir, filename string, requires map[string]bo
366364
return p, properties, info, nil
367365
}
368366

369-
func parseRequires(data []byte) map[string]bool {
370-
requires := make(map[string]bool)
371-
for s := bufio.NewScanner(bytes.NewReader(data)); s.Scan(); {
372-
const prefix = "# requires:"
373-
line := s.Text()
374-
if !strings.HasPrefix(line, prefix) {
375-
continue
376-
}
377-
for _, req := range strings.Fields(line[len(prefix):]) {
378-
positive := true
379-
if req[0] == '-' {
380-
positive = false
381-
req = req[1:]
382-
}
383-
requires[req] = positive
384-
}
385-
}
386-
return requires
387-
}
388-
389-
func checkArch(requires map[string]bool, arch string) bool {
390-
for req, positive := range requires {
391-
const prefix = "arch="
392-
if strings.HasPrefix(req, prefix) &&
393-
arch != req[len(prefix):] == positive {
394-
return false
395-
}
396-
}
397-
return true
398-
}
399-
400367
func (ctx *Context) produceTest(req *runRequest, name string, properties,
401368
requires map[string]bool, results *flatrpc.ProgInfo) {
402369
req.name = name
403370
req.results = results
404-
if !match(properties, requires) {
371+
if !manager.MatchRequirements(properties, requires) {
405372
req.skip = "excluded by constraints"
406373
}
407374
ctx.createTest(req)
@@ -445,27 +412,6 @@ func (ctx *Context) submit(req *runRequest) {
445412
req.executor.Submit(req.Request)
446413
}
447414

448-
func match(props, requires map[string]bool) bool {
449-
for req, positive := range requires {
450-
if positive {
451-
if !props[req] {
452-
return false
453-
}
454-
continue
455-
}
456-
matched := true
457-
for _, req1 := range strings.Split(req, ",") {
458-
if !props[req1] {
459-
matched = false
460-
}
461-
}
462-
if matched {
463-
return false
464-
}
465-
}
466-
return true
467-
}
468-
469415
func (ctx *Context) createSyzTest(p *prog.Prog, sandbox string, threaded, cov bool) (*runRequest, error) {
470416
var opts flatrpc.ExecOpts
471417
sandboxFlags, err := flatrpc.SandboxToFlags(sandbox)

pkg/runtest/run_test.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -589,24 +589,3 @@ func TestParsing(t *testing.T) {
589589
}
590590
}
591591
}
592-
593-
func TestRequires(t *testing.T) {
594-
{
595-
requires := parseRequires([]byte("# requires: manual arch=amd64"))
596-
if !checkArch(requires, "amd64") {
597-
t.Fatalf("amd64 does not pass check")
598-
}
599-
if checkArch(requires, "riscv64") {
600-
t.Fatalf("riscv64 passes check")
601-
}
602-
}
603-
{
604-
requires := parseRequires([]byte("# requires: -arch=arm64 manual -arch=riscv64"))
605-
if !checkArch(requires, "amd64") {
606-
t.Fatalf("amd64 does not pass check")
607-
}
608-
if checkArch(requires, "riscv64") {
609-
t.Fatalf("riscv64 passes check")
610-
}
611-
}
612-
}

0 commit comments

Comments
 (0)