Skip to content
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
2 changes: 2 additions & 0 deletions .github/workflows/libcxx-build-and-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,8 @@ jobs:
machine: llvm-premerge-libcxx-runners
- config: 'generic-asan'
machine: llvm-premerge-libcxx-runners
- config: 'generic-asan-in-tests-only'
machine: llvm-premerge-libcxx-runners
- config: 'generic-tsan'
machine: llvm-premerge-libcxx-runners
- config: 'generic-ubsan'
Expand Down
3 changes: 3 additions & 0 deletions libcxx/cmake/caches/Generic-asan-in-tests-only.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Don't build the library itself with ASAN support, but enable ASAN in the test suite.
set(LIBCXX_TEST_PARAMS "use_sanitizer=Address" CACHE STRING "")
set(LIBCXXABI_TEST_PARAMS "${LIBCXX_TEST_PARAMS}" CACHE STRING "")
35 changes: 30 additions & 5 deletions libcxx/include/__debug_utils/sanitizers.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,32 @@
# pragma GCC system_header
#endif

#if __has_feature(address_sanitizer)
// Within libc++, _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS determines whether the containers should
// provide ASAN container overflow checks. That setting attempts to honour ASAN's documented option
// __SANITIZER_DISABLE_CONTAINER_OVERFLOW__ which can be defined by users to disable container overflow
// checks.
//
// However, since parts of some containers (e.g. std::string) are compiled separately into the built
// library, there are caveats:
// - __SANITIZER_DISABLE_CONTAINER_OVERFLOW__ can't always be honoured, i.e. if the built library
// was compiled with ASAN container checks, it's impossible to turn them off afterwards. We diagnose
// this with an error to avoid the proliferation of invalid configurations that appear to work.
//
// - The container overflow checks themselves are not always available even when the user is compiling
// with -fsanitize=address. If a container is compiled separately like std::string, it can't provide
// container checks unless the separately compiled code was built with container checks enabled. These
// containers need to also conditionalize whether they provide overflow checks on `_LIBCPP_INSTRUMENTED_WITH_ASAN`.
#if __has_feature(address_sanitizer) && !defined(__SANITIZER_DISABLE_CONTAINER_OVERFLOW__)
# define _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS 1
#else
# define _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS 0
#endif

#if _LIBCPP_INSTRUMENTED_WITH_ASAN && !_LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
# error "We can't disable ASAN container checks when libc++ has been built with ASAN container checks enabled"
#endif

#if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS

extern "C" {
_LIBCPP_EXPORTED_FROM_ABI void
Expand All @@ -28,12 +53,12 @@ _LIBCPP_EXPORTED_FROM_ABI int
__sanitizer_verify_double_ended_contiguous_container(const void*, const void*, const void*, const void*);
}

#endif // __has_feature(address_sanitizer)
#endif // _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS

_LIBCPP_BEGIN_NAMESPACE_STD

// ASan choices
#if __has_feature(address_sanitizer)
#if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
# define _LIBCPP_HAS_ASAN_CONTAINER_ANNOTATIONS_FOR_ALL_ALLOCATORS 1
#endif

Expand All @@ -57,7 +82,7 @@ _LIBCPP_HIDE_FROM_ABI void __annotate_double_ended_contiguous_container(
const void* __last_old_contained,
const void* __first_new_contained,
const void* __last_new_contained) {
#if !__has_feature(address_sanitizer)
#if !_LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
(void)__first_storage;
(void)__last_storage;
(void)__first_old_contained;
Expand Down Expand Up @@ -86,7 +111,7 @@ _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX14 void __annotate_contiguous_c
const void* __last_storage,
const void* __old_last_contained,
const void* __new_last_contained) {
#if !__has_feature(address_sanitizer)
#if !_LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
(void)__first_storage;
(void)__last_storage;
(void)__old_last_contained;
Expand Down
26 changes: 13 additions & 13 deletions libcxx/include/deque
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ private:
(void)__end;
(void)__annotation_type;
(void)__place;
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
// __beg - index of the first item to annotate
// __end - index behind the last item to annotate (so last item + 1)
// __annotation_type - __asan_unposion or __asan_poison
Expand Down Expand Up @@ -1046,23 +1046,23 @@ private:
std::__annotate_double_ended_contiguous_container<_Allocator>(
__mem_beg, __mem_end, __old_beg, __old_end, __new_beg, __new_end);
}
# endif // __has_feature(address_sanitizer)
# endif // _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
}

