Skip to content
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

[libc++] Fix the behavior of throwing operator new under -fno-exceptions #69498

Merged
merged 15 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions libcxx/docs/ReleaseNotes/18.rst
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@ LLVM 20
ABI Affecting Changes
---------------------

- When the shared/static library is built with ``-fno-exceptions``, the behavior of ``operator new`` was changed
to make it standards-conforming. In LLVM 17 and before, the throwing versions of ``operator new`` would return
``nullptr`` upon failure to allocate, when the shared/static library was built with exceptions disabled. This
was non-conforming, since the throwing versions of ``operator new`` are never expected to return ``nullptr``, and
this non-conformance could actually lead to miscompiles in subtle cases.

Starting in LLVM 18, the throwing versions of ``operator new`` will abort the program when they fail to allocate
if the shared/static library has been built with ``-fno-exceptions``. This is consistent with the behavior of all
other potentially-throwing functions in the library, which abort the program instead of throwing when ``-fno-exceptions``
is used.

Furthermore, when the shared/static library is built with ``-fno-exceptions``, users who override the throwing
version of ``operator new`` will now need to also override the ``std::nothrow_t`` version of ``operator new`` if
they want to use it. Indeed, this is because there is no way to implement a conforming ``operator new(nothrow)``
from a conforming potentially-throwing ``operator new`` when compiled with ``-fno-exceptions``. In that case, using
``operator new(nothrow)`` without overriding it explicitly but after overriding the throwing ``operator new`` will
result in an error.

Note that this change only impacts vendors/users that build the shared/static library themselves and pass
``-DLIBCXX_ENABLE_EXCEPTIONS=OFF``, which is not the default configuration. If you are using the default
configuration of the library, the libc++ shared/static library will be built with exceptions enabled, and
there is no change between LLVM 17 and LLVM 18, even for users who build their own code using ``-fno-exceptions``.

- The symbol of a non-visible function part of ``std::system_error`` was removed.
This is not a breaking change as the private function ``__init`` was never referenced internally outside of the dylib.

Expand Down
119 changes: 119 additions & 0 deletions libcxx/src/include/overridable_function.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// -*- 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
//
//===----------------------------------------------------------------------===//

#ifndef _LIBCPP_SRC_INCLUDE_OVERRIDABLE_FUNCTION_H
#define _LIBCPP_SRC_INCLUDE_OVERRIDABLE_FUNCTION_H

#include <__config>
#include <cstdint>

#if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
# pragma GCC system_header
#endif

//
// This file provides the std::__is_function_overridden utility, which allows checking
// whether an overridable function (typically a weak symbol) like `operator new`
// has been overridden by a user or not.
//
// This is a low-level utility which does not work on all platforms, since it needs
// to make assumptions about the object file format in use. Furthermore, it requires
// the "base definition" of the function (the one we want to check whether it has been
// overridden) to be annotated with the _LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE macro.
//
// This currently works with Mach-O files (used on Darwin) and with ELF files (used on Linux
// and others). On platforms where we know how to implement this detection, the macro
// _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION is defined to 1, and it is defined to 0 on
// other platforms. The _LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE macro is defined to
// nothing on unsupported platforms so that it can be used to decorate functions regardless
// of whether detection is actually supported.
//
// How does this work?
// -------------------
//
// Let's say we want to check whether a weak function `f` has been overridden by the user.
// The general mechanism works by placing `f`'s definition (in the libc++ built library)
// inside a special section, which we do using the `__section__` attribute via the
// _LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE macro.
//
// Then, when comes the time to check whether the function has been overridden, we take
// the address of the function and we check whether it falls inside the special function
// we created. This can be done by finding pointers to the start and the end of the section
// (which is done differently for ELF and Mach-O), and then checking whether `f` falls
// within those bounds. If it falls within those bounds, then `f` is still inside the
// special section and so it is the version we defined in the libc++ built library, i.e.
// it was not overridden. Otherwise, it was overridden by the user because it falls
// outside of the section.
//
// Important note
// --------------
//
// This mechanism should never be used outside of the libc++ built library. In particular,
// attempting to use this within the libc++ headers will not work at all because we don't
// want to be defining special sections inside user's executables which use our headers.
// This is provided inside libc++'s include tree solely to make it easier to share with
// libc++abi, which needs the same mechanism.
//

