[compiler-rt] [asan] Adopt sanitizer_common operator-new framework (PR #196388)

Justin T. Gibbs via llvm-commits llvm-commits at lists.llvm.org
Wed Jul 29 09:51:15 PDT 2026


https://github.com/scsiguy updated https://github.com/llvm/llvm-project/pull/196388

>From 74ae735f8fc4561b488da75862016d0e9d0897c3 Mon Sep 17 00:00:00 2001
From: "Justin T. Gibbs" <gibbs at scsiguy.com>
Date: Sat, 30 May 2026 17:31:24 -0700
Subject: [PATCH 1/3] [sanitizer_common] Add operator new chain-handling
 framework
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Implement the operator new wrapper machinery required by
[new.delete.single]/3+/4 in shared sanitizer_common files so every
sanitizer can reuse it (avoids ~130 lines of duplication).

sanitizer_new_handler.h provides three main templates in namespace
__sanitizer (plus a NORETURN InvokeOnExhausted wrapper used internally):

  * RunNewHandlerChain<Alloc>(alloc)
      Runs std::get_new_handler() in a loop until either the
      allocation succeeds or the chain is exhausted (returns nullptr).

  * NewImplThrowing<Alloc, OnExhausted>(alloc, on_exhausted)
      Throwing operator new: runs the chain; on exhaustion either
      throws std::bad_alloc (AllocatorMayReturnNull()=true opts into
      standards-conformant throw) or invokes on_exhausted (default;
      historical abort-on-OOM).

  * NewImplNothrow<Alloc, OnExhausted>(alloc, on_exhausted) noexcept
      Nothrow operator new: per [new.delete.single]/4 behaves as-if
      the throwing form is called within a try/catch converting any
      exceptions to a nullptr return.

NOTE: Windows builds do not support exceptions, so the throwing
      std::bad_alloc behavior described above is converted to an
      invocation of on_exhausted(); a user new_handler that throws
      on Windows trips the noexcept on NewImplNothrow and aborts via
      std::terminate.

NOTE: The framework auto-detects exception support via the standard
      __cpp_exceptions feature-test macro: when a consuming TU is
      compiled with -fno-exceptions, the throw/try-catch logic is
      compiled out and the framework collapses to the abort path
      (same shape as Windows). This means TUs do not have to be
      built with -fexceptions, and a -fno-exceptions adopter incurs
      no std::bad_alloc symbol dependency. When -fexceptions IS used,
      instantiating the throwing-form template introduces a runtime
      dependency on std::bad_alloc — adopters must link a C++ ABI
      library (libstdc++ / libc++abi) into the resulting runtime.

sanitizer_new_operators.inc builds the eight standard
OPERATOR_NEW_BODY* macros on top of these templates. A consuming
sanitizer defines six ingredient macros (a stack-trace setup,
a report-OOM invocation, and four alloc helpers) and then includes the
file. The resulting OPERATOR_NEW_BODY / OPERATOR_NEW_BODY_NOTHROW /
OPERATOR_NEW_BODY_ARRAY / ... / OPERATOR_NEW_BODY_ALIGN_ARRAY_NOTHROW
macros can then be used directly as the bodies of the eight operator new
overrides.

NFC. No consumer yet — this is a prerequisite for a follow-up that
restructures compiler-rt/lib/asan/asan_new_delete.cpp to use the
shared framework.

Assisted by: Claude Opus 4.7
---
 .../lib/sanitizer_common/CMakeLists.txt       |   2 +
 .../sanitizer_common/sanitizer_new_handler.h  | 122 ++++++++++++++++++
 .../sanitizer_new_operators.inc               | 114 ++++++++++++++++
 3 files changed, 238 insertions(+)
 create mode 100644 compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h
 create mode 100644 compiler-rt/lib/sanitizer_common/sanitizer_new_operators.inc

diff --git a/compiler-rt/lib/sanitizer_common/CMakeLists.txt b/compiler-rt/lib/sanitizer_common/CMakeLists.txt
index 96c23c6d8ab82..07e9cad572b3d 100644
--- a/compiler-rt/lib/sanitizer_common/CMakeLists.txt
+++ b/compiler-rt/lib/sanitizer_common/CMakeLists.txt
@@ -170,6 +170,8 @@ set(SANITIZER_IMPL_HEADERS
   sanitizer_mac.h
   sanitizer_malloc_mac.inc
   sanitizer_mutex.h
+  sanitizer_new_handler.h
+  sanitizer_new_operators.inc
   sanitizer_placement_new.h
   sanitizer_platform.h
   sanitizer_platform_interceptors.h
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h b/compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h
new file mode 100644
index 0000000000000..6a5a361ec6324
--- /dev/null
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h
@@ -0,0 +1,122 @@
+//===-- sanitizer_new_handler.h ---------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file is shared between run-time libraries of sanitizers.
+//
+// It provides the operator new chain-handling framework required by
+// [new.delete.single]/3+/4: a std::get_new_handler() loop plus the
+// throwing / nothrow exhaustion policies that compose with it. Each
+// sanitizer's operator new wrapper supplies two small lambdas:
+//
+//   * Alloc        — invokes the sanitizer's internal allocator, returning
+//                    nullptr on OOM (never aborting on OOM). Other detected
+//                    failure modes (e.g. invalid alignment) should abort with
+//                    a diagnostic.
+//
+//   * OnExhausted  — invokes the sanitizer's "abort with diagnostic"
+//                    handler (e.g. asan's ReportOutOfMemory + Die()).
+//                    Required to never return (enforced by UNREACHABLE()).
+//===----------------------------------------------------------------------===//
+#ifndef SANITIZER_NEW_HANDLER_H
+#define SANITIZER_NEW_HANDLER_H
+
+#include "sanitizer_allocator.h"
+#include "sanitizer_internal_defs.h"
+#include "sanitizer_platform.h"
+
+#if !SANITIZER_WINDOWS && defined(__cpp_exceptions)
+#  include <new>
+#else
+// Builds without exception support can't include <new>: on Windows the
+// sanitizer runtimes are built without exceptions; elsewhere this TU may
+// have been compiled with -fno-exceptions (e.g. with
+// -DCOMPILER_RT_ASAN_ENABLE_EXCEPTIONS=OFF). Forward-declare just enough
+// of std for the loop; the real std::get_new_handler is supplied by the
+// C++ runtime the sanitizer links against (vcruntime / msvcprt /
+// MinGW libstdc++ / libstdc++ / libc++) — it doesn't need to come from
+// <new>.
+namespace std {
+struct nothrow_t {};
+enum class align_val_t : __sanitizer::usize {};
+using new_handler = void (*)();
+new_handler get_new_handler() noexcept;
+}  // namespace std
+#endif  // !SANITIZER_WINDOWS && defined(__cpp_exceptions)
+
+namespace __sanitizer {
+
+// Runs std::get_new_handler() per [new.delete.single]/3+/4 until the
+// allocation succeeds or the chain is exhausted. Returns the allocated
+// pointer on success, nullptr if the handler chain is exhausted.
+//
+// NOTE: Exceptions thrown by Alloc or std::new_handler callbacks escape this
+//       function. Callers that need to convert exceptions to a nullptr return
+//       (e.g. NewImplNothrow below) must wrap the call in try/catch themselves.
+template <typename Alloc>
+void* RunNewHandlerChain(Alloc alloc) {
+  for (;;) {
+    void* res = alloc();
+    if (LIKELY(res != nullptr))
+      return res;
+    std::new_handler handler = std::get_new_handler();
+    if (!handler)
+      return nullptr;
+    handler();
+  }
+}
+
+// NORETURN wrapper providing pre-C++23 compatibility.
+template <typename OnExhausted>
+NORETURN void InvokeOnExhausted(OnExhausted on_exhausted) {
+  on_exhausted();
+  UNREACHABLE("operator new OnExhausted callable returned");
+}
+
+// Throwing operator new: chain, then on exhaustion throw std::bad_alloc
+// when this TU was compiled with exception support and AllocatorMayReturnNull()
+// is true. Otherwise (Windows runtimes, -fno-exceptions builds, or the
+// default flag value) fall through to the abort path.
+template <typename Alloc, typename OnExhausted>
+void* NewImplThrowing(Alloc alloc, OnExhausted on_exhausted) {
+  void* res = RunNewHandlerChain(alloc);
+  if (LIKELY(res != nullptr))
+    return res;
+#if !SANITIZER_WINDOWS && defined(__cpp_exceptions)
+  if (AllocatorMayReturnNull())
+    throw std::bad_alloc();
+#endif
+  InvokeOnExhausted(on_exhausted);
+}
+
+// Nothrow operator new: per [new.delete.single]/4 behaves as-if the
+// throwing form is called within a try/catch. When exception support is
+// absent (Windows runtimes or -fno-exceptions TUs) we can't write the
+// try/catch literally — instead we avoid the throw path entirely, so the
+// main body matches NewImplThrowing with "throw std::bad_alloc()" replaced
+// by "return nullptr".
+template <typename Alloc, typename OnExhausted>
+void* NewImplNothrow(Alloc alloc, OnExhausted on_exhausted) noexcept {
+#if !SANITIZER_WINDOWS && defined(__cpp_exceptions)
+  try {
+#endif
+    void* res = RunNewHandlerChain(alloc);
+    if (LIKELY(res != nullptr))
+      return res;
+    if (AllocatorMayReturnNull())
+      return nullptr;
+    InvokeOnExhausted(on_exhausted);
+#if !SANITIZER_WINDOWS && defined(__cpp_exceptions)
+  } catch (...) {
+    return nullptr;
+  }
+#endif
+}
+
+}  // namespace __sanitizer
+
+#endif  // SANITIZER_NEW_HANDLER_H
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_new_operators.inc b/compiler-rt/lib/sanitizer_common/sanitizer_new_operators.inc
new file mode 100644
index 0000000000000..66dd83f2fddc5
--- /dev/null
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_new_operators.inc
@@ -0,0 +1,114 @@
+//===-- sanitizer_new_operators.inc -----------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Shared OPERATOR_NEW_BODY* macros for sanitizer operator new overrides.
+//
+// All eight variants compose the same per-call setup (a sanitizer-specific
+// stack-trace acquisition) with one of the two chain-handling templates from
+// sanitizer_new_handler.h and two sanitizer-supplied lambdas: an Alloc that
+// invokes the sanitizer's internal allocator (returning nullptr on failure)
+// and an OnExhausted that calls the sanitizer's "abort with diagnostic"
+// handler.
+//
+// This file is included after the consuming TU has defined the prerequisite
+// macros listed below. Names reference symbols (e.g. `stack`, `size`) that
+// exist only in the macro's expansion context, so this is an .inc, not a
+// freestanding header.
+//
+// The consuming TU must define before including:
+//
+//   SANITIZER_NEW_STACK_TRACE
+//       A statement that, when expanded inside an operator new body, puts a
+//       BufferedStackTrace named `stack` in scope. e.g. for asan:
+//         #define SANITIZER_NEW_STACK_TRACE GET_STACK_TRACE_MALLOC
+//
+//   SANITIZER_NEW_REPORT_OOM(size)
+//       The sanitizer's noreturn diagnostic path for chain-exhausted OOM.
+//       Must call a function that does not return (the framework will run
+//       UNREACHABLE() and abort if the macro's expansion ever returns).
+//       e.g. for asan:
+//         #define SANITIZER_NEW_REPORT_OOM(size) \
+//           ReportOutOfMemory(size, &stack)
+//
+//   SANITIZER_NEW(size)
+//   SANITIZER_NEW_ARRAY(size)
+//   SANITIZER_NEW_ALIGNED(size, align)
+//   SANITIZER_NEW_ARRAY_ALIGNED(size, align)
+//       The sanitizer's four internal alloc helpers. Each must return nullptr
+//       on OOM (so the chain-handling templates can drive the
+//       std::get_new_handler() loop).  Other failure modes (e.g. invalid
+//       alignment) should cause the helper to abort with a diagnostic.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef SANITIZER_NEW_STACK_TRACE
+#  error "SANITIZER_NEW_STACK_TRACE must be defined before including"
+#endif
+#ifndef SANITIZER_NEW_REPORT_OOM
+#  error "SANITIZER_NEW_REPORT_OOM must be defined before including"
+#endif
+#ifndef SANITIZER_NEW
+#  error "SANITIZER_NEW must be defined before including"
+#endif
+#ifndef SANITIZER_NEW_ARRAY
+#  error "SANITIZER_NEW_ARRAY must be defined before including"
+#endif
+#ifndef SANITIZER_NEW_ALIGNED
+#  error "SANITIZER_NEW_ALIGNED must be defined before including"
+#endif
+#ifndef SANITIZER_NEW_ARRAY_ALIGNED
+#  error "SANITIZER_NEW_ARRAY_ALIGNED must be defined before including"
+#endif
+
+#define OPERATOR_NEW_BODY                    \
+  SANITIZER_NEW_STACK_TRACE;                 \
+  return __sanitizer::NewImplThrowing(       \
+      [&]() { return SANITIZER_NEW(size); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })
+
+#define OPERATOR_NEW_BODY_NOTHROW            \
+  SANITIZER_NEW_STACK_TRACE;                 \
+  return __sanitizer::NewImplNothrow(        \
+      [&]() { return SANITIZER_NEW(size); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })
+
+#define OPERATOR_NEW_BODY_ARRAY                    \
+  SANITIZER_NEW_STACK_TRACE;                       \
+  return __sanitizer::NewImplThrowing(             \
+      [&]() { return SANITIZER_NEW_ARRAY(size); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })
+
+#define OPERATOR_NEW_BODY_ARRAY_NOTHROW            \
+  SANITIZER_NEW_STACK_TRACE;                       \
+  return __sanitizer::NewImplNothrow(              \
+      [&]() { return SANITIZER_NEW_ARRAY(size); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })
+
+#define OPERATOR_NEW_BODY_ALIGN                             \
+  SANITIZER_NEW_STACK_TRACE;                                \
+  return __sanitizer::NewImplThrowing(                      \
+      [&]() { return SANITIZER_NEW_ALIGNED(size, align); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })
+
+#define OPERATOR_NEW_BODY_ALIGN_NOTHROW                     \
+  SANITIZER_NEW_STACK_TRACE;                                \
+  return __sanitizer::NewImplNothrow(                       \
+      [&]() { return SANITIZER_NEW_ALIGNED(size, align); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })
+
+#define OPERATOR_NEW_BODY_ALIGN_ARRAY                             \
+  SANITIZER_NEW_STACK_TRACE;                                      \
+  return __sanitizer::NewImplThrowing(                            \
+      [&]() { return SANITIZER_NEW_ARRAY_ALIGNED(size, align); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })
+
+#define OPERATOR_NEW_BODY_ALIGN_ARRAY_NOTHROW                     \
+  SANITIZER_NEW_STACK_TRACE;                                      \
+  return __sanitizer::NewImplNothrow(                             \
+      [&]() { return SANITIZER_NEW_ARRAY_ALIGNED(size, align); }, \
+      [&]() { SANITIZER_NEW_REPORT_OOM(size); })

>From d4fa816ef9237d95c13c91607c96e3442f0b0f1b Mon Sep 17 00:00:00 2001
From: "Justin T. Gibbs" <gibbs at scsiguy.com>
Date: Sat, 30 May 2026 16:21:52 -0700
Subject: [PATCH 2/3] [asan] Adopt sanitizer_common operator-new framework
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Adopt the operator new chain-handling framework to give asan's eight operator
new overrides standards-conformant behavior on allocation failure.

The new behavior for handling OOM is opt-in via allocator_may_return_null=1.
With the flag set, throwing operator new throws std::bad_alloc on OOM and
nothrow operator new returns nullptr. Under the default flag value
(=0) both forms continue to ReportOutOfMemory + Die() — preserving
historical behavior. In all cases, the std::get_new_handler() chain
is executed as required by ([new.delete.single]/3+/4).

NOTE: The throwing form aborts (rather than throwing std::bad_alloc)
      when the asan_new_delete.cpp TU itself lacks exception support
      — i.e. on Windows, or on any build configured with
      -DCOMPILER_RT_ASAN_ENABLE_EXCEPTIONS=OFF. The framework auto-
      detects this via __cpp_exceptions; no explicit guard is needed
      in the asan source.

NOTE: The framework's typeinfo for std::bad_alloc dependency is
      satisfied for the asan dynamic .so via RTAsan_dynamic_cxx.
      Users who statically link clang_rt.asan_cxx-<arch>.a
      into their own binaries must arrange to link a C++ ABI library
      themselves.

NOTE: Throwing behavior requires user code built with -fexceptions.
      -fno-exceptions user code that triggers operator new failure
      under allocator_may_return_null=1 will reach std::terminate
      when the thrown std::bad_alloc propagates to a frame without
      a catch handler (asan's signal handler then reports the abort).

This closes the long-standing TODO in asan_new_delete.cpp ("throw
std::bad_alloc instead of dying on OOM") and aligns ASan with
libstdc++ / libc++ / tcmalloc. See https://github.com/google/sanitizers/issues/295.

Implementation (asan-specific; framework mechanics live in the prereq):

  * asan_new / asan_new_array / asan_new_aligned / asan_new_array_aligned
    in asan_allocator.cpp pass may_return_null=true to AllocateImpl()
    so the Alloc lambdas seen by the framework's NewImpl* templates
    always observe nullptr on failure rather than aborting. This
    includes asan_new_aligned's alignment-validation path — bad
    alignment now returns nullptr and lets the new_handler chain
    decide rather than aborting unconditionally.

  * Local definition of the OPERATOR_NEW_BODY* macros is replaced
    by definition of the six SANITIZER_NEW_* ingredient macros required
    by sanitizer_common/sanitizer_new_operators.inc. The nothrow operator
    new / delete overloads gain NOEXCEPT to match the standard
    library's exception specifications.

  * sanitizer_common/sanitizer_flags.inc: update the
    allocator_may_return_null docstring to document the new throw
    behavior and the Windows limitation.

  * Diagnostic change for the operator-new path: because asan_new
    now forces may_return_null=true on AllocateImpl(), the in-place
    "allocation-size-too-big" diagnostic that previously fired for
    oversize requests inside Allocate is replaced by a "WARNING:
    AddressSanitizer failed to allocate" line (from Allocate's
    may_return_null=true branch) followed by the chain-exhausted
    "out-of-memory" SUMMARY (or std::bad_alloc throw on flag=1).
    Other failure paths (malloc / calloc / realloc / aligned_alloc /
    posix_memalign) continue to emit "allocation-size-too-big" as
    before.

Test Plan:
New tests under compiler-rt/test/asan/TestCases/ covering the eight
OPERATOR_NEW_BODY* macro paths plus both flag values:

  Opt-in (allocator_may_return_null=1):
    * throw_bad_alloc_oversize.cpp                  — array throws bad_alloc
    * throw_bad_alloc_aligned.cpp                   — aligned array throws bad_alloc
    * throw_bad_alloc_single.cpp                    — single-object throws bad_alloc
    * throw_bad_alloc_aligned_single.cpp            — aligned single-object throws bad_alloc
    * nothrow_new_returns_null.cpp                  — array nothrow returns null
    * nothrow_new_single_returns_null.cpp           — single-object nothrow returns null
    * nothrow_new_aligned_single_returns_null.cpp   — aligned single-object nothrow returns null
    * nothrow_new_aligned_array_returns_null.cpp    — aligned array nothrow returns null
    * new_handler_invocation.cpp                    — handler runs, can break loop
    * new_handler_throws_other.cpp                  — non-bad_alloc handler exception propagates from throwing new
    * nothrow_new_handler_throws_other.cpp          — non-bad_alloc handler exception is swallowed by nothrow new (per /4 as-if try/catch)

  Default flag (allocator_may_return_null=0) — abort path:
    * throw_bad_alloc_default_aborts.cpp            — throwing new aborts with vanilla diagnostic
    * nothrow_new_default_aborts.cpp                — nothrow new aborts with vanilla diagnostic

The four nothrow tests run on Windows too (the Windows nothrow path is
now standards-conformant); the throwing tests stay UNSUPPORTED on
Windows since they require std::bad_alloc to be catchable.

Existing malloc-size-too-big.cpp continues to assert that malloc OOM
under allocator_may_return_null=0 aborts.

Updated existing sanitizer_common tests for the new-operator behavior:
  * compiler-rt/test/sanitizer_common/TestCases/allocator_returns_null.cpp
  * compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp
  * compiler-rt/test/sanitizer_common/TestCases/Linux/allocator_returns_null_std.cpp

For the flag=0 cells (CHECK-nCRASH, CHECK-nnCRASH, the std test), the
expected SUMMARY pattern is loosened to (allocation-size-too-big|out-of-memory)
— other sanitizers still emit "allocation-size-too-big".

For the flag=1 + throwing-new case, the test now wraps the operator
new call in a try/catch and converts std::bad_alloc to nullptr to
preserve the existing test invariants (errno=ENOMEM, x=nullptr ->
CHECK-NULL). This validates that the bad_alloc throw is catchable
from user code (the whole point of the new contract).

Assisted-by: Claude Opus 4.7
---
 compiler-rt/lib/asan/asan_allocator.cpp       | 15 ++--
 compiler-rt/lib/asan/asan_new_delete.cpp      | 77 ++++++-------------
 .../lib/sanitizer_common/sanitizer_flags.inc  | 10 ++-
 .../asan/TestCases/new_handler_invocation.cpp | 45 +++++++++++
 .../TestCases/new_handler_throws_other.cpp    | 36 +++++++++
 ...nothrow_new_aligned_array_returns_null.cpp | 27 +++++++
 ...othrow_new_aligned_single_returns_null.cpp | 26 +++++++
 .../TestCases/nothrow_new_default_aborts.cpp  | 33 ++++++++
 .../nothrow_new_handler_throws_other.cpp      | 32 ++++++++
 .../TestCases/nothrow_new_returns_null.cpp    | 26 +++++++
 .../nothrow_new_single_returns_null.cpp       | 25 ++++++
 .../TestCases/throw_bad_alloc_aligned.cpp     | 36 +++++++++
 .../throw_bad_alloc_aligned_single.cpp        | 34 ++++++++
 .../throw_bad_alloc_default_aborts.cpp        | 31 ++++++++
 .../TestCases/throw_bad_alloc_oversize.cpp    | 35 +++++++++
 .../asan/TestCases/throw_bad_alloc_single.cpp | 35 +++++++++
 .../Linux/allocator_returns_null_std.cpp      |  6 +-
 .../TestCases/allocator_returns_null.cpp      | 32 ++++++--
 .../TestCases/max_allocation_size.cpp         | 35 +++++++--
 19 files changed, 520 insertions(+), 76 deletions(-)
 create mode 100644 compiler-rt/test/asan/TestCases/new_handler_invocation.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/new_handler_throws_other.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/nothrow_new_aligned_array_returns_null.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/nothrow_new_aligned_single_returns_null.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/nothrow_new_default_aborts.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/nothrow_new_handler_throws_other.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/nothrow_new_returns_null.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/nothrow_new_single_returns_null.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned_single.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/throw_bad_alloc_default_aborts.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/throw_bad_alloc_oversize.cpp
 create mode 100644 compiler-rt/test/asan/TestCases/throw_bad_alloc_single.cpp

diff --git a/compiler-rt/lib/asan/asan_allocator.cpp b/compiler-rt/lib/asan/asan_allocator.cpp
index 6af197ab4cb1c..f4b2c969bcc97 100644
--- a/compiler-rt/lib/asan/asan_allocator.cpp
+++ b/compiler-rt/lib/asan/asan_allocator.cpp
@@ -1206,10 +1206,13 @@ uptr asan_malloc_usable_size(const void* ptr, uptr pc, uptr bp) {
 
 namespace {
 
+// Force may_return_null=true so the Alloc lambdas in asan_new_delete.cpp
+// satisfy the operator-new framework's contract (return nullptr on OOM
+// failure rather than abort).
 void* asan_new(uptr size, BufferedStackTrace* stack, bool array) {
-  return SetErrnoOnNull(instance.Allocate(size, /*alignment=*/0, stack,
-                                          array ? FROM_NEW_BR : FROM_NEW,
-                                          /*can_fill=*/true));
+  return SetErrnoOnNull(instance.AllocateImpl(
+      size, /*alignment=*/0, stack, array ? FROM_NEW_BR : FROM_NEW,
+      /*can_fill=*/true, /*may_return_null=*/true));
 }
 
 void* asan_new_aligned(uptr size, uptr alignment, BufferedStackTrace* stack,
@@ -1220,9 +1223,9 @@ void* asan_new_aligned(uptr size, uptr alignment, BufferedStackTrace* stack,
       return nullptr;
     ReportInvalidAllocationAlignment(alignment, stack);
   }
-  return SetErrnoOnNull(instance.Allocate(size, alignment, stack,
-                                          array ? FROM_NEW_BR : FROM_NEW,
-                                          /*can_fill=*/true));
+  return SetErrnoOnNull(instance.AllocateImpl(
+      size, alignment, stack, array ? FROM_NEW_BR : FROM_NEW,
+      /*can_fill=*/true, /*may_return_null=*/true));
 }
 
 void asan_delete(void* ptr, BufferedStackTrace* stack, bool array) {
diff --git a/compiler-rt/lib/asan/asan_new_delete.cpp b/compiler-rt/lib/asan/asan_new_delete.cpp
index 04f8082de3148..1f5bcc27a0c77 100644
--- a/compiler-rt/lib/asan/asan_new_delete.cpp
+++ b/compiler-rt/lib/asan/asan_new_delete.cpp
@@ -18,6 +18,7 @@
 #include "asan_report.h"
 #include "asan_stack.h"
 #include "interception/interception.h"
+#include "sanitizer_common/sanitizer_new_handler.h"
 
 // C++ operators can't have dllexport attributes on Windows. We export them
 // anyway by passing extra -export flags to the linker, which is exactly that
@@ -51,51 +52,17 @@ using namespace __asan;
 // This code has issues on OSX.
 // See https://github.com/google/sanitizers/issues/131.
 
-// Fake std::nothrow_t and std::align_val_t to avoid including <new>.
-namespace std {
-struct nothrow_t {};
-enum class align_val_t : size_t {};
-}  // namespace std
-
-// TODO(alekseyshl): throw std::bad_alloc instead of dying on OOM.
-// For local pool allocation, align to SHADOW_GRANULARITY to match asan
-// allocator behavior.
-#define OPERATOR_NEW_BODY             \
-  GET_STACK_TRACE_MALLOC;             \
-  void* res = asan_new(size, &stack); \
-  if (UNLIKELY(!res))                 \
-    ReportOutOfMemory(size, &stack);  \
-  return res
-#define OPERATOR_NEW_BODY_NOTHROW \
-  GET_STACK_TRACE_MALLOC;         \
-  return asan_new(size, &stack)
-#define OPERATOR_NEW_BODY_ARRAY             \
-  GET_STACK_TRACE_MALLOC;                   \
-  void* res = asan_new_array(size, &stack); \
-  if (UNLIKELY(!res))                       \
-    ReportOutOfMemory(size, &stack);        \
-  return res
-#define OPERATOR_NEW_BODY_ARRAY_NOTHROW \
-  GET_STACK_TRACE_MALLOC;               \
-  return asan_new_array(size, &stack)
-#define OPERATOR_NEW_BODY_ALIGN                                         \
-  GET_STACK_TRACE_MALLOC;                                               \
-  void* res = asan_new_aligned(size, static_cast<uptr>(align), &stack); \
-  if (UNLIKELY(!res))                                                   \
-    ReportOutOfMemory(size, &stack);                                    \
-  return res
-#define OPERATOR_NEW_BODY_ALIGN_NOTHROW \
-  GET_STACK_TRACE_MALLOC;               \
-  return asan_new_aligned(size, static_cast<uptr>(align), &stack)
-#define OPERATOR_NEW_BODY_ALIGN_ARRAY                                         \
-  GET_STACK_TRACE_MALLOC;                                                     \
-  void* res = asan_new_array_aligned(size, static_cast<uptr>(align), &stack); \
-  if (UNLIKELY(!res))                                                         \
-    ReportOutOfMemory(size, &stack);                                          \
-  return res
-#define OPERATOR_NEW_BODY_ALIGN_ARRAY_NOTHROW \
-  GET_STACK_TRACE_MALLOC;                     \
-  return asan_new_array_aligned(size, static_cast<uptr>(align), &stack)
+// SANITIZER_NEW_* macros below are inputs to the operator-new framework;
+// OPERATOR_NEW_BODY* macros are provided in return.
+#define SANITIZER_NEW_STACK_TRACE GET_STACK_TRACE_MALLOC
+#define SANITIZER_NEW_REPORT_OOM(size) ReportOutOfMemory(size, &stack)
+#define SANITIZER_NEW(size) asan_new(size, &stack)
+#define SANITIZER_NEW_ARRAY(size) asan_new_array(size, &stack)
+#define SANITIZER_NEW_ALIGNED(size, align) \
+  asan_new_aligned(size, static_cast<uptr>(align), &stack)
+#define SANITIZER_NEW_ARRAY_ALIGNED(size, align) \
+  asan_new_array_aligned(size, static_cast<uptr>(align), &stack)
+#include "sanitizer_common/sanitizer_new_operators.inc"
 
 // On OS X it's not enough to just provide our own 'operator new' and
 // 'operator delete' implementations, because they're going to be in the
@@ -110,11 +77,11 @@ void* operator new(size_t size) { OPERATOR_NEW_BODY; }
 CXX_OPERATOR_ATTRIBUTE
 void* operator new[](size_t size) { OPERATOR_NEW_BODY_ARRAY; }
 CXX_OPERATOR_ATTRIBUTE
-void* operator new(size_t size, std::nothrow_t const&) {
+void* operator new(size_t size, std::nothrow_t const&) NOEXCEPT {
   OPERATOR_NEW_BODY_NOTHROW;
 }
 CXX_OPERATOR_ATTRIBUTE
-void* operator new[](size_t size, std::nothrow_t const&) {
+void* operator new[](size_t size, std::nothrow_t const&) NOEXCEPT {
   OPERATOR_NEW_BODY_ARRAY_NOTHROW;
 }
 CXX_OPERATOR_ATTRIBUTE
@@ -126,12 +93,13 @@ void* operator new[](size_t size, std::align_val_t align) {
   OPERATOR_NEW_BODY_ALIGN_ARRAY;
 }
 CXX_OPERATOR_ATTRIBUTE
-void* operator new(size_t size, std::align_val_t align, std::nothrow_t const&) {
+void* operator new(size_t size, std::align_val_t align,
+                   std::nothrow_t const&) NOEXCEPT {
   OPERATOR_NEW_BODY_ALIGN_NOTHROW;
 }
 CXX_OPERATOR_ATTRIBUTE
 void* operator new[](size_t size, std::align_val_t align,
-                     std::nothrow_t const&) {
+                     std::nothrow_t const&) NOEXCEPT {
   OPERATOR_NEW_BODY_ALIGN_ARRAY_NOTHROW;
 }
 
@@ -177,9 +145,11 @@ void operator delete(void* ptr) NOEXCEPT { OPERATOR_DELETE_BODY; }
 CXX_OPERATOR_ATTRIBUTE
 void operator delete[](void* ptr) NOEXCEPT { OPERATOR_DELETE_BODY_ARRAY; }
 CXX_OPERATOR_ATTRIBUTE
-void operator delete(void* ptr, std::nothrow_t const&) { OPERATOR_DELETE_BODY; }
+void operator delete(void* ptr, std::nothrow_t const&) NOEXCEPT {
+  OPERATOR_DELETE_BODY;
+}
 CXX_OPERATOR_ATTRIBUTE
-void operator delete[](void* ptr, std::nothrow_t const&) {
+void operator delete[](void* ptr, std::nothrow_t const&) NOEXCEPT {
   OPERATOR_DELETE_BODY_ARRAY;
 }
 CXX_OPERATOR_ATTRIBUTE
@@ -199,12 +169,13 @@ void operator delete[](void* ptr, std::align_val_t align) NOEXCEPT {
   OPERATOR_DELETE_BODY_ALIGN_ARRAY;
 }
 CXX_OPERATOR_ATTRIBUTE
-void operator delete(void* ptr, std::align_val_t align, std::nothrow_t const&) {
+void operator delete(void* ptr, std::align_val_t align,
+                     std::nothrow_t const&) NOEXCEPT {
   OPERATOR_DELETE_BODY_ALIGN;
 }
 CXX_OPERATOR_ATTRIBUTE
 void operator delete[](void* ptr, std::align_val_t align,
-                       std::nothrow_t const&) {
+                       std::nothrow_t const&) NOEXCEPT {
   OPERATOR_DELETE_BODY_ALIGN_ARRAY;
 }
 CXX_OPERATOR_ATTRIBUTE
diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_flags.inc b/compiler-rt/lib/sanitizer_common/sanitizer_flags.inc
index 5f449907f6011..e7350ef615c27 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_flags.inc
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_flags.inc
@@ -83,8 +83,14 @@ COMMON_FLAG(
     "detect_leaks=false, or if __lsan_do_leak_check() is called before the "
     "handler has a chance to run.")
 COMMON_FLAG(bool, allocator_may_return_null, false,
-            "If false, the allocator will crash instead of returning 0 on "
-            "out-of-memory.")
+            "Controls allocator behavior on out-of-memory. "
+            "If 'false' (default), out-of-memory events force an abort. "
+            "If 'true', allocators conform to the C/C++ standard and either "
+            "return 'null' or throw the C++ std::bad_alloc exception as "
+            "expected. "
+            "NOTE: On Windows sanitizer runtimes are built without exceptions "
+            "so throwing allocator forms always abort, regardless of the "
+            "value of this flag.")
 COMMON_FLAG(bool, print_summary, true,
             "If false, disable printing error summaries in addition to error "
             "reports.")
diff --git a/compiler-rt/test/asan/TestCases/new_handler_invocation.cpp b/compiler-rt/test/asan/TestCases/new_handler_invocation.cpp
new file mode 100644
index 0000000000000..cd526eb764ccd
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/new_handler_invocation.cpp
@@ -0,0 +1,45 @@
+// Throwing operator new must invoke std::new_handler before throwing
+// std::bad_alloc, per [new.delete.single]/3 (and /4 for the nothrow form).
+// Opt-in via allocator_may_return_null=1: the handler chain runs regardless
+// of the flag, but the chain-exhausted action only becomes "throw bad_alloc"
+// when the flag is set; otherwise it's ReportOutOfMemory + Die().
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+static int handler_calls = 0;
+
+static void my_handler() {
+  ++handler_calls;
+  fprintf(stderr, "handler call %d\n", handler_calls);
+  // Break the loop. A real handler would free memory and return; this
+  // allocation is unrecoverable so we throw to terminate.
+  throw std::bad_alloc();
+}
+
+int main() {
+  std::set_new_handler(my_handler);
+  try {
+    char *p = new char[kHugeSize];
+    fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  } catch (const std::bad_alloc &) {
+    fprintf(stderr, "caught bad_alloc after %d handler call(s)\n",
+            handler_calls);
+  }
+  // CHECK: handler call 1
+  // CHECK: caught bad_alloc after 1 handler call(s)
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/new_handler_throws_other.cpp b/compiler-rt/test/asan/TestCases/new_handler_throws_other.cpp
new file mode 100644
index 0000000000000..8d64959228dd7
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/new_handler_throws_other.cpp
@@ -0,0 +1,36 @@
+// If the registered std::new_handler throws an exception other than
+// std::bad_alloc, that exception must propagate out of operator new
+// unmodified. Opt-in via allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+#include <stdexcept>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+static void my_handler() { throw std::runtime_error("oom-policy"); }
+
+int main() {
+  std::set_new_handler(my_handler);
+  try {
+    char *p = new char[kHugeSize];
+    fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  } catch (const std::bad_alloc &) {
+    fprintf(stderr, "FAIL: caught bad_alloc instead of runtime_error\n");
+  } catch (const std::runtime_error &e) {
+    fprintf(stderr, "caught runtime_error: %s\n", e.what());
+  }
+  // CHECK: caught runtime_error: oom-policy
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/nothrow_new_aligned_array_returns_null.cpp b/compiler-rt/test/asan/TestCases/nothrow_new_aligned_array_returns_null.cpp
new file mode 100644
index 0000000000000..1430c24f5cf57
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/nothrow_new_aligned_array_returns_null.cpp
@@ -0,0 +1,27 @@
+// Aligned array nothrow operator new must return nullptr on allocation
+// failure (OPERATOR_NEW_BODY_ALIGN_ARRAY_NOTHROW). Opt-in via
+// allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 -std=c++17 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+struct alignas(64) HugeAligned {
+#if __LP64__ || defined(_WIN64)
+  char data[(1ULL << 40) + 1];
+#else
+  char data[(3UL << 30) + 1];
+#endif
+};
+
+int main() {
+  HugeAligned *p = new (std::nothrow) HugeAligned[1];
+  fprintf(stderr, "nothrow aligned array returned %s\n",
+          p ? "non-null" : "null");
+  // CHECK: nothrow aligned array returned null
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/nothrow_new_aligned_single_returns_null.cpp b/compiler-rt/test/asan/TestCases/nothrow_new_aligned_single_returns_null.cpp
new file mode 100644
index 0000000000000..c0096f54bc255
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/nothrow_new_aligned_single_returns_null.cpp
@@ -0,0 +1,26 @@
+// Aligned single-object nothrow operator new must return nullptr on
+// allocation failure (OPERATOR_NEW_BODY_ALIGN_NOTHROW). Opt-in via
+// allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 -std=c++17 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+struct alignas(64) HugeAligned {
+#if __LP64__ || defined(_WIN64)
+  char data[(1ULL << 40) + 1];
+#else
+  char data[(3UL << 30) + 1];
+#endif
+};
+
+int main() {
+  HugeAligned *p = new (std::nothrow) HugeAligned;
+  fprintf(stderr, "nothrow aligned returned %s\n", p ? "non-null" : "null");
+  // CHECK: nothrow aligned returned null
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/nothrow_new_default_aborts.cpp b/compiler-rt/test/asan/TestCases/nothrow_new_default_aborts.cpp
new file mode 100644
index 0000000000000..f35b8c7bea92e
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/nothrow_new_default_aborts.cpp
@@ -0,0 +1,33 @@
+// With allocator_may_return_null=false (default), nothrow operator new
+// aborts on OOM. The handler chain runs (per [new.delete.single]/4); on
+// chain exhaustion the runtime emits the asan diagnostic and Die()s rather
+// than returning nullptr.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+int main() {
+  // No new_handler installed -> chain exhausts immediately -> default flag
+  // selects the abort path.
+  char *p = new (std::nothrow) char[kHugeSize];
+  fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  return 0;
+}
+
+// Linux's secondary mmap fails first (out of memory) and Windows's
+// kMaxAllowedMallocSize check trips first (requested allocation size); both
+// prove the default-flag abort path was taken.
+// CHECK: AddressSanitizer: {{out of memory|requested allocation size}}
+// CHECK: ABORTING
diff --git a/compiler-rt/test/asan/TestCases/nothrow_new_handler_throws_other.cpp b/compiler-rt/test/asan/TestCases/nothrow_new_handler_throws_other.cpp
new file mode 100644
index 0000000000000..e85eb7b6d8729
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/nothrow_new_handler_throws_other.cpp
@@ -0,0 +1,32 @@
+// If the registered std::new_handler throws an exception other than
+// std::bad_alloc, the nothrow operator new must catch it and return nullptr
+// (per [new.delete.single]/4 which specifies as-if try/catch behavior; the
+// framework's try/catch swallows all exception types, not just bad_alloc).
+// Opt-in via allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+#include <stdexcept>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+static void my_handler() { throw std::runtime_error("oom-policy"); }
+
+int main() {
+  std::set_new_handler(my_handler);
+  char *p = new (std::nothrow) char[kHugeSize];
+  fprintf(stderr, "nothrow returned %s\n", p ? "non-null" : "null");
+  // CHECK: nothrow returned null
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/nothrow_new_returns_null.cpp b/compiler-rt/test/asan/TestCases/nothrow_new_returns_null.cpp
new file mode 100644
index 0000000000000..19e73ca3c58d7
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/nothrow_new_returns_null.cpp
@@ -0,0 +1,26 @@
+// Per [new.delete.single]/4, nothrow operator new must return nullptr on
+// allocation failure after running the new_handler chain. Opt-in via
+// allocator_may_return_null=1; the default-flag abort case is covered by
+// nothrow_new_default_aborts.cpp.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+int main() {
+  char *p = new (std::nothrow) char[kHugeSize];
+  fprintf(stderr, "nothrow returned %s\n", p ? "non-null" : "null");
+  // CHECK: nothrow returned null
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/nothrow_new_single_returns_null.cpp b/compiler-rt/test/asan/TestCases/nothrow_new_single_returns_null.cpp
new file mode 100644
index 0000000000000..23c264c618437
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/nothrow_new_single_returns_null.cpp
@@ -0,0 +1,25 @@
+// Single-object nothrow operator new must return nullptr on allocation
+// failure (OPERATOR_NEW_BODY_NOTHROW). Opt-in via allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+struct alignas(1) Huge {
+#if __LP64__ || defined(_WIN64)
+  char data[(1ULL << 40) + 1];
+#else
+  char data[(3UL << 30) + 1];
+#endif
+};
+
+int main() {
+  Huge *p = new (std::nothrow) Huge;
+  fprintf(stderr, "nothrow returned %s\n", p ? "non-null" : "null");
+  // CHECK: nothrow returned null
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned.cpp b/compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned.cpp
new file mode 100644
index 0000000000000..7384a2fa91f2b
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned.cpp
@@ -0,0 +1,36 @@
+// Throwing aligned operator new must throw std::bad_alloc on allocation
+// failure, just like the unaligned form. Opt-in via allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 -std=c++17 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+struct alignas(64) Aligned {
+  char data[1];
+};
+
+int main() {
+  bool caught = false;
+  try {
+    Aligned *p = new Aligned[kHugeSize];
+    fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  } catch (const std::bad_alloc &) {
+    caught = true;
+  }
+  if (caught)
+    fprintf(stderr, "caught bad_alloc\n");
+  // CHECK: caught bad_alloc
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned_single.cpp b/compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned_single.cpp
new file mode 100644
index 0000000000000..e9966cb459080
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/throw_bad_alloc_aligned_single.cpp
@@ -0,0 +1,34 @@
+// Aligned single-object throwing operator new must throw std::bad_alloc on
+// allocation failure (OPERATOR_NEW_BODY_ALIGN). Opt-in via
+// allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 -std=c++17 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+struct alignas(64) HugeAligned {
+#if __LP64__ || defined(_WIN64)
+  char data[(1ULL << 40) + 1];
+#else
+  char data[(3UL << 30) + 1];
+#endif
+};
+
+int main() {
+  bool caught = false;
+  try {
+    HugeAligned *p = new HugeAligned;
+    fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  } catch (const std::bad_alloc &) {
+    caught = true;
+  }
+  if (caught)
+    fprintf(stderr, "caught bad_alloc\n");
+  // CHECK: caught bad_alloc
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/throw_bad_alloc_default_aborts.cpp b/compiler-rt/test/asan/TestCases/throw_bad_alloc_default_aborts.cpp
new file mode 100644
index 0000000000000..408747f083922
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/throw_bad_alloc_default_aborts.cpp
@@ -0,0 +1,31 @@
+// With allocator_may_return_null=false (default), throwing operator new
+// aborts on OOM. The handler chain runs (per [new.delete.single]/3); on
+// chain exhaustion the runtime emits the asan ERROR + SUMMARY block and
+// Die()s.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: not %run %t 2>&1 | FileCheck %s
+
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+int main() {
+  // No new_handler installed -> chain exhausts immediately -> default flag
+  // selects the abort path.
+  char *p = new char[kHugeSize];
+  fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  return 0;
+}
+
+// CHECK: AddressSanitizer: out of memory
+// CHECK: ABORTING
diff --git a/compiler-rt/test/asan/TestCases/throw_bad_alloc_oversize.cpp b/compiler-rt/test/asan/TestCases/throw_bad_alloc_oversize.cpp
new file mode 100644
index 0000000000000..dadf85724294e
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/throw_bad_alloc_oversize.cpp
@@ -0,0 +1,35 @@
+// Throwing operator new must throw std::bad_alloc on allocation failure
+// (here triggered by an oversize request) rather than aborting. Opt-in via
+// allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// Windows asan can't throw bad_alloc; see
+// sanitizer_common/sanitizer_new_handler.h.
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+static const size_t kHugeSize =
+#if __LP64__ || defined(_WIN64)
+    (1ULL << 40) + 1;
+#else
+    (3UL << 30) + 1;
+#endif
+
+int main() {
+  bool caught = false;
+  try {
+    char *p = new char[kHugeSize];
+    fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  } catch (const std::bad_alloc &) {
+    caught = true;
+  }
+  if (caught)
+    fprintf(stderr, "caught bad_alloc\n");
+  // CHECK: caught bad_alloc
+  return 0;
+}
diff --git a/compiler-rt/test/asan/TestCases/throw_bad_alloc_single.cpp b/compiler-rt/test/asan/TestCases/throw_bad_alloc_single.cpp
new file mode 100644
index 0000000000000..da019cb0a9f15
--- /dev/null
+++ b/compiler-rt/test/asan/TestCases/throw_bad_alloc_single.cpp
@@ -0,0 +1,35 @@
+// Single-object throwing operator new must throw std::bad_alloc on
+// allocation failure (OPERATOR_NEW_BODY). Opt-in via
+// allocator_may_return_null=1.
+
+// RUN: %clangxx_asan -O0 %s -o %t
+// RUN: %env_asan_opts=allocator_may_return_null=1 %run %t 2>&1 | FileCheck %s
+
+// UNSUPPORTED: target={{.*windows.*}}
+// REQUIRES: stable-runtime
+
+#include <cstdio>
+#include <new>
+
+// Single object whose size alone exceeds the allocator's limit.
+struct alignas(1) Huge {
+#if __LP64__ || defined(_WIN64)
+  char data[(1ULL << 40) + 1];
+#else
+  char data[(3UL << 30) + 1];
+#endif
+};
+
+int main() {
+  bool caught = false;
+  try {
+    Huge *p = new Huge;
+    fprintf(stderr, "FAIL: allocation unexpectedly returned %p\n", p);
+  } catch (const std::bad_alloc &) {
+    caught = true;
+  }
+  if (caught)
+    fprintf(stderr, "caught bad_alloc\n");
+  // CHECK: caught bad_alloc
+  return 0;
+}
diff --git a/compiler-rt/test/sanitizer_common/TestCases/Linux/allocator_returns_null_std.cpp b/compiler-rt/test/sanitizer_common/TestCases/Linux/allocator_returns_null_std.cpp
index 812cf049f2a9b..3d315cff02fd8 100644
--- a/compiler-rt/test/sanitizer_common/TestCases/Linux/allocator_returns_null_std.cpp
+++ b/compiler-rt/test/sanitizer_common/TestCases/Linux/allocator_returns_null_std.cpp
@@ -27,4 +27,8 @@ int main(int argc, char **argv) {
 }
 
 // CHECK: #{{[0-9]+.*}}allocator_returns_null_std.cpp
-// CHECK: {{SUMMARY: .*Sanitizer: allocation-size-too-big.*allocator_returns_null_std.cpp.*}} in main
+// std::vector::resize uses throwing operator new[]. asan forces
+// may_return_null=true on Allocate so std::get_new_handler() runs first; the
+// chain-exhausted abort path emits "out-of-memory" rather than the in-place
+// "allocation-size-too-big" emitted by other sanitizers.
+// CHECK: {{SUMMARY: .*Sanitizer: (allocation-size-too-big|out-of-memory).*allocator_returns_null_std.cpp.*}} in main
diff --git a/compiler-rt/test/sanitizer_common/TestCases/allocator_returns_null.cpp b/compiler-rt/test/sanitizer_common/TestCases/allocator_returns_null.cpp
index ca6f637b9a3f5..de7dcf0712197 100644
--- a/compiler-rt/test/sanitizer_common/TestCases/allocator_returns_null.cpp
+++ b/compiler-rt/test/sanitizer_common/TestCases/allocator_returns_null.cpp
@@ -28,14 +28,20 @@
 // RUN:   | FileCheck %s --check-prefix=CHECK-NULL
 // RUN: %env_tool_opts=allocator_may_return_null=0 not %run %t new 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=CHECK-nCRASH
-// RUN: %env_tool_opts=allocator_may_return_null=1 not %run %t new 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=CHECK-nCRASH-OOM
+// flag=1 + throwing new: asan throws bad_alloc, the test catches it and
+// converges to CHECK-NULL; other sanitizers abort inside operator new.
+// RUN: %if asan %{ %env_tool_opts=allocator_may_return_null=1     %run %t new 2>&1 | FileCheck %s --check-prefix=CHECK-NULL %}
+// RUN: %if !asan %{ %env_tool_opts=allocator_may_return_null=1 not %run %t new 2>&1 | FileCheck %s --check-prefix=CHECK-nCRASH-OOM %}
 // RUN: %env_tool_opts=allocator_may_return_null=0 not %run %t new-nothrow 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=CHECK-nnCRASH
 // RUN: %env_tool_opts=allocator_may_return_null=1     %run %t new-nothrow 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=CHECK-NULL
 
 // TODO(alekseyshl): win32 is disabled due to failing errno tests, fix it there.
+// Windows asan would also fail the flag=1 + new cell above: its runtime is
+// built without exceptions and never throws bad_alloc, so the throwing form
+// always falls back to ReportOutOfMemory + Die(). Re-enabling this test on
+// Windows requires tightening the %if asan dispatch to exclude Windows.
 // UNSUPPORTED: ubsan, target={{.*windows-msvc.*}}
 
 #include <assert.h>
@@ -79,7 +85,14 @@ int main(int argc, char **argv) {
     assert(*t == 42);
     free(t);
   } else if (!strcmp(action, "new")) {
-    x = operator new(kMaxAllowedMallocSizePlusOne);
+    try {
+      x = operator new(kMaxAllowedMallocSizePlusOne);
+      assert(0 && "throwing operator new returned without throwing -- "
+                  "violates [basic.stc.dynamic.allocation]/3");
+    } catch (const std::bad_alloc &) {
+      x = nullptr;
+      errno = ENOMEM;
+    }
   } else if (!strcmp(action, "new-nothrow")) {
     x = operator new(kMaxAllowedMallocSizePlusOne, std::nothrow);
   } else {
@@ -110,13 +123,18 @@ int main(int argc, char **argv) {
 // CHECK-mrCRASH: {{SUMMARY: .*Sanitizer: allocation-size-too-big.*allocator_returns_null.cpp.*}} in main
 // CHECK-nCRASH: new:
 // CHECK-nCRASH: #{{[0-9]+.*}}allocator_returns_null.cpp
-// CHECK-nCRASH: {{SUMMARY: .*Sanitizer: allocation-size-too-big.*allocator_returns_null.cpp.*}} in main
+// asan's throwing/nothrow operator new forces may_return_null=true on
+// Allocate so std::get_new_handler() runs first; the chain-exhausted abort
+// path emits "out-of-memory" rather than the in-place
+// "allocation-size-too-big" emitted by other sanitizers. Same alternation
+// applies to CHECK-nnCRASH.
+// CHECK-nCRASH: {{SUMMARY: .*Sanitizer: (allocation-size-too-big|out-of-memory).*allocator_returns_null.cpp.*}} in main
 // CHECK-nCRASH-OOM: new:
-// CHECK-nCRASH-O#{{[0-9]+.*}}allocator_returns_null.cpp
+// CHECK-nCRASH-OOM: #{{[0-9]+.*}}allocator_returns_null.cpp
 // CHECK-nCRASH-OOM: {{SUMMARY: .*Sanitizer: out-of-memory.*allocator_returns_null.cpp.*}} in main
 // CHECK-nnCRASH: new-nothrow:
 // CHECK-nnCRASH: #{{[0-9]+.*}}allocator_returns_null.cpp
-// CHECK-nnCRASH: {{SUMMARY: .*Sanitizer: allocation-size-too-big.*allocator_returns_null.cpp.*}} in main
+// CHECK-nnCRASH: {{SUMMARY: .*Sanitizer: (allocation-size-too-big|out-of-memory).*allocator_returns_null.cpp.*}} in main
 
-// CHECK-NULL: {{malloc|calloc|calloc-overflow|realloc|realloc-after-malloc|new-nothrow}}
+// CHECK-NULL: {{malloc|calloc|calloc-overflow|realloc|realloc-after-malloc|new-nothrow|new}}
 // CHECK-NULL: errno: 12, x: 0
diff --git a/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp b/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp
index 2fde16fbed3d2..94e71b9e8835c 100644
--- a/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp
+++ b/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp
@@ -28,8 +28,10 @@
 // RUN:   | FileCheck %s --check-prefix=CHECK-NULL
 // RUN: %env_tool_opts=max_allocation_size_mb=2:allocator_may_return_null=0 \
 // RUN:   not %run %t new 2>&1 | FileCheck %s --check-prefix=CHECK-nCRASH
-// RUN: %env_tool_opts=max_allocation_size_mb=2:allocator_may_return_null=1 \
-// RUN:   not %run %t new 2>&1 | FileCheck %s --check-prefix=CHECK-nCRASH-OOM
+// flag=1 + throwing new: asan throws bad_alloc, allocate() catches it and
+// converges to CHECK-NULL; other sanitizers abort inside operator new.
+// RUN: %if asan %{ %env_tool_opts=max_allocation_size_mb=2:allocator_may_return_null=1     %run %t new 2>&1 | FileCheck %s --check-prefix=CHECK-NULL %}
+// RUN: %if !asan %{ %env_tool_opts=max_allocation_size_mb=2:allocator_may_return_null=1 not %run %t new 2>&1 | FileCheck %s --check-prefix=CHECK-nCRASH-OOM %}
 // RUN: %env_tool_opts=max_allocation_size_mb=2:allocator_may_return_null=0 \
 // RUN:   not %run %t new-nothrow 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=CHECK-nnCRASH
@@ -41,6 +43,10 @@
 // RUN:   %run %t strndup 2>&1 | FileCheck %s --check-prefix=CHECK-NULL
 
 // win32 is disabled due to failing errno tests.
+// Windows asan would also fail the flag=1 + new cell above: its runtime is
+// built without exceptions and never throws bad_alloc, so the throwing form
+// always falls back to ReportOutOfMemory + Die(). Re-enabling this test on
+// Windows requires tightening the %if asan dispatch to exclude Windows.
 // UNSUPPORTED: ubsan, target={{.*windows-msvc.*}}
 
 // Symbolizer needs to allocated memory when reporting.
@@ -70,8 +76,18 @@ static void *allocate(const char *Action, size_t Size) {
     free(P);
     return nullptr;
   }
-  if (!strcmp(Action, "new"))
-    return ::operator new(Size);
+  if (!strcmp(Action, "new")) {
+    try {
+      void *p = ::operator new(Size);
+      assert(p != nullptr &&
+             "throwing operator new returned nullptr without throwing -- "
+             "violates [basic.stc.dynamic.allocation]/3");
+      return p;
+    } catch (const std::bad_alloc &) {
+      errno = ENOMEM;
+      return nullptr;
+    }
+  }
   if (!strcmp(Action, "new-nothrow"))
     return ::operator new(Size, std::nothrow);
   if (!strcmp(Action, "strndup")) {
@@ -136,18 +152,23 @@ int main(int Argc, char **Argv) {
 // CHECK-mrCRASH: {{SUMMARY: .*Sanitizer: allocation-size-too-big.* in allocate}}
 // CHECK-nCRASH: new:
 // CHECK-nCRASH: #{{[0-9]+.*}}max_allocation_size.cpp
-// CHECK-nCRASH: {{SUMMARY: .*Sanitizer: allocation-size-too-big.* in allocate}}
+// asan's throwing/nothrow operator new forces may_return_null=true on
+// Allocate so std::get_new_handler() runs first; the chain-exhausted abort
+// path emits "out-of-memory" rather than the in-place
+// "allocation-size-too-big" emitted by other sanitizers. Same alternation
+// applies to CHECK-nnCRASH.
+// CHECK-nCRASH: {{SUMMARY: .*Sanitizer: (allocation-size-too-big|out-of-memory).* in allocate}}
 // CHECK-nCRASH-OOM: new:
 // CHECK-nCRASH-OOM: #{{[0-9]+.*}}max_allocation_size.cpp
 // CHECK-nCRASH-OOM: {{SUMMARY: .*Sanitizer: out-of-memory.* in allocate}}
 // CHECK-nnCRASH: new-nothrow:
 // CHECK-nnCRASH: #{{[0-9]+.*}}max_allocation_size.cpp
-// CHECK-nnCRASH: {{SUMMARY: .*Sanitizer: allocation-size-too-big.* in allocate}}
+// CHECK-nnCRASH: {{SUMMARY: .*Sanitizer: (allocation-size-too-big|out-of-memory).* in allocate}}
 // CHECK-sCRASH: strndup:
 // CHECK-sCRASH: #{{[0-9]+.*}}max_allocation_size.cpp
 // CHECK-sCRASH: {{SUMMARY: .*Sanitizer: allocation-size-too-big.*}}
 
-// CHECK-NULL: {{malloc|calloc|calloc-overflow|realloc|realloc-after-malloc|new-nothrow|strndup}}
+// CHECK-NULL: {{malloc|calloc|calloc-overflow|realloc|realloc-after-malloc|new-nothrow|new|strndup}}
 // CHECK-NULL: errno: 12, P: 0
 //
 // CHECK-NOTNULL-NOT: P: 0

>From 97fd650c20f5a30bf1595ac454553c8086447641 Mon Sep 17 00:00:00 2001
From: "Justin T. Gibbs" <gibbs at scsiguy.com>
Date: Tue, 28 Jul 2026 10:54:38 -0700
Subject: [PATCH 3/3] DIAG (DO NOT MERGE): print std::bad_alloc typeinfo
 pointers on both sides
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

max_allocation_size.cpp fails on macOS asan with an uncaught std::bad_alloc,
though on Linux the same test passes end-to-end. The leading hypothesis is a
typeinfo-identity mismatch across the runtime/test dylib boundary — but
theory alone can't decide it, since std::bad_alloc's typeinfo is nominally
sourced from a single libc++abi.dylib on Darwin.

Add temporary logging so a Darwin CI run tells us directly:

  * Runtime side (sanitizer_new_handler.h, right before `throw
    std::bad_alloc()`): print &_ZTISt9bad_alloc via a forward-declared
    Itanium ABI symbol reference (the -fno-rtti TU can't use typeid).

  * Test side (max_allocation_size.cpp, in the "new" branch): print
    &typeid(std::bad_alloc) before the operator new call, and in the typed
    catch clause alongside &typeid(e) for the caught object. Fall back to
    catch(...) with abi::__cxa_current_exception_type() so we also see the
    type_info* of whatever actually arrives when the typed catch doesn't
    match.

Log interpretation matrix:

  * All three DIAG lines appear and pointers agree → catch fired, typeinfo
    identity holds, our failure hypothesis is wrong.
  * DIAG-TEST-BADALLOC-TINFO + DIAG-RUNTIME-BADALLOC-TINFO appear but
    neither DIAG-TEST-CAUGHT-BADALLOC nor DIAG-TEST-CAUGHT-OTHER does →
    exception died before reaching user code (unwinder/interposition path).
  * DIAG-TEST-CAUGHT-OTHER appears with a bad_alloc typeinfo pointer that
    differs from the expected one → cross-dylib typeinfo identity failure;
    fix is at the link/loader layer.
  * DIAG-TEST-CAUGHT-OTHER appears with an unrelated type_info name →
    something other than bad_alloc is being thrown; fix is at the throw
    site.

Verified locally on Linux — all pointers match at 0x554cf0, typed catch
fires, catch(...) fallback is silent, CHECK-NULL still matches. To be
reverted once the Darwin data is in.
---
 .../sanitizer_common/sanitizer_new_handler.h  | 17 +++++++++-
 .../TestCases/max_allocation_size.cpp         | 31 ++++++++++++++++++-
 2 files changed, 46 insertions(+), 2 deletions(-)

diff --git a/compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h b/compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h
index 6a5a361ec6324..0e8a2aa52262b 100644
--- a/compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h
+++ b/compiler-rt/lib/sanitizer_common/sanitizer_new_handler.h
@@ -77,6 +77,15 @@ NORETURN void InvokeOnExhausted(OnExhausted on_exhausted) {
   UNREACHABLE("operator new OnExhausted callable returned");
 }
 
+// DIAGNOSTIC (DO NOT MERGE): forward-declare the Itanium ABI typeinfo symbol
+// for std::bad_alloc so we can print its address from a -fno-rtti TU without
+// invoking typeid. Guarded to the same condition as the throw itself.
+#if !SANITIZER_WINDOWS && defined(__cpp_exceptions)
+extern "C" {
+extern const char _ZTISt9bad_alloc[];
+}
+#endif
+
 // Throwing operator new: chain, then on exhaustion throw std::bad_alloc
 // when this TU was compiled with exception support and AllocatorMayReturnNull()
 // is true. Otherwise (Windows runtimes, -fno-exceptions builds, or the
@@ -87,8 +96,14 @@ void* NewImplThrowing(Alloc alloc, OnExhausted on_exhausted) {
   if (LIKELY(res != nullptr))
     return res;
 #if !SANITIZER_WINDOWS && defined(__cpp_exceptions)
-  if (AllocatorMayReturnNull())
+  if (AllocatorMayReturnNull()) {
+    // DIAGNOSTIC (DO NOT MERGE): print the runtime-side typeinfo pointer for
+    // std::bad_alloc right before the throw, so CI logs let us compare it to
+    // the pointer observed by the test binary at the catch site.
+    Printf("DIAG-RUNTIME-BADALLOC-TINFO: %p\n",
+           (const void*)&_ZTISt9bad_alloc);
     throw std::bad_alloc();
+  }
 #endif
   InvokeOnExhausted(on_exhausted);
 }
diff --git a/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp b/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp
index 94e71b9e8835c..3d8b872cbce01 100644
--- a/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp
+++ b/compiler-rt/test/sanitizer_common/TestCases/max_allocation_size.cpp
@@ -53,12 +53,14 @@
 // UNSUPPORTED: internal_symbolizer
 
 #include <assert.h>
+#include <cxxabi.h>
 #include <errno.h>
 #include <limits>
 #include <new>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <typeinfo>
 
 constexpr size_t MaxAllocationSize = size_t{2} << 20;
 
@@ -77,13 +79,40 @@ static void *allocate(const char *Action, size_t Size) {
     return nullptr;
   }
   if (!strcmp(Action, "new")) {
+    // DIAGNOSTIC (DO NOT MERGE): print the test-side typeinfo pointer for
+    // std::bad_alloc so CI logs let us compare it to the runtime-side pointer
+    // printed just before the throw. If the pointers agree, the catch failure
+    // is not a typeinfo-identity issue; if they differ, it is.
+    fprintf(stderr, "DIAG-TEST-BADALLOC-TINFO: %p\n",
+            (const void *)&typeid(std::bad_alloc));
     try {
       void *p = ::operator new(Size);
       assert(p != nullptr &&
              "throwing operator new returned nullptr without throwing -- "
              "violates [basic.stc.dynamic.allocation]/3");
       return p;
-    } catch (const std::bad_alloc &) {
+    } catch (const std::bad_alloc &e) {
+      fprintf(stderr, "DIAG-TEST-CAUGHT-BADALLOC: caught=%p thrown=%p\n",
+              (const void *)&typeid(std::bad_alloc),
+              (const void *)&typeid(e));
+      errno = ENOMEM;
+      return nullptr;
+    } catch (...) {
+      // DIAGNOSTIC (DO NOT MERGE): fallback so we can distinguish
+      //   (a) bad_alloc arrived but the typed catch above didn't match
+      //       (typeinfo identity issue), from
+      //   (b) some other type arrived (wrong throw), from
+      //   (c) no exception reached user code at all (terminated before
+      //       unwinding to this frame — neither this line nor the typed
+      //       catch's line will appear in the log).
+      // __cxa_current_exception_type() returns the type_info* of whatever
+      // is currently being handled, without needing to name the type.
+      const std::type_info *ti = abi::__cxa_current_exception_type();
+      fprintf(stderr,
+              "DIAG-TEST-CAUGHT-OTHER: expected_badalloc=%p got_ti=%p "
+              "got_name=%s\n",
+              (const void *)&typeid(std::bad_alloc), (const void *)ti,
+              ti ? ti->name() : "<null>");
       errno = ENOMEM;
       return nullptr;
     }



More information about the llvm-commits mailing list