[llvm] [WebAssembly] Fix: fixCallUnwindMismatches after fixCatchUnwindMismatches (PR #187484)

Hood Chatham via llvm-commits llvm-commits at lists.llvm.org
Thu Apr 2 16:49:55 PDT 2026


https://github.com/hoodmane updated https://github.com/llvm/llvm-project/pull/187484

>From 0b5715d54e60a2ffed5704a1222502cc6c92f614 Mon Sep 17 00:00:00 2001
From: Hood Chatham <roberthoodchatham at gmail.com>
Date: Thu, 19 Mar 2026 11:45:58 +0100
Subject: [PATCH 1/2] [WebAssembly] Fix: Run `fixCallUnwindMismatches()` after
 `fixCatchUnwindMismatches()`

 `fixCallUnwindMismatches()` adds an extra try block around call sites with
incorrect unwind targets. `fixCatchUnwindMismatches()` handles catch blocks
that have incorrect next unwind destinations. Previously we ran
`fixCallUnwindMismatches()` first and then ran `fixCatchUnwindMismatches()`. The
problem is that `fixCatchUnwindMismatches()` wraps entire try blocks which can
change the unwind destination of the calls inside. If the calls had an incorrect
unwind target to begin with, they will be wrapped already and so the outer
wrapping can't alter their unwind target. However, if they have a correct unwind
target, they won't get wrapped and then that can be messed up by
`fixCatchUnwindMismatches()`.

The fix is to run `fixCatchUnwindMismatches()` first.
`fixCallUnwindMismatches()` never messes up the result of
`fixCatchUnwindMismatches()` so this is the correct order.

Resolves #187302
---
 .../WebAssembly/WebAssemblyCFGStackify.cpp    |  17 ++-
 .../WebAssembly/cfg-stackify-eh-legacy.ll     |  10 +-
 .../CodeGen/WebAssembly/cfg-stackify-eh.ll    |   2 +-
 .../CodeGen/WebAssembly/exception-legacy.ll   | 105 +++++++++++++++++-
 llvm/test/CodeGen/WebAssembly/exception.ll    |  49 ++++----
 5 files changed, 148 insertions(+), 35 deletions(-)

diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp b/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp
index 7fd2fb692549e..6ff5b091ede5a 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp
@@ -1836,6 +1836,8 @@ bool WebAssemblyCFGStackify::fixCallUnwindMismatches(MachineFunction &MF) {
     for (auto &MI : reverse(MBB)) {
       if (WebAssembly::isTry(MI.getOpcode()))
         EHPadStack.pop_back();
+      else if (MI.getOpcode() == WebAssembly::DELEGATE)
+        EHPadStack.push_back(MI.getOperand(0).getMBB());
       else if (WebAssembly::isCatch(MI.getOpcode()))
         EHPadStack.push_back(MI.getParent());
 
@@ -1847,6 +1849,8 @@ bool WebAssemblyCFGStackify::fixCallUnwindMismatches(MachineFunction &MF) {
           !WebAssembly::mayThrow(MI))
         continue;
       SeenThrowableInstInBB = true;
+      LLVM_DEBUG(dbgs() << "- fixCallUnwindMismatches considering: " << MI
+                        << "  in MBB = " << MBB.getName() << "\n");
 
       // If the EH pad on the stack top is where this instruction should unwind
       // next, we're good.
@@ -1923,8 +1927,10 @@ bool WebAssemblyCFGStackify::fixCallUnwindMismatches(MachineFunction &MF) {
         RecordCallerMismatchRange(EHPadStack.back());
 
       // If EHPadStack is empty, that means it correctly unwinds to the caller
-      // if it throws, so we're good. If MI does not throw, we're good too.
-      else if (EHPadStack.empty() || !MayThrow) {
+      // if it throws, so we're good. A delegate targeting FakeCallerBB also
+      // correctly unwinds to the caller. If MI does not throw, we're good too.
+      else if (EHPadStack.empty() || EHPadStack.back() == FakeCallerBB ||
+               !MayThrow) {
       }
 
       // We found an instruction that unwinds to the caller but currently has an
@@ -1940,6 +1946,8 @@ bool WebAssemblyCFGStackify::fixCallUnwindMismatches(MachineFunction &MF) {
       // Update EHPadStack.
       if (WebAssembly::isTry(MI.getOpcode()))
         EHPadStack.pop_back();
+      else if (MI.getOpcode() == WebAssembly::DELEGATE)
+        EHPadStack.push_back(MI.getOperand(0).getMBB());
       else if (WebAssembly::isCatch(MI.getOpcode()))
         EHPadStack.push_back(MI.getParent());
     }
@@ -2474,8 +2482,11 @@ void WebAssemblyCFGStackify::placeMarkers(MachineFunction &MF) {
     // Add an 'unreachable' after 'end_try_table's.
     addUnreachableAfterTryTables(MF, TII);
     // Fix mismatches in unwind destinations induced by linearizing the code.
-    fixCallUnwindMismatches(MF);
+    // Run fixCatchUnwindMismatches() first so that fixCallUnwindMismatches()
+    // will see and correct any new call/rethrow unwind mismatches introduced by
+    // fixCatchUnwindMismatches().
     fixCatchUnwindMismatches(MF);
+    fixCallUnwindMismatches(MF);
     // addUnreachableAfterTryTables and fixUnwindMismatches create new BBs, so
     // we need to recalculate ScopeTops.
     recalculateScopeTops(MF);
diff --git a/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh-legacy.ll b/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh-legacy.ll
index f0a1d9805c806..b104c48d89273 100644
--- a/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh-legacy.ll
+++ b/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh-legacy.ll
@@ -659,11 +659,9 @@ try.cont:                                         ; preds = %catch.start0
 ; --- try-delegate ends (call unwind mismatch)
 ; NOSORT:     catch
 ; NOSORT:       call  {{.*}} __cxa_begin_catch
-; --- try-delegate starts (call unwind mismatch)
-; NOSORT:       try
-; NOSORT:         call  __cxa_end_catch
-; NOSORT:       delegate    3            # label/catch{{[0-9]+}}: to caller
-; --- try-delegate ends (call unwind mismatch)
+; __cxa_end_catch doesn't need its own try-delegate because the enclosing
+; catch-mismatch delegate already routes to caller.
+; NOSORT:       call  __cxa_end_catch
 ; NOSORT:     end_try
 ; NOSORT:   delegate    1                # label/catch{{[0-9]+}}: to caller
 ; --- try-delegate ends (catch unwind mismatch)
@@ -1736,7 +1734,7 @@ unreachable:                                      ; preds = %rethrow, %entry
 }
 
 ; Check if the unwind destination mismatch stats are correct
-; NOSORT: 24 wasm-cfg-stackify    - Number of call unwind mismatches found
+; NOSORT: 23 wasm-cfg-stackify    - Number of call unwind mismatches found
 ; NOSORT:  5 wasm-cfg-stackify    - Number of catch unwind mismatches found
 
 declare void @foo()
diff --git a/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh.ll b/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh.ll
index 98de9a267b95a..93937fd96d887 100644
--- a/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh.ll
+++ b/llvm/test/CodeGen/WebAssembly/cfg-stackify-eh.ll
@@ -1507,7 +1507,7 @@ unreachable:                                      ; preds = %rethrow, %entry
 }
 
 ; Check if the unwind destination mismatch stats are correct
-; NOSORT: 23 wasm-cfg-stackify    - Number of call unwind mismatches found
+; NOSORT: 24 wasm-cfg-stackify    - Number of call unwind mismatches found
 ; NOSORT:  4 wasm-cfg-stackify    - Number of catch unwind mismatches found
 
 declare void @foo()
diff --git a/llvm/test/CodeGen/WebAssembly/exception-legacy.ll b/llvm/test/CodeGen/WebAssembly/exception-legacy.ll
index b96a664166f42..ab47034084e75 100644
--- a/llvm/test/CodeGen/WebAssembly/exception-legacy.ll
+++ b/llvm/test/CodeGen/WebAssembly/exception-legacy.ll
@@ -429,16 +429,16 @@ unreachable:                                      ; preds = %rethrow
 ; CHECK:   try
 ; CHECK:     try
 ; CHECK:       call  __cxa_throw
-; CHECK:       catch
+; CHECK:     catch   {{.*}} __cpp_exception
+; CHECK:       call  $drop=, __cxa_begin_catch
 ; CHECK:       call  __cxa_end_catch
 ; CHECK:       try
 ; CHECK:         try
 ; Note that this rethrow targets the top-level catch_all
 ; CHECK:           rethrow   4
-; CHECK:         catch
-; CHECK:           try
-; CHECK:             call  __cxa_end_catch
-; CHECK:           delegate    5
+; CHECK:         catch   {{.*}} __cpp_exception
+; CHECK:           call  $drop=, __cxa_begin_catch
+; CHECK:           call  __cxa_end_catch
 ; CHECK:           return
 ; CHECK:         end_try
 ; CHECK:       delegate    3
@@ -569,6 +569,101 @@ declare void @__cxa_throw(ptr, ptr, ptr) #1
 declare void @_ZSt9terminatev()
 declare ptr @_ZN4TempD2Ev(ptr returned)
 
+; Regression test for issue #187302: fixCallUnwindMismatches() was run first and
+; found that the rethrow had the correct unwind target so did not wrap it in a
+; try/delegate. Then fixCatchUnwindMismatches() added a try/delegate for
+;
+; invoke void @do_catch unwind label %terminate_catchswitch
+;
+; because otherwise it was going to unwind to outer_catchswitch rather than
+; terminate_catchswitch. But this made the rethrow incorrectly transfer control
+; to terminate_catchswitch rather than outer_catchswitch.
+; Fixed by running fixCatchUnwindMismatches() before fixCallUnwindMismatches().
+;
+; This pattern occurs in Rust's catch_unwind inside a destructor.
+
+; CHECK-LABEL: catch_unwind_in_cleanup:
+; CHECK:        catch_all
+; CHECK:          try
+; CHECK:            try
+; CHECK:              call  resume_unwind
+; CHECK:            catch   {{.*}} __cpp_exception
+; CHECK:              call  do_catch
+; end_cleanup and rethrow are inside the inner catch's scope, but each gets
+; its own try-delegate to route exceptions to the correct destination rather
+; than going through the catch-mismatch delegate to the terminate handler.
+; CHECK:              try
+; CHECK:                call  end_cleanup
+; CHECK:              delegate    7
+;                                (caller)
+; CHECK:              try
+; CHECK:                rethrow   7
+;                                (Rethrow exception caught by outermost catch_all)
+; CHECK:              delegate    2
+;                                (outer_catchswitch)
+; CHECK:            end_try
+; CHECK:          delegate    3
+;                            (terminate)
+; CHECK:          catch   {{.*}} __cpp_exception
+; CHECK:            call  do_catch
+; CHECK:            return
+define void @catch_unwind_in_cleanup() personality ptr @__gxx_wasm_personality_v0 {
+start:
+  invoke void @resume_unwind(i32 1)
+          to label %unreachable unwind label %outer_cleanuppad
+
+outer_cleanuppad:
+  %outer_pad = cleanuppad within none []
+  call void @start_cleanup() [ "funclet"(token %outer_pad) ]
+  invoke void @resume_unwind(i32 2) [ "funclet"(token %outer_pad) ]
+          to label %unreachable unwind label %inner_catchswitch
+
+inner_catchswitch:
+  %inner_cs = catchswitch within %outer_pad [label %inner_catchpad] unwind label %terminate_catchswitch
+
+inner_catchpad:
+  %inner_cp = catchpad within %inner_cs [ptr null]
+  %exn = call ptr @llvm.wasm.get.exception(token %inner_cp)
+  %sel = call i32 @llvm.wasm.get.ehselector(token %inner_cp)
+  invoke void @do_catch(ptr %exn, i32 2) [ "funclet"(token %inner_cp) ]
+          to label %inner_catchret unwind label %terminate_catchswitch
+
+terminate_catchswitch:
+  %term_cs = catchswitch within %outer_pad [label %terminate_catchpad] unwind label %outer_catchswitch
+
+terminate_catchpad:
+  %term_pad = catchpad within %term_cs [ptr null]
+  call void @panic_in_cleanup() [ "funclet"(token %term_pad) ]
+  unreachable
+
+inner_catchret:
+  catchret from %inner_cp to label %outer_cleanupret
+
+outer_cleanupret:
+  call void @end_cleanup() [ "funclet"(token %outer_pad) ]
+  cleanupret from %outer_pad unwind label %outer_catchswitch
+
+outer_catchswitch:
+  %outer_cs = catchswitch within none [label %outer_catchpad] unwind to caller
+outer_catchpad:
+  %outer_cp = catchpad within %outer_cs [ptr null]
+  %exn2 = call ptr @llvm.wasm.get.exception(token %outer_cp)
+  %sel2 = call i32 @llvm.wasm.get.ehselector(token %outer_cp)
+  call void @do_catch(ptr %exn2, i32 1) [ "funclet"(token %outer_cp) ]
+  catchret from %outer_cp to label %done
+
+done:
+  ret void
+unreachable:
+  unreachable
+}
+
+declare void @resume_unwind(i32) #1
+declare void @do_catch(ptr, i32) #0
+declare void @start_cleanup()
+declare void @end_cleanup()
+declare void @panic_in_cleanup() #2
+
 attributes #0 = { nounwind }
 attributes #1 = { noreturn }
 attributes #2 = { noreturn nounwind }
diff --git a/llvm/test/CodeGen/WebAssembly/exception.ll b/llvm/test/CodeGen/WebAssembly/exception.ll
index 873386b3dcc6d..5cdabfca4cc29 100644
--- a/llvm/test/CodeGen/WebAssembly/exception.ll
+++ b/llvm/test/CodeGen/WebAssembly/exception.ll
@@ -484,34 +484,43 @@ unreachable:                                      ; preds = %rethrow
 ; try_table (catch_all_ref 0)'s caught exception is stored in local 2
 ; CHECK:     local.set  2
 ; CHECK:     block
+; catch_all 0 dispatches to %terminate.i
 ; CHECK:       try_table    (catch_all 0)
-; CHECK:         block
-; CHECK:           block     i32
-; CHECK:             try_table    (catch __cpp_exception 0)
-; CHECK:               call  __cxa_throw
+; CHECK:         block     exnref
+; CHECK:           block
+; CHECK:             block     i32
+; CHECK:               try_table    (catch __cpp_exception 0)
+; CHECK:                 call  __cxa_throw
+; CHECK:               end_try_table
+; CHECK:             end_block
+;
+; invoke __cxa_end_catch... unwind label %terminate.i
+; should unwind to %terminate.i. catch_all_ref 1 points to the
+; try_table (catch_all 0) which in turn dispatches to %terminate.i
+; CHECK:             try_table    (catch_all_ref 1)
+; CHECK:               call  __cxa_end_catch
 ; CHECK:             end_try_table
-; CHECK:           end_block
-; CHECK:           call  __cxa_end_catch
-; CHECK:           block     i32
-; CHECK:             try_table    (catch_all_ref 5)
-; CHECK:               try_table    (catch __cpp_exception 1)
+; CHECK:             block     i32
+; CHECK:               try_table    (catch_all_ref 6)
+; CHECK:                 try_table    (catch __cpp_exception 1)
 ; Note that the throw_ref below targets the top-level catch_all_ref (local 2)
-; CHECK:                 local.get  2
-; CHECK:                 throw_ref
+; CHECK:                   local.get  2
+; CHECK:                   throw_ref
+; CHECK:                 end_try_table
 ; CHECK:               end_try_table
+; CHECK:             end_block
+; CHECK:             try_table    (catch_all_ref 5)
+; CHECK:               call  __cxa_end_catch
 ; CHECK:             end_try_table
+; CHECK:             return
 ; CHECK:           end_block
-; CHECK:           try_table    (catch_all_ref 4)
-; CHECK:             call  __cxa_end_catch
-; CHECK:           end_try_table
-; CHECK:           return
-; CHECK:         end_block
-; CHECK:       end_try_table
+; CHECK:           throw_ref
+; CHECK:         end_try_table
+; CHECK:       end_block
+; CHECK:       call  _ZSt9terminatev
 ; CHECK:     end_block
-; CHECK:     call  _ZSt9terminatev
 ; CHECK:   end_block
-; CHECK: end_block
-; CHECK: throw_ref
+; CHECK:   throw_ref
 define void @inlined_cleanupret() personality ptr @__gxx_wasm_personality_v0 {
 entry:
   %exception = tail call ptr @__cxa_allocate_exception(i32 4)

>From 8a445dd976de3ad021f05230cb438522b1a80837 Mon Sep 17 00:00:00 2001
From: Hood Chatham <roberthoodchatham at gmail.com>
Date: Thu, 2 Apr 2026 16:49:46 -0700
Subject: [PATCH 2/2] Update
 llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp

Co-authored-by: Heejin Ahn <aheejin at gmail.com>
---
 llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp | 2 --
 1 file changed, 2 deletions(-)

diff --git a/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp b/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp
index 6ff5b091ede5a..f85dcde2a3cb7 100644
--- a/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyCFGStackify.cpp
@@ -1849,8 +1849,6 @@ bool WebAssemblyCFGStackify::fixCallUnwindMismatches(MachineFunction &MF) {
           !WebAssembly::mayThrow(MI))
         continue;
       SeenThrowableInstInBB = true;
-      LLVM_DEBUG(dbgs() << "- fixCallUnwindMismatches considering: " << MI
-                        << "  in MBB = " << MBB.getName() << "\n");
 
       // If the EH pad on the stack top is where this instruction should unwind
       // next, we're good.



More information about the llvm-commits mailing list