llvm/clang/utils/modfuzz.py

#!/usr/bin/env python

# To use:
#  1) Update the 'decls' list below with your fuzzing configuration.
#  2) Run with the clang binary as the command-line argument.

from __future__ import absolute_import, division, print_function
import random
import subprocess
import sys
import os

clang = sys.argv[1]
none_opts = 0.3


class Decl(object):
    def __init__(self, text, depends=[], provides=[], conflicts=[]):
        self.text = text
        self.depends = depends
        self.provides = provides
        self.conflicts = conflicts

    def valid(self, model):
        for i in self.depends:
            if i not in model.decls:
                return False
        for i in self.conflicts:
            if i in model.decls:
                return False
        return True

    def apply(self, model, name):
        for i in self.provides:
            model.decls[i] = True
        model.source += self.text % {"name": name}


decls = [
    Decl("struct X { int n; };\n", provides=["X"], conflicts=["X"]),
    Decl('static_assert(X{.n=1}.n == 1, "");\n', depends=["X"]),
    Decl("X %(name)s;\n", depends=["X"]),
]


class FS(object):
    def __init__(self):
        self.fs = {}
        self.prevfs = {}

    def write(self, path, contents):
        self.fs[path] = contents

    def done(self):
        for f, s in self.fs.items():
            if self.prevfs.get(f) != s:
                f = file(f, "w")
                f.write(s)
                f.close()

        for f in self.prevfs:
            if f not in self.fs:
                os.remove(f)

        self.prevfs, self.fs = self.fs, {}


fs = FS()


class CodeModel(object):
    def __init__(self):
        self.source = ""
        self.modules = {}
        self.decls = {}
        self.i = 0

    def make_name(self):
        self.i += 1
        return "n" + str(self.i)

    def fails(self):
        fs.write(
            "module.modulemap",
            "".join(
                'module %s { header "%s.h" export * }\n' % (m, m)
                for m in self.modules.keys()
            ),
        )

        for m, (s, _) in self.modules.items():
            fs.write("%s.h" % m, s)

        fs.write("main.cc", self.source)
        fs.done()

        return (
            subprocess.call(
                [clang, "-std=c++11", "-c", "-fmodules", "main.cc", "-o", "/dev/null"]
            )
            != 0
        )


def generate():
    model = CodeModel()
    m = []

    try:
        for d in mutations(model):
            d(model)
            m.append(d)
        if not model.fails():
            return
    except KeyboardInterrupt:
        print()
        return True

    sys.stdout.write("\nReducing:\n")
    sys.stdout.flush()

    try:
        while True:
            assert m, "got a failure with no steps; broken clang binary?"
            i = random.choice(list(range(len(m))))
            x = m[0:i] + m[i + 1 :]
            m2 = CodeModel()
            for d in x:
                d(m2)
            if m2.fails():
                m = x
                model = m2
            else:
                sys.stdout.write(".")
                sys.stdout.flush()
    except KeyboardInterrupt:
        # FIXME: Clean out output directory first.
        model.fails()
        return model


def choose(options):
    while True:
        i = int(random.uniform(0, len(options) + none_opts))
        if i >= len(options):
            break
        yield options[i]


def mutations(model):
    options = [create_module, add_top_level_decl]
    for opt in choose(options):
        yield opt(model, options)


def create_module(model, options):
    n = model.make_name()

    def go(model):
        model.modules[n] = (model.source, model.decls)
        (model.source, model.decls) = ("", {})

    options += [lambda model, options: add_import(model, options, n)]
    return go


def add_top_level_decl(model, options):
    n = model.make_name()
    d = random.choice([decl for decl in decls if decl.valid(model)])

    def go(model):
        if not d.valid(model):
            return
        d.apply(model, n)

    return go


def add_import(model, options, module_name):
    def go(model):
        if module_name in model.modules:
            model.source += '#include "%s.h"\n' % module_name
            model.decls.update(model.modules[module_name][1])

    return go


sys.stdout.write("Finding bug: ")
while True:
    if generate():
        break
    sys.stdout.write(".")
    sys.stdout.flush()