1# Run a fuzz test to verify robustness against corrupted/malicious data.
2
3import sys
4import time
5import zipfile
6import random
7import subprocess
8
9Import("env", "malloc_env")
10
11def set_pkgname(src, dst, pkgname):
12    data = open(str(src)).read()
13    placeholder = '// package name placeholder'
14    assert placeholder in data
15    data = data.replace(placeholder, 'package %s;' % pkgname)
16    open(str(dst), 'w').write(data)
17
18# We want both pointer and static versions of the AllTypes message
19# Prefix them with package name.
20env.Command("alltypes_static.proto", "#alltypes/alltypes.proto",
21            lambda target, source, env: set_pkgname(source[0], target[0], 'alltypes_static'))
22env.Command("alltypes_pointer.proto", "#alltypes/alltypes.proto",
23            lambda target, source, env: set_pkgname(source[0], target[0], 'alltypes_pointer'))
24
25env.NanopbProto(["alltypes_pointer", "alltypes_pointer.options"])
26env.NanopbProto(["alltypes_static", "alltypes_static.options"])
27
28# Do the same for proto3 versions
29env.Command("alltypes_proto3_static.proto", "#alltypes_proto3/alltypes.proto",
30            lambda target, source, env: set_pkgname(source[0], target[0], 'alltypes_proto3_static'))
31env.Command("alltypes_proto3_pointer.proto", "#alltypes_proto3/alltypes.proto",
32            lambda target, source, env: set_pkgname(source[0], target[0], 'alltypes_proto3_pointer'))
33
34env.NanopbProto(["alltypes_proto3_pointer", "alltypes_proto3_pointer.options"])
35env.NanopbProto(["alltypes_proto3_static", "alltypes_proto3_static.options"])
36
37# And also a callback version
38env.Command("alltypes_callback.proto", "#alltypes/alltypes.proto",
39            lambda target, source, env: set_pkgname(source[0], target[0], 'alltypes_callback'))
40env.NanopbProto(["alltypes_callback", "alltypes_callback.options"])
41
42common_objs = [env.Object("random_data.c"),
43               env.Object("validation.c"),
44               env.Object("flakystream.c"),
45               env.Object("alltypes_pointer.pb.c"),
46               env.Object("alltypes_static.pb.c"),
47               env.Object("alltypes_callback.pb.c"),
48               env.Object("alltypes_proto3_pointer.pb.c"),
49               env.Object("alltypes_proto3_static.pb.c"),
50                "$COMMON/malloc_wrappers.o"]
51objs_malloc = ["$COMMON/pb_encode_with_malloc.o",
52               "$COMMON/pb_decode_with_malloc.o",
53               "$COMMON/pb_common_with_malloc.o"] + common_objs
54objs_static = ["$COMMON/pb_encode.o",
55               "$COMMON/pb_decode.o",
56               "$COMMON/pb_common.o"] + common_objs
57
58fuzz = malloc_env.Program(["fuzztest.c"] + objs_malloc)
59
60# Run the stand-alone fuzz tester
61seed = int(time.time())
62if env.get('EMBEDDED'):
63    iterations = 100
64else:
65    iterations = 1000
66env.RunTest(fuzz, ARGS = [str(seed), str(iterations)])
67
68generate_message = malloc_env.Program(["generate_message.c"] + objs_static)
69
70# Test the message generator
71env.RunTest(generate_message, ARGS = [str(seed)])
72env.RunTest("generate_message.output.fuzzed", [fuzz, "generate_message.output"])
73
74# Run against the latest corpus from ossfuzz
75# This allows quick testing against regressions and also lets us more
76# completely test slow embedded targets. To reduce runtime, only a subset
77# of the corpus is fuzzed each time.
78def run_against_corpus(target, source, env):
79    corpus = zipfile.ZipFile(str(source[1]), 'r')
80    count = 0
81    args = [str(source[0])]
82
83    if "TEST_RUNNER" in env:
84        args = [env["TEST_RUNNER"]] + args
85
86    if "FUZZTEST_CORPUS_SAMPLESIZE" in env:
87        samplesize = int(env["FUZZTEST_CORPUS_SAMPLESIZE"])
88    elif env.get('EMBEDDED'):
89        samplesize = 100
90    else:
91        samplesize = 4096
92
93    files = [n for n in corpus.namelist() if not n.endswith('/')]
94    files = random.sample(files, min(samplesize, len(files)))
95    for filename in files:
96        sys.stdout.write("Fuzzing: %5d/%5d: %-40.40s\r" % (count, len(files), filename))
97        sys.stdout.flush()
98
99        count += 1
100
101        maxsize = env.get('CPPDEFINES', {}).get('FUZZTEST_BUFSIZE', 256*1024)
102        data_in = corpus.read(filename)[:maxsize]
103
104        try:
105            process = subprocess.Popen(args, stdin=subprocess.PIPE,
106                                       stdout=subprocess.PIPE, stderr=subprocess.PIPE)
107            stdout, stderr = process.communicate(input = data_in)
108            result = process.wait()
109        except OSError as e:
110            if e.errno == 22:
111                print("Warning: OSError 22 when running with input " + filename)
112                result = process.wait()
113            else:
114                raise
115
116        if result != 0:
117            stdout += stderr
118            print(stdout)
119            print('\033[31m[FAIL]\033[0m   Program ' + str(args) + ' returned ' + str(result) + ' with input ' + filename + ' from ' + str(source[1]))
120            return result
121
122    open(str(target[0]), 'w').write(str(count))
123    print('\033[32m[ OK ]\033[0m   Ran ' + str(args) + " against " + str(source[1]) + " (" + str(count) + " entries)")
124
125env.Command("corpus.zip.fuzzed", [fuzz, "corpus.zip"], run_against_corpus)
126env.Command("regressions.zip.fuzzed", [fuzz, "regressions.zip"], run_against_corpus)
127
128# Build separate fuzzers for each test case.
129# Having them separate speeds up control flow based fuzzer engines.
130# These are mainly used by oss-fuzz project.
131env_proto2_static = env.Clone()
132env_proto2_static.Append(CPPDEFINES = {'FUZZTEST_PROTO2_STATIC': '1'})
133env_proto2_static.Program("fuzztest_proto2_static",
134    [env_proto2_static.Object("fuzztest_proto2_static.o", "fuzztest.c")] + objs_static)
135
136env_proto2_pointer = malloc_env.Clone()
137env_proto2_pointer.Append(CPPDEFINES = {'FUZZTEST_PROTO2_POINTER': '1'})
138env_proto2_pointer.Program("fuzztest_proto2_pointer",
139    [env_proto2_pointer.Object("fuzztest_proto2_pointer.o", "fuzztest.c")] + objs_malloc)
140
141env_proto3_static = env.Clone()
142env_proto3_static.Append(CPPDEFINES = {'FUZZTEST_PROTO3_STATIC': '1'})
143env_proto3_static.Program("fuzztest_proto3_static",
144    [env_proto3_static.Object("fuzztest_proto3_static.o", "fuzztest.c")] + objs_static)
145
146env_proto3_pointer = malloc_env.Clone()
147env_proto3_pointer.Append(CPPDEFINES = {'FUZZTEST_PROTO3_POINTER': '1'})
148env_proto3_pointer.Program("fuzztest_proto3_pointer",
149    [env_proto3_pointer.Object("fuzztest_proto3_pointer.o", "fuzztest.c")] + objs_malloc)
150
151env_io_errors = malloc_env.Clone()
152env_io_errors.Append(CPPDEFINES = {'FUZZTEST_IO_ERRORS': '1'})
153env_io_errors.Program("fuzztest_io_errors",
154    [env_io_errors.Object("fuzztest_io_errors.o", "fuzztest.c")] + objs_malloc)
155
156