blob: ae3e9421bb923cce6953a7a95fd12d9a21c041fc [file] [log] [blame]
// Copyright 2020 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 executor
import (
"bytes"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"testing"
)
func TestExecutorMistakes(t *testing.T) {
checks := []*struct {
pattern string
suppression string
message string
tests []string
commonOnly bool // only applies to common*.h files
fuzzerOnly bool // only applies to files used during fuzzing
}{
{
pattern: `\)\n\t*(debug|debug_dump_data)\(`,
message: "debug() calls are stripped from C reproducers, this code will break. Use {} around debug() to fix",
commonOnly: true,
tests: []string{
`
if (foo)
debug("foo failed");
`, `
if (x + y)
debug_dump_data(data, len);
`,
},
},
{
pattern: `\) {\n[^\n}]+?\n\t*}\n`,
suppression: "debug|__except",
message: "Don't use single-line compound statements (remove {})",
tests: []string{
`
if (foo) {
bar();
}
`, `
while (x + y) {
foo(a, y);
}
`,
},
},
{
// These are also not properly stripped by pkg/csource.
pattern: `/\*[^{/"]`,
message: "Don't use /* */ block comments. Use // line comments instead",
tests: []string{
`/* C++ comment */`,
},
},
{
pattern: `#define __NR_`,
message: "Don't define syscall __NR_foo constants.\n" +
"These should be guarded by #ifndef __NR_foo, but this is dependent on the host " +
"and may break on other machines (after pkg/csource processing).\n" +
"Define sys_foo constants instead.",
commonOnly: true,
tests: []string{
`
#ifndef __NR_io_uring_setup
#ifdef __alpha__
#define __NR_io_uring_setup 535
#else // !__alpha__
#define __NR_io_uring_setup 425
#endif
#endif // __NR_io_uring_setup
`,
},
},
{
pattern: `//[^\s]`,
suppression: `https?://|//%`,
message: "Add a space after //",
tests: []string{
`//foo`,
},
},
{
// This detects C89-style variable declarations in the beginning of block in a best-effort manner.
// Struct fields look exactly as C89 variable declarations, to filter them out we look for "{"
// at the beginning of the line.
// nolint: lll
pattern: `
{[^{]*
\s+((unsigned )?([A-Z][A-Z0-9_]+|[a-z][a-z0-9_]+)\s*\*?|(struct )?[a-zA-Z][a-zA-Z0-9_]+\*)\s+([a-zA-Z][a-zA-Z0-9_]*(,\s*)?)+;
`,
suppression: `return |goto |va_list |pthread_|zx_`,
message: "Don't use C89 var declarations. Declare vars where they are needed and combine with initialization",
tests: []string{
`
{
int i;
`,
`
{
socklen_t optlen;
`,
`
{
int fd, rv;
`,
`
{
int fd, rv;
`,
`
{
struct nlattr* attr;
`,
`
{
int* i;
`,
`
{
DIR* dp;
`,
},
},
{
pattern: `(fail|exitf)\(".*\\n`,
message: "Don't use \\n in fail/exitf messages",
tests: []string{
`fail("some message with new line\n");`,
},
},
{
pattern: `fail(msg)?\("[^"]*%`,
message: "DON'T",
tests: []string{
`fail("format %s string")`,
`failmsg("format %s string", "format")`,
},
},
{
pattern: `ifn?def\s+SYZ_`,
message: "SYZ_* are always defined, use #if instead of #ifdef",
tests: []string{
`#ifndef SYZ_EXECUTOR_USES_FORK_SERVER`,
`#ifdef SYZ_EXECUTOR_USES_FORK_SERVER`,
},
},
{
// Dynamic memory allocation reduces test reproducibility across
// different libc versions and kernels. Malloc will cause unspecified
// number of additional mmap's at unspecified locations.
// For small objects prefer stack allocations, for larger -- either global
// objects (this may have issues with concurrency), or controlled mmaps.
fuzzerOnly: true,
pattern: `(malloc|calloc|operator new)\(|new [a-zA-Z]`,
message: "Don't use standard allocation functions," +
" they disturb address space and issued syscalls",
suppression: `// `,
tests: []string{
`malloc(10)`,
`malloc(sizeof(int))`,
`calloc(sizeof T, n)`,
`operator new(10)`,
`new int`,
},
},
{
// Exit/_exit do not necessary work (e.g. if fuzzer sets seccomp
// filter that prohibits exit_group). Use doexit instead.
pattern: `\b[_]?exit\(`,
suppression: `doexit\(|syz_exit`,
message: "Don't use [_]exit, use doexit/exitf/fail instead",
tests: []string{
`_exit(1)`,
`exit(FAILURE)`,
},
},
}
for _, check := range checks {
re := regexp.MustCompile(check.pattern)
for _, test := range check.tests {
if !re.MatchString(test) {
t.Fatalf("pattern %q does not match test %q", check.pattern, test)
}
}
}
runnerFiles := regexp.MustCompile(`(executor_runner|conn|shmem|files)\.h`)
for _, file := range executorFiles(t) {
data, err := os.ReadFile(file)
if err != nil {
t.Fatal(err)
}
for _, check := range checks {
if check.commonOnly && !strings.Contains(file, "common") {
continue
}
if check.fuzzerOnly && runnerFiles.MatchString(file) {
continue
}
re := regexp.MustCompile(check.pattern)
supp := regexp.MustCompile(check.suppression)
for _, match := range re.FindAllIndex(data, -1) {
end := match[1] - 1
for end != len(data) && data[end] != '\n' {
end++
}
// Match suppressions against all lines of the match.
start := match[0] - 1
for start != 0 && data[start-1] != '\n' {
start--
}
if check.suppression != "" && supp.Match(data[start:end]) {
continue
}
// Locate the last line of the match, that's where we assume the error is.
start = end - 1
for start != 0 && data[start-1] != '\n' {
start--
}
line := bytes.Count(data[:start], []byte{'\n'}) + 1
t.Errorf("\nexecutor/%v:%v: %v\n%s", file, line, check.message, data[start:end])
}
}
}
}
func executorFiles(t *testing.T) []string {
cc, err := filepath.Glob("*.cc")
if err != nil {
t.Fatal(err)
}
h, err := filepath.Glob("*.h")
if err != nil {
t.Fatal(err)
}
if len(cc) == 0 || len(h) == 0 {
t.Fatal("found no executor files")
}
res := append(cc, h...)
sort.Strings(res)
return res
}