Skip to content

[libc++] Fix noexcept behaviour of operator new helper functions #74337

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 22, 2024

Conversation

azhan92
Copy link
Contributor

@azhan92 azhan92 commented Dec 4, 2023

This patch removes the noexcept specifier introduced in #69407 since the
Standard allows a new handler to throw an exception of type bad_alloc
(or derived from it). With the noexcept specifier on the helper functions,
we would immediately terminate the program.

The patch also adds tests for the case that had regressed.

@azhan92 azhan92 requested review from a team as code owners December 4, 2023 16:40
@llvmbot llvmbot added libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. libc++abi libc++abi C++ Runtime Library. Not libc++. labels Dec 4, 2023
@llvmbot
Copy link
Member

llvmbot commented Dec 4, 2023

@llvm/pr-subscribers-libcxx

@llvm/pr-subscribers-libcxxabi

Author: None (azhan92)

Changes

This PR removes the noexcept specification introduced in 69407 since the standard allows

> throw an exception of type bad_alloc or a class derived from bad_alloc

as a behaviour of a new_handler function. The PR also adds a test to catch this.


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

3 Files Affected:

  • (modified) libcxx/src/new.cpp (+2-2)
  • (modified) libcxxabi/src/stdlib_new_delete.cpp (+2-2)
  • (added) libcxxabi/test/test_memory_alloc.pass.cpp (+34)
diff --git a/libcxx/src/new.cpp b/libcxx/src/new.cpp
index 033bba5c1fc95..cb8b4aae8d5f9 100644
--- a/libcxx/src/new.cpp
+++ b/libcxx/src/new.cpp
@@ -20,7 +20,7 @@
 // in this shared library, so that they can be overridden by programs
 // that define non-weak copies of the functions.
 
