xref: /llvm-project/clang/utils/modfuzz.py (revision dd3c26a045c081620375a878159f536758baba6e)
1#!/usr/bin/env python
2
3# To use:
4#  1) Update the 'decls' list below with your fuzzing configuration.
5#  2) Run with the clang binary as the command-line argument.
6
7from __future__ import absolute_import, division, print_function
8import random
9import subprocess
10import sys
11import os
12
13clang = sys.argv[1]
14none_opts = 0.3
15
16
17class Decl(object):
18    def __init__(self, text, depends=[], provides=[], conflicts=[]):
19        self.text = text
20        self.depends = depends
21        self.provides = provides
22        self.conflicts = conflicts
23
24    def valid(self, model):
25        for i in self.depends:
26            if i not in model.decls:
27                return False
28        for i in self.conflicts:
29            if i in model.decls:
30                return False
31        return True
32
33    def apply(self, model, name):
34        for i in self.provides:
35            model.decls[i] = True
36        model.source += self.text % {"name": name}
37
38
39decls = [
40    Decl("struct X { int n; };\n", provides=["X"], conflicts=["X"]),
41    Decl('static_assert(X{.n=1}.n == 1, "");\n', depends=["X"]),
42    Decl("X %(name)s;\n", depends=["X"]),
43]
44
45
46class FS(object):
47    def __init__(self):
48        self.fs = {}
49        self.prevfs = {}
50
51    def write(self, path, contents):
52        self.fs[path] = contents
53
54    def done(self):
55        for f, s in self.fs.items():
56            if self.prevfs.get(f) != s:
57                f = file(f, "w")
58                f.write(s)
59                f.close()
60
61        for f in self.prevfs:
62            if f not in self.fs:
63                os.remove(f)
64
65        self.prevfs, self.fs = self.fs, {}
66
67
68fs = FS()
69
70
71class CodeModel(object):
72    def __init__(self):
73        self.source = ""
74        self.modules = {}
75        self.decls = {}
76        self.i = 0
77
78    def make_name(self):
79        self.i += 1
80        return "n" + str(self.i)
81
82    def fails(self):
83        fs.write(
84            "module.modulemap",
85            "".join(
86                'module %s { header "%s.h" export * }\n' % (m, m)
87                for m in self.modules.keys()
88            ),
89        )
90
91        for m, (s, _) in self.modules.items():
92            fs.write("%s.h" % m, s)
93
94        fs.write("main.cc", self.source)
95        fs.done()
96
97        return (
98            subprocess.call(
99                [clang, "-std=c++11", "-c", "-fmodules", "main.cc", "-o", "/dev/null"]
100            )
101            != 0
102        )
103
104
105def generate():
106    model = CodeModel()
107    m = []
108
109    try:
110        for d in mutations(model):
111            d(model)
112            m.append(d)
113        if not model.fails():
114            return
115    except KeyboardInterrupt:
116        print()
117        return True
118
119    sys.stdout.write("\nReducing:\n")
120    sys.stdout.flush()
121
122    try:
123        while True:
124            assert m, "got a failure with no steps; broken clang binary?"
125            i = random.choice(list(range(len(m))))
126            x = m[0:i] + m[i + 1 :]
127            m2 = CodeModel()
128            for d in x:
129                d(m2)
130            if m2.fails():
131                m = x
132                model = m2
133            else:
134                sys.stdout.write(".")
135                sys.stdout.flush()
136    except KeyboardInterrupt:
137        # FIXME: Clean out output directory first.
138        model.fails()
139        return model
140
141
142def choose(options):
143    while True:
144        i = int(random.uniform(0, len(options) + none_opts))
145        if i >= len(options):
146            break
147        yield options[i]
148
149
150def mutations(model):
151    options = [create_module, add_top_level_decl]
152    for opt in choose(options):
153        yield opt(model, options)
154
155
156def create_module(model, options):
157    n = model.make_name()
158
159    def go(model):
160        model.modules[n] = (model.source, model.decls)
161        (model.source, model.decls) = ("", {})
162
163    options += [lambda model, options: add_import(model, options, n)]
164    return go
165
166
167def add_top_level_decl(model, options):
168    n = model.make_name()
169    d = random.choice([decl for decl in decls if decl.valid(model)])
170
171    def go(model):
172        if not d.valid(model):
173            return
174        d.apply(model, n)
175
176    return go
177
178
179def add_import(model, options, module_name):
180    def go(model):
181        if module_name in model.modules:
182            model.source += '#include "%s.h"\n' % module_name
183            model.decls.update(model.modules[module_name][1])
184
185    return go
186
187
188sys.stdout.write("Finding bug: ")
189while True:
190    if generate():
191        break
192    sys.stdout.write(".")
193    sys.stdout.flush()
194