Skip to content

Commit c285a11

Browse files
committed
[WebAssembly] Make Emscripten EH work with Emscripten SjLj
When Emscripten EH mixes with Emscripten SjLj, we are not currently handling some of them correctly. There are three cases: 1. The current function calls `setjmp` and there is an `invoke` to a function that can either throw or longjmp. In this case, we have to check both for exception and longjmp. We are currently handling this case correctly: https://github.com/llvm/llvm-project/blob/0c0eb76782d5224b8d81a5afbb9a152bcf7c94c7/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp#L1058-L1090 When inserting routines for functions that can longjmp, which we do only for setjmp-calling functions, we check if the function was previously an `invoke` and handle it correctly. 2. The current function does NOT call `setjmp` and there is an `invoke` to a function that can either throw or longjmp. Because there is no `setjmp` call, we haven't been doing any check for functions that can longjmp. But in that case, for `invoke`, we only check for an exception and if it is not an exception we reset `__THREW__` to 0, which can silently swallow the longjmp: https://github.com/llvm/llvm-project/blob/0c0eb76782d5224b8d81a5afbb9a152bcf7c94c7/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp#L70-L80 This CL fixes this. 3. The current function calls `setjmp` and there is no `invoke`. Because it is not an `invoke`, we haven't been doing any check for functions that can throw, and only insert longjmp-checking routines for functions that can longjmp. But in that case, if a longjmpable function throws, we only check for a longjmp so if it is not a longjmp we reset `__THREW__` to 0, which can silently swallow the exception: https://github.com/llvm/llvm-project/blob/0c0eb76782d5224b8d81a5afbb9a152bcf7c94c7/llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp#L156-L169 This CL fixes this. To do that, this moves around some code, so we register necessary functions for both EH and SjLj and precompute some data (the set of functions that contains `setjmp`) before doing actual EH or SjLj transformation. This CL makes 2nd and 3rd tests in emscripten-core/emscripten#14732 work. Reviewed By: dschuff Differential Revision: https://reviews.llvm.org/D106525
1 parent 41b17c4 commit c285a11

File tree

4 files changed

+249
-63
lines changed

4 files changed

+249
-63
lines changed

llvm/lib/Target/WebAssembly/WebAssembly.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class ModulePass;
2525
class FunctionPass;
2626

2727
// LLVM IR passes.
28-
ModulePass *createWebAssemblyLowerEmscriptenEHSjLj(bool DoEH, bool DoSjLj);
28+
ModulePass *createWebAssemblyLowerEmscriptenEHSjLj(bool EnableEH,
29+
bool EnableSjLj);
2930
ModulePass *createWebAssemblyLowerGlobalDtors();
3031
ModulePass *createWebAssemblyAddMissingPrototypes();
3132
ModulePass *createWebAssemblyFixFunctionBitcasts();

llvm/lib/Target/WebAssembly/WebAssemblyLowerEmscriptenEHSjLj.cpp

