[clang] [WinEH] Fix crash when deleting C++ objects inside SEH __try (PR #180144)

via cfe-commits cfe-commits at lists.llvm.org
Wed Apr 29 08:57:00 PDT 2026


https://github.com/GkvJwa updated https://github.com/llvm/llvm-project/pull/180144

>From 01715d5bf4ec7ac668e888ef21d48b73c5fe7f12 Mon Sep 17 00:00:00 2001
From: GkvJwa <gkvjwa at gmail.com>
Date: Wed, 29 Apr 2026 20:28:37 +0800
Subject: [PATCH] [WinEH] Fix crash when deleting C++ objects inside SEH __try

When compiling with -EHa, delete expressions inside __try could create
C++ cleanup scopes that were confused with SEH finally cleanups. This
could produce mismatched seh.scope/try markers and invalid IR.

Track SEH finally cleanups explicitly and emit the appropriate
seh.try.end or seh.scope.end based on the cleanup kind.
---
 clang/lib/CodeGen/CGCleanup.cpp               |  25 ++--
 clang/lib/CodeGen/CGCleanup.h                 |   7 +
 clang/lib/CodeGen/CGException.cpp             |   6 +-
 clang/lib/CodeGen/CGExprCXX.cpp               |  12 ++
 clang/lib/CodeGen/EHScopeStack.h              |   3 +
 .../CodeGen/windows-seh-EHa-TryInFinally.cpp  | 123 +++++++++++++++++-
 clang/test/CodeGenCXX/exceptions-seh.cpp      |  25 ++++
 7 files changed, 183 insertions(+), 18 deletions(-)

diff --git a/clang/lib/CodeGen/CGCleanup.cpp b/clang/lib/CodeGen/CGCleanup.cpp
index 9adc928c11585..12042a292d8fe 100644
--- a/clang/lib/CodeGen/CGCleanup.cpp
+++ b/clang/lib/CodeGen/CGCleanup.cpp
@@ -159,6 +159,7 @@ void *EHScopeStack::pushCleanup(CleanupKind Kind, size_t Size) {
   bool IsEHCleanup = Kind & EHCleanup;
   bool IsLifetimeMarker = Kind & LifetimeMarker;
   bool IsFakeUse = Kind & FakeUse;
+  bool IsSEHFinallyCleanup = Kind & SEHFinallyCleanup;
 
   // Per C++ [except.terminate], it is implementation-defined whether none,
   // some, or all cleanups are called before std::terminate. Thus, when
@@ -183,6 +184,8 @@ void *EHScopeStack::pushCleanup(CleanupKind Kind, size_t Size) {
     Scope->setLifetimeMarker();
   if (IsFakeUse)
     Scope->setFakeUse();
+  if (IsSEHFinallyCleanup)
+    Scope->setSEHFinallyCleanup();
 
   // With Windows -EHa, Invoke llvm.seh.scope.begin() for EHCleanup
   // If exceptions are disabled/ignored and SEH is not in use, then there is no
@@ -191,7 +194,8 @@ void *EHScopeStack::pushCleanup(CleanupKind Kind, size_t Size) {
   // consistent with MSVC's behavior, except in the presence of -EHa.
   // Check getInvokeDest() to generate llvm.seh.scope.begin() as needed.
   if (CGF->getLangOpts().EHAsynch && IsEHCleanup && !IsLifetimeMarker &&
-      CGF->getTarget().getCXXABI().isMicrosoft() && CGF->getInvokeDest())
+      !IsSEHFinallyCleanup && CGF->getTarget().getCXXABI().isMicrosoft() &&
+      CGF->getInvokeDest())
     CGF->EmitSehCppScopeBegin();
 
   return Scope->getCleanupBuffer();
@@ -784,7 +788,7 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough,
 
   // Under -EHa, invoke seh.scope.end() to mark scope end before dtor
   bool IsEHa = getLangOpts().EHAsynch && !Scope.isLifetimeMarker();
-  const EHPersonality &Personality = EHPersonality::get(*this);
+  bool IsSEHFinallyCleanup = Scope.isSEHFinallyCleanup();
   if (!RequiresNormalCleanup) {
     // Mark CPP scope end for passed-by-value Arg temp
     //   per Windows ABI which is "normally" Cleanup in callee
@@ -794,7 +798,7 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough,
       // block.
       if (NormalDeactivateOrigIP.isSet())
         Builder.restoreIP(NormalDeactivateOrigIP);
-      if (Personality.isMSVCXXPersonality() && Builder.GetInsertBlock())
+      if (Builder.GetInsertBlock() && !IsSEHFinallyCleanup)
         EmitSehCppScopeEnd();
       if (NormalDeactivateOrigIP.isSet())
         NormalDeactivateOrigIP = Builder.saveAndClearIP();
@@ -810,10 +814,10 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough,
 
       // mark SEH scope end for fall-through flow
       if (IsEHa && getInvokeDest()) {
-        if (Personality.isMSVCXXPersonality())
-          EmitSehCppScopeEnd();
-        else
+        if (Scope.isSEHFinallyCleanup())
           EmitSehTryScopeEnd();
+        else
+          EmitSehCppScopeEnd();
       }
 
       destroyOptimisticNormalEntry(*this, Scope);
@@ -852,10 +856,10 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough,
 
       // intercept normal cleanup to mark SEH scope end
       if (IsEHa && getInvokeDest()) {
-        if (Personality.isMSVCXXPersonality())
-          EmitSehCppScopeEnd();
-        else
+        if (Scope.isSEHFinallyCleanup())
           EmitSehTryScopeEnd();
+        else
+          EmitSehCppScopeEnd();
       }
 
       // III.  Figure out where we're going and build the cleanup
@@ -1055,7 +1059,8 @@ void CodeGenFunction::PopCleanupBlock(bool FallthroughIsBranchThrough,
       EHStack.pushTerminate();
       PushedTerminate = true;
     } else if (IsEHa && getInvokeDest()) {
-      EmitSehCppScopeEnd();
+      if (!IsSEHFinallyCleanup)
+        EmitSehCppScopeEnd();
     }
 
     // We only actually emit the cleanup code if the cleanup is either
diff --git a/clang/lib/CodeGen/CGCleanup.h b/clang/lib/CodeGen/CGCleanup.h
index ba78e5478ac37..ded8be13c5fae 100644
--- a/clang/lib/CodeGen/CGCleanup.h
+++ b/clang/lib/CodeGen/CGCleanup.h
@@ -99,6 +99,9 @@ class EHScope {
     LLVM_PREFERRED_TYPE(bool)
     unsigned TestFlagInEHCleanup : 1;
 
+    LLVM_PREFERRED_TYPE(bool)
+    unsigned IsSEHFinallyCleanup : 1;
+
     /// The amount of extra storage needed by the Cleanup.
     /// Always a multiple of the scope-stack alignment.
     unsigned CleanupSize : 12;
@@ -357,6 +360,7 @@ class alignas(8) EHCleanupScope : public EHScope {
     CleanupBits.IsActive = true;
     CleanupBits.IsLifetimeMarker = false;
     CleanupBits.IsFakeUse = false;
+    CleanupBits.IsSEHFinallyCleanup = false;
     CleanupBits.TestFlagInNormalCleanup = false;
     CleanupBits.TestFlagInEHCleanup = false;
     CleanupBits.CleanupSize = cleanupSize;
@@ -392,6 +396,9 @@ class alignas(8) EHCleanupScope : public EHScope {
   bool isFakeUse() const { return CleanupBits.IsFakeUse; }
   void setFakeUse() { CleanupBits.IsFakeUse = true; }
 
+  bool isSEHFinallyCleanup() const { return CleanupBits.IsSEHFinallyCleanup; }
+  void setSEHFinallyCleanup() { CleanupBits.IsSEHFinallyCleanup = true; }
+
   bool hasActiveFlag() const { return ActiveFlag.isValid(); }
   Address getActiveFlag() const {
     return ActiveFlag;
diff --git a/clang/lib/CodeGen/CGException.cpp b/clang/lib/CodeGen/CGException.cpp
index f7c6f7521c98e..0576582d34543 100644
--- a/clang/lib/CodeGen/CGException.cpp
+++ b/clang/lib/CodeGen/CGException.cpp
@@ -2181,7 +2181,8 @@ llvm::Value *CodeGenFunction::EmitSEHAbnormalTermination() {
 
 void CodeGenFunction::pushSEHCleanup(CleanupKind Kind,
                                      llvm::Function *FinallyFunc) {
-  EHStack.pushCleanup<PerformSEHFinally>(Kind, FinallyFunc);
+  EHStack.pushCleanup<PerformSEHFinally>(
+      static_cast<CleanupKind>(Kind | SEHFinallyCleanup), FinallyFunc);
 }
 
 void CodeGenFunction::EnterSEHTryStmt(const SEHTryStmt &S) {
@@ -2193,7 +2194,8 @@ void CodeGenFunction::EnterSEHTryStmt(const SEHTryStmt &S) {
         HelperCGF.GenerateSEHFinallyFunction(*this, *Finally);
 
     // Push a cleanup for __finally blocks.
-    EHStack.pushCleanup<PerformSEHFinally>(NormalAndEHCleanup, FinallyFunc);
+    EHStack.pushCleanup<PerformSEHFinally>(NormalAndEHSEHFinallyCleanup,
+                                           FinallyFunc);
     return;
   }
 
diff --git a/clang/lib/CodeGen/CGExprCXX.cpp b/clang/lib/CodeGen/CGExprCXX.cpp
index 82300c3ede183..c585523f2718f 100644
--- a/clang/lib/CodeGen/CGExprCXX.cpp
+++ b/clang/lib/CodeGen/CGExprCXX.cpp
@@ -1720,6 +1720,18 @@ llvm::Value *CodeGenFunction::EmitCXXNewExpr(const CXXNewExpr *E) {
   llvm::Instruction *cleanupDominator = nullptr;
   if (E->getOperatorDelete() &&
       !E->getOperatorDelete()->isReservedGlobalPlacementOperator()) {
+    // A potentially-throwing constructor inside __try requires C++ object
+    // unwinding, which is incompatible with SEH.
+    if (getLangOpts().CXXExceptions && currentFunctionUsesSEHTry()) {
+      if (const auto *ConstructExpr = E->getConstructExpr()) {
+        const auto *FPT = ConstructExpr->getConstructor()
+                              ->getType()
+                              ->castAs<FunctionProtoType>();
+        if (!FPT->isNothrow())
+          getContext().getDiagnostics().Report(E->getBeginLoc(),
+                                               diag::err_seh_object_unwinding);
+      }
+    }
     EnterNewDeleteCleanup(*this, E, TypeIdentityArg, allocation, allocSize,
                           allocAlign, allocatorArgs);
     operatorDeleteCleanup = EHStack.stable_begin();
diff --git a/clang/lib/CodeGen/EHScopeStack.h b/clang/lib/CodeGen/EHScopeStack.h
index b9b8021191d62..2f5e056e7db3b 100644
--- a/clang/lib/CodeGen/EHScopeStack.h
+++ b/clang/lib/CodeGen/EHScopeStack.h
@@ -92,6 +92,9 @@ enum CleanupKind : unsigned {
   // markers chiefly to be ignored in most contexts.
   FakeUse = 0x10,
   NormalFakeUse = FakeUse | NormalCleanup,
+
+  SEHFinallyCleanup = 0x20,
+  NormalAndEHSEHFinallyCleanup = SEHFinallyCleanup | NormalAndEHCleanup,
 };
 
 /// A stack of scopes which respond to exceptions, including cleanups
diff --git a/clang/test/CodeGen/windows-seh-EHa-TryInFinally.cpp b/clang/test/CodeGen/windows-seh-EHa-TryInFinally.cpp
index 462ba9afb5b30..560f7e84fdf19 100644
--- a/clang/test/CodeGen/windows-seh-EHa-TryInFinally.cpp
+++ b/clang/test/CodeGen/windows-seh-EHa-TryInFinally.cpp
@@ -43,11 +43,11 @@ int main() {
 }
 
 // CHECK-LABEL:@"?foo@@YAXXZ"()
-// CHECK: invoke.cont:
 // CHECK: invoke void @llvm.seh.try.begin()
+// CHECK: invoke.cont:
 // CHECK: store volatile i32 1, ptr %cleanup.dest.slot
 // CHECK: invoke void @llvm.seh.try.end()
-// CHECK: invoke.cont2:
+// CHECK: invoke.cont1:
 // CHECK: %cleanup.dest = load i32, ptr %cleanup.dest.slot
 // CHECK: %1 = icmp ne i32 %cleanup.dest, 0
 // CHECK: %2 = zext i1 %1 to i8
@@ -69,14 +69,13 @@ void foo()
 }
 
 // CHECK-LABEL:@"?bar@@YAHXZ"()
-// CHECK: invoke.cont:
 // CHECK: invoke void @llvm.seh.try.begin()
-// CHECK: invoke.cont1:
+// CHECK: invoke.cont:
 // CHECK: store volatile i32 1, ptr %cleanup.dest.slot
 // CHECK: invoke void @llvm.seh.try.end()
-// CHECK: invoke.cont2:
+// CHECK: invoke.cont1:
 // CHECK: call void @"?fin$0 at 0@bar@@"
-// CHECK: %cleanup.dest3 = load i32, ptr %cleanup.dest.slot
+// CHECK: %cleanup.dest2 = load i32, ptr %cleanup.dest.slot
 // CHECK: return:
 // CHECK: ret i32 11
 int bar()
@@ -90,3 +89,115 @@ int bar()
     }
   }
 }
+
+// CHECK-LABEL: @"?foo1@@YAXXZ"()
+//
+// CHECK-NOT: invoke void @llvm.seh.scope.begin()
+// CHECK:     invoke void @llvm.seh.try.begin()
+//
+// CHECK-NOT: invoke void @llvm.seh.try.begin()
+// CHECK:     invoke.cont1:
+//
+// CHECK:     invoke void @llvm.seh.scope.begin()
+// CHECK:     invoke noundef ptr @"??0A@@QEAA at XZ"
+//
+// CHECK:     invoke void @llvm.seh.scope.end()
+//
+// CHECK-NOT: @llvm.seh
+//
+// CHECK:     invoke void @llvm.seh.scope.begin()
+//
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     invoke void @"??1A@@QEAA at XZ"
+//
+// CHECK:     invoke void @llvm.seh.scope.end()
+// CHECK-NOT: @llvm.seh
+// CHECK:     br label %delete.end
+//
+// CHECK:     invoke void @llvm.seh.try.end()
+//
+// CHECK-NOT: invoke void @llvm.seh.try.end()
+// CHECK:     invoke.cont15:
+//
+// CHECK:     call void @"?fin$0 at 0@foo1@@"(i8 noundef 0
+// CHECK-NOT: @llvm.seh
+//
+// CHECK:     ehcleanup:
+// CHECK:     invoke void @llvm.seh.scope.end()
+// CHECK:     invoke.cont7:
+// CHECK:     cleanupret
+//
+// CHECK:     invoke void @llvm.seh.scope.end()
+// CHECK:     invoke.cont14:
+// CHECK:     cleanupret
+//
+// CHECK:     ehcleanup16:
+// CHECK:     call void @"?fin$0 at 0@foo1@@"(i8 noundef 1
+// CHECK-NOT: @llvm.seh
+// CHECK:     cleanupret
+struct A {
+	A() noexcept;
+	~A();
+};
+
+void foo1() {
+	__try {
+		A* a = new A;
+		delete a;
+	}
+	__finally {
+	}
+}
+
+// CHECK-LABEL: @"?seh_finally_no_obj@@YAXXZ"()
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     invoke void @llvm.seh.try.begin()
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     invoke void @"?might_throw@@YAXXZ"()
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     invoke void @llvm.seh.try.end()
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     call void @"?fin$0 at 0@seh_finally_no_obj@@"(i8 noundef 0
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     call void @"?fin$0 at 0@seh_finally_no_obj@@"(i8 noundef 1
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     cleanupret
+void might_throw();
+void seh_finally_no_obj() {
+  __try {
+    might_throw();
+  }
+  __finally {
+  }
+}
+
+// CHECK-LABEL: @"?seh_finally_with_obj@@YAXPEAUA@@@Z"(
+//
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     invoke void @llvm.seh.try.begin()
+//
+// CHECK:     invoke void @llvm.seh.scope.begin()
+// CHECK:     invoke void @"??1A@@QEAA at XZ"
+//
+// CHECK:     invoke void @llvm.seh.scope.end()
+//
+// CHECK-NOT: @llvm.seh.scope
+//
+// CHECK:     invoke void @llvm.seh.try.end()
+//
+// CHECK-NOT: @llvm.seh.scope
+// CHECK:     call void @"?fin$0 at 0@seh_finally_with_obj@@"(i8 noundef 0
+//
+// CHECK:     invoke void @llvm.seh.scope.end()
+// CHECK:     cleanupret
+//
+// CHECK:     call void @"?fin$0 at 0@seh_finally_with_obj@@"(i8 noundef 1
+// CHECK-NOT: @llvm.seh
+// CHECK:     cleanupret
+void seh_finally_with_obj(A* p) {
+  __try {
+    delete p;
+  }
+  __finally {
+  }
+}
diff --git a/clang/test/CodeGenCXX/exceptions-seh.cpp b/clang/test/CodeGenCXX/exceptions-seh.cpp
index c4b191377b844..9cf04619a3687 100644
--- a/clang/test/CodeGenCXX/exceptions-seh.cpp
+++ b/clang/test/CodeGenCXX/exceptions-seh.cpp
@@ -16,6 +16,10 @@
 // RUN:         -fms-extensions -x c++ -emit-llvm-only -verify %s -DERR_SYNC2
 // RUN: %clang_cc1 -triple x86_64-windows -fcxx-exceptions -fexceptions \
 // RUN:         -fms-extensions -x c++ -emit-llvm-only -verify %s -DERR_SYNC3
+// RUN: %clang_cc1 -triple x86_64-windows -fasync-exceptions -fcxx-exceptions -fexceptions \
+// RUN:         -fms-extensions -x c++ -emit-llvm-only -verify %s -DERR_NEW_THROWING_CTOR
+// RUN: %clang_cc1 -triple x86_64-windows -fasync-exceptions -fcxx-exceptions -fexceptions \
+// RUN:         -fms-extensions -x c++ -emit-llvm-only -verify %s -DNOERR_NEW_NOEXCEPT_CTOR
 // RUN: %clang_cc1 -triple x86_64-windows \
 // RUN:         -fms-extensions -x c++ -emit-llvm-only -verify %s -DNOERR
 
@@ -211,6 +215,27 @@ void seh_unwinding() {
   } __except (1) {
   }
 }
+#elif defined(ERR_NEW_THROWING_CTOR)
+void seh_unwinding() {
+  __try {
+    HasCleanup *p = new HasCleanup; // expected-error{{'__try' is not permitted in functions that require object unwinding}}
+    delete p;
+  } __except (1) {
+  }
+}
+#elif defined(NOERR_NEW_NOEXCEPT_CTOR)
+// new-expression with a noexcept constructor should be fine.
+struct NoThrowCtor {
+  NoThrowCtor() noexcept;
+  ~NoThrowCtor();
+};
+void seh_unwinding() {
+  __try {
+    NoThrowCtor *p = new NoThrowCtor; // expected-no-diagnostics
+    delete p;
+  } __except (1) {
+  }
+}
 #elif defined(NOERR)
 void seh_unwinding() {
   __try {



More information about the cfe-commits mailing list