Skip to content

Commit 2bf71b8

Browse files
authored
[WebAssembly] Fix phi handling for Wasm SjLj (#99730)
In Wasm SjLj, longjmpable `call`s that in functions that call `setjmp` are converted into `invoke`s. Those `invoke`s are meant to unwind to `catch.dispatch.longjmp` to figure out which `setjmp` those `longjmp` buffers belong to: https://github.com/llvm/llvm-project/blob/fada9227325b3eaa0bdc09a486f29a7f08b7b3fb/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp#L250-L260 But in case a longjmpable call is within another `catchpad` or `cleanuppad` scope, to maintain the nested scope structure, we should make them unwind to the scope's next unwind destination and not directly to `catch.dispatch.longjmp`: https://github.com/llvm/llvm-project/blob/fada9227325b3eaa0bdc09a486f29a7f08b7b3fb/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp#L1698-L1727 In this case the longjmps will eventually unwind to `catch.dispatch.longjmp` and be handled there. In this case, it is possible that the unwind destination (which is an existing `catchpad` or `cleanuppad`) may already have `phi`s. And because the unwind destinations get new predecessors because of the newly created `invoke`s, those `phi`s need to have new entries for those new predecessors. This adds new preds as new incoming blocks to those `phi`s, and we use a separate `SSAUpdater` to calculate the correct incoming values to those blocks. I have assumed `SSAUpdaterBulk` used in `rebuildSSA` would take care of these things, but apparently it doesn't. It takes available defs and adds `phi`s in the defs' dominance frontiers, i.e., where each def's dominance ends, and rewrites other uses based on the newly added `phi`s. But it doesn't add entries to existing `phi`s, and the case in this bug may not even involve dominance frontiers; this bug is simply about existing `phis`s that have gained new preds need new entries for them. It is kind of surprising that this bug was only reported recently, given that this pass has not been changed much in years. Fixes #97496 and fixes emscripten-core/emscripten#22170.
1 parent 39c23a3 commit 2bf71b8

File tree

2 files changed

+176
-0
lines changed

2 files changed

+176
-0
lines changed

llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,8 @@ void WebAssemblyLowerEmscriptenEHSjLj::rebuildSSA(Function &F) {
777777
SSAUpdaterBulk SSA;
778778
for (BasicBlock &BB : F) {
779779
for (Instruction &I : BB) {
780+
if (I.getType()->isVoidTy())
781+
continue;
780782
unsigned VarID = SSA.AddVariable(I.getName(), I.getType());
781783
// If a value is defined by an invoke instruction, it is only available in
782784
// its normal destination and not in its unwind destination.
@@ -1687,6 +1689,8 @@ void WebAssemblyLowerEmscriptenEHSjLj::handleLongjmpableCallsForWasmSjLj(
16871689
}
16881690
}
16891691

1692+
SmallDenseMap<BasicBlock *, SmallPtrSet<BasicBlock *, 4>, 4>
1693+
UnwindDestToNewPreds;
16901694
for (auto *CI : LongjmpableCalls) {
16911695
// Even if the callee function has attribute 'nounwind', which is true for
16921696
// all C functions, it can longjmp, which means it can throw a Wasm
@@ -1724,6 +1728,11 @@ void WebAssemblyLowerEmscriptenEHSjLj::handleLongjmpableCallsForWasmSjLj(
17241728
}
17251729
if (!UnwindDest)
17261730
UnwindDest = CatchDispatchLongjmpBB;
1731+
// Because we are changing a longjmpable call to an invoke, its unwind
1732+
// destination can be an existing EH pad that already have phis, and the BB
1733+
// with the newly created invoke will become a new predecessor of that EH
1734+
// pad. In this case we need to add the new predecessor to those phis.
1735+
UnwindDestToNewPreds[UnwindDest].insert(CI->getParent());
17271736
changeToInvokeAndSplitBasicBlock(CI, UnwindDest);
17281737
}
17291738

@@ -1752,4 +1761,45 @@ void WebAssemblyLowerEmscriptenEHSjLj::handleLongjmpableCallsForWasmSjLj(
17521761

17531762
for (Instruction *I : ToErase)
17541763
I->eraseFromParent();
1764+
1765+
// Add entries for new predecessors to phis in unwind destinations. We use
1766+
// 'undef' as a placeholder value. We should make sure the phis have a valid
1767+
// set of predecessors before running SSAUpdater, because SSAUpdater
1768+
// internally can use existing phis to gather predecessor info rather than
1769+
// scanning the actual CFG (See FindPredecessorBlocks in SSAUpdater.cpp for
1770+
// details).
1771+
for (auto &[UnwindDest, NewPreds] : UnwindDestToNewPreds) {
1772+
for (PHINode &PN : UnwindDest->phis()) {
1773+
for (auto *NewPred : NewPreds) {
1774+
assert(PN.getBasicBlockIndex(NewPred) == -1);
1775+
PN.addIncoming(UndefValue::get(PN.getType()), NewPred);
1776+
}
1777+
}
1778+
}
1779+
1780+
// For unwind destinations for newly added invokes to longjmpable functions,
1781+
// calculate incoming values for the newly added predecessors using
1782+
// SSAUpdater. We add existing values in the phis to SSAUpdater as available
1783+
// values and let it calculate what the value should be at the end of new
1784+
// incoming blocks.
1785+
for (auto &[UnwindDest, NewPreds] : UnwindDestToNewPreds) {
1786+
for (PHINode &PN : UnwindDest->phis()) {
1787+
SSAUpdater SSA;
1788+
SSA.Initialize(PN.getType(), PN.getName());
1789+
for (unsigned Idx = 0, E = PN.getNumIncomingValues(); Idx != E; ++Idx) {
1790+
if (NewPreds.contains(PN.getIncomingBlock(Idx)))
1791+
continue;
1792+
Value *V = PN.getIncomingValue(Idx);
1793+
if (auto *II = dyn_cast<InvokeInst>(V))
1794+
SSA.AddAvailableValue(II->getNormalDest(), II);
1795+
else if (auto *I = dyn_cast<Instruction>(V))
1796+
SSA.AddAvailableValue(I->getParent(), I);
1797+
else
1798+
SSA.AddAvailableValue(PN.getIncomingBlock(Idx), V);
1799+
}
1800+
for (auto *NewPred : NewPreds)
1801+
PN.setIncomingValueForBlock(NewPred, SSA.GetValueAtEndOfBlock(NewPred));
1802+
assert(PN.isComplete());
1803+
}
1804+
}
17551805
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
; RUN: opt < %s -wasm-lower-em-ehsjlj -wasm-enable-eh -wasm-enable-sjlj -S | FileCheck %s
2+
3+
target triple = "wasm32-unknown-emscripten"
4+
5+
%struct.__jmp_buf_tag = type { [6 x i32], i32, [32 x i32] }
6+
@buf = internal global [1 x %struct.__jmp_buf_tag] zeroinitializer, align 16
7+
8+
; When longjmpable calls are coverted into invokes in Wasm SjLj transformation
9+
; and their unwind destination is an existing catchpad or cleanuppad due to
10+
; maintain the scope structure, the new pred BBs created by invokes and the
11+
; correct incoming values should be added the existing phis in those unwind
12+
; destinations.
13+
14+
; When longjmpable calls are within a cleanuppad.
15+
define void @longjmpable_invoke_phi0() personality ptr @__gxx_wasm_personality_v0 {
16+
; CHECK-LABEL: @longjmpable_invoke_phi0
17+
entry:
18+
%val.entry = call i32 @llvm.wasm.memory.size.i32(i32 0)
19+
%0 = call i32 @setjmp(ptr @buf) #2
20+
invoke void @foo()
21+
to label %bb1 unwind label %ehcleanup1
22+
23+
bb1: ; preds = %entry
24+
; We use llvm.wasm.memory.size intrinsic just to get/use an i32 value. The
25+
; reason we use an intrinsic here is to make it not longjmpable. If this can
26+
; longjmp, the result will be more complicated and hard to check.
27+
%val.bb1 = call i32 @llvm.wasm.memory.size.i32(i32 0)
28+
invoke void @foo()
29+
to label %bb2 unwind label %ehcleanup0
30+
31+
bb2: ; preds = %bb1
32+
unreachable
33+
34+
ehcleanup0: ; preds = %bb1
35+
%1 = cleanuppad within none []
36+
call void @longjmpable() [ "funclet"(token %1) ]
37+
; CHECK: ehcleanup0
38+
; CHECK: invoke void @longjmpable
39+
; CHECK-NEXT: to label %.noexc unwind label %ehcleanup1
40+
invoke void @foo() [ "funclet"(token %1) ]
41+
to label %bb3 unwind label %ehcleanup1
42+
43+
bb3: ; preds = %ehcleanup0
44+
%val.bb3 = call i32 @llvm.wasm.memory.size.i32(i32 0)
45+
call void @longjmpable() [ "funclet"(token %1) ]
46+
; CHECK: bb3:
47+
; CHECK: invoke void @longjmpable
48+
; CHECK-NEXT: to label %.noexc1 unwind label %ehcleanup1
49+
cleanupret from %1 unwind label %ehcleanup1
50+
51+
ehcleanup1: ; preds = %bb3, %ehcleanup0, %entry
52+
%phi = phi i32 [ %val.entry, %entry ], [ %val.bb1, %ehcleanup0 ], [ %val.bb3, %bb3 ]
53+
; CHECK: ehcleanup1:
54+
; CHECK-NEXT: %phi = phi i32 [ %val.entry2, %entry.split.split ], [ %val.bb1, %.noexc ], [ %val.bb3, %.noexc1 ], [ %val.bb1, %ehcleanup0 ], [ %val.bb3, %bb3 ]
55+
%2 = cleanuppad within none []
56+
%3 = call i32 @llvm.wasm.memory.size.i32(i32 %phi)
57+
cleanupret from %2 unwind to caller
58+
}
59+
60+
; When longjmpable calls are within a catchpad.
61+
define void @longjmpable_invoke_phi1() personality ptr @__gxx_wasm_personality_v0 {
62+
; CHECK-LABEL: @longjmpable_invoke_phi1
63+
entry:
64+
%val.entry = call i32 @llvm.wasm.memory.size.i32(i32 0)
65+
%0 = call i32 @setjmp(ptr @buf) #2
66+
invoke void @foo()
67+
to label %bb1 unwind label %ehcleanup
68+
69+
bb1: ; preds = %entry
70+
%val.bb1 = call i32 @llvm.wasm.memory.size.i32(i32 0)
71+
invoke void @foo()
72+
to label %bb2 unwind label %catch.dispatch
73+
74+
bb2: ; preds = %bb1
75+
unreachable
76+
77+
catch.dispatch: ; preds = %bb1
78+
%1 = catchswitch within none [label %catch.start] unwind label %ehcleanup
79+
80+
catch.start: ; preds = %catch.dispatch
81+
%2 = catchpad within %1 [ptr null]
82+
%3 = call ptr @llvm.wasm.get.exception(token %2)
83+
%4 = call i32 @llvm.wasm.get.ehselector(token %2)
84+
call void @longjmpable() [ "funclet"(token %2) ]
85+
; CHECK: catch.start:
86+
; CHECK: invoke void @longjmpable
87+
; CHECK-NEXT: to label %.noexc unwind label %ehcleanup
88+
invoke void @foo() [ "funclet"(token %2) ]
89+
to label %bb3 unwind label %ehcleanup
90+
91+
bb3: ; preds = %catch.start
92+
%val.bb3 = call i32 @llvm.wasm.memory.size.i32(i32 0)
93+
call void @longjmpable() [ "funclet"(token %2) ]
94+
; CHECK: bb3:
95+
; CHECK: invoke void @longjmpable
96+
; CHECK-NEXT: to label %.noexc1 unwind label %ehcleanup
97+
invoke void @foo() [ "funclet"(token %2) ]
98+
to label %bb4 unwind label %ehcleanup
99+
100+
bb4: ; preds = %bb3
101+
unreachable
102+
103+
ehcleanup: ; preds = %bb3, %catch.start, %catch.dispatch, %entry
104+
%phi = phi i32 [ %val.entry, %entry ], [ %val.bb1, %catch.dispatch ], [ %val.bb1, %catch.start ], [ %val.bb3, %bb3 ]
105+
; CHECK: ehcleanup:
106+
; CHECK-NEXT: %phi = phi i32 [ %val.entry2, %entry.split.split ], [ %val.bb1, %catch.dispatch ], [ %val.bb1, %.noexc ], [ %val.bb3, %.noexc1 ], [ %val.bb1, %catch.start ], [ %val.bb3, %bb3 ]
107+
%5 = cleanuppad within none []
108+
%6 = call i32 @llvm.wasm.memory.size.i32(i32 %phi)
109+
cleanupret from %5 unwind to caller
110+
}
111+
112+
declare i32 @setjmp(ptr)
113+
declare i32 @__gxx_wasm_personality_v0(...)
114+
declare void @foo()
115+
declare void @longjmpable()
116+
declare void @use_i32(i32)
117+
; Function Attrs: nocallback nofree nosync nounwind willreturn
118+
declare i32 @llvm.wasm.get.ehselector(token) #0
119+
; Function Attrs: nocallback nofree nosync nounwind willreturn
120+
declare ptr @llvm.wasm.get.exception(token) #0
121+
; Function Attrs: nocallback nofree nosync nounwind willreturn memory(read)
122+
declare i32 @llvm.wasm.memory.size.i32(i32) #1
123+
124+
attributes #0 = { nocallback nofree nosync nounwind willreturn }
125+
attributes #1 = { nocallback nofree nosync nounwind willreturn memory(read) }
126+
attributes #2 = { returns_twice }

0 commit comments

Comments
 (0)