-static void* operator_new_impl(std::size_t size) noexcept {
+static void* operator_new_impl(std::size_t size) {
   if (size == 0)
     size = 1;
   void* p;
@@ -87,7 +87,7 @@ _LIBCPP_WEAK void operator delete[](void* ptr, size_t) noexcept { ::operator del
 
 #  if !defined(_LIBCPP_HAS_NO_LIBRARY_ALIGNED_ALLOCATION)
 
-static void* operator_new_aligned_impl(std::size_t size, std::align_val_t alignment) noexcept {
+static void* operator_new_aligned_impl(std::size_t size, std::align_val_t alignment) {
   if (size == 0)
     size = 1;
   if (static_cast<size_t>(alignment) < sizeof(void*))
diff --git a/libcxxabi/src/stdlib_new_delete.cpp b/libcxxabi/src/stdlib_new_delete.cpp
index 6c9990f063dde..f8a00ec584256 100644
--- a/libcxxabi/src/stdlib_new_delete.cpp
+++ b/libcxxabi/src/stdlib_new_delete.cpp
@@ -30,7 +30,7 @@
 // in this shared library, so that they can be overridden by programs
 // that define non-weak copies of the functions.
 
-static void* operator_new_impl(std::size_t size) noexcept {
+static void* operator_new_impl(std::size_t size) {
   if (size == 0)
     size = 1;
   void* p;
@@ -107,7 +107,7 @@ void operator delete[](void* ptr, size_t) noexcept { ::operator delete[](ptr); }
 
 #if !defined(_LIBCPP_HAS_NO_LIBRARY_ALIGNED_ALLOCATION)
 
-static void* operator_new_aligned_impl(std::size_t size, std::align_val_t alignment) noexcept {
+static void* operator_new_aligned_impl(std::size_t size, std::align_val_t alignment) {
   if (size == 0)
     size = 1;
   if (static_cast<size_t>(alignment) < sizeof(void*))
diff --git a/libcxxabi/test/test_memory_alloc.pass.cpp b/libcxxabi/test/test_memory_alloc.pass.cpp
new file mode 100644
index 0000000000000..219b85b9afcaf
--- /dev/null
+++ b/libcxxabi/test/test_memory_alloc.pass.cpp
@@ -0,0 +1,34 @@
+//===----------------------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+
+// UNSUPPORTED: no-exceptions
+
+#include <new>
+#include <cassert>
+#include <limits>
+
+int new_handler_called = 0;
+
+void my_new_handler() {
+  ++new_handler_called;
+  throw std::bad_alloc();
+}
+
+int main(int, char**) {
+  std::set_new_handler(my_new_handler);
+  try {
+    void* x = operator new[](std::numeric_limits<std::size_t>::max());
+    (void)x;
+    assert(false);
+  } catch (std::bad_alloc const&) {
+    assert(new_handler_called == 1);
+  } catch (...) {
+    assert(false);
+  }
+  return 0;
+}

Copy link
Member

@ldionne ldionne left a comment

Choose a reason for hiding this comment

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

Thanks for the fix! I have some comments about how to test this.

@@ -0,0 +1,34 @@
//===----------------------------------------------------------------------===//
Copy link
Member

Choose a reason for hiding this comment

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

We have a bunch of tests located in e.g. libcxx/test/std/language.support/support.dynamic/new.delete/new.delete.array/new.size.pass.cpp. Those are testing the various version of operator new we have:

void* operator new(std::size_t);
void* operator new[](std::size_t);
void* operator new(std::size_t, std::align_val_t);
void* operator new[](std::size_t, std::align_val_t);

You'll see each overload has its own test file. Can you add one test for each such overload? Note that we also have std::nothrow_t overloads of operator new but I don't think it makes sense to test that the new_handler throws bad_alloc for those, since the only thing it would do is terminate.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You'll see each overload has its own test file. Can you add one test for each such overload? Note that we also have std::nothrow_t overloads of operator new but I don't think it makes sense to test that the new_handler throws bad_alloc for those, since the only thing it would do is terminate.

@ldionne @azhan92: If I read the following correctly, testing the std::nothrow_t overloads is meaningful (to ensure that the exception is caught by the default implementation):

Default behavior: Calls operator new(size), or operator new(size, alignment), respectively. If
the call returns normally, returns the result of that call. Otherwise, returns a null pointer.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, yes, you're right!

void* x = operator new[](std::numeric_limits<std::size_t>::max());
(void)x;
assert(false);
} catch (std::bad_alloc const&) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe it would be prudent to throw (and catch), within the test, an exception of a class type (defined as part of the test itself) derived from std::bad_alloc. If extra paranoid, the class type should also record the value of the this pointer on construction using a "tag" class type and have a copy constructor that abort()s:

struct my_bad_alloc : std::bad_alloc {
  my_bad_alloc(const my_bad_alloc &) { abort(); }
  my_bad_alloc(construction_key) self(this) {}
  const my_bad_alloc *const self;
};

The recorded pointer value can then be checked against the address of the caught exception object in the catch block.

Copy link

github-actions bot commented Jan 12, 2024

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

//===----------------------------------------------------------------------===//

// UNSUPPORTED: no-exceptions
#include <new>
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick, please add a newline between the UNSUPPORTED and the #include <new>. Here and in the other tests too.

@@ -0,0 +1,42 @@
//===----------------------------------------------------------------------===//
Copy link
Member

Choose a reason for hiding this comment

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

Let's move these tests under libcxx/test/std/language.support/support.dynamic/new.delete. They test the general conformance of operator new, so they should be in the libc++ conformance suite instead. Please also look at e.g. libcxx/test/std/language.support/support.dynamic/new.delete/new.delete.single & friends and see if it would make sense to add these tests to existing files instead, guarded by #ifndef TEST_HAS_NO_EXCEPTIONS.

@ldionne
Copy link
Member

ldionne commented Jan 16, 2024

Also, the latest CI runs shows that your tests seem to be failing under the sanitizers. IDK if that shows a problem with your test or if it's unavoidable, so it would have to be investigated a bit.

@ldionne ldionne added this to the LLVM 18.0.X Release milestone Jan 16, 2024
@azhan92
Copy link
Contributor Author

azhan92 commented Jan 16, 2024

Also, the latest CI runs shows that your tests seem to be failing under the sanitizers. IDK if that shows a problem with your test or if it's unavoidable, so it would have to be investigated a bit.

I noticed that sanitizer-new-delete is unsupported for libcxx/test/std/language.support/support.dynamic/new.delete/new.delete.single/new.size.pass.cpp and similar files so asan/msan will not call the new handler. Does this have something to do with these sanitizer failures?

@ldionne
Copy link
Member

ldionne commented Jan 16, 2024

Almost certainly! Moving the tests like I requested would hence also have the side effect of fixing this.

@azhan92
Copy link
Contributor Author

azhan92 commented Jan 17, 2024

Almost certainly! Moving the tests like I requested would hence also have the side effect of fixing this.

Thank you! I looked at the ones found in libcxx/test/std/language.support/support.dynamic/new.delete/new.delete.single, and it doesn't seem like it makes sense to move the new tests into any of the existing files there. Would it be fine to keep them as separate files?

Copy link
Member

@ldionne ldionne left a comment

Choose a reason for hiding this comment

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

LGTM, thanks a lot for the fix and the comprehensive tests. The CI is passing now (the only failure is unrelated).

I appreciate that you updated this in time for the LLVM 18 branch, this is an important fix. I'll merge this now.

@ldionne
Copy link
Member

ldionne commented Jan 22, 2024

@azhan92 Can you please uncheck the "keep my email private" checkbox from the Github preferences? Otherwise, this will get committed with an anonymous email address and we're trying to steer away from that.

@azhan92
Copy link
Contributor Author

azhan92 commented Jan 22, 2024

@azhan92 Can you please uncheck the "keep my email private" checkbox from the Github preferences? Otherwise, this will get committed with an anonymous email address and we're trying to steer away from that.

Yes, done!

@ldionne ldionne merged commit 4207ad5 into llvm:main Jan 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++abi libc++abi C++ Runtime Library. Not libc++. libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants