Skip to content

Conversation

@fmayer
Copy link
Contributor

@fmayer fmayer commented Nov 19, 2025

No description provided.

Created using spr 1.3.7
@llvmbot llvmbot added clang Clang issues not falling into any other category compiler-rt clang:codegen IR generation bugs: mangling, exceptions, etc. compiler-rt:ubsan Undefined behavior sanitizer compiler-rt:sanitizer llvm:transforms labels Nov 19, 2025
@llvmbot
Copy link
Member

llvmbot commented Nov 19, 2025

@llvm/pr-subscribers-clang

@llvm/pr-subscribers-clang-codegen

Author: Florian Mayer (fmayer)

Changes

Full diff: https://github.com/llvm/llvm-project/pull/168645.diff

9 Files Affected:

  • (modified) clang/lib/CodeGen/BackendUtil.cpp (+2)
  • (modified) clang/lib/CodeGen/CGExpr.cpp (+7)
  • (modified) clang/test/CodeGen/cfi-icall-trap-recover-runtime.c (+36)
  • (modified) clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp (+24)
  • (modified) compiler-rt/test/ubsan_minimal/TestCases/override-callback.c (+7)
  • (modified) llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h (+3-2)
  • (modified) llvm/lib/Passes/PassBuilder.cpp (+10)
  • (modified) llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp (+4-1)
  • (modified) llvm/test/Instrumentation/BoundsChecking/runtimes.ll (+16)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 6f63e6470270e..a243b2e222716 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -1134,6 +1134,8 @@ void EmitAssemblyHelper::RunOptimizationPipeline(
                   CodeGenOpts.SanitizeMinimalRuntime),
               /*MayReturn=*/
               CodeGenOpts.SanitizeRecover.has(SanitizerKind::LocalBounds),
+              /*PreserveRt=*/static_cast<bool>(
+              CodeGenOpts.SanitizePreserveRuntime),
           };
         }
         FPM.addPass(BoundsCheckingPass(Options));
diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp
index a837f00732748..9f58c0a77e526 100644
--- a/clang/lib/CodeGen/CGExpr.cpp
+++ b/clang/lib/CodeGen/CGExpr.cpp
@@ -3789,6 +3789,7 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
   bool NeedsAbortSuffix =
       IsFatal && RecoverKind != CheckRecoverableKind::Unrecoverable;
   bool MinimalRuntime = CGF.CGM.getCodeGenOpts().SanitizeMinimalRuntime;
+  bool PreserveRuntime = CGF.CGM.getCodeGenOpts().SanitizePreserveRuntime;
   const SanitizerHandlerInfo &CheckInfo = SanitizerHandlers[CheckHandler];
   const StringRef CheckName = CheckInfo.Name;
   std::string FnName = "__ubsan_handle_" + CheckName.str();
@@ -3798,6 +3799,8 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
     FnName += "_minimal";
   if (NeedsAbortSuffix)
     FnName += "_abort";
+  else if (MinimalRuntime && PreserveRuntime)
+    FnName += "_preserve";
   bool MayReturn =
       !IsFatal || RecoverKind == CheckRecoverableKind::AlwaysRecoverable;
 
@@ -3818,6 +3821,10 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
             (CGF.CurCodeDecl && CGF.CurCodeDecl->hasAttr<OptimizeNoneAttr>());
   if (NoMerge)
     HandlerCall->addFnAttr(llvm::Attribute::NoMerge);
