Skip to content

Commit 6b99d14

Browse files
authored
Better fuzzing (#1735)
* Recombine function pieces after randomly generating them, by creating copies and moving them around. This gives a realistic probability to seeing duplicate expressions, which some optimizations look for, which otherwise the fuzzer would have almost never reached. * Mutate function pieces after recombination, giving not only perfect duplicates but also near-duplicates. These operations take into account the type, but not the nesting and uniqueness of labels, so we fix that up afterwards (when something is broken, we replace it with something trivial).
1 parent ea158f2 commit 6b99d14

File tree

3 files changed

+991
-1118
lines changed

3 files changed

+991
-1118
lines changed

check.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def run_wasm_reduce_tests():
313313
before = os.stat('a.wasm').st_size
314314
run_command(WASM_REDUCE + ['a.wasm', '--command=%s b.wasm --fuzz-exec' % WASM_OPT[0], '-t', 'b.wasm', '-w', 'c.wasm'])
315315
after = os.stat('c.wasm').st_size
316-
assert after < 0.333 * before, [before, after]
316+
assert after < 0.6 * before, [before, after]
317317

318318

319319
def run_spec_tests():

src/tools/fuzzing.h

Lines changed: 189 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ high chance for set at start of loop
2626
*/
2727

2828
#include <wasm-builder.h>
29+
#include <ir/find_all.h>
2930
#include <ir/literal-utils.h>
31+
#include <ir/manipulation.h>
32+
#include <ir/utils.h>
3033

3134
namespace wasm {
3235

@@ -392,11 +395,18 @@ class TranslateToFuzzReader {
392395
} else {
393396
func->body = make(bodyType);
394397
}
398+
// Recombinations create duplicate code patterns.
399+
recombine(func);
400+
// Mutations add random small changes, which can subtly break duplicate code
401+
// patterns.
402+
mutate(func);
403+
// TODO: liveness operations on gets, with some prob alter a get to one with
404+
// more possible sets
405+
// Recombination, mutation, etc. can break validation; fix things up after.
406+
fixLabels(func);
407+
// Add hang limit checks after all other operations on the function body.
395408
if (HANG_LIMIT > 0) {
396-
func->body = builder.makeSequence(
397-
makeHangLimitCheck(),
398-
func->body
399-
);
409+
addHangLimitChecks(func);
400410
}
401411
assert(breakableStack.empty());
402412
assert(hangStack.empty());
@@ -420,6 +430,181 @@ class TranslateToFuzzReader {
420430
return func;
421431
}
422432

433+
void addHangLimitChecks(Function* func) {
434+
// loop limit
435+
FindAll<Loop> loops(func->body);
436+
for (auto* loop : loops.list) {
437+
loop->body = builder.makeSequence(
438+
makeHangLimitCheck(),
439+
loop->body
440+
);
441+
}
442+
// recursion limit
443+
func->body = builder.makeSequence(
444+
makeHangLimitCheck(),
445+
func->body
446+
);
447+
}
448+
449+
void recombine(Function* func) {
450+
// Don't always do this.
451+
if (oneIn(2)) return;
452+
// First, scan and group all expressions by type.
453+
struct Scanner : public PostWalker<Scanner, UnifiedExpressionVisitor<Scanner>> {
454+
// A map of all expressions, categorized by type.
455+
std::map<Type, std::vector<Expression*>> exprsByType;
456+
457+
void visitExpression(Expression* curr) {
458+
exprsByType[curr->type].push_back(curr);
459+
}
460+
};
461+
Scanner scanner;
462+
scanner.walk(func->body);
463+
// Potentially trim the list of possible picks, so replacements are more likely
464+
// to collide.
465+
for (auto& pair : scanner.exprsByType) {
466+
if (oneIn(2)) continue;
467+
auto& list = pair.second;
468+
std::vector<Expression*> trimmed;
469+
size_t num = upToSquared(list.size());
470+
for (size_t i = 0; i < num; i++) {
471+
trimmed.push_back(vectorPick(list));
472+
}
473+
if (trimmed.empty()) {
474+
trimmed.push_back(vectorPick(list));
475+
}
476+
list.swap(trimmed);
477+
}
478+
// Replace them with copies, to avoid a copy into one altering another copy
479+
for (auto& pair : scanner.exprsByType) {
480+
for (auto*& item : pair.second) {
481+
item = ExpressionManipulator::copy(item, wasm);
482+
}
483+
}
484+
// Second, with some probability replace an item with another item having
485+
// the same type. (This is not always valid due to nesting of labels, but
486+
// we'll fix that up later.)
487+
struct Modder : public PostWalker<Modder, UnifiedExpressionVisitor<Modder>> {
488+
Module& wasm;
489+
Scanner& scanner;
490+
TranslateToFuzzReader& parent;
491+
492+
Modder(Module& wasm, Scanner& scanner, TranslateToFuzzReader& parent) : wasm(wasm), scanner(scanner), parent(parent) {}
493+
494+
void visitExpression(Expression* curr) {
495+
if (parent.oneIn(10)) {
496+
// Replace it!
497+
auto& candidates = scanner.exprsByType[curr->type];
498+
assert(!candidates.empty()); // this expression itself must be there
499+
replaceCurrent(ExpressionManipulator::copy(parent.vectorPick(candidates), wasm));
500+
}
501+
}
502+
};
503+
Modder modder(wasm, scanner, *this);
504+
modder.walk(func->body);
505+
}
506+
507+
void mutate(Function* func) {
508+
// Don't always do this.
509+
if (oneIn(2)) return;
510+
struct Modder : public PostWalker<Modder, UnifiedExpressionVisitor<Modder>> {
511+
Module& wasm;
512+
TranslateToFuzzReader& parent;
513+
514+
Modder(Module& wasm, TranslateToFuzzReader& parent) : wasm(wasm), parent(parent) {}
515+
516+
void visitExpression(Expression* curr) {
517+
if (parent.oneIn(10)) {
518+
// Replace it!
519+
// (This is not always valid due to nesting of labels, but
520+
// we'll fix that up later.)
521+
replaceCurrent(parent.make(curr->type));
522+
}
523+
}
524+
};
525+
Modder modder(wasm, *this);
526+
modder.walk(func->body);
527+
}
528+
529+
// Fix up changes that may have broken validation - types are correct in our
530+
// modding, but not necessarily labels.
531+
void fixLabels(Function* func) {
532+
struct Fixer : public ControlFlowWalker<Fixer> {
533+
Module& wasm;
534+
TranslateToFuzzReader& parent;
535+
536+
Fixer(Module& wasm, TranslateToFuzzReader& parent) : wasm(wasm), parent(parent) {}
537+
538+
// Track seen names to find duplication, which is invalid.
539+
std::set<Name> seen;
540+
541+
void visitBlock(Block* curr) {
542+
if (curr->name.is()) {
543+
if (seen.count(curr->name)) {
544+
replace();
545+
} else {
546+
seen.insert(curr->name);
547+
}
548+
}
549+
}
550+
551+
void visitLoop(Loop* curr) {
552+
if (curr->name.is()) {
553+
if (seen.count(curr->name)) {
554+
replace();
555+
} else {
556+
seen.insert(curr->name);
557+
}
558+
}
559+
}
560+
561+
void visitSwitch(Switch* curr) {
562+
for (auto name : curr->targets) {
563+
if (replaceIfInvalid(name)) return;
564+
}
565+
replaceIfInvalid(curr->default_);
566+
}
567+
568+
void visitBreak(Break* curr) {
569+
replaceIfInvalid(curr->name);
570+
}
571+
572+
bool replaceIfInvalid(Name target) {
573+
if (!hasBreakTarget(target)) {
574+
// There is no valid parent, replace with something trivially safe.
575+
replace();
576+
return true;
577+
}
578+
return false;
579+
}
580+
581+
void replace() {
582+
replaceCurrent(parent.makeTrivial(getCurrent()->type));
583+
}
584+
585+
bool hasBreakTarget(Name name) {
586+
if (controlFlowStack.empty()) return false;
587+
Index i = controlFlowStack.size() - 1;
588+
while (1) {
589+
auto* curr = controlFlowStack[i];
590+
if (Block* block = curr->template dynCast<Block>()) {
591+
if (name == block->name) return true;
592+
} else if (Loop* loop = curr->template dynCast<Loop>()) {
593+
if (name == loop->name) return true;
594+
} else {
595+
// an if, ignorable
596+
assert(curr->template is<If>());
597+
}
598+
if (i == 0) return false;
599+
i--;
600+
}
601+
}
602+
};
603+
Fixer fixer(wasm, *this);
604+
fixer.walk(func->body);
605+
ReFinalize().walkFunctionInModule(func, &wasm);
606+
}
607+
423608
// the fuzzer external interface sends in zeros (simpler to compare
424609
// across invocations from JS or wasm-opt etc.). Add invocations in
425610
// the wasm, so they run everywhere
@@ -651,12 +836,6 @@ class TranslateToFuzzReader {
651836
}
652837
breakableStack.pop_back();
653838
hangStack.pop_back();
654-
if (HANG_LIMIT > 0) {
655-
ret->body = builder.makeSequence(
656-
makeHangLimitCheck(),
657-
ret->body
658-
);
659-
}
660839
ret->finalize();
661840
return ret;
662841
}

0 commit comments

Comments
 (0)