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

Enable a seconary allocator support (e.g. GWP-Asan) #737

Merged
merged 11 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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
47 changes: 46 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,52 @@ jobs:
run: |
cd ${{github.workspace}}/build
ctest --parallel


gwp-asan:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm]
profile: [RelWithDebInfo, Debug]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Install Ninja
run: |
sudo apt-get install -y ninja-build
- name: Install Compiler-RT
shell: bash
run: |
cd ..
git clone https://github.com/llvm/llvm-project --depth=1 -b llvmorg-19.1.7
mkdir compiler-rt
cmake -G Ninja \
-S llvm-project/runtimes \
-B llvm-project/build \
-DCMAKE_BUILD_TYPE=${{ matrix.profile }}\
-DLLVM_ENABLE_RUNTIMES=compiler-rt \
-DCMAKE_CXX_COMPILER=clang++-18 \
-DCMAKE_C_COMPILER=clang-18 \
-DCMAKE_INSTALL_PREFIX=$(realpath compiler-rt)
cmake --build llvm-project/build --parallel
cmake --build llvm-project/build --target=install
- name: Configure SnMalloc
run: >
cmake -GNinja
-B${{github.workspace}}/build
-DCMAKE_BUILD_TYPE=${{ matrix.profile }}
-DCMAKE_CXX_COMPILER=clang++-18
-DCMAKE_C_COMPILER=clang-18
-DSNMALLOC_ENABLE_GWP_ASAN_INTEGRATION=On
-DSNMALLOC_GWP_ASAN_INCLUDE_PATH=${{github.workspace}}/../llvm-project/compiler-rt/lib
-DSNMALLOC_GWP_ASAN_LIBRARY_PATH=${{github.workspace}}/../compiler-rt/lib/linux
- name: Build
run: cmake --build ${{github.workspace}}/build --parallel
- name: Test
run: |
cd ${{github.workspace}}/build
ctest --parallel

all-checks:
# Currently FreeBSD and NetBSD CI are not working, so we do not require them to pass.
# Add fuzzing back when the memove issue is fixed.
Expand Down
20 changes: 20 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ cmake_dependent_option(SNMALLOC_STATIC_LIBRARY "Build static libraries" ON "NOT
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)
cmake_dependent_option(SNMALLOC_OPTIMISE_FOR_CURRENT_MACHINE "Compile for current machine architecture" Off "NOT SNMALLOC_HEADER_ONLY_LIBRARY" OFF)
cmake_dependent_option(SNMALLOC_PAGEID "Set an id to memory regions" OFF "NOT SNMALLOC_PAGEID" OFF)

# GwpAsan secondary allocator
option(SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION "Enable GwpAsan as a secondary allocator" OFF)
set(SNMALLOC_GWP_ASAN_INCLUDE_PATH "" CACHE PATH "GwpAsan header directory")
set(SNMALLOC_GWP_ASAN_LIBRARY_PATH "" CACHE PATH "GwpAsan library directory")

if (NOT SNMALLOC_HEADER_ONLY_LIBRARY)
# Pick a sensible default for the thread cleanup mechanism
if (${CMAKE_SYSTEM_NAME} STREQUAL FreeBSD)
Expand Down Expand Up @@ -241,6 +247,20 @@ if(SNMALLOC_USE_SELF_VENDORED_STL)
target_compile_definitions(snmalloc INTERFACE SNMALLOC_USE_SELF_VENDORED_STL)
endif()

if (SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION)
if (NOT EXISTS ${SNMALLOC_GWP_ASAN_INCLUDE_PATH})
message(FATAL_ERROR "GwpAsan cannot be enabled without setting SNMALLOC_GWP_ASAN_INCLUDE_PATH")
endif()
if (NOT EXISTS ${SNMALLOC_GWP_ASAN_LIBRARY_PATH})
message(FATAL_ERROR "GwpAsan cannot be enabled without setting SNMALLOC_GWP_ASAN_LIBRARY_PATH")
endif()
message(STATUS "GwpAsan is enabled: ${SNMALLOC_GWP_ASAN_LIBRARY_PATH}/libclang_rt.gwp_asan-${CMAKE_SYSTEM_PROCESSOR}.a")
target_include_directories(snmalloc INTERFACE ${SNMALLOC_GWP_ASAN_INCLUDE_PATH})
target_link_directories(snmalloc INTERFACE ${SNMALLOC_GWP_ASAN_LIBRARY_PATH})
target_compile_definitions(snmalloc INTERFACE -DSNMALLOC_ENABLE_GWP_ASAN_INTEGRATION)
target_link_libraries(snmalloc INTERFACE clang_rt.gwp_asan-${CMAKE_SYSTEM_PROCESSOR})
endif()

