Skip to content

Commit 47f9a78

Browse files
authored
Fix GUFA on calls to function refs in open world (#7135)
In open world we must assume that a funcref that escapes to the outside might be called.
1 parent 87f9dac commit 47f9a78

File tree

4 files changed

+106
-15
lines changed

4 files changed

+106
-15
lines changed

src/ir/possible-contents.cpp

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,12 @@ struct CollectedFuncInfo {
501501
// when we update the child we can find the parent and handle any special
502502
// behavior we need there.
503503
std::unordered_map<Expression*, Expression*> childParents;
504+
505+
// All functions that might be called from the outside. Any RefFunc suggests
506+
// that, in open world. (We could be more precise and use our flow analysis to
507+
// see which, in fact, flow outside, but it is unclear how useful that would
508+
// be. Anyhow, closed-world is more important to optimize, and avoids this.)
509+
std::unordered_set<Name> calledFromOutside;
504510
};
505511

506512
// Does a walk while maintaining a map of names of branch targets to those
@@ -528,8 +534,10 @@ struct BreakTargetWalker : public PostWalker<SubType, VisitorType> {
528534
struct InfoCollector
529535
: public BreakTargetWalker<InfoCollector, OverriddenVisitor<InfoCollector>> {
530536
CollectedFuncInfo& info;
537+
const PassOptions& options;
531538

532-
InfoCollector(CollectedFuncInfo& info) : info(info) {}
539+
InfoCollector(CollectedFuncInfo& info, const PassOptions& options)
540+
: info(info), options(options) {}
533541

534542
// Check if a type is relevant for us. If not, we can ignore it entirely.
535543
bool isRelevant(Type type) {
@@ -665,6 +673,10 @@ struct InfoCollector
665673
info.links.push_back(
666674
{ResultLocation{func, i}, SignatureResultLocation{func->type, i}});
667675
}
676+
677+
if (!options.closedWorld) {
678+
info.calledFromOutside.insert(curr->func);
679+
}
668680
}
669681
void visitRefEq(RefEq* curr) {
670682
addRoot(curr);
@@ -2092,7 +2104,7 @@ Flower::Flower(Module& wasm, const PassOptions& options)
20922104
// First, collect information from each function.
20932105
ModuleUtils::ParallelFunctionAnalysis<CollectedFuncInfo> analysis(
20942106
wasm, [&](Function* func, CollectedFuncInfo& info) {
2095-
InfoCollector finder(info);
2107+
InfoCollector finder(info, options);
20962108

20972109
if (func->imported()) {
20982110
// Imports return unknown values.
@@ -2114,7 +2126,7 @@ Flower::Flower(Module& wasm, const PassOptions& options)
21142126
// Also walk the global module code (for simplicity, also add it to the
21152127
// function map, using a "function" key of nullptr).
21162128
auto& globalInfo = analysis.map[nullptr];
2117-
InfoCollector finder(globalInfo);
2129+
InfoCollector finder(globalInfo, options);
21182130
finder.walkModuleCode(&wasm);
21192131

21202132
#ifdef POSSIBLE_CONTENTS_DEBUG
@@ -2153,6 +2165,16 @@ Flower::Flower(Module& wasm, const PassOptions& options)
21532165
// above.
21542166
InsertOrderedMap<Location, PossibleContents> roots;
21552167

2168+
// Any function that may be called from the outside, like an export, is a
2169+
// root, since they can be called with unknown parameters.
2170+
auto calledFromOutside = [&](Name funcName) {
2171+
auto* func = wasm.getFunction(funcName);
2172+
auto params = func->getParams();
2173+
for (Index i = 0; i < func->getParams().size(); i++) {
2174+
roots[ParamLocation{func, i}] = PossibleContents::fromType(params[i]);
2175+
}
2176+
};
2177+
21562178
for (auto& [func, info] : analysis.map) {
21572179
for (auto& link : info.links) {
21582180
links.insert(getIndexes(link));
@@ -2171,6 +2193,10 @@ Flower::Flower(Module& wasm, const PassOptions& options)
21712193
childParents[getIndex(ExpressionLocation{child, 0})] =
21722194
getIndex(ExpressionLocation{parent, 0});
21732195
}
2196+
2197+
for (auto func : info.calledFromOutside) {
2198+
calledFromOutside(func);
2199+
}
21742200
}
21752201

21762202
// We no longer need the function-level info.
@@ -2180,16 +2206,7 @@ Flower::Flower(Module& wasm, const PassOptions& options)
21802206
std::cout << "external phase\n";
21812207
#endif
21822208

2183-
// Parameters of exported functions are roots, since exports can have callers
2184-
// that we can't see, so anything might arrive there.
2185-
auto calledFromOutside = [&](Name funcName) {
2186-
auto* func = wasm.getFunction(funcName);
2187-
auto params = func->getParams();
2188-
for (Index i = 0; i < func->getParams().size(); i++) {
2189-
roots[ParamLocation{func, i}] = PossibleContents::fromType(params[i]);
2190-
}
2191-
};
2192-
2209+
// Exports can be modified from the outside.
21932210
for (auto& ex : wasm.exports) {
21942211
if (ex->kind == ExternalKind::Function) {
21952212
calledFromOutside(ex->value);

test/lit/passes/gufa-closed-open.wast

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
2+
;; RUN: foreach %s %t wasm-opt -all --gufa -S -o - | filecheck %s --check-prefix OPEND
3+
;; RUN: foreach %s %t wasm-opt -all --gufa --closed-world -S -o - | filecheck %s --check-prefix CLOSE
4+
5+
;; Compare behavior on closed and open world. In open world we must assume that
6+
;; funcrefs, for example, can be called from outside.
7+
8+
(module
9+
;; OPEND: (type $0 (func (param funcref)))
10+
11+
;; OPEND: (type $1 (func))
12+
13+
;; OPEND: (type $2 (func (param i32)))
14+
15+
;; OPEND: (import "fuzzing-support" "call-ref-catch" (func $external-caller (type $0) (param funcref)))
16+
;; CLOSE: (type $0 (func (param funcref)))
17+
18+
;; CLOSE: (type $1 (func))
19+
20+
;; CLOSE: (type $2 (func (param i32)))
21+
22+
;; CLOSE: (import "fuzzing-support" "call-ref-catch" (func $external-caller (type $0) (param funcref)))
23+
(import "fuzzing-support" "call-ref-catch" (func $external-caller (param funcref)))
24+
25+
;; OPEND: (elem declare func $func)
26+
27+
;; OPEND: (export "call-import" (func $call-import))
28+
29+
;; OPEND: (func $call-import (type $1)
30+
;; OPEND-NEXT: (call $external-caller
31+
;; OPEND-NEXT: (ref.func $func)
32+
;; OPEND-NEXT: )
33+
;; OPEND-NEXT: )
34+
;; CLOSE: (elem declare func $func)
35+
36+
;; CLOSE: (export "call-import" (func $call-import))
37+
38+
;; CLOSE: (func $call-import (type $1)
39+
;; CLOSE-NEXT: (call $external-caller
40+
;; CLOSE-NEXT: (ref.func $func)
41+
;; CLOSE-NEXT: )
42+
;; CLOSE-NEXT: )
43+
(func $call-import (export "call-import")
44+
;; Send a reference to $func to the outside, which may call it.
45+
(call $external-caller
46+
(ref.func $func)
47+
)
48+
)
49+
50+
;; OPEND: (func $func (type $2) (param $0 i32)
51+
;; OPEND-NEXT: (drop
52+
;; OPEND-NEXT: (local.get $0)
53+
;; OPEND-NEXT: )
54+
;; OPEND-NEXT: )
55+
;; CLOSE: (func $func (type $2) (param $0 i32)
56+
;; CLOSE-NEXT: (drop
57+
;; CLOSE-NEXT: (unreachable)
58+
;; CLOSE-NEXT: )
59+
;; CLOSE-NEXT: )
60+
(func $func (param $0 i32)
61+
;; This is called from the outside, so this is not dead code, and nothing
62+
;; should change here in open world. In closed world, this can become an
63+
;; unreachable, since nothing can call it.
64+
(drop
65+
(local.get $0)
66+
)
67+
)
68+
)

test/lit/passes/gufa-refs.wast

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
2-
;; RUN: foreach %s %t wasm-opt -all --gufa -S -o - | filecheck %s
2+
;; RUN: foreach %s %t wasm-opt -all --gufa --closed-world -S -o - | filecheck %s
3+
4+
;; Closed-world is applied here to avoid treating all ref.funcs as callable
5+
;; from outside (and this is the more important mode to test on).
36

47
(module
58
;; CHECK: (type $struct (struct))

test/lit/passes/gufa.wast

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
2-
;; RUN: foreach %s %t wasm-opt -all --gufa -S -o - | filecheck %s
2+
;; RUN: foreach %s %t wasm-opt -all --gufa --closed-world -S -o - | filecheck %s
3+
4+
;; Closed-world is applied here to avoid treating all ref.funcs as callable
5+
;; from outside (and this is the more important mode to test on).
36

47
(module
58
;; CHECK: (type $0 (func (result i32)))

0 commit comments

Comments
 (0)