#if defined(_LIBCPP_OBJECT_FORMAT_MACHO)

# define _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION 1
# define _LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE \
__attribute__((__section__("__TEXT,__lcxx_override,regular,pure_instructions")))

_LIBCPP_BEGIN_NAMESPACE_STD
template <class _Ret, class... _Args>
_LIBCPP_HIDE_FROM_ABI bool __is_function_overridden(_Ret (*__fptr)(_Args...)) noexcept {
// Declare two dummy bytes and give them these special `__asm` values. These values are
// defined by the linker, which means that referring to `&__lcxx_override_start` will
// effectively refer to the address where the section starts (and same for the end).
extern char __lcxx_override_start __asm("section$start$__TEXT$__lcxx_override");
extern char __lcxx_override_end __asm("section$end$__TEXT$__lcxx_override");

// Now get a uintptr_t out of these locations, and out of the function pointer.
uintptr_t __start = reinterpret_cast<uintptr_t>(&__lcxx_override_start);
uintptr_t __end = reinterpret_cast<uintptr_t>(&__lcxx_override_end);
uintptr_t __ptr = reinterpret_cast<uintptr_t>(__fptr);

// Finally, the function was overridden if it falls outside of the section's bounds.
return __ptr < __start || __ptr > __end;
}
_LIBCPP_END_NAMESPACE_STD

#elif defined(_LIBCPP_OBJECT_FORMAT_ELF)

# define _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION 1
# define _LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE __attribute__((__section__("__lcxx_override")))

// This is very similar to what we do for Mach-O above. The ELF linker will implicitly define
// variables with those names corresponding to the start and the end of the section.
//
// See https://stackoverflow.com/questions/16552710/how-do-you-get-the-start-and-end-addresses-of-a-custom-elf-section
extern char __start___lcxx_override;
extern char __stop___lcxx_override;

_LIBCPP_BEGIN_NAMESPACE_STD
template <class _Ret, class... _Args>
_LIBCPP_HIDE_FROM_ABI bool __is_function_overridden(_Ret (*__fptr)(_Args...)) noexcept {
uintptr_t __start = reinterpret_cast<uintptr_t>(&__start___lcxx_override);
uintptr_t __end = reinterpret_cast<uintptr_t>(&__stop___lcxx_override);
uintptr_t __ptr = reinterpret_cast<uintptr_t>(__fptr);

return __ptr < __start || __ptr > __end;
}
_LIBCPP_END_NAMESPACE_STD

#else

# define _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION 0
# define _LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE /* nothing */

#endif

#endif // _LIBCPP_SRC_INCLUDE_OVERRIDABLE_FUNCTION_H
103 changes: 77 additions & 26 deletions libcxx/src/new.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
//
//===----------------------------------------------------------------------===//

#include "include/overridable_function.h"
#include <__memory/aligned_alloc.h>
#include <cstddef>
#include <cstdlib>
#include <new>

Expand All @@ -15,6 +17,10 @@
// The code below is copied as-is into libc++abi's libcxxabi/src/stdlib_new_delete.cpp
// file. The version in this file is the canonical one.

inline void __throw_bad_alloc_shim() { std::__throw_bad_alloc(); }

# define _LIBCPP_ASSERT_SHIM(expr, str) _LIBCPP_ASSERT(expr, str)

// ------------------ BEGIN COPY ------------------
// Implement all new and delete operators as weak definitions
// in this shared library, so that they can be overridden by programs
Expand All @@ -36,41 +42,63 @@ static void* operator_new_impl(std::size_t size) {
return p;
}