# https://learn.microsoft.com/en-us/cpp/build/reference/zc-cplusplus
if(MSVC)
target_compile_options(snmalloc INTERFACE "/Zc:__cplusplus")
Expand Down
3 changes: 3 additions & 0 deletions src/snmalloc/backend/globalconfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "../backend_helpers/backend_helpers.h"
#include "backend.h"
#include "meta_protected_range.h"
#include "snmalloc/mem/secondary.h"
#include "standard_range.h"

namespace snmalloc
Expand Down Expand Up @@ -107,6 +108,8 @@ namespace snmalloc
if (initialised)
return;

SecondaryAllocator::initialize();

LocalEntropy entropy;
entropy.init<Pal>();
// Initialise key for remote deallocation lists
Expand Down
5 changes: 5 additions & 0 deletions src/snmalloc/mem/corealloc.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "metadata.h"
#include "pool.h"
#include "remotecache.h"
#include "secondary.h"
#include "sizeclasstable.h"
#include "snmalloc/stl/new.h"
#include "ticker.h"
Expand Down Expand Up @@ -885,6 +886,10 @@ namespace snmalloc
{
size_t rsize = sizeclass_to_size(sizeclass);

void* result = SecondaryAllocator::allocate(rsize);
if (result != nullptr)
return capptr::Alloc<void>::unsafe_from(result);

Copy link
Member

Choose a reason for hiding this comment

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

Thinking about this a bit more. I wonder if this should be at the start of small_alloc. I think there are allocation patterns that would never hit this path. It is possible to work completely without having to grab a new chunk from the backend.

Sorry, I think I had this wrong in my original issue.

// No existing free list get a new slab.
size_t slab_size = sizeclass_to_slab_size(sizeclass);

Expand Down
18 changes: 11 additions & 7 deletions src/snmalloc/mem/localalloc.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "snmalloc/aal/address.h"
#include "snmalloc/mem/secondary.h"
#if defined(_MSC_VER)
# define ALLOCATOR __declspec(allocator) __declspec(restrict)
#elif __has_attribute(malloc)
Expand Down Expand Up @@ -186,6 +187,10 @@ namespace snmalloc
return small_alloc<NoZero>(1);
}

void* result = SecondaryAllocator::allocate(size);
if (result != nullptr)
return capptr::Alloc<void>::unsafe_from(result);

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 we should probably move this inside the check_init call. I.e. just before // Grab slab of correct size, I think we might have initialisation issues if we haven't fully initialised snmalloc as the pagemap won't exist when we look up a GWP Asan allocation.

