Skip to content

Commit 47a72fb

Browse files
authored
Fuzz @binaryen.js.called in closed-world (#8343)
Closed-world fuzzing disabled the `callRef*` imports until now. Those imports get a wasm funcref and call it from JS, so they break the closed world assumption. This PR enables `callRef*` imports in that mode, by only sending them valid functions, ones marked with `@binaryen.js.called`. To do this in the fuzzer, * Fix up closed world for each function, finding `callRef*` calls and ensuring they are given a valid funcref, not a random value. * Avoid calling `callRef*` indirectly in closed world. We have no good way to avoid a random funcref being sent out there, as we do not see those calls statically, so we can't fix them up.
1 parent fd5e86e commit 47a72fb

File tree

6 files changed

+241
-119
lines changed

6 files changed

+241
-119
lines changed

src/ir/intrinsics.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ class Intrinsics {
130130
return getAnnotations(nullptr, func);
131131
}
132132

133+
void setAnnotations(Function* func,
134+
Expression* curr,
135+
const CodeAnnotation& value) {
136+
func->codeAnnotations[curr] = value;
137+
}
138+
139+
void setAnnotations(Function* func, const CodeAnnotation& value) {
140+
setAnnotations(func, nullptr, value);
141+
}
142+
133143
// Given a call in a function, return all the annotations for it. The call may
134144
// be annotated itself (which takes precedence), or the function it calls be
135145
// annotated.

src/tools/fuzzing.h

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class TranslateToFuzzReader {
143143
bool closedWorld;
144144
Builder builder;
145145
Random random;
146+
Intrinsics intrinsics;
146147

147148
// Whether to emit memory operations like loads and stores.
148149
bool allowMemory = true;
@@ -218,6 +219,9 @@ class TranslateToFuzzReader {
218219
// All tags that are valid as exception tags (which cannot have results).
219220
std::vector<Tag*> exceptionTags;
220221

222+
// All functions marked jsCalled.
223+
std::vector<Name> jsCalled;
224+
221225
Index numAddedFunctions = 0;
222226

223227
// The name of an empty tag.
@@ -393,7 +397,9 @@ class TranslateToFuzzReader {
393397
}
394398
void recombine(Function* func);
395399
void mutate(Function* func);
396-
// Fix up the IR after recombination and mutation.
400+
// Fix up the IR for closed world.
401+
void fixClosedWorld(Function* func);
402+
// Fix up the IR after recombination and mutation (which may break the IR).
397403
void fixAfterChanges(Function* func);
398404
void modifyInitialFunctions();
399405

@@ -568,6 +574,12 @@ class TranslateToFuzzReader {
568574
Name getTargetName(Expression* target);
569575
Type getTargetType(Expression* target);
570576

577+
// Checks if a function is valid to take a ref.func of.
578+
bool isValidRefFuncTarget(Name func);
579+
580+
// Checks if a function is a callRef* import (call-ref or call-ref-catch).
581+
bool isCallRefImport(Name func);
582+
571583
// statistical distributions
572584

573585
// 0 to the limit, logarithmic scale

src/tools/fuzzing/fuzzing.cpp

Lines changed: 125 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ TranslateToFuzzReader::TranslateToFuzzReader(Module& wasm,
6464
std::vector<char>&& input,
6565
bool closedWorld)
6666
: wasm(wasm), closedWorld(closedWorld), builder(wasm),
67-
random(std::move(input), wasm.features),
67+
random(std::move(input), wasm.features), intrinsics(wasm),
6868
loggableTypes(getLoggableTypes(wasm.features)),
6969
atomicMemoryOrders(getMemoryOrders(wasm.features)),
7070

@@ -1000,25 +1000,6 @@ void TranslateToFuzzReader::addImportCallingSupport() {
10001000
return;
10011001
}
10021002

1003-
if (wasm.features.hasReferenceTypes() && closedWorld) {
1004-
// In closed world mode we must *remove* the call-ref* imports, if they
1005-
// exist in the initial content. These are not valid to call in closed-world
1006-
// mode as they call function references. (Another solution here would be to
1007-
// make closed-world issue validation errors on these imports, but that
1008-
// would require changes to the general-purpose validator.)
1009-
for (auto& func : wasm.functions) {
1010-
if (func->imported() && func->module == "fuzzing-support" &&
1011-
func->base.startsWith("call-ref")) {
1012-
// Make it non-imported, and with a simple body.
1013-
func->module = func->base = Name();
1014-
func->type = func->type.with(Exact);
1015-
auto results = func->getResults();
1016-
func->body =
1017-
results.isConcrete() ? makeConst(results) : makeNop(Type::none);
1018-
}
1019-
}
1020-
}
1021-
10221003
// Only add these some of the time, as they inhibit some fuzzing (things like
10231004
// wasm-ctor-eval and wasm-merge are sensitive to the wasm being able to call
10241005
// its own exports, and to care about the indexes of the exports).
@@ -1058,9 +1039,7 @@ void TranslateToFuzzReader::addImportCallingSupport() {
10581039
wasm.addFunction(std::move(func));
10591040
}
10601041

1061-
// If the wasm will be used for closed-world testing, we cannot use the
1062-
// call-ref variants, as mentioned before.
1063-
if (wasm.features.hasReferenceTypes() && !closedWorld) {
1042+
if (wasm.features.hasReferenceTypes()) {
10641043
if (choice & 4) {
10651044
// Given an funcref, call it from JS.
10661045
callRefImportName = Names::getValidFunctionName(wasm, "call-ref");
@@ -1638,6 +1617,16 @@ void TranslateToFuzzReader::processFunctions() {
16381617
}
16391618
}
16401619
}
1620+
1621+
// Also fix up closed world, if we need to. We must do this at the end, so
1622+
// nothing can break the closed world assumptions after.
1623+
if (closedWorld) {
1624+
for (auto& func : wasm.functions) {
1625+
if (!func->imported()) {
1626+
fixClosedWorld(func.get());
1627+
}
1628+
}
1629+
}
16411630
}
16421631

16431632
// TODO: return std::unique_ptr<Function>
@@ -1695,8 +1684,8 @@ Function* TranslateToFuzzReader::addFunction() {
16951684
func->body = make(bodyType);
16961685
}
16971686

1698-
// Add hang limit checks after all other operations on the function body.
16991687
wasm.addFunction(std::move(allocation));
1688+
17001689
// Export some functions, but not all (to allow inlining etc.). Try to export
17011690
// at least one, though, to keep each testcase interesting. Avoid non-
17021691
// nullable params, as those cannot be constructed by the fuzzer on the
@@ -1713,7 +1702,10 @@ Function* TranslateToFuzzReader::addFunction() {
17131702
wasm.addExport(
17141703
Builder::makeExport(func->name, func->name, ExternalKind::Function));
17151704
}
1716-
// add some to an elem segment TODO we could do this for imported funcs too
1705+
1706+
// Add some to an elem segment TODO we could do this for imported funcs too,
1707+
// but in closed world must be careful of callRef*, see the jsCalled logic
1708+
// and isValidRefFuncTarget.
17171709
while (oneIn(3) && !random.finished()) {
17181710
auto type = func->type;
17191711
std::vector<ElementSegment*> compatibleSegments;
@@ -1725,6 +1717,23 @@ Function* TranslateToFuzzReader::addFunction() {
17251717
auto& randomElem = compatibleSegments[upTo(compatibleSegments.size())];
17261718
randomElem->data.push_back(builder.makeRefFunc(func->name));
17271719
}
1720+
1721+
// Mark some functions as jsCalled. This allows them to be called by
1722+
// reference even in closed world, using the callRef* imports.
1723+
// TODO: We could do this, and exporting above, to initial content too.
1724+
if (oneIn(4)) {
1725+
auto annotations = intrinsics.getAnnotations(func);
1726+
annotations.jsCalled = true;
1727+
intrinsics.setAnnotations(func, annotations);
1728+
1729+
// We cannot actually use this as jsCalled if it does not have a type
1730+
// compatible with the callRef* imports. They send a funcref, so we must
1731+
// only send non-shared functions.
1732+
if (!func->type.getHeapType().isShared()) {
1733+
jsCalled.push_back(func->name);
1734+
}
1735+
}
1736+
17281737
numAddedFunctions++;
17291738
return func;
17301739
}
@@ -2125,6 +2134,60 @@ void TranslateToFuzzReader::mutate(Function* func) {
21252134
modder.walkFunctionInModule(func, &wasm);
21262135
}
21272136

2137+
void TranslateToFuzzReader::fixClosedWorld(Function* func) {
2138+
assert(closedWorld);
2139+
2140+
struct Fixer
2141+
: public ExpressionStackWalker<Fixer, UnifiedExpressionVisitor<Fixer>> {
2142+
TranslateToFuzzReader& parent;
2143+
2144+
Fixer(TranslateToFuzzReader& parent) : parent(parent) {}
2145+
2146+
void visitCall(Call* curr) {
2147+
// In closed world, the callRef* imports can cause misoptimization later:
2148+
// they send a funcref to JS to call, and in closed world we assume such
2149+
// calls do not happen unless the function is annotated as jsCalled. We
2150+
// must therefore ensure that calls to these imports only send a jsCalled
2151+
// method and nothing else.
2152+
if (!parent.isCallRefImport(curr->target)) {
2153+
return;
2154+
}
2155+
2156+
// These imports take a funcref as the first param.
2157+
assert(!curr->operands.empty());
2158+
if (curr->operands[0]->type == Type::unreachable) {
2159+
// This call is not executed anyhow, and we shouldn't replace it, as
2160+
// that could change the type.
2161+
return;
2162+
}
2163+
2164+
if (parent.jsCalled.empty()) {
2165+
// There is nothing valid to call at all. Keep the children (we may
2166+
// need them to validate, e.g. if there is a `pop`), but remove the
2167+
// call.
2168+
std::vector<Expression*> list;
2169+
for (auto* child : curr->operands) {
2170+
list.push_back(parent.builder.makeDrop(child));
2171+
}
2172+
list.push_back(parent.makeTrivial(curr->type));
2173+
replaceCurrent(parent.builder.makeBlock(list));
2174+
return;
2175+
}
2176+
2177+
// Set something valid. As above, we must keep the old child for
2178+
// validation reasons.
2179+
auto old = parent.builder.makeDrop(curr->operands[0]);
2180+
auto target = parent.pick(parent.jsCalled);
2181+
assert(parent.isValidRefFuncTarget(target));
2182+
auto new_ = parent.builder.makeRefFunc(target);
2183+
curr->operands[0] = parent.builder.makeSequence(old, new_);
2184+
}
2185+
} fixer(*this);
2186+
2187+
FunctionCreationContext context(*this, func);
2188+
fixer.walk(func->body);
2189+
}
2190+
21282191
void TranslateToFuzzReader::fixAfterChanges(Function* func) {
21292192
struct Fixer
21302193
: public ExpressionStackWalker<Fixer, UnifiedExpressionVisitor<Fixer>> {
@@ -3002,6 +3065,10 @@ Expression* TranslateToFuzzReader::makeCallRef(Type type) {
30023065
target = wasm.functions[upTo(wasm.functions.size())].get();
30033066
isReturn = type == Type::unreachable && wasm.features.hasTailCall() &&
30043067
funcContext->func->getResults() == target->getResults();
3068+
if (!isValidRefFuncTarget(target->name)) {
3069+
i++;
3070+
continue;
3071+
}
30053072
if (target->getResults() == type || isReturn) {
30063073
break;
30073074
}
@@ -3638,7 +3705,9 @@ Expression* TranslateToFuzzReader::makeRefFuncConst(Type type) {
36383705
funcContext->func->type.getHeapType().getShared() == share &&
36393706
!oneIn(4)) {
36403707
auto* target = funcContext->func;
3641-
return builder.makeRefFunc(target->name, target->type);
3708+
if (isValidRefFuncTarget(target->name)) {
3709+
return builder.makeRefFunc(target->name, target->type);
3710+
}
36423711
}
36433712
}
36443713
// Look for a proper function starting from a random location, and loop from
@@ -3648,7 +3717,8 @@ Expression* TranslateToFuzzReader::makeRefFuncConst(Type type) {
36483717
Index i = start;
36493718
do {
36503719
auto& func = wasm.functions[i];
3651-
if (Type::isSubType(func->type, type)) {
3720+
if (Type::isSubType(func->type, type) &&
3721+
isValidRefFuncTarget(func->name)) {
36523722
return builder.makeRefFunc(func->name);
36533723
}
36543724
i = (i + 1) % wasm.functions.size();
@@ -3688,6 +3758,7 @@ Expression* TranslateToFuzzReader::makeRefFuncConst(Type type) {
36883758
Type(heapType, NonNullable, Exact),
36893759
{},
36903760
body));
3761+
assert(isValidRefFuncTarget(func->name));
36913762
return builder.makeRefFunc(func->name);
36923763
}
36933764

@@ -6028,4 +6099,31 @@ Type TranslateToFuzzReader::getTargetType(Expression* target) {
60286099
WASM_UNREACHABLE("unexpected expr type");
60296100
}
60306101

6102+
bool TranslateToFuzzReader::isValidRefFuncTarget(Name func) {
6103+
// In closed world, we must not take callRef* by reference: if we do, then we
6104+
// might end up calling them indirectly, and with any possible function
6105+
// reference, but in that mode we must only pass in jsCalled functions. We
6106+
// handle direct calls in fixClosedWorld, but cannot handle indirect ones
6107+
// easily, so just disallow taking references of those functions.
6108+
if (!closedWorld) {
6109+
return true;
6110+
}
6111+
return !isCallRefImport(func);
6112+
}
6113+
6114+
bool TranslateToFuzzReader::isCallRefImport(Name target) {
6115+
// Check the import module and base. Note we do not just compare to
6116+
// callRefImportName / callRefCatchImportName because those are the names of
6117+
// the methods we added, but the initial content may import those methods
6118+
// under other internal names.
6119+
auto* func = wasm.getFunctionOrNull(target);
6120+
if (!func) {
6121+
// We are called while a function is still being constructed. Such a new
6122+
// defined function can never be an import.
6123+
return false;
6124+
}
6125+
return func->imported() && func->module == "fuzzing-support" &&
6126+
func->base.startsWith("call-ref");
6127+
}
6128+
60316129
} // namespace wasm
Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
Metrics
22
total
3-
[exports] : 139
4-
[funcs] : 217
3+
[exports] : 85
4+
[funcs] : 123
55
[globals] : 7
66
[imports] : 6
77
[memories] : 1
88
[memory-data] : 23
9-
[table-data] : 71
9+
[table-data] : 44
1010
[tables] : 1
1111
[tags] : 0
12-
[total] : 15937
13-
[vars] : 628
14-
Binary : 1172
15-
Block : 2747
16-
Break : 485
17-
Call : 628
18-
CallIndirect : 146
19-
Const : 2648
20-
Drop : 197
21-
GlobalGet : 1418
22-
GlobalSet : 1052
23-
If : 866
24-
Load : 259
25-
LocalGet : 1011
26-
LocalSet : 708
27-
Loop : 309
28-
Nop : 227
29-
RefFunc : 71
30-
Return : 159
31-
Select : 83
32-
Store : 105
33-
Switch : 1
34-
Unary : 1112
35-
Unreachable : 533
12+
[total] : 13644
13+
[vars] : 404
14+
Binary : 996
15+
Block : 2154
16+
Break : 426
17+
Call : 468
18+
CallIndirect : 135
19+
Const : 2312
20+
Drop : 113
21+
GlobalGet : 1167
22+
GlobalSet : 818
23+
If : 765
24+
Load : 231
25+
LocalGet : 1187
26+
LocalSet : 698
27+
Loop : 286
28+
Nop : 169
29+
RefFunc : 44
30+
Return : 134
31+
Select : 119
32+
Store : 92
33+
Switch : 4
34+
Unary : 910
35+
Unreachable : 416

0 commit comments

Comments
 (0)