+112-22
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ namespace {
216216
class WebAssemblyLowerEmscriptenEHSjLj final : public ModulePass {
217217
bool EnableEH; // Enable exception handling
218218
bool EnableSjLj; // Enable setjmp/longjmp handling
219+
bool DoSjLj; // Whether we actually perform setjmp/longjmp handling
219220

220221
GlobalVariable *ThrewGV = nullptr;
221222
GlobalVariable *ThrewValueGV = nullptr;
@@ -234,6 +235,8 @@ class WebAssemblyLowerEmscriptenEHSjLj final : public ModulePass {
234235
StringMap<Function *> InvokeWrappers;
235236
// Set of allowed function names for exception handling
236237
std::set<std::string> EHAllowlistSet;
238+
// Functions that contains calls to setjmp
239+
SmallPtrSet<Function *, 8> SetjmpUsers;
237240

238241
StringRef getPassName() const override {
239242
return "WebAssembly Lower Emscripten Exceptions";
@@ -252,6 +255,10 @@ class WebAssemblyLowerEmscriptenEHSjLj final : public ModulePass {
252255
bool areAllExceptionsAllowed() const { return EHAllowlistSet.empty(); }
253256
bool canLongjmp(Module &M, const Value *Callee) const;
254257
bool isEmAsmCall(Module &M, const Value *Callee) const;
258+
bool supportsException(const Function *F) const {
259+
return EnableEH && (areAllExceptionsAllowed() ||
260+
EHAllowlistSet.count(std::string(F->getName())));
261+
}
255262

256263
void rebuildSSA(Function &F);
257264

@@ -287,7 +294,7 @@ static bool canThrow(const Value *V) {
287294
return false;
288295
StringRef Name = F->getName();
289296
// leave setjmp and longjmp (mostly) alone, we process them properly later
290-
if (Name == "setjmp" || Name == "longjmp")
297+
if (Name == "setjmp" || Name == "longjmp" || Name == "emscripten_longjmp")
291298
return false;
292299
return !F->doesNotThrow();
293300
}
@@ -693,7 +700,7 @@ bool WebAssemblyLowerEmscriptenEHSjLj::runOnModule(Module &M) {
693700
Function *LongjmpF = M.getFunction("longjmp");
694701
bool SetjmpUsed = SetjmpF && !SetjmpF->use_empty();
695702
bool LongjmpUsed = LongjmpF && !LongjmpF->use_empty();
696-
bool DoSjLj = EnableSjLj && (SetjmpUsed || LongjmpUsed);
703+
DoSjLj = EnableSjLj && (SetjmpUsed || LongjmpUsed);
697704

698705
auto *TPC = getAnalysisIfAvailable<TargetPassConfig>();
699706
assert(TPC && "Expected a TargetPassConfig");
@@ -718,7 +725,7 @@ bool WebAssemblyLowerEmscriptenEHSjLj::runOnModule(Module &M) {
718725

719726
bool Changed = false;
720727

721-
// Exception handling
728+
// Function registration for exception handling
722729
if (EnableEH) {
723730
// Register __resumeException function
724731
FunctionType *ResumeFTy =
@@ -729,26 +736,15 @@ bool WebAssemblyLowerEmscriptenEHSjLj::runOnModule(Module &M) {
729736
FunctionType *EHTypeIDTy =
730737
FunctionType::get(IRB.getInt32Ty(), IRB.getInt8PtrTy(), false);
731738
EHTypeIDF = getEmscriptenFunction(EHTypeIDTy, "llvm_eh_typeid_for", &M);
732-
733-
for (Function &F : M) {
734-
if (F.isDeclaration())
735-
continue;
736-
Changed |= runEHOnFunction(F);
737-
}
738739
}
739740

740-
// Setjmp/longjmp handling
741+
// Function registration and data pre-gathering for setjmp/longjmp handling
741742
if (DoSjLj) {
742-
Changed = true; // We have setjmp or longjmp somewhere
743-
744743
// Register emscripten_longjmp function
745744
FunctionType *FTy = FunctionType::get(
746745
IRB.getVoidTy(), {getAddrIntType(&M), IRB.getInt32Ty()}, false);
747746
EmLongjmpF = getEmscriptenFunction(FTy, "emscripten_longjmp", &M);
748747

749-
if (LongjmpF)
750-
replaceLongjmpWithEmscriptenLongjmp(LongjmpF, EmLongjmpF);
751-
752748
if (SetjmpF) {
753749
// Register saveSetjmp function
754750
FunctionType *SetjmpFTy = SetjmpF->getFunctionType();
@@ -765,16 +761,33 @@ bool WebAssemblyLowerEmscriptenEHSjLj::runOnModule(Module &M) {
765761
false);
766762
TestSetjmpF = getEmscriptenFunction(FTy, "testSetjmp", &M);
767763

768-
// Only traverse functions that uses setjmp in order not to insert
769-
// unnecessary prep / cleanup code in every function
770-
SmallPtrSet<Function *, 8> SetjmpUsers;
764+
// Precompute setjmp users
771765
for (User *U : SetjmpF->users()) {
772766
auto *UI = cast<Instruction>(U);
773767
SetjmpUsers.insert(UI->getFunction());
774768
}
769+
}
770+
}
771+
772+
// Exception handling transformation
773+
if (EnableEH) {
774+
for (Function &F : M) {
775+
if (F.isDeclaration())
776+
continue;
777+
Changed |= runEHOnFunction(F);
778+
}
779+
}
780+
781+
// Setjmp/longjmp handling transformation
782+
if (DoSjLj) {
783+
Changed = true; // We have setjmp or longjmp somewhere
784+
if (LongjmpF)
785+
replaceLongjmpWithEmscriptenLongjmp(LongjmpF, EmLongjmpF);
786+
// Only traverse functions that uses setjmp in order not to insert
787+
// unnecessary prep / cleanup code in every function
788+
if (SetjmpF)
775789
for (Function *F : SetjmpUsers)
776790
runSjLjOnFunction(*F);
777-
}
778791
}
779792

780793
if (!Changed) {
@@ -802,8 +815,6 @@ bool WebAssemblyLowerEmscriptenEHSjLj::runEHOnFunction(Function &F) {
802815
bool Changed = false;
803816
SmallVector<Instruction *, 64> ToErase;
804817
SmallPtrSet<LandingPadInst *, 32> LandingPads;
805-
bool AllowExceptions = areAllExceptionsAllowed() ||
806-
EHAllowlistSet.count(std::string(F.getName()));
807818

808819
for (BasicBlock &BB : F) {
809820
auto *II = dyn_cast<InvokeInst>(BB.getTerminator());
@@ -813,12 +824,51 @@ bool WebAssemblyLowerEmscriptenEHSjLj::runEHOnFunction(Function &F) {
813824
LandingPads.insert(II->getLandingPadInst());
814825
IRB.SetInsertPoint(II);
815826

816-
bool NeedInvoke = AllowExceptions && canThrow(II->getCalledOperand());
827+
const Value *Callee = II->getCalledOperand();
828+
bool NeedInvoke = supportsException(&F) && canThrow(Callee);
817829
if (NeedInvoke) {
818830
// Wrap invoke with invoke wrapper and generate preamble/postamble
819831
Value *Threw = wrapInvoke(II);
820832
ToErase.push_back(II);
821833

834+
// If setjmp/longjmp handling is enabled, the thrown value can be not an
835+
// exception but a longjmp. If the current function contains calls to
836+
// setjmp, it will be appropriately handled in runSjLjOnFunction. But even
837+
// if the function does not contain setjmp calls, we shouldn't silently
838+
// ignore longjmps; we should rethrow them so they can be correctly
839+
// handled in somewhere up the call chain where setjmp is.
840+
// __THREW__'s value is 0 when nothing happened, 1 when an exception is
841+
// thrown, other values when longjmp is thrown.
842+
//
843+
// if (%__THREW__.val == 0 || %__THREW__.val == 1)
844+
// goto %tail
845+
// else
846+
// goto %longjmp.rethrow
847+
//
848+
// longjmp.rethrow: ;; This is longjmp. Rethrow it
849+
// %__threwValue.val = __threwValue
850+
// emscripten_longjmp(%__THREW__.val, %__threwValue.val);
851+
//
852+
// tail: ;; Nothing happened or an exception is thrown
853+
// ... Continue exception handling ...
854+
if (DoSjLj && !SetjmpUsers.count(&F) && canLongjmp(M, Callee)) {
855+
BasicBlock *Tail = BasicBlock::Create(C, "tail", &F);
856+
BasicBlock *RethrowBB = BasicBlock::Create(C, "longjmp.rethrow", &F);
857+
Value *CmpEqOne =
858+
IRB.CreateICmpEQ(Threw, getAddrSizeInt(&M, 1), "cmp.eq.one");
859+
Value *CmpEqZero =
860+
IRB.CreateICmpEQ(Threw, getAddrSizeInt(&M, 0), "cmp.eq.zero");
861+
Value *Or = IRB.CreateOr(CmpEqZero, CmpEqOne, "or");
862+
IRB.CreateCondBr(Or, Tail, RethrowBB);
863+
IRB.SetInsertPoint(RethrowBB);
864+
Value *ThrewValue = IRB.CreateLoad(IRB.getInt32Ty(), ThrewValueGV,
865+
ThrewValueGV->getName() + ".val");
866+
IRB.CreateCall(EmLongjmpF, {Threw, ThrewValue});
867+
868+
IRB.CreateUnreachable();
869+
IRB.SetInsertPoint(Tail);
870+
}
871+
822872
// Insert a branch based on __THREW__ variable
823873
Value *Cmp = IRB.CreateICmpEQ(Threw, getAddrSizeInt(&M, 1), "cmp");
824874
IRB.CreateCondBr(Cmp, II->getUnwindDest(), II->getNormalDest());
@@ -1098,6 +1148,46 @@ bool WebAssemblyLowerEmscriptenEHSjLj::runSjLjOnFunction(Function &F) {
10981148
Threw = wrapInvoke(CI);
10991149
ToErase.push_back(CI);
11001150
Tail = SplitBlock(BB, CI->getNextNode());
1151+
1152+
// If exception handling is enabled, the thrown value can be not a
1153+
// longjmp but an exception, in which case we shouldn't silently ignore
1154+
// exceptions; we should rethrow them.
1155+
// __THREW__'s value is 0 when nothing happened, 1 when an exception is
1156+
// thrown, other values when longjmp is thrown.
1157+
//
1158+
// if (%__THREW__.val == 1)
1159+
// goto %eh.rethrow
1160+
// else
1161+
// goto %normal
1162+
//
1163+
// eh.rethrow: ;; Rethrow exception
1164+
// %exn = call @__cxa_find_matching_catch_2() ;; Retrieve thrown ptr
1165+
// __resumeException(%exn)
1166+
//
1167+
// normal:
1168+
// <-- Insertion point. Will insert sjlj handling code from here
1169+
// goto %tail
1170+
//
1171+
// tail:
1172+
// ...
1173+
if (supportsException(&F) && canThrow(Callee)) {
1174+
IRB.SetInsertPoint(CI);
1175+
// We will add a new conditional branch. So remove the branch created
1176+
// when we split the BB
1177+
ToErase.push_back(BB->getTerminator());
1178+
BasicBlock *NormalBB = BasicBlock::Create(C, "normal", &F);
1179+
BasicBlock *RethrowBB = BasicBlock::Create(C, "eh.rethrow", &F);
1180+
Value *CmpEqOne =
1181+
IRB.CreateICmpEQ(Threw, getAddrSizeInt(&M, 1), "cmp.eq.one");
1182+
IRB.CreateCondBr(CmpEqOne, RethrowBB, NormalBB);
1183+
IRB.SetInsertPoint(RethrowBB);
1184+
CallInst *Exn = IRB.CreateCall(getFindMatchingCatch(M, 0), {}, "exn");
1185+
IRB.CreateCall(ResumeF, {Exn});
1186+
IRB.CreateUnreachable();
1187+
IRB.SetInsertPoint(NormalBB);
1188+
IRB.CreateBr(Tail);
1189+
BB = NormalBB; // New insertion point to insert testSetjmp()
1190+
}
11011191
}
11021192

11031193
// We need to replace the terminator in Tail - SplitBlock makes BB go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
; RUN: opt < %s -wasm-lower-em-ehsjlj -S | FileCheck %s
2+
; RUN: llc < %s
3+
4+
; Tests for cases when exception handling and setjmp/longjmp handling are mixed.
5+
6+
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128"
7+
target triple = "wasm32-unknown-unknown"
8+
9+
%struct.__jmp_buf_tag = type { [6 x i32], i32, [32 x i32] }
10+
11+
; There is a function call (@foo) that can either throw an exception or longjmp
12+
; and there is also a setjmp call. When @foo throws, we have to check both for
13+
; exception and longjmp and jump to exception or longjmp handling BB depending
14+
; on the result.
15+
define void @setjmp_longjmp_exception() personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
16+
; CHECK-LABEL: @setjmp_longjmp_exception
17+
entry:
18+
%buf = alloca [1 x %struct.__jmp_buf_tag], align 16
19+
%arraydecay = getelementptr inbounds [1 x %struct.__jmp_buf_tag], [1 x %struct.__jmp_buf_tag]* %buf, i32 0, i32 0
20+
%call = call i32 @setjmp(%struct.__jmp_buf_tag* %arraydecay) #0
21+
invoke void @foo()
22+
to label %try.cont unwind label %lpad
23+
24+
; CHECK: entry.split:
25+
; CHECK: %[[CMP0:.*]] = icmp ne i32 %__THREW__.val, 0
26+
; CHECK-NEXT: %__threwValue.val = load i32, i32* @__threwValue
27+
; CHECK-NEXT: %[[CMP1:.*]] = icmp ne i32 %__threwValue.val, 0
28+
; CHECK-NEXT: %[[CMP:.*]] = and i1 %[[CMP0]], %[[CMP1]]
29+
; CHECK-NEXT: br i1 %[[CMP]], label %if.then1, label %if.else1
30+
31+
; This is exception checking part. %if.else1 leads here
32+
; CHECK: entry.split.split:
33+
; CHECK-NEXT: %[[CMP:.*]] = icmp eq i32 %__THREW__.val, 1
34+
; CHECK-NEXT: br i1 %[[CMP]], label %lpad, label %try.cont
35+
36+
; longjmp checking part
37+
; CHECK: if.then1:
38+
; CHECK: call i32 @testSetjmp
39+
40+
lpad: ; preds = %entry
41+
%0 = landingpad { i8*, i32 }
42+
catch i8* null
43+
%1 = extractvalue { i8*, i32 } %0, 0
44+
%2 = extractvalue { i8*, i32 } %0, 1
45+
%3 = call i8* @__cxa_begin_catch(i8* %1) #2
46+
call void @__cxa_end_catch()
47+
br label %try.cont
48+
49+
try.cont: ; preds = %entry, %lpad
50+
ret void
51+
}
52+
53+
; @foo can either throw an exception or longjmp. Because this function doesn't
54+
; have any setjmp calls, we only handle exceptions in this function. But because
55+
; sjlj is enabled, we check if the thrown value is longjmp and if so rethrow it
56+
; by calling @emscripten_longjmp.
57+
define void @rethrow_longjmp() personality i8* bitcast (i32 (...)* @__gxx_personality_v0 to i8*) {
58+
; CHECK-LABEL: @rethrow_longjmp
59+
entry:
60+
invoke void @foo()
61+
to label %try.cont unwind label %lpad
62+
; CHECK: entry:
63+
; CHECK: %cmp.eq.one = icmp eq i32 %__THREW__.val, 1
64+
; CHECK-NEXT: %cmp.eq.zero = icmp eq i32 %__THREW__.val, 0
65+
; CHECK-NEXT: %or = or i1 %cmp.eq.zero, %cmp.eq.one
66+
; CHECK-NEXT: br i1 %or, label %tail, label %longjmp.rethrow
67+
68+
; CHECK: tail:
69+
; CHECK-NEXT: %cmp = icmp eq i32 %__THREW__.val, 1
70+
; CHECK-NEXT: br i1 %cmp, label %lpad, label %try.cont
71+
72+
; CHECK: longjmp.rethrow:
73+
; CHECK-NEXT: %__threwValue.val = load i32, i32* @__threwValue, align 4
74+
; CHECK-NEXT: call void @emscripten_longjmp(i32 %__THREW__.val, i32 %__threwValue.val)
75+
; CHECK-NEXT: unreachable
76+
77+
lpad: ; preds = %entry
78+
%0 = landingpad { i8*, i32 }
79+
catch i8* null
80+
%1 = extractvalue { i8*, i32 } %0, 0
81+
%2 = extractvalue { i8*, i32 } %0, 1
82+
%3 = call i8* @__cxa_begin_catch(i8* %1) #5
83+
call void @__cxa_end_catch()
84+
br label %try.cont
85+
86+
try.cont: ; preds = %entry, %lpad
87+
ret void
88+
}
89+
90+
; This function contains a setjmp call and no invoke, so we only handle longjmp
91+
; here. But @foo can also throw an exception, so we check if an exception is
92+
; thrown and if so rethrow it by calling @__resumeException.
93+
define void @rethrow_exception() {
94+
; CHECK-LABEL: @rethrow_exception
95+
entry:
96+
%buf = alloca [1 x %struct.__jmp_buf_tag], align 16
97+
%arraydecay = getelementptr inbounds [1 x %struct.__jmp_buf_tag], [1 x %struct.__jmp_buf_tag]* %buf, i32 0, i32 0
98+
%call = call i32 @setjmp(%struct.__jmp_buf_tag* %arraydecay) #0
99+
%cmp = icmp ne i32 %call, 0
100+
br i1 %cmp, label %return, label %if.end
101+
102+
if.end: ; preds = %entry
103+
call void @foo()
104+
br label %return
105+
106+
; CHECK: if.end:
107+
; CHECK: %cmp.eq.one = icmp eq i32 %__THREW__.val, 1
108+
; CHECK-NEXT: br i1 %cmp.eq.one, label %eh.rethrow, label %normal
109+
110+
; CHECK: normal:
111+
; CHECK-NEXT: icmp ne i32 %__THREW__.val, 0
112+
113+
; CHECK: eh.rethrow:
114+
; CHECK-NEXT: %exn = call i8* @__cxa_find_matching_catch_2()
115+
; CHECK-NEXT: call void @__resumeException(i8* %exn)
116+
; CHECK-NEXT: unreachable
117+
118+
return: ; preds = %entry, %if.end
119+
ret void
120+
}
121+
122+
declare void @foo()
123+
; Function Attrs: returns_twice
124+
declare i32 @setjmp(%struct.__jmp_buf_tag*)
125+
; Function Attrs: noreturn
126+
declare void @longjmp(%struct.__jmp_buf_tag*, i32)
127+
declare i32 @__gxx_personality_v0(...)
128+
declare i8* @__cxa_begin_catch(i8*)
129+
declare void @__cxa_end_catch()
130+
131+
attributes #0 = { returns_twice }
132+
attributes #1 = { noreturn }

0 commit comments

Comments
 (0)