/src/hobbes/fuzz/fuzz_parse_expr.C
Line | Count | Source |
1 | | // Fuzz the lexer and LALR parser via hobbes::cc::readExpr. |
2 | | // |
3 | | // Evaluating Hobbes code is running trusted native code, but *reading* source |
4 | | // text sits on the near side of that trust boundary: lexing and parsing must |
5 | | // be safe on arbitrary bytes. Nothing here is evaluated. |
6 | | // |
7 | | // Reading is not entirely free of compilation, though, and the difference |
8 | | // shows up over a campaign rather than on any one input. Several things are |
9 | | // compiled where they are read: a regex literal becomes a matching function |
10 | | // (makeRegexFn determinizes it and defines the result in the compiler under |
11 | | // a fresh name), a match expression is compiled to a decision tree, and a |
12 | | // `parse {}` block becomes an LALR parser. Each definition stays in the |
13 | | // compiler, and the types it took are interned in the process-wide type |
14 | | // memo (tctorMaps in lang/type.C), which holds a reference of its own to |
15 | | // each of them; nothing removes the definition, and only |
16 | | // compactMTypeMemory() lets go of the types. So a `cc` reused across |
17 | | // inputs, with the memo left alone, grows by everything it has ever read: |
18 | | // about 168KB per read of the twenty character regex in OSS-Fuzz testcase |
19 | | // 4850385077207040, and, on a corpus the fuzzer had grown from the seeds |
20 | | // (3.7k inputs, mostly near-misses), about 270KB per input in the compiler |
21 | | // and 400KB per input in the memo -- measured by reading them all and then |
22 | | // destroying the compiler and compacting the memo, which gave every byte |
23 | | // back. Left alone, such a campaign runs the process out of memory. That is |
24 | | // what the testcase reports: an out-of-memory that ClusterFuzz could not |
25 | | // minimize, because no single input causes it. |
26 | | // |
27 | | // So two things are done periodically, one for each half. The type memo is |
28 | | // compacted every few dozen inputs, as the decoder harnesses do; that gives |
29 | | // back the memo's half and costs about as much as a parse. The compiler is |
30 | | // replaced every thousand inputs; that gives back its half, and costs what |
31 | | // it takes to build one. Neither alone is enough: each leaves the other half |
32 | | // growing without bound. An earlier version replaced the compiler only after |
33 | | // a couple of thousand inputs containing a quote character, on the reasoning |
34 | | // that only a regex literal could have grown it; matches and grammars grow it |
35 | | // just as well, and on the corpus above that policy let it reach a gigabyte. |
36 | | // Counts of inputs are a coarse stand-in for how much has accumulated, but a |
37 | | // portable and predictable one -- resident size is not, because freeing |
38 | | // memory does not hand it back to the operating system, so a harness that |
39 | | // watched RSS rebuilt the compiler on every input once it first went over. |
40 | | // |
41 | | // Building a compiler is the one expensive thing this harness does: it |
42 | | // compiles the bootstrap modules, about half a second unsanitized and several |
43 | | // seconds under ASan. That matters under AFL++, whose driver forks a fresh |
44 | | // process for the run and times every input against a fixed budget (OSS-Fuzz |
45 | | // runs it with -t 5000+): a compiler built on the first input is charged to |
46 | | // that input, and the seed corpus dry run then fails the build with a timeout |
47 | | // before fuzzing starts. So the first compiler is built in |
48 | | // LLVMFuzzerInitialize, which every driver runs before the fork server and |
49 | | // before any input is timed. Replacements still land inside a timed input; |
50 | | // a thousand inputs between them keeps them rare. |
51 | | |
52 | | #include <hobbes/hobbes.H> |
53 | | |
54 | | #include <cstddef> |
55 | | #include <cstdint> |
56 | | #include <exception> |
57 | | #include <memory> |
58 | | #include <string> |
59 | | |
60 | | // Hobbes reclaims evaluation memory by resetting an arena at the end of a |
61 | | // transaction, not by releasing objects one at a time -- so a per-allocation |
62 | | // leak audit reports transaction-scoped data as lost by design. For this |
63 | | // target that means two families of expected reports: allocations made by |
64 | | // grammar actions during a parse (yyparse and the paths into it), which the |
65 | | // compiler's bootstrap parse strands once per cc, and a little that LLVM |
66 | | // keeps for itself. The .options file ships detect_leaks=0 for exactly this |
67 | | // reason. ClusterFuzz's libFuzzer and AFL engines apply that file when they |
68 | | // replay a testcase; its honggfuzz engine does not, so on the honggfuzz job |
69 | | // every replay ends in an at-exit leak report. A leak report is a crash, and |
70 | | // for a Stack-overflow testcase progression accepts any crash as "still |
71 | | // reproduces" (it compares crash state for the other types, but a stack |
72 | | // overflow's state is too unstable to compare), so a fixed stack overflow |
73 | | // found by honggfuzz stays open for as long as the bootstrap leaks are |
74 | | // reported. OSS-Fuzz 549863810 and 556791547 sat that way. |
75 | | // |
76 | | // An earlier version of this file named the expected stacks in |
77 | | // __lsan_default_suppressions, to keep the checker alive for everything |
78 | | // else. That does not work where it is needed: ClusterFuzz runs every target |
79 | | // with symbolize=0 and symbolizes offline, and with symbolization off LSan |
80 | | // has no function names to match `leak:yyparse` against, so the suppressions |
81 | | // matched locally and never on a bot. Turning the checker off is the only |
82 | | // form of the policy that is binding whatever the engine or environment; |
83 | | // nothing is given up, since the shipped .options already turns it off for |
84 | | // the engines that honor it. |
85 | 0 | extern "C" int __lsan_is_turned_off() { |
86 | 0 | return 1; |
87 | 0 | } |
88 | | |
89 | | namespace { |
90 | | |
91 | | const unsigned long inputsPerCompaction = 64; |
92 | | const unsigned long inputsPerCompiler = 1024; |
93 | | |
94 | | // The first compiler is built in the slot's initializer rather than on first |
95 | | // use. Constructing a cc also constructs the LLVM statics it depends on, and |
96 | | // at exit everything static is destroyed in reverse order of construction: a |
97 | | // slot that was registered empty and filled afterwards would be destroyed |
98 | | // after those statics, and the cc inside it would tear down against an LLVM |
99 | | // context that was already gone. |
100 | 6.95k | std::unique_ptr<hobbes::cc>& compilerSlot() { |
101 | 6.95k | static std::unique_ptr<hobbes::cc> c(new hobbes::cc()); |
102 | 6.95k | return c; |
103 | 6.95k | } |
104 | | |
105 | 6.95k | hobbes::cc& compiler() { |
106 | 6.95k | std::unique_ptr<hobbes::cc>& c = compilerSlot(); |
107 | 6.95k | if (!c) { |
108 | 6 | c = std::unique_ptr<hobbes::cc>(new hobbes::cc()); |
109 | 6 | } |
110 | 6.95k | return *c; |
111 | 6.95k | } |
112 | | |
113 | 6.95k | void reclaimPeriodically() { |
114 | 6.95k | static unsigned long read = 0; |
115 | 6.95k | ++read; |
116 | 6.95k | if (read % inputsPerCompiler == 0) { |
117 | 6 | compilerSlot().reset(); |
118 | 6 | } |
119 | 6.95k | if (read % inputsPerCompaction == 0) { |
120 | | // after the compiler is let go, where that happened, so that the types |
121 | | // only it was holding are released too |
122 | 108 | hobbes::compactMTypeMemory(); |
123 | 108 | } |
124 | 6.95k | } |
125 | | |
126 | | } // namespace |
127 | | |
128 | 4 | extern "C" int LLVMFuzzerInitialize(int*, char***) { |
129 | 4 | compiler(); |
130 | 4 | return 0; |
131 | 4 | } |
132 | | |
133 | 17.7k | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { |
134 | 17.7k | std::string src(reinterpret_cast<const char*>(data), size); |
135 | 17.7k | try { |
136 | 17.7k | compiler().readExpr(src); |
137 | 17.7k | } catch (const std::exception&) { |
138 | | // rejecting malformed source is the expected behavior |
139 | 14.2k | } |
140 | 17.7k | reclaimPeriodically(); |
141 | 17.7k | return 0; |
142 | 17.7k | } |