_LIBCPP_HIDE_FROM_ABI void __annotate_new(size_type __current_size) const _NOEXCEPT {
(void)__current_size;
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
if (__current_size == 0)
__annotate_from_to(0, __map_.size() * __block_size, __asan_poison, __asan_back_moved);
else {
__annotate_from_to(0, __start_, __asan_poison, __asan_front_moved);
__annotate_from_to(__start_ + __current_size, __map_.size() * __block_size, __asan_poison, __asan_back_moved);
}
# endif // __has_feature(address_sanitizer)
# endif // _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
}

_LIBCPP_HIDE_FROM_ABI void __annotate_delete() const _NOEXCEPT {
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
if (empty()) {
for (size_t __i = 0; __i < __map_.size(); ++__i) {
__annotate_whole_block(__i, __asan_unposion);
Expand All @@ -1071,35 +1071,35 @@ private:
__annotate_from_to(0, __start_, __asan_unposion, __asan_front_moved);
__annotate_from_to(__start_ + size(), __map_.size() * __block_size, __asan_unposion, __asan_back_moved);
}
# endif // __has_feature(address_sanitizer)
# endif // _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
}

_LIBCPP_HIDE_FROM_ABI void __annotate_increase_front(size_type __n) const _NOEXCEPT {
(void)__n;
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
__annotate_from_to(__start_ - __n, __start_, __asan_unposion, __asan_front_moved);
# endif
}

_LIBCPP_HIDE_FROM_ABI void __annotate_increase_back(size_type __n) const _NOEXCEPT {
(void)__n;
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
__annotate_from_to(__start_ + size(), __start_ + size() + __n, __asan_unposion, __asan_back_moved);
# endif
}

_LIBCPP_HIDE_FROM_ABI void __annotate_shrink_front(size_type __old_size, size_type __old_start) const _NOEXCEPT {
(void)__old_size;
(void)__old_start;
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
__annotate_from_to(__old_start, __old_start + (__old_size - size()), __asan_poison, __asan_front_moved);
# endif
}

_LIBCPP_HIDE_FROM_ABI void __annotate_shrink_back(size_type __old_size, size_type __old_start) const _NOEXCEPT {
(void)__old_size;
(void)__old_start;
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
__annotate_from_to(__old_start + size(), __old_start + __old_size, __asan_poison, __asan_back_moved);
# endif
}
Expand All @@ -1112,7 +1112,7 @@ private:
__annotate_whole_block(size_t __block_index, __asan_annotation_type __annotation_type) const _NOEXCEPT {
(void)__block_index;
(void)__annotation_type;
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
__map_const_iterator __block_it = __map_.begin() + __block_index;
const void* __block_start = std::__to_address(*__block_it);
const void* __block_end = std::__to_address(*__block_it + __block_size);
Expand All @@ -1125,7 +1125,7 @@ private:
}
# endif
}
# if __has_feature(address_sanitizer)
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS

public:
_LIBCPP_HIDE_FROM_ABI bool __verify_asan_annotations() const _NOEXCEPT {
Expand Down Expand Up @@ -1187,7 +1187,7 @@ public:
}