return check_init([&](CoreAlloc* core_alloc) {
if (size > bits::one_at_bit(bits::BITS - 1))
{
Expand Down Expand Up @@ -697,13 +702,7 @@ namespace snmalloc
return;
}

// If p_tame is not null, then dealloc has been call on something
// it shouldn't be called on.
// TODO: Should this be tested even in the !CHECK_CLIENT case?
snmalloc_check_client(
mitigations(sanity_checks),
p_tame == nullptr,
"Not allocated by snmalloc.");
SecondaryAllocator::deallocate(p_tame.unsafe_ptr());

# ifdef SNMALLOC_TRACING
message<1024>("nullptr deallocation");
Expand All @@ -719,6 +718,8 @@ namespace snmalloc
#else
if constexpr (mitigations(sanity_checks))
{
if (SecondaryAllocator::has_secondary_ownership(p))
return;
size = size == 0 ? 1 : size;
auto sc = size_to_sizeclass_full(size);
auto pm_sc =
Expand Down Expand Up @@ -767,6 +768,9 @@ namespace snmalloc
#ifdef SNMALLOC_PASS_THROUGH
return external_alloc::malloc_usable_size(const_cast<void*>(p_raw));
#else

if (SecondaryAllocator::has_secondary_ownership(p_raw))
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer this to check that the allocation is not owned by snmalloc, rather than rely on the secondary allocator having this. For GWP Asan, it is fine.

But I would also been keen to use this to build an approach on Windows to replace the allocator, and pass any original frees back to the Windows Heap.

Copy link
Collaborator Author

@SchrodingerZhu SchrodingerZhu Jan 29, 2025

Choose a reason for hiding this comment

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

    SNMALLOC_FAST_PATH size_t alloc_size(const void* p_raw)
    {
#ifdef SNMALLOC_PASS_THROUGH
      return external_alloc::malloc_usable_size(const_cast<void*>(p_raw));
#else

      if (!check_domestication(p_raw))
        return SecondaryAllocator::alloc_size(p_raw);
      
      // ......
  }

Could you illustrate a better way to do this? If do it the way above, we will always have the overhead for domestication test.

Copy link
Member

Choose a reason for hiding this comment

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

      const PagemapEntry& entry =
        Config::Backend::get_metaentry(address_cast(p_raw));

      // Check is managed by snmalloc
      if (SNMALLOC_LIKELY(entry.get_remote() != nullptr)
        return sizeclass_full_to_size(entry.get_sizeclass());

      // If not managed by snmalloc, pass to secondary allocator.
      return SecondaryAllocator::alloc_size(p_raw);

return SecondaryAllocator::alloc_size(p_raw);
// TODO What's the domestication policy here? At the moment we just
// probe the pagemap with the raw address, without checks. There could
// be implicit domestication through the `Config::Pagemap` or
Expand Down
16 changes: 16 additions & 0 deletions src/snmalloc/mem/secondary.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma once
#ifdef SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION
# include "snmalloc/mem/secondary/gwp_asan.h"

namespace snmalloc
{
using SecondaryAllocator = GwpAsanSecondaryAllocator;
} // namespace snmalloc
#else
# include "snmalloc/mem/secondary/default.h"

namespace snmalloc
{
using SecondaryAllocator = DefaultSecondaryAllocator;
} // namespace snmalloc
#endif
49 changes: 49 additions & 0 deletions src/snmalloc/mem/secondary/default.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#pragma once

#include "snmalloc/ds_core/defines.h"
#include "snmalloc/ds_core/mitigations.h"

#include <stddef.h>

namespace snmalloc
{
class DefaultSecondaryAllocator
{
public:
SNMALLOC_FAST_PATH
static void initialize() {}

SNMALLOC_FAST_PATH
static void* allocate([[maybe_unused]] size_t size)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
static void* allocate([[maybe_unused]] size_t size)
static void* allocate(size_t)

{
return nullptr;
}

SNMALLOC_FAST_PATH
static void deallocate(void* pointer)
{
// If pointer is not null, then dealloc has been call on something
// it shouldn't be called on.
// TODO: Should this be tested even in the !CHECK_CLIENT case?
snmalloc_check_client(
mitigations(sanity_checks),
pointer == nullptr,
"Not allocated by snmalloc.");
}

SNMALLOC_FAST_PATH
static bool has_secondary_ownership([[maybe_unused]] const void* pointer)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
static bool has_secondary_ownership([[maybe_unused]] const void* pointer)
static bool has_secondary_ownership(const void*)

{
return false;
}

SNMALLOC_FAST_PATH
static size_t alloc_size([[maybe_unused]] const void* pointer)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
static size_t alloc_size([[maybe_unused]] const void* pointer)
static size_t alloc_size(const void*)

{
SNMALLOC_ASSERT(
false &&
"secondary alloc_size should never be invoked with default setup");
return 0;
}
};
} // namespace snmalloc
72 changes: 72 additions & 0 deletions src/snmalloc/mem/secondary/gwp_asan.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#pragma once

#include "gwp_asan/guarded_pool_allocator.h"
#include "snmalloc/ds_core/defines.h"
#include "snmalloc/mem/sizeclasstable.h"
#if defined(SNMALLOC_BACKTRACE_HEADER)
# include SNMALLOC_BACKTRACE_HEADER
#endif

namespace snmalloc
{
class GwpAsanSecondaryAllocator
{
static inline gwp_asan::GuardedPoolAllocator singleton;
static inline size_t max_allocation_size;

public:
static void initialize() noexcept
{
// for now, we use default options
gwp_asan::options::Options opt;
opt.setDefaults();
#ifdef SNMALLOC_BACKTRACE_HEADER
opt.Backtrace = [](uintptr_t* buf, size_t length) {
return static_cast<size_t>(
::backtrace(reinterpret_cast<void**>(buf), static_cast<int>(length)));
};
#endif
singleton.init(opt);
max_allocation_size =
singleton.getAllocatorState()->maximumAllocationSize();
}

SNMALLOC_FAST_PATH static void* allocate(size_t size)
{
if (SNMALLOC_UNLIKELY(singleton.shouldSample()))
Copy link
Member

Choose a reason for hiding this comment

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

As this is only getting called when a free list is exhausted should we be doing something else here. I.e. should we be passing how many requests of this size have been handled since we last called this allocator?

Copy link
Collaborator Author

@SchrodingerZhu SchrodingerZhu Jan 28, 2025

Choose a reason for hiding this comment

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

shouldSample is implemented as

    // NextSampleCounter == 0 means we "should regenerate the counter".
    //                   == 1 means we "should sample this allocation".
    // AdjustedSampleRatePlusOne is designed to intentionally underflow. This
    // class must be valid when zero-initialised, and we wish to sample as
    // infrequently as possible when this is the case, hence we underflow to
    // UINT32_MAX.
    if (GWP_ASAN_UNLIKELY(getThreadLocals()->NextSampleCounter == 0))
      getThreadLocals()->NextSampleCounter =
          ((getRandomUnsigned32() % (AdjustedSampleRatePlusOne - 1)) + 1) &
          ThreadLocalPackedVariables::NextSampleCounterMask;

    return GWP_ASAN_UNLIKELY(--getThreadLocals()->NextSampleCounter == 0);

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I meant. Normally usage of GWP Asan, every call to malloc will call shouldSample. But this usage has moved it from the fast path, so we will call it every new free list for that size. This in the case of 16 byte allocations worst case could be 1024 allocations between calls. This will alter the likelihood of it being called, and might bias away from small allocations. I think for GWP Asan on we would want randomisation too, so that the free list that gets exhausted.

I think this is probably the same information that would be required to unbias profiling, so should probably be left to a subsequent PR, but we should perhaps add either an issue or a comment about this.

{
if (size > max_allocation_size)
return nullptr;
auto alignment = natural_alignment(size);
return singleton.allocate(size, alignment);
}
return nullptr;
}

SNMALLOC_FAST_PATH
static void deallocate(void* pointer)
{
if (SNMALLOC_LIKELY(pointer == nullptr))
return;

snmalloc_check_client(
mitigations(sanity_checks),
singleton.pointerIsMine(pointer),
"Not allocated by snmalloc or secondary allocator");

singleton.deallocate(pointer);
}

SNMALLOC_FAST_PATH
static bool has_secondary_ownership([[maybe_unused]] const void* pointer)
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: do not think [maybe_unused] is necessary here ?

{
return singleton.pointerIsMine(pointer);
}

SNMALLOC_FAST_PATH
static size_t alloc_size(const void* pointer)
{
return singleton.getSize(pointer);
}
};
} // namespace snmalloc
3 changes: 2 additions & 1 deletion src/test/func/client_meta/client_meta.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ namespace snmalloc

int main()
{
#ifdef SNMALLOC_PASS_THROUGH
#if defined(SNMALLOC_PASS_THROUGH) || \
defined(SNMALLOC_ENABLE_GWP_ASAN_INTEGRATION)
// This test does not make sense in pass-through
return 0;
#else
Expand Down
8 changes: 8 additions & 0 deletions src/test/func/fixed_region/fixed_region.cc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "snmalloc/mem/secondary.h"
#include "test/setup.h"

#include <iostream>
Expand Down Expand Up @@ -38,9 +39,16 @@ int main()
while (true)
{
auto r1 = a.alloc(object_size);

count += object_size;
i++;

if (SecondaryAllocator::has_secondary_ownership(r1))
{
a.dealloc(r1);
continue;
}

if (i == 1024)
{
i = 0;
Expand Down
2 changes: 2 additions & 0 deletions src/test/perf/external_pointer/externalpointer.cc
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ namespace test
size_t rand = (size_t)r.next();
size_t oid = rand & (((size_t)1 << count_log) - 1);
size_t* external_ptr = objects[oid];
if (SecondaryAllocator::has_secondary_ownership(external_ptr))
continue;
size_t size = *external_ptr;
size_t offset = (size >> 4) * (rand & 15);
void* interior_ptr = pointer_offset(external_ptr, offset);
Expand Down
Loading