_LIBCPP_WEAK void* operator new(std::size_t size) _THROW_BAD_ALLOC {
_LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE _LIBCPP_WEAK void* operator new(std::size_t size) _THROW_BAD_ALLOC {
void* p = operator_new_impl(size);
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
if (p == nullptr)
throw std::bad_alloc();
# endif
__throw_bad_alloc_shim();
return p;
}

_LIBCPP_WEAK void* operator new(size_t size, const std::nothrow_t&) noexcept {
# ifdef _LIBCPP_HAS_NO_EXCEPTIONS
# if _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION
_LIBCPP_ASSERT_SHIM(
Copy link
Member Author

Choose a reason for hiding this comment

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

@var-const This is a rare example of a case where I think it makes sense to unconditionally have a _LIBCPP_ASSERT -- this should be enabled regardless of the hardening mode IMO. It is similar to when we call std::__throw_logic_error() and friends with -fno-exceptions -- that aborts the program regardless of the hardening mode.

Copy link
Member

Choose a reason for hiding this comment

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

I think you're forgetting how hot of a path this is. I don't think we want to be doing anything more than we need in the majority of cases.

Copy link
Member Author

@ldionne ldionne Oct 20, 2023

Choose a reason for hiding this comment

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

We could make this part of the DEBUG_LITE mode then, I guess. Would that address your concerns?

!std::__is_function_overridden(static_cast<void* (*)(std::size_t)>(&operator new)),
"libc++ was configured with exceptions disabled and `operator new(size_t)` has been overridden, "
"but `operator new(size_t, nothrow_t)` has not been overridden. This is problematic because "
"`operator new(size_t, nothrow_t)` must call `operator new(size_t)`, which will terminate in case "
"it fails to allocate, making it impossible for `operator new(size_t, nothrow_t)` to fulfill its "
"contract (since it should return nullptr upon failure). Please make sure you override "
"`operator new(size_t, nothrow_t)` as well.");
# endif

return operator_new_impl(size);
# else
void* p = nullptr;
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
try {
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
p = ::operator new(size);
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
} catch (...) {
}
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
return p;
# endif
}

_LIBCPP_WEAK void* operator new[](size_t size) _THROW_BAD_ALLOC { return ::operator new(size); }
_LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE _LIBCPP_WEAK void* operator new[](size_t size) _THROW_BAD_ALLOC {
return ::operator new(size);
}

_LIBCPP_WEAK void* operator new[](size_t size, const std::nothrow_t&) noexcept {
# ifdef _LIBCPP_HAS_NO_EXCEPTIONS
# if _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION
_LIBCPP_ASSERT_SHIM(
!std::__is_function_overridden(static_cast<void* (*)(std::size_t)>(&operator new[])),
"libc++ was configured with exceptions disabled and `operator new[](size_t)` has been overridden, "
"but `operator new[](size_t, nothrow_t)` has not been overridden. This is problematic because "
"`operator new[](size_t, nothrow_t)` must call `operator new[](size_t)`, which will terminate in case "
"it fails to allocate, making it impossible for `operator new[](size_t, nothrow_t)` to fulfill its "
"contract (since it should return nullptr upon failure). Please make sure you override "
"`operator new[](size_t, nothrow_t)` as well.");
# endif

return operator_new_impl(size);
# else
void* p = nullptr;
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
try {
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
p = ::operator new[](size);
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
} catch (...) {
}
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
return p;
# endif
}

_LIBCPP_WEAK void operator delete(void* ptr) noexcept { std::free(ptr); }
Expand Down Expand Up @@ -107,43 +135,66 @@ static void* operator_new_aligned_impl(std::size_t size, std::align_val_t alignm
return p;
}

_LIBCPP_WEAK void* operator new(std::size_t size, std::align_val_t alignment) _THROW_BAD_ALLOC {
_LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE _LIBCPP_WEAK void*
operator new(std::size_t size, std::align_val_t alignment) _THROW_BAD_ALLOC {
void* p = operator_new_aligned_impl(size, alignment);
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
if (p == nullptr)
throw std::bad_alloc();
# endif
__throw_bad_alloc_shim();
return p;
}

_LIBCPP_WEAK void* operator new(size_t size, std::align_val_t alignment, const std::nothrow_t&) noexcept {
# ifdef _LIBCPP_HAS_NO_EXCEPTIONS
# if _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION
_LIBCPP_ASSERT_SHIM(
!std::__is_function_overridden(static_cast<void* (*)(std::size_t, std::align_val_t)>(&operator new)),
"libc++ was configured with exceptions disabled and `operator new(size_t, align_val_t)` has been overridden, "
"but `operator new(size_t, align_val_t, nothrow_t)` has not been overridden. This is problematic because "
"`operator new(size_t, align_val_t, nothrow_t)` must call `operator new(size_t, align_val_t)`, which will "
"terminate in case it fails to allocate, making it impossible for `operator new(size_t, align_val_t, nothrow_t)` "
"to fulfill its contract (since it should return nullptr upon failure). Please make sure you override "
"`operator new(size_t, align_val_t, nothrow_t)` as well.");
# endif

return operator_new_aligned_impl(size, alignment);
# else
void* p = nullptr;
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
try {
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
p = ::operator new(size, alignment);
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
} catch (...) {
}
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
return p;
# endif
}

_LIBCPP_WEAK void* operator new[](size_t size, std::align_val_t alignment) _THROW_BAD_ALLOC {
_LIBCPP_MAKE_OVERRIDABLE_FUNCTION_DETECTABLE _LIBCPP_WEAK void*
operator new[](size_t size, std::align_val_t alignment) _THROW_BAD_ALLOC {
return ::operator new(size, alignment);
}

_LIBCPP_WEAK void* operator new[](size_t size, std::align_val_t alignment, const std::nothrow_t&) noexcept {
# ifdef _LIBCPP_HAS_NO_EXCEPTIONS
# if _LIBCPP_CAN_DETECT_OVERRIDDEN_FUNCTION
_LIBCPP_ASSERT_SHIM(
!std::__is_function_overridden(static_cast<void* (*)(std::size_t, std::align_val_t)>(&operator new[])),
"libc++ was configured with exceptions disabled and `operator new[](size_t, align_val_t)` has been overridden, "
"but `operator new[](size_t, align_val_t, nothrow_t)` has not been overridden. This is problematic because "
"`operator new[](size_t, align_val_t, nothrow_t)` must call `operator new[](size_t, align_val_t)`, which will "
"terminate in case it fails to allocate, making it impossible for `operator new[](size_t, align_val_t, "
"nothrow_t)` to fulfill its contract (since it should return nullptr upon failure). Please make sure you "
"override "
"`operator new[](size_t, align_val_t, nothrow_t)` as well.");
# endif

return operator_new_aligned_impl(size, alignment);
# else
void* p = nullptr;
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
try {
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
p = ::operator new[](size, alignment);
# ifndef _LIBCPP_HAS_NO_EXCEPTIONS
} catch (...) {
}
# endif // _LIBCPP_HAS_NO_EXCEPTIONS
return p;
# endif
}

_LIBCPP_WEAK void operator delete(void* ptr, std::align_val_t) noexcept { std::__libcpp_aligned_free(ptr); }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

// void* operator new(std::size_t, const std::nothrow_t&);
// void* operator new(std::size_t, std::align_val_t, const std::nothrow_t&);
// void* operator new[](std::size_t, const std::nothrow_t&);
// void* operator new[](std::size_t, std::align_val_t, const std::nothrow_t&);

// This test ensures that we catch the case where `new` has been overridden but `new(nothrow)`
// has not been overridden, and the library is compiled with -fno-exceptions.
//
// In that case, it is impossible for libc++ to provide a Standards conforming implementation
// of `new(nothrow)`, so the only viable option is to terminate the program.

// REQUIRES: has-unix-headers
// UNSUPPORTED: c++03

// We only know how to diagnose this on platforms that use the ELF or Mach-O object file formats.
// XFAIL: target={{.+}}-windows-{{.+}}

// TODO: We currently don't have a way to express that the built library was
// compiled with -fno-exceptions, so if the library was built with support
// for exceptions but we run the test suite without exceptions, this will
// spuriously fail.
// REQUIRES: no-exceptions

#include <cstddef>
#include <new>

#include "check_assertion.h"

// Override the throwing versions of operator new, but not the nothrow versions.
alignas(32) char DummyData[32 * 3];
void* operator new(std::size_t) { return DummyData; }
void* operator new(std::size_t, std::align_val_t) { return DummyData; }
void* operator new[](std::size_t) { return DummyData; }
void* operator new[](std::size_t, std::align_val_t) { return DummyData; }

void operator delete(void*) noexcept {}
void operator delete(void*, std::align_val_t) noexcept {}
void operator delete[](void*) noexcept {}
void operator delete[](void*, std::align_val_t) noexcept {}

int main(int, char**) {
std::size_t size = 3;
std::align_val_t align = static_cast<std::align_val_t>(32);
EXPECT_ANY_DEATH((void)operator new(size, std::nothrow));
EXPECT_ANY_DEATH((void)operator new(size, align, std::nothrow));
EXPECT_ANY_DEATH((void)operator new[](size, std::nothrow));
EXPECT_ANY_DEATH((void)operator new[](size, align, std::nothrow));

return 0;
}
Loading