Skip to content

Commit 19f8cc7

Browse files
authored
Add "interposition" to the fuzzer's mutate() method (#6427)
Before this PR we only mutated by replacing an expression with another, which replaced all the children. With this PR we also do these two patterns: (A (B) (C) ) => ;; keep children, replace A (block (drop (B)) (drop (C)) (NEW) ) , (D (A (B) (C) ) ) => ;; keep A, replace it in the parent (D (block (drop (A (B) (C) ) ) (NEW) ) ) We also try to replace onto the new D (either A itself, or A's children).
1 parent 431e858 commit 19f8cc7

File tree

3 files changed

+216
-72
lines changed

3 files changed

+216
-72
lines changed

src/tools/fuzzing/fuzzing.cpp

Lines changed: 145 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#include "tools/fuzzing.h"
1818
#include "ir/gc-type-utils.h"
19+
#include "ir/iteration.h"
1920
#include "ir/local-structural-dominance.h"
2021
#include "ir/module-utils.h"
2122
#include "ir/subtypes.h"
@@ -900,6 +901,26 @@ void TranslateToFuzzReader::recombine(Function* func) {
900901
};
901902
Modder modder(wasm, scanner, *this);
902903
modder.walk(func->body);
904+
// TODO: A specific form of recombination we should perhaps do more often is
905+
// to recombine among an expression's children, and in particular to
906+
// reorder them.
907+
}
908+
909+
// Given two expressions, try to replace one of the children of the first with
910+
// the second. For example, given i32.add and an i32.const, the i32.const could
911+
// be placed as either of the children of the i32.add. This tramples the
912+
// existing content there. Returns true if we found a place.
913+
static bool replaceChildWith(Expression* expr, Expression* with) {
914+
for (auto*& child : ChildIterator(expr)) {
915+
// To replace, we must have an appropriate type, and we cannot replace a
916+
// Pop under any circumstances.
917+
if (Type::isSubType(with->type, child->type) &&
918+
FindAll<Pop>(child).list.empty()) {
919+
child = with;
920+
return true;
921+
}
922+
}
923+
return false;
903924
}
904925

