Skip to content

Commit 16b9624

Browse files
Enable a seconary allocator support (e.g. GWP-Asan) (#737)
1 parent 32495fd commit 16b9624

File tree

12 files changed

+290
-33
lines changed

12 files changed

+290
-33
lines changed

.github/workflows/main.yml

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -542,7 +542,52 @@ jobs:
542542
run: |
543543
cd ${{github.workspace}}/build
544544
ctest --parallel
545-
545+
546+
gwp-asan:
547+
strategy:
548+
fail-fast: false
549+
matrix:
550+
os: [ubuntu-24.04, ubuntu-24.04-arm]
551+
profile: [RelWithDebInfo, Debug]
552+
runs-on: ${{ matrix.os }}
553+
steps:
554+
- uses: actions/checkout@v4
555+
- name: Install Ninja
556+
run: |
557+
sudo apt-get install -y ninja-build
558+
- name: Install Compiler-RT
559+
shell: bash
560+
run: |
561+
cd ..
562+
git clone https://github.com/llvm/llvm-project --depth=1 -b llvmorg-19.1.7
563+
mkdir compiler-rt
564+
cmake -G Ninja \
565+
-S llvm-project/runtimes \
566+
-B llvm-project/build \
567+
-DCMAKE_BUILD_TYPE=${{ matrix.profile }}\
568+
-DLLVM_ENABLE_RUNTIMES=compiler-rt \
569+
-DCMAKE_CXX_COMPILER=clang++-18 \
570+
-DCMAKE_C_COMPILER=clang-18 \
571+
-DCMAKE_INSTALL_PREFIX=$(realpath compiler-rt)
572+
cmake --build llvm-project/build --parallel
573+
cmake --build llvm-project/build --target=install
574+
- name: Configure SnMalloc
575+
run: >
576+
cmake -GNinja
577+
-B${{github.workspace}}/build
578+
-DCMAKE_BUILD_TYPE=${{ matrix.profile }}
579+
-DCMAKE_CXX_COMPILER=clang++-18
580+
-DCMAKE_C_COMPILER=clang-18
581+
-DSNMALLOC_ENABLE_GWP_ASAN_INTEGRATION=On
582+
-DSNMALLOC_GWP_ASAN_INCLUDE_PATH=${{github.workspace}}/../llvm-project/compiler-rt/lib
583+
-DSNMALLOC_GWP_ASAN_LIBRARY_PATH=${{github.workspace}}/../compiler-rt/lib/linux
584+
- name: Build
585+
run: cmake --build ${{github.workspace}}/build --parallel
586+
- name: Test
587+
run: |
588+
cd ${{github.workspace}}/build
589+
ctest --parallel --output-on-failure
590+
546591
all-checks:
547592
# Currently FreeBSD and NetBSD CI are not working, so we do not require them to pass.
548593
# Add fuzzing back when the memove issue is fixed.

CMakeLists.txt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ cmake_dependent_option(SNMALLOC_STATIC_LIBRARY "Build static libraries" ON "NOT
3636
cmake_dependent_option(SNMALLOC_CHECK_LOADS "Perform bounds checks on the source argument to memcpy with heap objects" OFF "NOT SNMALLOC_HEADER_ONLY_LIBRARY" OFF)
3737
cmake_dependent_option(SNMALLOC_OPTIMISE_FOR_CURRENT_MACHINE "Compile for current machine architecture" Off "NOT SNMALLOC_HEADER_ONLY_LIBRARY" OFF)
3838
cmake_dependent_option(SNMALLOC_PAGEID "Set an id to memory regions" OFF "NOT SNMALLOC_PAGEID" OFF)
39+
40+
# GwpAsan secondary allocator
41+
option(SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION "Enable GwpAsan as a secondary allocator" OFF)
42+
set(SNMALLOC_GWP_ASAN_INCLUDE_PATH "" CACHE PATH "GwpAsan header directory")
43+
set(SNMALLOC_GWP_ASAN_LIBRARY_PATH "" CACHE PATH "GwpAsan library directory")
44+
3945
if (NOT SNMALLOC_HEADER_ONLY_LIBRARY)
4046
# Pick a sensible default for the thread cleanup mechanism
4147
if (${CMAKE_SYSTEM_NAME} STREQUAL FreeBSD)
@@ -241,6 +247,20 @@ if(SNMALLOC_USE_SELF_VENDORED_STL)
241247
target_compile_definitions(snmalloc INTERFACE SNMALLOC_USE_SELF_VENDORED_STL)
242248
endif()
243249

250+
if (SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION)
251+
if (NOT EXISTS ${SNMALLOC_GWP_ASAN_INCLUDE_PATH})
252+
message(FATAL_ERROR "GwpAsan cannot be enabled without setting SNMALLOC_GWP_ASAN_INCLUDE_PATH")
253+
endif()
254+
if (NOT EXISTS ${SNMALLOC_GWP_ASAN_LIBRARY_PATH})
255+
message(FATAL_ERROR "GwpAsan cannot be enabled without setting SNMALLOC_GWP_ASAN_LIBRARY_PATH")
256+
endif()
257+
message(STATUS "GwpAsan is enabled: ${SNMALLOC_GWP_ASAN_LIBRARY_PATH}/libclang_rt.gwp_asan-${CMAKE_SYSTEM_PROCESSOR}.a")
258+
target_include_directories(snmalloc INTERFACE ${SNMALLOC_GWP_ASAN_INCLUDE_PATH})
259+
target_link_directories(snmalloc INTERFACE ${SNMALLOC_GWP_ASAN_LIBRARY_PATH})
260+
target_compile_definitions(snmalloc INTERFACE -DSNMALLOC_ENABLE_GWP_ASAN_INTEGRATION)
261+
target_link_libraries(snmalloc INTERFACE clang_rt.gwp_asan-${CMAKE_SYSTEM_PROCESSOR})
262+
endif()
263+
244264
# https://learn.microsoft.com/en-us/cpp/build/reference/zc-cplusplus
245265
if(MSVC)
246266
target_compile_options(snmalloc INTERFACE "/Zc:__cplusplus")

src/snmalloc/backend/globalconfig.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "../backend_helpers/backend_helpers.h"
44
#include "backend.h"
55
#include "meta_protected_range.h"
6+
#include "snmalloc/mem/secondary.h"
67
#include "standard_range.h"
78

89
namespace snmalloc
@@ -107,6 +108,8 @@ namespace snmalloc
107108
if (initialised)
108109
return;
109110

111+
SecondaryAllocator::initialize();
112+
110113
LocalEntropy entropy;
111114
entropy.init<Pal>();
112115
// Initialise key for remote deallocation lists

src/snmalloc/ds_core/ptrwrap.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
#include "defines.h"
55
#include "snmalloc/stl/atomic.h"
66

7+
#include <stdint.h>
8+
79
namespace snmalloc
810
{
911
/*

src/snmalloc/mem/corealloc.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "metadata.h"
66
#include "pool.h"
77
#include "remotecache.h"
8+
#include "secondary.h"
89
#include "sizeclasstable.h"
910
#include "snmalloc/stl/new.h"
1011
#include "ticker.h"
@@ -817,6 +818,14 @@ namespace snmalloc
817818
SNMALLOC_SLOW_PATH capptr::Alloc<void>
818819
small_alloc(smallsizeclass_t sizeclass, freelist::Iter<>& fast_free_list)
819820
{
821+
void* result = SecondaryAllocator::allocate(
822+
[sizeclass]() -> stl::Pair<size_t, size_t> {
823+
auto size = sizeclass_to_size(sizeclass);
824+
return {size, natural_alignment(size)};
825+
});
826+
827+
if (result != nullptr)
828+
return capptr::Alloc<void>::unsafe_from(result);
820829
// Look to see if we can grab a free list.
821830
auto& sl = alloc_classes[sizeclass].available;
822831
if (SNMALLOC_LIKELY(alloc_classes[sizeclass].length > 0))

src/snmalloc/mem/localalloc.h

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#pragma once
22

33
#include "snmalloc/aal/address.h"
4+
#include "snmalloc/mem/remoteallocator.h"
5+
#include "snmalloc/mem/secondary.h"
46
#if defined(_MSC_VER)
57
# define ALLOCATOR __declspec(allocator) __declspec(restrict)
68
#elif __has_attribute(malloc)
@@ -194,6 +196,15 @@ namespace snmalloc
194196
errno = ENOMEM;
195197
return capptr::Alloc<void>{nullptr};
196198
}
199+
200+
// Check if secondary allocator wants to offer the memory
201+
void* result =
202+
SecondaryAllocator::allocate([size]() -> stl::Pair<size_t, size_t> {
203+
return {size, natural_alignment(size)};
204+
});
205+
if (result != nullptr)
206+
return capptr::Alloc<void>::unsafe_from(result);
207+
197208
// Grab slab of correct size
198209
// Set remote as large allocator remote.
199210
auto [chunk, meta] = Config::Backend::alloc_chunk(
@@ -606,17 +617,13 @@ namespace snmalloc
606617
#endif
607618
}
608619

609-
SNMALLOC_FAST_PATH void dealloc(void* p_raw)
610-
{
611-
#ifdef SNMALLOC_PASS_THROUGH
612-
external_alloc::free(p_raw);
613-
#else
614-
// Care is needed so that dealloc(nullptr) works before init
615-
// The backend allocator must ensure that a minimal page map exists
616-
// before init, that maps null to a remote_deallocator that will never
617-
// be in thread local state.
620+
// The domestic pointer with its origin allocator
621+
using DomesticInfo = stl::Pair<capptr::Alloc<void>, const PagemapEntry&>;
618622

619-
# ifdef __CHERI_PURE_CAPABILITY__
623+
// Check whether the raw pointer is owned by snmalloc
624+
SNMALLOC_FAST_PATH_INLINE DomesticInfo get_domestic_info(const void* p_raw)
625+
{
626+
#ifdef __CHERI_PURE_CAPABILITY__
620627
/*
621628
* On CHERI platforms, snap the provided pointer to its base, ignoring
622629
* any client-provided offset, which may have taken the pointer out of
@@ -632,10 +639,29 @@ namespace snmalloc
632639
* start of the allocation and so the offset is zero.
633640
*/
634641
p_raw = __builtin_cheri_offset_set(p_raw, 0);
635-
# endif
642+
#endif
643+
capptr::AllocWild<void> p_wild =
644+
capptr_from_client(const_cast<void*>(p_raw));
645+
auto p_tame =
646+
capptr_domesticate<Config>(core_alloc->backend_state_ptr(), p_wild);
647+
const PagemapEntry& entry =
648+
Config::Backend::get_metaentry(address_cast(p_tame));
649+
return {p_tame, entry};
650+
}
636651

637-
capptr::AllocWild<void> p_wild = capptr_from_client(p_raw);
652+
// Check if a pointer is domestic to SnMalloc
653+
SNMALLOC_FAST_PATH bool is_snmalloc_owned(const void* p_raw)
654+
{
655+
auto [_, entry] = get_domestic_info(p_raw);
656+
RemoteAllocator* remote = entry.get_remote();
657+
return remote != nullptr;
658+
}
638659

660+
SNMALLOC_FAST_PATH void dealloc(void* p_raw)
661+
{
662+
#ifdef SNMALLOC_PASS_THROUGH
663+
external_alloc::free(p_raw);
664+
#else
639665
/*
640666
* p_tame may be nullptr, even if p_raw/p_wild are not, in the case
641667
* where domestication fails. We exclusively use p_tame below so that
@@ -648,11 +674,7 @@ namespace snmalloc
648674
* well-formedness) of this pointer. The remainder of the logic will
649675
* deal with the object's extent.
650676
*/
651-
capptr::Alloc<void> p_tame =
652-
capptr_domesticate<Config>(core_alloc->backend_state_ptr(), p_wild);
653-
654-
const PagemapEntry& entry =
655-
Config::Backend::get_metaentry(address_cast(p_tame));
677+
auto [p_tame, entry] = get_domestic_info(p_raw);
656678

657679
if (SNMALLOC_LIKELY(local_cache.remote_allocator == entry.get_remote()))
658680
{
@@ -697,18 +719,15 @@ namespace snmalloc
697719
return;
698720
}
699721

700-
// If p_tame is not null, then dealloc has been call on something
701-
// it shouldn't be called on.
702-
// TODO: Should this be tested even in the !CHECK_CLIENT case?
703-
snmalloc_check_client(
704-
mitigations(sanity_checks),
705-
p_tame == nullptr,
706-
"Not allocated by snmalloc.");
707-
722+
if (SNMALLOC_LIKELY(p_tame == nullptr))
723+
{
708724
# ifdef SNMALLOC_TRACING
709-
message<1024>("nullptr deallocation");
725+
message<1024>("nullptr deallocation");
710726
# endif
711-
return;
727+
return;
728+
}
729+
730+
SecondaryAllocator::deallocate(p_tame.unsafe_ptr());
712731
#endif
713732
}
714733

@@ -719,6 +738,8 @@ namespace snmalloc
719738
#else
720739
if constexpr (mitigations(sanity_checks))
721740
{
741+
if (!is_snmalloc_owned(p))
742+
return;
722743
size = size == 0 ? 1 : size;
723744
auto sc = size_to_sizeclass_full(size);
724745
auto pm_sc =
@@ -767,6 +788,11 @@ namespace snmalloc
767788
#ifdef SNMALLOC_PASS_THROUGH
768789
return external_alloc::malloc_usable_size(const_cast<void*>(p_raw));
769790
#else
791+
792+
if (
793+
!SecondaryAllocator::pass_through && !is_snmalloc_owned(p_raw) &&
794+
p_raw != nullptr)
795+
return SecondaryAllocator::alloc_size(p_raw);
770796
// TODO What's the domestication policy here? At the moment we just
771797
// probe the pagemap with the raw address, without checks. There could
772798
// be implicit domestication through the `Config::Pagemap` or

src/snmalloc/mem/secondary.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#pragma once
2+
3+
#include "snmalloc/ds_core/defines.h"
4+
#include "snmalloc/ds_core/ptrwrap.h"
5+
6+
#ifdef SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION
7+
# include "snmalloc/mem/secondary/gwp_asan.h"
8+
9+
namespace snmalloc
10+
{
11+
using SecondaryAllocator = GwpAsanSecondaryAllocator;
12+
} // namespace snmalloc
13+
#else
14+
# include "snmalloc/mem/secondary/default.h"
15+
16+
namespace snmalloc
17+
{
18+
using SecondaryAllocator = DefaultSecondaryAllocator;
19+
} // namespace snmalloc
20+
#endif

src/snmalloc/mem/secondary/default.h

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#pragma once
2+
3+
#include "snmalloc/ds_core/defines.h"
4+
#include "snmalloc/ds_core/mitigations.h"
5+
6+
#include <stddef.h>
7+
8+
namespace snmalloc
9+
{
10+
class DefaultSecondaryAllocator
11+
{
12+
public:
13+
// This flag is used to turn off checks on fast paths if the secondary
14+
// allocator does not own the memory at all.
15+
static constexpr inline bool pass_through = true;
16+
17+
SNMALLOC_FAST_PATH
18+
static void initialize() {}
19+
20+
template<class SizeAlign>
21+
SNMALLOC_FAST_PATH static void* allocate(SizeAlign&&)
22+
{
23+
return nullptr;
24+
}
25+
26+
SNMALLOC_FAST_PATH
27+
static void deallocate(void* pointer)
28+
{
29+
// If pointer is not null, then dealloc has been call on something
30+
// it shouldn't be called on.
31+
// TODO: Should this be tested even in the !CHECK_CLIENT case?
32+
snmalloc_check_client(
33+
mitigations(sanity_checks),
34+
pointer == nullptr,
35+
"Not allocated by snmalloc.");
36+
}
37+
38+
SNMALLOC_FAST_PATH
39+
static size_t alloc_size(const void*)
40+
{
41+
SNMALLOC_ASSERT(
42+
false &&
43+
"secondary alloc_size should never be invoked with default setup");
44+
return 0;
45+
}
46+
};
47+
} // namespace snmalloc

0 commit comments

Comments
 (0)