private:
# endif // __has_feature(address_sanitizer)
# endif // _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
_LIBCPP_HIDE_FROM_ABI bool __maybe_remove_front_spare(bool __keep_one = true) {
if (__front_spare_blocks() >= 2 || (!__keep_one && __front_spare_blocks())) {
__annotate_whole_block(0, __asan_unposion);
Expand Down
24 changes: 15 additions & 9 deletions libcxx/include/string
Original file line number Diff line number Diff line change
Expand Up @@ -678,14 +678,20 @@ basic_string<char32_t> operator""s( const char32_t *str, size_t len );
_LIBCPP_PUSH_MACROS
# include <__undef_macros>

# if __has_feature(address_sanitizer) && _LIBCPP_INSTRUMENTED_WITH_ASAN
// Since std::string is partially instantiated in the built library, we require that the library
// has been built with ASAN support in order to enable the container checks in std::string.
// Within the <string> implementation, use `_LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING`
// to determine whether ASAN container checks should be provided.
//
// The _LIBCPP_STRING_INTERNAL_MEMORY_ACCESS macro disables ASAN instrumentation for a specific
// function, allowing memory accesses that would normally trigger ASan errors to proceed without
// crashing. This is useful for accessing parts of objects memory, which should not be accessed,
// such as unused bytes in short strings, that should never be accessed by other parts of the program.
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS && _LIBCPP_INSTRUMENTED_WITH_ASAN
# define _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING 1
# define _LIBCPP_STRING_INTERNAL_MEMORY_ACCESS __attribute__((__no_sanitize__("address")))
// This macro disables AddressSanitizer (ASan) instrumentation for a specific function,
// allowing memory accesses that would normally trigger ASan errors to proceed without crashing.
// This is useful for accessing parts of objects memory, which should not be accessed,
// such as unused bytes in short strings, that should never be accessed
// by other parts of the program.
# else
# define _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING 0
# define _LIBCPP_STRING_INTERNAL_MEMORY_ACCESS
# endif

Expand Down Expand Up @@ -749,7 +755,7 @@ public:
//
// This string implementation doesn't contain any references into itself. It only contains a bit that says whether
// it is in small or large string mode, so the entire structure is trivially relocatable if its members are.
# if __has_feature(address_sanitizer) && _LIBCPP_INSTRUMENTED_WITH_ASAN
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING
// When compiling with AddressSanitizer (ASan), basic_string cannot be trivially
// relocatable. Because the object's memory might be poisoned when its content
// is kept inside objects memory (short string optimization), instead of in allocated
Expand All @@ -764,7 +770,7 @@ public:
void>;
# endif

# if __has_feature(address_sanitizer) && _LIBCPP_INSTRUMENTED_WITH_ASAN
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING
_LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR_SINCE_CXX20 pointer __asan_volatile_wrapper(pointer const& __ptr) const {
if (__libcpp_is_constant_evaluated())
return __ptr;
Expand Down Expand Up @@ -2321,7 +2327,7 @@ private:
__annotate_contiguous_container(const void* __old_mid, const void* __new_mid) const {
(void)__old_mid;
(void)__new_mid;
# if _LIBCPP_INSTRUMENTED_WITH_ASAN
# if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING
# if defined(__APPLE__)
// TODO: remove after addressing issue #96099 (https://llvm.org/PR96099)
if (!__is_long())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

// XFAIL: FROZEN-CXX03-HEADERS-FIXME

// Check that we diagnose when libc++ has been built with ASAN instrumentation
// and the user requests turning off the ASAN container checks. Since that is
// impossible to implement, we diagnose this with an error instead.
//
// REQUIRES: libcpp-instrumented-with-asan
// ADDITIONAL_COMPILE_FLAGS: -D__SANITIZER_DISABLE_CONTAINER_OVERFLOW__

#include <deque>
#include <string>
#include <vector>

// expected-error@*:* {{We can't disable ASAN container checks when libc++ has been built with ASAN container checks enabled}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

// Check that libc++ honors when __SANITIZER_DISABLE_CONTAINER_OVERFLOW__ is set
// and disables the container overflow checks.
//
// REQUIRES: asan
// ADDITIONAL_COMPILE_FLAGS: -D__SANITIZER_DISABLE_CONTAINER_OVERFLOW__

// When libc++ is built with ASAN instrumentation, we can't turn off the ASAN checks,
// and that is diagnosed as an error.
// XFAIL: libcpp-instrumented-with-asan

// std::basic_string::data is const util C++17
// UNSUPPORTED: c++03, c++11, c++14

// The protocol checked by this test is specific to Clang and compiler-rt
// UNSUPPORTED: gcc

#include <deque>
#include <string>
#include <vector>

// This check is somewhat weak because it would pass if we renamed the libc++-internal
// macro and forgot to update this test. But it doesn't hurt to check it in addition to
// the tests below.
#if _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
# error "Container overflow checks should be disabled in libc++"
#endif

void vector() {
std::vector<int> v;
v.reserve(100);
int* data = v.data();

// This is illegal with respect to std::vector, but legal from the core language perspective since
// we do own that allocated memory and `int` is an implicit lifetime type. If container overflow
// checks are enabled, this would fail.
data[4] = 42;
}

// For std::string, we must use a custom char_traits class to reliably test this behavior. Since
Copy link
Member Author

Choose a reason for hiding this comment

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

@DanBlackwell @padriff

I realized something rather important while re-writing this test. Basically, __SANITIZER_DISABLE_CONTAINER_OVERFLOW__ doesn't really work for any container that is (partly or fully) compiled in a separate library. That's kind of obvious when you think about it, but it means that e.g. std::string won't be able to honour this setting because the char specialization with default char_traits is partly instantiated in the dylib. That's really unfortunate.

So, basically, if libc++.dylib has been built with ASAN enabled, you'll be unable to turn off the sanitizer checks by defining __SANITIZER_DISABLE_CONTAINER_OVERFLOW__ in your own translation unit. The reverse is not a problem per se: if libc++.dylib has been built with ASAN disabled, std::string will never enable container overflow checks, because it knows it can't actually implement it. Hence, it doesn't matter whether you define __SANITIZER_DISABLE_CONTAINER_OVERFLOW__ or not, you're never getting any checks.

IMO this goes back to my initial comments about ASAN being a fundamentally ABI-affecting property that really needs "a different slice". Mixing TUs built with ASAN and TUs built without ASAN is always going to be fragile.

Copy link
Member Author

@ldionne ldionne Dec 4, 2025

Choose a reason for hiding this comment

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

Thinking about this some more, we could actually "prevent" the invalid/surprising behaviour in std::string. We could say that attempting to disable container overflow checks when using a libc++.dylib that has been built with ASAN enabled is just not valid, and produce an error. Something like:

#if __has_feature(address_sanitizer) && !defined(__SANITIZER_DISABLE_CONTAINER_OVERFLOW__)
#  define _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS 1
#else
#  define _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS 0
#endif

#if _LIBCPP_INSTRUMENTED_WITH_ASAN && !_LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS
#  error "We can't disable ASAN container checks when the library has been built with these checks enabled"
#endif

That way, we catch the potential misuse immediately. In practice, this should affect a much smaller number of users since I'm not aware of anyone shipping a sanitized version of libc++ in their toolchain.

WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

I like this, it seems in line with what I'd expect as a user. My only comment would be that the wording should indicate that it's libcxx (rather than just 'the library'), but I presume that's not the final wording in your comment anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the approach. I agree that it is very unlikely for someone to ship a sanitized libcxx. If they are building that way it is more than likely to allow them to have a fully sanitized stack for testing purposes

// std::string is externally instantiated in the built library, __SANITIZER_DISABLE_CONTAINER_OVERFLOW__
// will not be honored for any function that happens to be in the built library. Using a custom
// char_traits class ensures that this doesn't get in the way.
struct my_char_traits : std::char_traits<char> {};

void string() {
std::basic_string<char, my_char_traits> s;
s.reserve(100);
char* data = s.data();
data[4] = 'x';
}

void deque() {
std::deque<int> d;
d.push_back(1);
d.push_back(2);
d.push_back(3);
int* last_element = &d[2];
d.pop_back();

// This reference is technically invalidated according to the library. However since
// we know std::deque is implemented using segments of a fairly large size and we know
// the non-erased elements are not invalidated by pop_front() (per the Standard), we can
// rely on the fact that the last element still exists in memory that is owned by the
// std::deque.
//
// If container overflow checks were enabled, this would obviously fail.
*last_element = 42;
}

int main(int, char**) {
vector();
string();
deque();
return 0;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//
//===----------------------------------------------------------------------===//

// REQUIRES: asan
// REQUIRES: libcpp-instrumented-with-asan
// UNSUPPORTED: c++03

// Basic test if ASan annotations work for basic_string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//
//===----------------------------------------------------------------------===//

// REQUIRES: asan
// REQUIRES: libcpp-instrumented-with-asan
// UNSUPPORTED: c++03

// <string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ static_assert(!std::__libcpp_is_trivially_relocatable<std::array<NotTriviallyCop
static_assert(std::__libcpp_is_trivially_relocatable<std::array<std::unique_ptr<int>, 1> >::value, "");

// basic_string
#if !__has_feature(address_sanitizer) || !_LIBCPP_INSTRUMENTED_WITH_ASAN
#if !_LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING
struct MyChar {
char c;
};
Expand Down
2 changes: 1 addition & 1 deletion libcxx/test/support/asan_testing.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ TEST_CONSTEXPR bool is_double_ended_contiguous_container_asan_correct(const std:
}
#endif

#if TEST_HAS_FEATURE(address_sanitizer)
#if TEST_HAS_FEATURE(address_sanitizer) && _LIBCPP_ENABLE_ASAN_CONTAINER_CHECKS_FOR_STRING
template <typename ChrT, typename TraitsT, typename Alloc>
TEST_CONSTEXPR bool is_string_asan_correct(const std::basic_string<ChrT, TraitsT, Alloc>& c) {
if (TEST_IS_CONSTANT_EVALUATED)
Expand Down
7 changes: 7 additions & 0 deletions libcxx/utils/ci/run-buildbot
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,13 @@ generic-asan)
generate-cmake -C "${MONOREPO_ROOT}/libcxx/cmake/caches/Generic-asan.cmake"
check-runtimes
;;
generic-asan-in-tests-only)
# This builds the library without ASAN support, but then turns on ASAN support
# within the test suite.
clean
generate-cmake -C "${MONOREPO_ROOT}/libcxx/cmake/caches/Generic-asan-in-tests-only.cmake"
check-runtimes
;;
generic-msan)
clean
generate-cmake -C "${MONOREPO_ROOT}/libcxx/cmake/caches/Generic-msan.cmake"
Expand Down
Loading
Loading