905926
void TranslateToFuzzReader::mutate(Function* func) {
@@ -930,16 +951,15 @@ void TranslateToFuzzReader::mutate(Function* func) {
930951
percentChance = std::max(percentChance, Index(3));
931952

932953
struct Modder : public PostWalker<Modder, UnifiedExpressionVisitor<Modder>> {
933-
Module& wasm;
934954
TranslateToFuzzReader& parent;
935955
Index percentChance;
936956

937957
// Whether to replace with unreachable. This can lead to less code getting
938958
// executed, so we don't want to do it all the time even in a big function.
939959
bool allowUnreachable;
940960

941-
Modder(Module& wasm, TranslateToFuzzReader& parent, Index percentChance)
942-
: wasm(wasm), parent(parent), percentChance(percentChance) {
961+
Modder(TranslateToFuzzReader& parent, Index percentChance)
962+
: parent(parent), percentChance(percentChance) {
943963
// If the parent allows it then sometimes replace with an unreachable, and
944964
// sometimes not. Even if we allow it, only do it in certain functions
945965
// (half the time) and only do it rarely (see below).
@@ -949,29 +969,135 @@ void TranslateToFuzzReader::mutate(Function* func) {
949969
void visitExpression(Expression* curr) {
950970
if (parent.upTo(100) < percentChance &&
951971
parent.canBeArbitrarilyReplaced(curr)) {
952-
if (allowUnreachable && parent.oneIn(20)) {
972+
// We can replace in various modes, see below. Generate a random number
973+
// up to 100 to help us there.
974+
int mode = parent.upTo(100);
975+
976+
if (allowUnreachable && mode < 5) {
953977
replaceCurrent(parent.make(Type::unreachable));
954978
return;
955979
}
980+
956981
// For constants, perform only a small tweaking in some cases.
982+
// TODO: more minor tweaks to immediates, like making a load atomic or
983+
// not, changing an offset, etc.
957984
if (auto* c = curr->dynCast<Const>()) {
958-
if (parent.oneIn(2)) {
985+
if (mode < 50) {
959986
c->value = parent.tweak(c->value);
960-
return;
987+
} else {
988+
// Just replace the entire thing.
989+
replaceCurrent(parent.make(curr->type));
990+
}
991+
return;
992+
}
993+
994+
// Generate a replacement for the expression, and by default replace all
995+
// of |curr| (including children) with that replacement, but in some
996+
// cases we can do more subtle things.
997+
//
998+
// Note that such a replacement is not always valid due to nesting of
999+
// labels, but we'll fix that up later. Note also that make() picks a
1000+
// subtype, so this has a chance to replace us with anything that is
1001+
// valid to put here.
1002+
auto* rep = parent.make(curr->type);
1003+
if (mode < 33 && rep->type != Type::none) {
1004+
// This has a non-none type. Replace the output, keeping the
1005+
// expression and its children in a drop. This "interposes" between
1006+
// this expression and its parent, something like this:
1007+
//
1008+
// (D
1009+
// (A
1010+
// (B)
1011+
// (C)
1012+
// )
1013+
// )
1014+
////
1015+
// => ;; keep A, replace it in the parent
1016+
//
1017+
// (D
1018+
// (block
1019+
// (drop
1020+
// (A
1021+
// (B)
1022+
// (C)
1023+
// )
1024+
// )
1025+
// (NEW)
1026+
// )
1027+
// )
1028+
//
1029+
// We also sometimes try to insert A as a child of NEW, so we actually
1030+
// interpose directly:
1031+
//
1032+
// (D
1033+
// (NEW
1034+
// (A
1035+
// (B)
1036+
// (C)
1037+
// )
1038+
// )
1039+
// )
1040+
//
1041+
// We do not do that all the time, as inserting a drop is actually an
1042+
// important situation to test: the drop makes the output of A unused,
1043+
// which may let optimizations remove it.
1044+
if ((mode & 1) && replaceChildWith(rep, curr)) {
1045+
// We managed to replace one of the children with curr, and have
1046+
// nothing more to do.
1047+
} else {
1048+
// Drop curr and append.
1049+
rep =
1050+
parent.builder.makeSequence(parent.builder.makeDrop(curr), rep);
1051+
}
1052+
} else if (mode >= 66 && !Properties::isControlFlowStructure(curr)) {
1053+
ChildIterator children(curr);
1054+
auto numChildren = children.getNumChildren();
1055+
if (numChildren > 0 && numChildren < 5) {
1056+
// This is a normal (non-control-flow) expression with at least one
1057+
// child (and not an excessive amount of them; see the processing
1058+
// below). "Interpose" between the children and this expression by
1059+
// keeping them and replacing the parent |curr|. We do this by
1060+
// generating drops of the children, like this:
1061+
//
1062+
// (A
1063+
// (B)
1064+
// (C)
1065+
// )
1066+
//
1067+
// => ;; keep children, replace A
1068+
//
1069+
// (block
1070+
// (drop (B))
1071+
// (drop (C))
1072+
// (NEW)
1073+
// )
1074+
//
1075+
auto* block = parent.builder.makeBlock();
1076+
for (auto* child : children) {
1077+
// Only drop the child if we can't replace it as one of NEW's
1078+
// children. This does a linear scan of |rep| which is the reason
1079+
// for the above limit on the number of children.
1080+
if (!replaceChildWith(rep, child)) {
1081+
block->list.push_back(parent.builder.makeDrop(child));
1082+
}
1083+
}
1084+
1085+
if (!block->list.empty()) {
1086+
// We need the block, that is, we did not find a place for all the
1087+
// children.
1088+
block->list.push_back(rep);
1089+
block->finalize();
1090+
rep = block;
1091+
}
9611092
}
9621093
}
963-
// TODO: more minor tweaks to immediates, like making a load atomic or
964-
// not, changing an offset, etc.
965-
// Perform a general replacement. (This is not always valid due to
966-
// nesting of labels, but we'll fix that up later.) Note that make()
967-
// picks a subtype, so this has a chance to replace us with anything
968-
// that is valid to put here.
969-
replaceCurrent(parent.make(curr->type));
1094+
replaceCurrent(rep);
9701095
}
9711096
}
9721097
};
973-
Modder modder(wasm, *this, percentChance);
974-
modder.walk(func->body);
1098+
1099+
Modder modder(*this, percentChance);
1100+
modder.walkFunctionInModule(func, &wasm);
9751101
}
9761102

9771103
void TranslateToFuzzReader::fixAfterChanges(Function* func) {
@@ -1078,6 +1204,10 @@ void TranslateToFuzzReader::modifyInitialFunctions() {
10781204
recombine(func);
10791205
mutate(func);
10801206
fixAfterChanges(func);
1207+
// TODO: This triad of functions appears in another place as well, and
1208+
// could be handled by a single function. That function could also
1209+
// decide to reorder recombine and mutate or even run more cycles of
1210+
// them.
10811211
}
10821212
}
10831213
// Remove a start function - the fuzzing harness expects code to run only
Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
total
2-
[exports] : 29
3-
[funcs] : 39
2+
[exports] : 18
3+
[funcs] : 21
44
[globals] : 9
55
[imports] : 4
66
[memories] : 1
77
[memory-data] : 2
8-
[table-data] : 6
8+
[table-data] : 7
99
[tables] : 1
1010
[tags] : 0
11-
[total] : 5494
12-
[vars] : 119
13-
Binary : 400
14-
Block : 892
15-
Break : 210
16-
Call : 232
17-
CallIndirect : 12
18-
Const : 898
19-
Drop : 49
20-
GlobalGet : 421
21-
GlobalSet : 333
22-
If : 289
23-
Load : 113
24-
LocalGet : 434
25-
LocalSet : 306
26-
Loop : 118
27-
Nop : 85
28-
RefFunc : 6
29-
Return : 62
30-
Select : 52
31-
Store : 45
32-
Switch : 1
33-
Unary : 380
34-
Unreachable : 156
11+
[total] : 11685
12+
[vars] : 64
13+
Binary : 848
14+
Block : 1846
15+
Break : 456
16+
Call : 275
17+
CallIndirect : 117
18+
Const : 1952
19+
Drop : 86
20+
GlobalGet : 844
21+
GlobalSet : 679
22+
If : 625
23+
Load : 236
24+
LocalGet : 1050
25+
LocalSet : 764
26+
Loop : 301
27+
Nop : 143
28+
RefFunc : 7
29+
Return : 103
30+
Select : 77
31+
Store : 111
32+
Switch : 3
33+
Unary : 835
34+
Unreachable : 327
Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,56 @@
11
total
22
[exports] : 3
3-
[funcs] : 6
3+
[funcs] : 5
44
[globals] : 1
55
[imports] : 5
66
[memories] : 1
77
[memory-data] : 20
88
[table-data] : 1
99
[tables] : 1
1010
[tags] : 2
11-
[total] : 314
12-
[vars] : 38
13-
ArrayNew : 2
14-
ArrayNewFixed : 1
11+
[total] : 661
12+
[vars] : 21
13+
ArrayGet : 1
14+
ArrayLen : 1
15+
ArrayNew : 4
16+
ArrayNewFixed : 6
1517
AtomicFence : 1
16-
Binary : 58
17-
Block : 28
18-
Break : 6
19-
Call : 10
20-
Const : 72
21-
Drop : 3
22-
GlobalGet : 10
23-
GlobalSet : 10
18+
AtomicRMW : 1
19+
Binary : 87
20+
Block : 78
21+
Break : 17
22+
Call : 11
23+
Const : 125
24+
DataDrop : 1
25+
Drop : 7
26+
GlobalGet : 26
27+
GlobalSet : 26
2428
I31Get : 1
25-
If : 7
26-
Load : 18
27-
LocalGet : 36
28-
LocalSet : 21
29-
Loop : 1
30-
Nop : 2
31-
RefEq : 1
32-
RefFunc : 2
33-
RefI31 : 2
34-
RefNull : 1
35-
Return : 2
36-
Select : 1
37-
Store : 1
38-
StructGet : 1
29+
If : 24
30+
Load : 22
31+
LocalGet : 65
32+
LocalSet : 38
33+
Loop : 9
34+
MemoryCopy : 1
35+
Nop : 9
36+
RefAs : 8
37+
RefCast : 1
38+
RefEq : 2
39+
RefFunc : 1
40+
RefI31 : 3
41+
RefIsNull : 2
42+
RefNull : 13
43+
RefTest : 1
44+
Return : 4
45+
SIMDExtract : 3
46+
SIMDLoad : 1
47+
Select : 2
48+
Store : 4
49+
StructGet : 2
3950
StructNew : 3
40-
TupleMake : 2
41-
Unary : 6
42-
Unreachable : 5
51+
Throw : 1
52+
Try : 1
53+
TupleExtract : 2
54+
TupleMake : 5
55+
Unary : 28
56+
Unreachable : 13

0 commit comments

Comments
 (0)