+  if (MinimalRuntime && PreserveRuntime) {
+    // N.B. there is also a clang::CallingConv which is not what we want here.
+    HandlerCall->setCallingConv(llvm::CallingConv::PreserveAll);
+  }
   if (!MayReturn) {
     HandlerCall->setDoesNotReturn();
     CGF.Builder.CreateUnreachable();
diff --git a/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c b/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
index 117672a9d4368..33be31e09bb56 100644
--- a/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
+++ b/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
@@ -9,6 +9,9 @@
 
 // RUN: %clang_cc1 -fsanitize=cfi-icall -fno-sanitize-trap=cfi-icall -fsanitize-recover=cfi-icall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=RECOVER_MIN %s
 
+// RUN: %clang_cc1 -fsanitize=cfi-icall -fno-sanitize-trap=cfi-icall -fsanitize-recover=cfi-icall -fsanitize-minimal-runtime -fsanitize-preserve-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=PRESERVE_MIN %s
+
+
 // TRAP-LABEL: define hidden void @f(
 // TRAP-SAME: ) #[[ATTR0:[0-9]+]] !type [[META6:![0-9]+]] !type [[META7:![0-9]+]] {
 // TRAP-NEXT:  [[ENTRY:.*:]]
@@ -34,6 +37,11 @@
 // RECOVER_MIN-NEXT:  [[ENTRY:.*:]]
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @f(
+// PRESERVE_MIN-SAME: ) #[[ATTR0:[0-9]+]] !type [[META6:![0-9]+]] !type [[META7:![0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    ret void
+//
 void f() {
 }
 
@@ -146,6 +154,27 @@ void xf();
 // RECOVER_MIN-NEXT:    call void (...) [[TMP2]]()
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @g(
+// PRESERVE_MIN-SAME: i32 noundef [[B:%.*]]) #[[ATTR0]] !type [[META8:![0-9]+]] !type [[META9:![0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    [[B_ADDR:%.*]] = alloca i32, align 4
+// PRESERVE_MIN-NEXT:    [[FP:%.*]] = alloca ptr, align 8
+// PRESERVE_MIN-NEXT:    store i32 [[B]], ptr [[B_ADDR]], align 4
+// PRESERVE_MIN-NEXT:    [[TMP0:%.*]] = load i32, ptr [[B_ADDR]], align 4
+// PRESERVE_MIN-NEXT:    [[TOBOOL:%.*]] = icmp ne i32 [[TMP0]], 0
+// PRESERVE_MIN-NEXT:    [[TMP1:%.*]] = zext i1 [[TOBOOL]] to i64
+// PRESERVE_MIN-NEXT:    [[COND:%.*]] = select i1 [[TOBOOL]], ptr @f, ptr @xf
+// PRESERVE_MIN-NEXT:    store ptr [[COND]], ptr [[FP]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP2:%.*]] = load ptr, ptr [[FP]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP3:%.*]] = call i1 @llvm.type.test(ptr [[TMP2]], metadata !"_ZTSFvE"), !nosanitize [[META10:![0-9]+]]
+// PRESERVE_MIN-NEXT:    br i1 [[TMP3]], label %[[CONT:.*]], label %[[HANDLER_CFI_CHECK_FAIL:.*]], !prof [[PROF11:![0-9]+]], !nosanitize [[META10]]
+// PRESERVE_MIN:       [[HANDLER_CFI_CHECK_FAIL]]:
+// PRESERVE_MIN-NEXT:    call preserve_allcc void @__ubsan_handle_cfi_check_fail_minimal_preserve() #[[ATTR4:[0-9]+]], !nosanitize [[META10]]
+// PRESERVE_MIN-NEXT:    br label %[[CONT]], !nosanitize [[META10]]
+// PRESERVE_MIN:       [[CONT]]:
+// PRESERVE_MIN-NEXT:    call void (...) [[TMP2]]()
+// PRESERVE_MIN-NEXT:    ret void
+//
 void g(int b) {
   void (*fp)() = b ? f : xf;
   fp();
@@ -186,3 +215,10 @@ void g(int b) {
 // RECOVER_MIN: [[META10]] = !{}
 // RECOVER_MIN: [[PROF11]] = !{!"branch_weights", i32 1048575, i32 1}
 //.
+// PRESERVE_MIN: [[META6]] = !{i64 0, !"_ZTSFvE"}
+// PRESERVE_MIN: [[META7]] = !{i64 0, !"_ZTSFvE.generalized"}
+// PRESERVE_MIN: [[META8]] = !{i64 0, !"_ZTSFviE"}
+// PRESERVE_MIN: [[META9]] = !{i64 0, !"_ZTSFviE.generalized"}
+// PRESERVE_MIN: [[META10]] = !{}
+// PRESERVE_MIN: [[PROF11]] = !{!"branch_weights", i32 1048575, i32 1}
+//.
diff --git a/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp b/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
index 3e9328ac0e3ca..dd6a009cb7409 100644
--- a/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
+++ b/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
@@ -9,6 +9,8 @@
 
 // RUN: %clang_cc1 -fsanitize=cfi-vcall -fno-sanitize-trap=cfi-vcall -fsanitize-recover=cfi-vcall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=RECOVER_MIN %s
 
+// RUN: %clang_cc1 -fsanitize=cfi-vcall -fno-sanitize-trap=cfi-vcall -fsanitize-recover=cfi-vcall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -fsanitize-preserve-runtime -emit-llvm -o - %s | FileCheck --check-prefix=PRESERVE_MIN %s
+
 struct S1 {
   virtual void f();
 };
@@ -111,6 +113,25 @@ struct S1 {
 // RECOVER_MIN-NEXT:    call void [[TMP3]](ptr noundef nonnull align 8 dereferenceable(8) [[TMP0]])
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @_Z3s1fP2S1(
+// PRESERVE_MIN-SAME: ptr noundef [[S1:%.*]]) #[[ATTR0:[0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    [[S1_ADDR:%.*]] = alloca ptr, align 8
+// PRESERVE_MIN-NEXT:    store ptr [[S1]], ptr [[S1_ADDR]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP0:%.*]] = load ptr, ptr [[S1_ADDR]], align 8
+// PRESERVE_MIN-NEXT:    [[VTABLE:%.*]] = load ptr, ptr [[TMP0]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP1:%.*]] = call i1 @llvm.type.test(ptr [[VTABLE]], metadata !"_ZTS2S1"), !nosanitize [[META5:![0-9]+]]
+// PRESERVE_MIN-NEXT:    [[TMP2:%.*]] = call i1 @llvm.type.test(ptr [[VTABLE]], metadata !"all-vtables"), !nosanitize [[META5]]
+// PRESERVE_MIN-NEXT:    br i1 [[TMP1]], label %[[CONT:.*]], label %[[HANDLER_CFI_CHECK_FAIL:.*]], !prof [[PROF6:![0-9]+]], !nosanitize [[META5]]
+// PRESERVE_MIN:       [[HANDLER_CFI_CHECK_FAIL]]:
+// PRESERVE_MIN-NEXT:    call preserve_allcc void @__ubsan_handle_cfi_check_fail_minimal_preserve() #[[ATTR3:[0-9]+]], !nosanitize [[META5]]
+// PRESERVE_MIN-NEXT:    br label %[[CONT]], !nosanitize [[META5]]
+// PRESERVE_MIN:       [[CONT]]:
+// PRESERVE_MIN-NEXT:    [[VFN:%.*]] = getelementptr inbounds ptr, ptr [[VTABLE]], i64 0
+// PRESERVE_MIN-NEXT:    [[TMP3:%.*]] = load ptr, ptr [[VFN]], align 8
+// PRESERVE_MIN-NEXT:    call void [[TMP3]](ptr noundef nonnull align 8 dereferenceable(8) [[TMP0]])
+// PRESERVE_MIN-NEXT:    ret void
+//
 void s1f(S1 *s1) {
   s1->f();
 }
@@ -130,3 +151,6 @@ void s1f(S1 *s1) {
 // RECOVER_MIN: [[META5]] = !{}
 // RECOVER_MIN: [[PROF6]] = !{!"branch_weights", i32 1048575, i32 1}
 //.
+// PRESERVE_MIN: [[META5]] = !{}
+// PRESERVE_MIN: [[PROF6]] = !{!"branch_weights", i32 1048575, i32 1}
+//.
diff --git a/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c b/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
index aaed134b3ae81..45e2980855134 100644
--- a/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
+++ b/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
@@ -1,4 +1,5 @@
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change                                        %s -o %t &&             %run %t 2>&1 | FileCheck %s
+// RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fsanitize-preserve-runtime            %s -o %t &&             %run %t 2>&1 | FileCheck %s --check-prefixes=PRESERVE
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fno-sanitize-recover=all              %s -o %t && not --crash %run %t 2>&1 | FileCheck %s
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fno-sanitize-recover=all -DOVERRIDE=1 %s -o %t && not --crash %run %t 2>&1 | FileCheck %s --check-prefixes=FATAL
 
@@ -12,6 +13,11 @@ void __ubsan_report_error(const char *kind, uintptr_t caller) {
   fprintf(stderr, "CUSTOM_CALLBACK: %s\n", kind);
 }
 
+[[clang::preserve_all]] void __ubsan_report_error_preserve(const char *kind,
+                                                           uintptr_t caller) {
+  fprintf(stderr, "CUSTOM_CALLBACK_PRESERVE: %s\n", kind);
+}
+
 #if OVERRIDE
 void __ubsan_report_error_fatal(const char *kind, uintptr_t caller) {
   fprintf(stderr, "FATAL_CALLBACK: %s\n", kind);
@@ -21,5 +27,6 @@ void __ubsan_report_error_fatal(const char *kind, uintptr_t caller) {
 int main(int argc, const char **argv) {
   int32_t t0 = (~((uint32_t)0));
   // CHECK: CUSTOM_CALLBACK: implicit-conversion
+  // PRESERVE: CUSTOM_CALLBACK_PRESERVE: implicit-conversion
   // FATAL: FATAL_CALLBACK: implicit-conversion
 }
diff --git a/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h b/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
index 8e7df5e6b10f0..2f5aacf2825f6 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
@@ -23,10 +23,11 @@ class BoundsCheckingPass : public PassInfoMixin<BoundsCheckingPass> {
 public:
   struct Options {
     struct Runtime {
-      Runtime(bool MinRuntime, bool MayReturn)
-          : MinRuntime(MinRuntime), MayReturn(MayReturn) {}
+      Runtime(bool MinRuntime, bool MayReturn, bool PreserveRt)
+          : MinRuntime(MinRuntime), MayReturn(MayReturn), PreserveRt(PreserveRt) {}
       bool MinRuntime;
       bool MayReturn;
+      bool PreserveRt;
     };
     std::optional<Runtime> Rt; // Trap if empty.
     bool Merge = false;
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index 0d190ea448931..b079fd679f66d 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -1590,21 +1590,31 @@ parseBoundsCheckingOptions(StringRef Params) {
       Options.Rt = {
           /*MinRuntime=*/false,
           /*MayReturn=*/true,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "rt-abort") {
       Options.Rt = {
           /*MinRuntime=*/false,
           /*MayReturn=*/false,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "min-rt") {
       Options.Rt = {
           /*MinRuntime=*/true,
           /*MayReturn=*/true,
+          /*PreserveRt=*/false,
+      };
+    } else if (ParamName == "min-rt-preserve") {
+      Options.Rt = {
+          /*MinRuntime=*/true,
+          /*MayReturn=*/true,
+          /*PreserveRt=*/true,
       };
     } else if (ParamName == "min-rt-abort") {
       Options.Rt = {
           /*MinRuntime=*/true,
           /*MayReturn=*/false,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "merge") {
       Options.Merge = true;
diff --git a/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp b/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
index 9239ae8741afb..21d502ad95fab 100644
--- a/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
+++ b/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
@@ -174,10 +174,13 @@ static void insertBoundsCheck(Value *Or, BuilderTy &IRB, GetTrapBBT GetTrapBB) {
 static std::string
 getRuntimeCallName(const BoundsCheckingPass::Options::Runtime &Opts) {
   std::string Name = "__ubsan_handle_local_out_of_bounds";
-  if (Opts.MinRuntime)
+  if (Opts.MinRuntime) {
     Name += "_minimal";
+  }
   if (!Opts.MayReturn)
     Name += "_abort";
+  if (Opts.PreserveRt && Opts.MinRuntime)
+    Name += "_preserve";
   return Name;
 }
 
diff --git a/llvm/test/Instrumentation/BoundsChecking/runtimes.ll b/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
index 84dd51cd3fa28..ebfdf5b79fba9 100644
--- a/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
+++ b/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
@@ -111,6 +111,22 @@ define void @f1(i64 %x) nounwind {
 ; MINRT-NOMERGE-NEXT:    call void @__ubsan_handle_local_out_of_bounds_minimal() #[[ATTR1:[0-9]+]], !nosanitize [[META0]]
 ; MINRT-NOMERGE-NEXT:    br label %[[BB7]], !nosanitize [[META0]]
 ;
+; MINRT-PRESERVE-NOMERGE-LABEL: define void @f1(
+; MINRT-PRESERVE-NOMERGE-SAME: i64 [[X:%.*]]) #[[ATTR0:[0-9]+]] {
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP1:%.*]] = mul i64 16, [[X]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP2:%.*]] = alloca i128, i64 [[X]], align 8
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP3:%.*]] = sub i64 [[TMP1]], 0, !nosanitize [[META0:![0-9]+]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP4:%.*]] = icmp ult i64 [[TMP3]], 16, !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP5:%.*]] = or i1 false, [[TMP4]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP6:%.*]] = or i1 false, [[TMP5]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    br i1 [[TMP6]], label %[[TRAP:.*]], label %[[BB7:.*]]
+; MINRT-PRESERVE-NOMERGE:       [[BB7]]:
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP8:%.*]] = load i128, ptr [[TMP2]], align 4
+; MINRT-PRESERVE-NOMERGE-NEXT:    ret void
+; MINRT-PRESERVE-NOMERGE:       [[TRAP]]:
+; MINRT-PRESERVE-NOMERGE-NEXT:    call void @__ubsan_handle_local_out_of_bounds_minimal_preserve() #[[ATTR1:[0-9]+]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    br label %[[BB7]], !nosanitize [[META0]]
+;
 ; MINRTABORT-NOMERGE-LABEL: define void @f1(
 ; MINRTABORT-NOMERGE-SAME: i64 [[X:%.*]]) #[[ATTR0:[0-9]+]] {
 ; MINRTABORT-NOMERGE-NEXT:    [[TMP1:%.*]] = mul i64 16, [[X]]

@llvmbot
Copy link
Member

llvmbot commented Nov 19, 2025

@llvm/pr-subscribers-llvm-transforms

Author: Florian Mayer (fmayer)

Changes

Full diff: https://github.com/llvm/llvm-project/pull/168645.diff

9 Files Affected:

  • (modified) clang/lib/CodeGen/BackendUtil.cpp (+2)
  • (modified) clang/lib/CodeGen/CGExpr.cpp (+7)
  • (modified) clang/test/CodeGen/cfi-icall-trap-recover-runtime.c (+36)
  • (modified) clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp (+24)
  • (modified) compiler-rt/test/ubsan_minimal/TestCases/override-callback.c (+7)
  • (modified) llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h (+3-2)
  • (modified) llvm/lib/Passes/PassBuilder.cpp (+10)
  • (modified) llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp (+4-1)
  • (modified) llvm/test/Instrumentation/BoundsChecking/runtimes.ll (+16)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 6f63e6470270e..a243b2e222716 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -1134,6 +1134,8 @@ void EmitAssemblyHelper::RunOptimizationPipeline(
                   CodeGenOpts.SanitizeMinimalRuntime),
               /*MayReturn=*/
               CodeGenOpts.SanitizeRecover.has(SanitizerKind::LocalBounds),
+              /*PreserveRt=*/static_cast<bool>(
+              CodeGenOpts.SanitizePreserveRuntime),
           };
         }
         FPM.addPass(BoundsCheckingPass(Options));
diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp
index a837f00732748..9f58c0a77e526 100644
--- a/clang/lib/CodeGen/CGExpr.cpp
+++ b/clang/lib/CodeGen/CGExpr.cpp
@@ -3789,6 +3789,7 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
   bool NeedsAbortSuffix =
       IsFatal && RecoverKind != CheckRecoverableKind::Unrecoverable;
   bool MinimalRuntime = CGF.CGM.getCodeGenOpts().SanitizeMinimalRuntime;
+  bool PreserveRuntime = CGF.CGM.getCodeGenOpts().SanitizePreserveRuntime;
   const SanitizerHandlerInfo &CheckInfo = SanitizerHandlers[CheckHandler];
   const StringRef CheckName = CheckInfo.Name;
   std::string FnName = "__ubsan_handle_" + CheckName.str();
@@ -3798,6 +3799,8 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
     FnName += "_minimal";
   if (NeedsAbortSuffix)
     FnName += "_abort";
+  else if (MinimalRuntime && PreserveRuntime)
+    FnName += "_preserve";
   bool MayReturn =
       !IsFatal || RecoverKind == CheckRecoverableKind::AlwaysRecoverable;
 
@@ -3818,6 +3821,10 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
             (CGF.CurCodeDecl && CGF.CurCodeDecl->hasAttr<OptimizeNoneAttr>());
   if (NoMerge)
     HandlerCall->addFnAttr(llvm::Attribute::NoMerge);
+  if (MinimalRuntime && PreserveRuntime) {
+    // N.B. there is also a clang::CallingConv which is not what we want here.
+    HandlerCall->setCallingConv(llvm::CallingConv::PreserveAll);
+  }
   if (!MayReturn) {
     HandlerCall->setDoesNotReturn();
     CGF.Builder.CreateUnreachable();
diff --git a/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c b/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
index 117672a9d4368..33be31e09bb56 100644
--- a/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
+++ b/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
@@ -9,6 +9,9 @@
 
 // RUN: %clang_cc1 -fsanitize=cfi-icall -fno-sanitize-trap=cfi-icall -fsanitize-recover=cfi-icall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=RECOVER_MIN %s
 
+// RUN: %clang_cc1 -fsanitize=cfi-icall -fno-sanitize-trap=cfi-icall -fsanitize-recover=cfi-icall -fsanitize-minimal-runtime -fsanitize-preserve-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=PRESERVE_MIN %s
+
+
 // TRAP-LABEL: define hidden void @f(
 // TRAP-SAME: ) #[[ATTR0:[0-9]+]] !type [[META6:![0-9]+]] !type [[META7:![0-9]+]] {
 // TRAP-NEXT:  [[ENTRY:.*:]]
@@ -34,6 +37,11 @@
 // RECOVER_MIN-NEXT:  [[ENTRY:.*:]]
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @f(
+// PRESERVE_MIN-SAME: ) #[[ATTR0:[0-9]+]] !type [[META6:![0-9]+]] !type [[META7:![0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    ret void
+//
 void f() {
 }
 
@@ -146,6 +154,27 @@ void xf();
 // RECOVER_MIN-NEXT:    call void (...) [[TMP2]]()
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @g(
+// PRESERVE_MIN-SAME: i32 noundef [[B:%.*]]) #[[ATTR0]] !type [[META8:![0-9]+]] !type [[META9:![0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    [[B_ADDR:%.*]] = alloca i32, align 4
+// PRESERVE_MIN-NEXT:    [[FP:%.*]] = alloca ptr, align 8
+// PRESERVE_MIN-NEXT:    store i32 [[B]], ptr [[B_ADDR]], align 4
+// PRESERVE_MIN-NEXT:    [[TMP0:%.*]] = load i32, ptr [[B_ADDR]], align 4
+// PRESERVE_MIN-NEXT:    [[TOBOOL:%.*]] = icmp ne i32 [[TMP0]], 0
+// PRESERVE_MIN-NEXT:    [[TMP1:%.*]] = zext i1 [[TOBOOL]] to i64
+// PRESERVE_MIN-NEXT:    [[COND:%.*]] = select i1 [[TOBOOL]], ptr @f, ptr @xf
+// PRESERVE_MIN-NEXT:    store ptr [[COND]], ptr [[FP]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP2:%.*]] = load ptr, ptr [[FP]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP3:%.*]] = call i1 @llvm.type.test(ptr [[TMP2]], metadata !"_ZTSFvE"), !nosanitize [[META10:![0-9]+]]
+// PRESERVE_MIN-NEXT:    br i1 [[TMP3]], label %[[CONT:.*]], label %[[HANDLER_CFI_CHECK_FAIL:.*]], !prof [[PROF11:![0-9]+]], !nosanitize [[META10]]
+// PRESERVE_MIN:       [[HANDLER_CFI_CHECK_FAIL]]:
+// PRESERVE_MIN-NEXT:    call preserve_allcc void @__ubsan_handle_cfi_check_fail_minimal_preserve() #[[ATTR4:[0-9]+]], !nosanitize [[META10]]
+// PRESERVE_MIN-NEXT:    br label %[[CONT]], !nosanitize [[META10]]
+// PRESERVE_MIN:       [[CONT]]:
+// PRESERVE_MIN-NEXT:    call void (...) [[TMP2]]()
+// PRESERVE_MIN-NEXT:    ret void
+//
 void g(int b) {
   void (*fp)() = b ? f : xf;
   fp();
@@ -186,3 +215,10 @@ void g(int b) {
 // RECOVER_MIN: [[META10]] = !{}
 // RECOVER_MIN: [[PROF11]] = !{!"branch_weights", i32 1048575, i32 1}
 //.
+// PRESERVE_MIN: [[META6]] = !{i64 0, !"_ZTSFvE"}
+// PRESERVE_MIN: [[META7]] = !{i64 0, !"_ZTSFvE.generalized"}
+// PRESERVE_MIN: [[META8]] = !{i64 0, !"_ZTSFviE"}
+// PRESERVE_MIN: [[META9]] = !{i64 0, !"_ZTSFviE.generalized"}
+// PRESERVE_MIN: [[META10]] = !{}
+// PRESERVE_MIN: [[PROF11]] = !{!"branch_weights", i32 1048575, i32 1}
+//.
diff --git a/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp b/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
index 3e9328ac0e3ca..dd6a009cb7409 100644
--- a/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
+++ b/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
@@ -9,6 +9,8 @@
 
 // RUN: %clang_cc1 -fsanitize=cfi-vcall -fno-sanitize-trap=cfi-vcall -fsanitize-recover=cfi-vcall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=RECOVER_MIN %s
 
+// RUN: %clang_cc1 -fsanitize=cfi-vcall -fno-sanitize-trap=cfi-vcall -fsanitize-recover=cfi-vcall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -fsanitize-preserve-runtime -emit-llvm -o - %s | FileCheck --check-prefix=PRESERVE_MIN %s
+
 struct S1 {
   virtual void f();
 };
@@ -111,6 +113,25 @@ struct S1 {
 // RECOVER_MIN-NEXT:    call void [[TMP3]](ptr noundef nonnull align 8 dereferenceable(8) [[TMP0]])
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @_Z3s1fP2S1(
+// PRESERVE_MIN-SAME: ptr noundef [[S1:%.*]]) #[[ATTR0:[0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    [[S1_ADDR:%.*]] = alloca ptr, align 8
+// PRESERVE_MIN-NEXT:    store ptr [[S1]], ptr [[S1_ADDR]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP0:%.*]] = load ptr, ptr [[S1_ADDR]], align 8
+// PRESERVE_MIN-NEXT:    [[VTABLE:%.*]] = load ptr, ptr [[TMP0]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP1:%.*]] = call i1 @llvm.type.test(ptr [[VTABLE]], metadata !"_ZTS2S1"), !nosanitize [[META5:![0-9]+]]
+// PRESERVE_MIN-NEXT:    [[TMP2:%.*]] = call i1 @llvm.type.test(ptr [[VTABLE]], metadata !"all-vtables"), !nosanitize [[META5]]
+// PRESERVE_MIN-NEXT:    br i1 [[TMP1]], label %[[CONT:.*]], label %[[HANDLER_CFI_CHECK_FAIL:.*]], !prof [[PROF6:![0-9]+]], !nosanitize [[META5]]
+// PRESERVE_MIN:       [[HANDLER_CFI_CHECK_FAIL]]:
+// PRESERVE_MIN-NEXT:    call preserve_allcc void @__ubsan_handle_cfi_check_fail_minimal_preserve() #[[ATTR3:[0-9]+]], !nosanitize [[META5]]
+// PRESERVE_MIN-NEXT:    br label %[[CONT]], !nosanitize [[META5]]
+// PRESERVE_MIN:       [[CONT]]:
+// PRESERVE_MIN-NEXT:    [[VFN:%.*]] = getelementptr inbounds ptr, ptr [[VTABLE]], i64 0
+// PRESERVE_MIN-NEXT:    [[TMP3:%.*]] = load ptr, ptr [[VFN]], align 8
+// PRESERVE_MIN-NEXT:    call void [[TMP3]](ptr noundef nonnull align 8 dereferenceable(8) [[TMP0]])
+// PRESERVE_MIN-NEXT:    ret void
+//
 void s1f(S1 *s1) {
   s1->f();
 }
@@ -130,3 +151,6 @@ void s1f(S1 *s1) {
 // RECOVER_MIN: [[META5]] = !{}
 // RECOVER_MIN: [[PROF6]] = !{!"branch_weights", i32 1048575, i32 1}
 //.
+// PRESERVE_MIN: [[META5]] = !{}
+// PRESERVE_MIN: [[PROF6]] = !{!"branch_weights", i32 1048575, i32 1}
+//.
diff --git a/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c b/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
index aaed134b3ae81..45e2980855134 100644
--- a/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
+++ b/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
@@ -1,4 +1,5 @@
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change                                        %s -o %t &&             %run %t 2>&1 | FileCheck %s
+// RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fsanitize-preserve-runtime            %s -o %t &&             %run %t 2>&1 | FileCheck %s --check-prefixes=PRESERVE
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fno-sanitize-recover=all              %s -o %t && not --crash %run %t 2>&1 | FileCheck %s
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fno-sanitize-recover=all -DOVERRIDE=1 %s -o %t && not --crash %run %t 2>&1 | FileCheck %s --check-prefixes=FATAL
 
@@ -12,6 +13,11 @@ void __ubsan_report_error(const char *kind, uintptr_t caller) {
   fprintf(stderr, "CUSTOM_CALLBACK: %s\n", kind);
 }
 
+[[clang::preserve_all]] void __ubsan_report_error_preserve(const char *kind,
+                                                           uintptr_t caller) {
+  fprintf(stderr, "CUSTOM_CALLBACK_PRESERVE: %s\n", kind);
+}
+
 #if OVERRIDE
 void __ubsan_report_error_fatal(const char *kind, uintptr_t caller) {
   fprintf(stderr, "FATAL_CALLBACK: %s\n", kind);
@@ -21,5 +27,6 @@ void __ubsan_report_error_fatal(const char *kind, uintptr_t caller) {
 int main(int argc, const char **argv) {
   int32_t t0 = (~((uint32_t)0));
   // CHECK: CUSTOM_CALLBACK: implicit-conversion
+  // PRESERVE: CUSTOM_CALLBACK_PRESERVE: implicit-conversion
   // FATAL: FATAL_CALLBACK: implicit-conversion
 }
diff --git a/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h b/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
index 8e7df5e6b10f0..2f5aacf2825f6 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
@@ -23,10 +23,11 @@ class BoundsCheckingPass : public PassInfoMixin<BoundsCheckingPass> {
 public:
   struct Options {
     struct Runtime {
-      Runtime(bool MinRuntime, bool MayReturn)
-          : MinRuntime(MinRuntime), MayReturn(MayReturn) {}
+      Runtime(bool MinRuntime, bool MayReturn, bool PreserveRt)
+          : MinRuntime(MinRuntime), MayReturn(MayReturn), PreserveRt(PreserveRt) {}
       bool MinRuntime;
       bool MayReturn;
+      bool PreserveRt;
     };
     std::optional<Runtime> Rt; // Trap if empty.
     bool Merge = false;
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index 0d190ea448931..b079fd679f66d 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -1590,21 +1590,31 @@ parseBoundsCheckingOptions(StringRef Params) {
       Options.Rt = {
           /*MinRuntime=*/false,
           /*MayReturn=*/true,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "rt-abort") {
       Options.Rt = {
           /*MinRuntime=*/false,
           /*MayReturn=*/false,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "min-rt") {
       Options.Rt = {
           /*MinRuntime=*/true,
           /*MayReturn=*/true,
+          /*PreserveRt=*/false,
+      };
+    } else if (ParamName == "min-rt-preserve") {
+      Options.Rt = {
+          /*MinRuntime=*/true,
+          /*MayReturn=*/true,
+          /*PreserveRt=*/true,
       };
     } else if (ParamName == "min-rt-abort") {
       Options.Rt = {
           /*MinRuntime=*/true,
           /*MayReturn=*/false,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "merge") {
       Options.Merge = true;
diff --git a/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp b/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
index 9239ae8741afb..21d502ad95fab 100644
--- a/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
+++ b/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
@@ -174,10 +174,13 @@ static void insertBoundsCheck(Value *Or, BuilderTy &IRB, GetTrapBBT GetTrapBB) {
 static std::string
 getRuntimeCallName(const BoundsCheckingPass::Options::Runtime &Opts) {
   std::string Name = "__ubsan_handle_local_out_of_bounds";
-  if (Opts.MinRuntime)
+  if (Opts.MinRuntime) {
     Name += "_minimal";
+  }
   if (!Opts.MayReturn)
     Name += "_abort";
+  if (Opts.PreserveRt && Opts.MinRuntime)
+    Name += "_preserve";
   return Name;
 }
 
diff --git a/llvm/test/Instrumentation/BoundsChecking/runtimes.ll b/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
index 84dd51cd3fa28..ebfdf5b79fba9 100644
--- a/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
+++ b/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
@@ -111,6 +111,22 @@ define void @f1(i64 %x) nounwind {
 ; MINRT-NOMERGE-NEXT:    call void @__ubsan_handle_local_out_of_bounds_minimal() #[[ATTR1:[0-9]+]], !nosanitize [[META0]]
 ; MINRT-NOMERGE-NEXT:    br label %[[BB7]], !nosanitize [[META0]]
 ;
+; MINRT-PRESERVE-NOMERGE-LABEL: define void @f1(
+; MINRT-PRESERVE-NOMERGE-SAME: i64 [[X:%.*]]) #[[ATTR0:[0-9]+]] {
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP1:%.*]] = mul i64 16, [[X]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP2:%.*]] = alloca i128, i64 [[X]], align 8
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP3:%.*]] = sub i64 [[TMP1]], 0, !nosanitize [[META0:![0-9]+]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP4:%.*]] = icmp ult i64 [[TMP3]], 16, !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP5:%.*]] = or i1 false, [[TMP4]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP6:%.*]] = or i1 false, [[TMP5]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    br i1 [[TMP6]], label %[[TRAP:.*]], label %[[BB7:.*]]
+; MINRT-PRESERVE-NOMERGE:       [[BB7]]:
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP8:%.*]] = load i128, ptr [[TMP2]], align 4
+; MINRT-PRESERVE-NOMERGE-NEXT:    ret void
+; MINRT-PRESERVE-NOMERGE:       [[TRAP]]:
+; MINRT-PRESERVE-NOMERGE-NEXT:    call void @__ubsan_handle_local_out_of_bounds_minimal_preserve() #[[ATTR1:[0-9]+]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    br label %[[BB7]], !nosanitize [[META0]]
+;
 ; MINRTABORT-NOMERGE-LABEL: define void @f1(
 ; MINRTABORT-NOMERGE-SAME: i64 [[X:%.*]]) #[[ATTR0:[0-9]+]] {
 ; MINRTABORT-NOMERGE-NEXT:    [[TMP1:%.*]] = mul i64 16, [[X]]

@llvmbot
Copy link
Member

llvmbot commented Nov 19, 2025

@llvm/pr-subscribers-compiler-rt-sanitizer

Author: Florian Mayer (fmayer)

Changes

Full diff: https://github.com/llvm/llvm-project/pull/168645.diff

9 Files Affected:

  • (modified) clang/lib/CodeGen/BackendUtil.cpp (+2)
  • (modified) clang/lib/CodeGen/CGExpr.cpp (+7)
  • (modified) clang/test/CodeGen/cfi-icall-trap-recover-runtime.c (+36)
  • (modified) clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp (+24)
  • (modified) compiler-rt/test/ubsan_minimal/TestCases/override-callback.c (+7)
  • (modified) llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h (+3-2)
  • (modified) llvm/lib/Passes/PassBuilder.cpp (+10)
  • (modified) llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp (+4-1)
  • (modified) llvm/test/Instrumentation/BoundsChecking/runtimes.ll (+16)
diff --git a/clang/lib/CodeGen/BackendUtil.cpp b/clang/lib/CodeGen/BackendUtil.cpp
index 6f63e6470270e..a243b2e222716 100644
--- a/clang/lib/CodeGen/BackendUtil.cpp
+++ b/clang/lib/CodeGen/BackendUtil.cpp
@@ -1134,6 +1134,8 @@ void EmitAssemblyHelper::RunOptimizationPipeline(
                   CodeGenOpts.SanitizeMinimalRuntime),
               /*MayReturn=*/
               CodeGenOpts.SanitizeRecover.has(SanitizerKind::LocalBounds),
+              /*PreserveRt=*/static_cast<bool>(
+              CodeGenOpts.SanitizePreserveRuntime),
           };
         }
         FPM.addPass(BoundsCheckingPass(Options));
diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp
index a837f00732748..9f58c0a77e526 100644
--- a/clang/lib/CodeGen/CGExpr.cpp
+++ b/clang/lib/CodeGen/CGExpr.cpp
@@ -3789,6 +3789,7 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
   bool NeedsAbortSuffix =
       IsFatal && RecoverKind != CheckRecoverableKind::Unrecoverable;
   bool MinimalRuntime = CGF.CGM.getCodeGenOpts().SanitizeMinimalRuntime;
+  bool PreserveRuntime = CGF.CGM.getCodeGenOpts().SanitizePreserveRuntime;
   const SanitizerHandlerInfo &CheckInfo = SanitizerHandlers[CheckHandler];
   const StringRef CheckName = CheckInfo.Name;
   std::string FnName = "__ubsan_handle_" + CheckName.str();
@@ -3798,6 +3799,8 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
     FnName += "_minimal";
   if (NeedsAbortSuffix)
     FnName += "_abort";
+  else if (MinimalRuntime && PreserveRuntime)
+    FnName += "_preserve";
   bool MayReturn =
       !IsFatal || RecoverKind == CheckRecoverableKind::AlwaysRecoverable;
 
@@ -3818,6 +3821,10 @@ static void emitCheckHandlerCall(CodeGenFunction &CGF,
             (CGF.CurCodeDecl && CGF.CurCodeDecl->hasAttr<OptimizeNoneAttr>());
   if (NoMerge)
     HandlerCall->addFnAttr(llvm::Attribute::NoMerge);
+  if (MinimalRuntime && PreserveRuntime) {
+    // N.B. there is also a clang::CallingConv which is not what we want here.
+    HandlerCall->setCallingConv(llvm::CallingConv::PreserveAll);
+  }
   if (!MayReturn) {
     HandlerCall->setDoesNotReturn();
     CGF.Builder.CreateUnreachable();
diff --git a/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c b/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
index 117672a9d4368..33be31e09bb56 100644
--- a/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
+++ b/clang/test/CodeGen/cfi-icall-trap-recover-runtime.c
@@ -9,6 +9,9 @@
 
 // RUN: %clang_cc1 -fsanitize=cfi-icall -fno-sanitize-trap=cfi-icall -fsanitize-recover=cfi-icall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=RECOVER_MIN %s
 
+// RUN: %clang_cc1 -fsanitize=cfi-icall -fno-sanitize-trap=cfi-icall -fsanitize-recover=cfi-icall -fsanitize-minimal-runtime -fsanitize-preserve-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=PRESERVE_MIN %s
+
+
 // TRAP-LABEL: define hidden void @f(
 // TRAP-SAME: ) #[[ATTR0:[0-9]+]] !type [[META6:![0-9]+]] !type [[META7:![0-9]+]] {
 // TRAP-NEXT:  [[ENTRY:.*:]]
@@ -34,6 +37,11 @@
 // RECOVER_MIN-NEXT:  [[ENTRY:.*:]]
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @f(
+// PRESERVE_MIN-SAME: ) #[[ATTR0:[0-9]+]] !type [[META6:![0-9]+]] !type [[META7:![0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    ret void
+//
 void f() {
 }
 
@@ -146,6 +154,27 @@ void xf();
 // RECOVER_MIN-NEXT:    call void (...) [[TMP2]]()
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @g(
+// PRESERVE_MIN-SAME: i32 noundef [[B:%.*]]) #[[ATTR0]] !type [[META8:![0-9]+]] !type [[META9:![0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    [[B_ADDR:%.*]] = alloca i32, align 4
+// PRESERVE_MIN-NEXT:    [[FP:%.*]] = alloca ptr, align 8
+// PRESERVE_MIN-NEXT:    store i32 [[B]], ptr [[B_ADDR]], align 4
+// PRESERVE_MIN-NEXT:    [[TMP0:%.*]] = load i32, ptr [[B_ADDR]], align 4
+// PRESERVE_MIN-NEXT:    [[TOBOOL:%.*]] = icmp ne i32 [[TMP0]], 0
+// PRESERVE_MIN-NEXT:    [[TMP1:%.*]] = zext i1 [[TOBOOL]] to i64
+// PRESERVE_MIN-NEXT:    [[COND:%.*]] = select i1 [[TOBOOL]], ptr @f, ptr @xf
+// PRESERVE_MIN-NEXT:    store ptr [[COND]], ptr [[FP]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP2:%.*]] = load ptr, ptr [[FP]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP3:%.*]] = call i1 @llvm.type.test(ptr [[TMP2]], metadata !"_ZTSFvE"), !nosanitize [[META10:![0-9]+]]
+// PRESERVE_MIN-NEXT:    br i1 [[TMP3]], label %[[CONT:.*]], label %[[HANDLER_CFI_CHECK_FAIL:.*]], !prof [[PROF11:![0-9]+]], !nosanitize [[META10]]
+// PRESERVE_MIN:       [[HANDLER_CFI_CHECK_FAIL]]:
+// PRESERVE_MIN-NEXT:    call preserve_allcc void @__ubsan_handle_cfi_check_fail_minimal_preserve() #[[ATTR4:[0-9]+]], !nosanitize [[META10]]
+// PRESERVE_MIN-NEXT:    br label %[[CONT]], !nosanitize [[META10]]
+// PRESERVE_MIN:       [[CONT]]:
+// PRESERVE_MIN-NEXT:    call void (...) [[TMP2]]()
+// PRESERVE_MIN-NEXT:    ret void
+//
 void g(int b) {
   void (*fp)() = b ? f : xf;
   fp();
@@ -186,3 +215,10 @@ void g(int b) {
 // RECOVER_MIN: [[META10]] = !{}
 // RECOVER_MIN: [[PROF11]] = !{!"branch_weights", i32 1048575, i32 1}
 //.
+// PRESERVE_MIN: [[META6]] = !{i64 0, !"_ZTSFvE"}
+// PRESERVE_MIN: [[META7]] = !{i64 0, !"_ZTSFvE.generalized"}
+// PRESERVE_MIN: [[META8]] = !{i64 0, !"_ZTSFviE"}
+// PRESERVE_MIN: [[META9]] = !{i64 0, !"_ZTSFviE.generalized"}
+// PRESERVE_MIN: [[META10]] = !{}
+// PRESERVE_MIN: [[PROF11]] = !{!"branch_weights", i32 1048575, i32 1}
+//.
diff --git a/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp b/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
index 3e9328ac0e3ca..dd6a009cb7409 100644
--- a/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
+++ b/clang/test/CodeGenCXX/cfi-vcall-trap-recover-runtime.cpp
@@ -9,6 +9,8 @@
 
 // RUN: %clang_cc1 -fsanitize=cfi-vcall -fno-sanitize-trap=cfi-vcall -fsanitize-recover=cfi-vcall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -emit-llvm -o - %s | FileCheck --check-prefix=RECOVER_MIN %s
 
+// RUN: %clang_cc1 -fsanitize=cfi-vcall -fno-sanitize-trap=cfi-vcall -fsanitize-recover=cfi-vcall -fsanitize-minimal-runtime -flto -fvisibility=hidden -triple x86_64-unknown-linux -fwhole-program-vtables -fsanitize-preserve-runtime -emit-llvm -o - %s | FileCheck --check-prefix=PRESERVE_MIN %s
+
 struct S1 {
   virtual void f();
 };
@@ -111,6 +113,25 @@ struct S1 {
 // RECOVER_MIN-NEXT:    call void [[TMP3]](ptr noundef nonnull align 8 dereferenceable(8) [[TMP0]])
 // RECOVER_MIN-NEXT:    ret void
 //
+// PRESERVE_MIN-LABEL: define hidden void @_Z3s1fP2S1(
+// PRESERVE_MIN-SAME: ptr noundef [[S1:%.*]]) #[[ATTR0:[0-9]+]] {
+// PRESERVE_MIN-NEXT:  [[ENTRY:.*:]]
+// PRESERVE_MIN-NEXT:    [[S1_ADDR:%.*]] = alloca ptr, align 8
+// PRESERVE_MIN-NEXT:    store ptr [[S1]], ptr [[S1_ADDR]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP0:%.*]] = load ptr, ptr [[S1_ADDR]], align 8
+// PRESERVE_MIN-NEXT:    [[VTABLE:%.*]] = load ptr, ptr [[TMP0]], align 8
+// PRESERVE_MIN-NEXT:    [[TMP1:%.*]] = call i1 @llvm.type.test(ptr [[VTABLE]], metadata !"_ZTS2S1"), !nosanitize [[META5:![0-9]+]]
+// PRESERVE_MIN-NEXT:    [[TMP2:%.*]] = call i1 @llvm.type.test(ptr [[VTABLE]], metadata !"all-vtables"), !nosanitize [[META5]]
+// PRESERVE_MIN-NEXT:    br i1 [[TMP1]], label %[[CONT:.*]], label %[[HANDLER_CFI_CHECK_FAIL:.*]], !prof [[PROF6:![0-9]+]], !nosanitize [[META5]]
+// PRESERVE_MIN:       [[HANDLER_CFI_CHECK_FAIL]]:
+// PRESERVE_MIN-NEXT:    call preserve_allcc void @__ubsan_handle_cfi_check_fail_minimal_preserve() #[[ATTR3:[0-9]+]], !nosanitize [[META5]]
+// PRESERVE_MIN-NEXT:    br label %[[CONT]], !nosanitize [[META5]]
+// PRESERVE_MIN:       [[CONT]]:
+// PRESERVE_MIN-NEXT:    [[VFN:%.*]] = getelementptr inbounds ptr, ptr [[VTABLE]], i64 0
+// PRESERVE_MIN-NEXT:    [[TMP3:%.*]] = load ptr, ptr [[VFN]], align 8
+// PRESERVE_MIN-NEXT:    call void [[TMP3]](ptr noundef nonnull align 8 dereferenceable(8) [[TMP0]])
+// PRESERVE_MIN-NEXT:    ret void
+//
 void s1f(S1 *s1) {
   s1->f();
 }
@@ -130,3 +151,6 @@ void s1f(S1 *s1) {
 // RECOVER_MIN: [[META5]] = !{}
 // RECOVER_MIN: [[PROF6]] = !{!"branch_weights", i32 1048575, i32 1}
 //.
+// PRESERVE_MIN: [[META5]] = !{}
+// PRESERVE_MIN: [[PROF6]] = !{!"branch_weights", i32 1048575, i32 1}
+//.
diff --git a/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c b/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
index aaed134b3ae81..45e2980855134 100644
--- a/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
+++ b/compiler-rt/test/ubsan_minimal/TestCases/override-callback.c
@@ -1,4 +1,5 @@
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change                                        %s -o %t &&             %run %t 2>&1 | FileCheck %s
+// RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fsanitize-preserve-runtime            %s -o %t &&             %run %t 2>&1 | FileCheck %s --check-prefixes=PRESERVE
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fno-sanitize-recover=all              %s -o %t && not --crash %run %t 2>&1 | FileCheck %s
 // RUN: %clang_min_runtime -fsanitize=implicit-integer-sign-change -fno-sanitize-recover=all -DOVERRIDE=1 %s -o %t && not --crash %run %t 2>&1 | FileCheck %s --check-prefixes=FATAL
 
@@ -12,6 +13,11 @@ void __ubsan_report_error(const char *kind, uintptr_t caller) {
   fprintf(stderr, "CUSTOM_CALLBACK: %s\n", kind);
 }
 
+[[clang::preserve_all]] void __ubsan_report_error_preserve(const char *kind,
+                                                           uintptr_t caller) {
+  fprintf(stderr, "CUSTOM_CALLBACK_PRESERVE: %s\n", kind);
+}
+
 #if OVERRIDE
 void __ubsan_report_error_fatal(const char *kind, uintptr_t caller) {
   fprintf(stderr, "FATAL_CALLBACK: %s\n", kind);
@@ -21,5 +27,6 @@ void __ubsan_report_error_fatal(const char *kind, uintptr_t caller) {
 int main(int argc, const char **argv) {
   int32_t t0 = (~((uint32_t)0));
   // CHECK: CUSTOM_CALLBACK: implicit-conversion
+  // PRESERVE: CUSTOM_CALLBACK_PRESERVE: implicit-conversion
   // FATAL: FATAL_CALLBACK: implicit-conversion
 }
diff --git a/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h b/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
index 8e7df5e6b10f0..2f5aacf2825f6 100644
--- a/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
+++ b/llvm/include/llvm/Transforms/Instrumentation/BoundsChecking.h
@@ -23,10 +23,11 @@ class BoundsCheckingPass : public PassInfoMixin<BoundsCheckingPass> {
 public:
   struct Options {
     struct Runtime {
-      Runtime(bool MinRuntime, bool MayReturn)
-          : MinRuntime(MinRuntime), MayReturn(MayReturn) {}
+      Runtime(bool MinRuntime, bool MayReturn, bool PreserveRt)
+          : MinRuntime(MinRuntime), MayReturn(MayReturn), PreserveRt(PreserveRt) {}
       bool MinRuntime;
       bool MayReturn;
+      bool PreserveRt;
     };
     std::optional<Runtime> Rt; // Trap if empty.
     bool Merge = false;
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index 0d190ea448931..b079fd679f66d 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -1590,21 +1590,31 @@ parseBoundsCheckingOptions(StringRef Params) {
       Options.Rt = {
           /*MinRuntime=*/false,
           /*MayReturn=*/true,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "rt-abort") {
       Options.Rt = {
           /*MinRuntime=*/false,
           /*MayReturn=*/false,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "min-rt") {
       Options.Rt = {
           /*MinRuntime=*/true,
           /*MayReturn=*/true,
+          /*PreserveRt=*/false,
+      };
+    } else if (ParamName == "min-rt-preserve") {
+      Options.Rt = {
+          /*MinRuntime=*/true,
+          /*MayReturn=*/true,
+          /*PreserveRt=*/true,
       };
     } else if (ParamName == "min-rt-abort") {
       Options.Rt = {
           /*MinRuntime=*/true,
           /*MayReturn=*/false,
+          /*PreserveRt=*/false,
       };
     } else if (ParamName == "merge") {
       Options.Merge = true;
diff --git a/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp b/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
index 9239ae8741afb..21d502ad95fab 100644
--- a/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
+++ b/llvm/lib/Transforms/Instrumentation/BoundsChecking.cpp
@@ -174,10 +174,13 @@ static void insertBoundsCheck(Value *Or, BuilderTy &IRB, GetTrapBBT GetTrapBB) {
 static std::string
 getRuntimeCallName(const BoundsCheckingPass::Options::Runtime &Opts) {
   std::string Name = "__ubsan_handle_local_out_of_bounds";
-  if (Opts.MinRuntime)
+  if (Opts.MinRuntime) {
     Name += "_minimal";
+  }
   if (!Opts.MayReturn)
     Name += "_abort";
+  if (Opts.PreserveRt && Opts.MinRuntime)
+    Name += "_preserve";
   return Name;
 }
 
diff --git a/llvm/test/Instrumentation/BoundsChecking/runtimes.ll b/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
index 84dd51cd3fa28..ebfdf5b79fba9 100644
--- a/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
+++ b/llvm/test/Instrumentation/BoundsChecking/runtimes.ll
@@ -111,6 +111,22 @@ define void @f1(i64 %x) nounwind {
 ; MINRT-NOMERGE-NEXT:    call void @__ubsan_handle_local_out_of_bounds_minimal() #[[ATTR1:[0-9]+]], !nosanitize [[META0]]
 ; MINRT-NOMERGE-NEXT:    br label %[[BB7]], !nosanitize [[META0]]
 ;
+; MINRT-PRESERVE-NOMERGE-LABEL: define void @f1(
+; MINRT-PRESERVE-NOMERGE-SAME: i64 [[X:%.*]]) #[[ATTR0:[0-9]+]] {
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP1:%.*]] = mul i64 16, [[X]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP2:%.*]] = alloca i128, i64 [[X]], align 8
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP3:%.*]] = sub i64 [[TMP1]], 0, !nosanitize [[META0:![0-9]+]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP4:%.*]] = icmp ult i64 [[TMP3]], 16, !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP5:%.*]] = or i1 false, [[TMP4]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP6:%.*]] = or i1 false, [[TMP5]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    br i1 [[TMP6]], label %[[TRAP:.*]], label %[[BB7:.*]]
+; MINRT-PRESERVE-NOMERGE:       [[BB7]]:
+; MINRT-PRESERVE-NOMERGE-NEXT:    [[TMP8:%.*]] = load i128, ptr [[TMP2]], align 4
+; MINRT-PRESERVE-NOMERGE-NEXT:    ret void
+; MINRT-PRESERVE-NOMERGE:       [[TRAP]]:
+; MINRT-PRESERVE-NOMERGE-NEXT:    call void @__ubsan_handle_local_out_of_bounds_minimal_preserve() #[[ATTR1:[0-9]+]], !nosanitize [[META0]]
+; MINRT-PRESERVE-NOMERGE-NEXT:    br label %[[BB7]], !nosanitize [[META0]]
+;
 ; MINRTABORT-NOMERGE-LABEL: define void @f1(
 ; MINRTABORT-NOMERGE-SAME: i64 [[X:%.*]]) #[[ATTR0:[0-9]+]] {
 ; MINRTABORT-NOMERGE-NEXT:    [[TMP1:%.*]] = mul i64 16, [[X]]

@github-actions
Copy link

github-actions bot commented Nov 19, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

Created using spr 1.3.7
@vitalybuka vitalybuka self-requested a review November 19, 2025 02:17
@vitalybuka vitalybuka marked this pull request as draft November 19, 2025 02:17
Created using spr 1.3.7
Created using spr 1.3.7
@fmayer fmayer changed the title [UBSan] Use -fsanitize-recover-runtime in codegen [UBSan] Use -fsanitize-handler-preserve-all-regs in codegen Nov 20, 2025
@fmayer fmayer marked this pull request as ready for review November 20, 2025 07:43
Created using spr 1.3.7
Created using spr 1.3.7
Created using spr 1.3.7
Created using spr 1.3.7
Created using spr 1.3.7
Created using spr 1.3.7
@fmayer
Copy link
Contributor Author

fmayer commented Nov 20, 2025

@vitalybuka PTAL. I updated the tests, and fixed the logic to not apply the preserve_all cc for abort functions (though it shouldn't really matter)

Created using spr 1.3.7
@github-actions
Copy link

github-actions bot commented Nov 21, 2025

🐧 Linux x64 Test Results

  • 193015 tests passed
  • 6209 tests skipped

Created using spr 1.3.7
@fmayer
Copy link
Contributor Author

fmayer commented Nov 24, 2025

@vitalybuka PTAL. I restricted to aarch64 and x86_64

Created using spr 1.3.7
Created using spr 1.3.7
@fmayer fmayer requested a review from vitalybuka November 24, 2025 23:16
// local-bounds. Make sure to change that too.
const auto &T = CGF.CGM.getTriple();
bool HandlerPreserveAllRegs =
CGF.CGM.getCodeGenOpts().SanitizeHandlerPreserveAllRegs &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, can you make sure SanitizeHandlerPreserveAllRegs already set only on supported arches?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but what do we gain from that? it makes the logic harder to follow, because now it is split between codegen and driver. also now the driver and frontend flag have different meanings

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be arch specific logic in codegen

Eg. there are some UBSAN/sanitizer file which flip on/of features deppending on context
your patch is exporting the logic outside that thing

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eg. there are some UBSAN/sanitizer file which flip on/of features deppending on context
your patch is exporting the logic outside that thing

Please clarify

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang/lib/Driver/SanitizerArgs.cpp does some filtering by triple and many other factors
This should go there as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang/lib/Driver/SanitizerArgs.cpp does some filtering by triple and many other factors
This should go there as well

How does this work if we have some check trapping and some recoverable? Then it depends on which check we are looking at whether we should have preserve-all. I can handle Abort / NoAbort in the CGExpr / BoundsChecking, but then the logic is split

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did that. Lmk what you think

Created using spr 1.3.7
Created using spr 1.3.7
@fmayer fmayer requested a review from vitalybuka November 26, 2025 02:54
@fmayer fmayer merged commit 9782413 into users/fmayer/spr/main.ubsan-use-fsanitize-recover-runtime-in-codegen Nov 27, 2025
15 of 17 checks passed
@fmayer fmayer deleted the users/fmayer/spr/ubsan-use-fsanitize-recover-runtime-in-codegen branch November 27, 2025 01:20
fmayer added a commit that referenced this pull request Nov 27, 2025
@fmayer
Copy link
Contributor Author

fmayer commented Nov 27, 2025

Actually submitted: e2a29ec

llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Nov 27, 2025
tanji-dg pushed a commit to tanji-dg/llvm-project that referenced this pull request Nov 27, 2025
GeneraluseAI pushed a commit to GeneraluseAI/llvm-project that referenced this pull request Nov 27, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:codegen IR generation bugs: mangling, exceptions, etc. clang Clang issues not falling into any other category compiler-rt:sanitizer compiler-rt:ubsan Undefined behavior sanitizer compiler-rt llvm:transforms

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants