Factor checks under separate feature flags. (#587)

All the checks and mitigations have been placed under feature flags.
These can be controlled by defining

  SNMALLOC_CHECK_CLIENT_MITIGATIONS

This can take a term that represents the mitigations that should be enabled.
E.g.
  -DSNMALLOC_CHECK_CLIENT_MITIGATIONS=nochecks+random_pagemap

The CMake uses this to build numerous versions of the LD_PRELOAD library and
tests to allow individual features to be benchmarked.

Co-authored-by: Nathaniel Wesley Filardo <nfilardo@microsoft.com>
This commit is contained in:
Matthew Parkinson
2023-03-23 13:41:02 +00:00
committed by GitHub
parent 848db74cfc
commit ccca98a709
25 changed files with 753 additions and 435 deletions

View File

@@ -58,6 +58,13 @@ jobs:
build-type: Debug
self-host: true
extra-cmake-flags: "-DSNMALLOC_USE_PTHREAD_DESTRUCTORS=On"
# Extra build to check using individual mitigations works.
- os: "ubuntu-latest"
variant: "individual mitigations"
dependencies: "sudo apt install ninja-build"
build-type: Release
self-host: true
extra-cmake-flags: "-DSNMALLOC_BENCHMARK_INDIVIDUAL_MITIGATIONS=On -DBUILD_TESTING=Off"
# Check that we can build specifically with libstdc++
- os: "ubuntu-latest"
variant: "libstdc++ (Build only)"
@@ -92,7 +99,7 @@ jobs:
run: NINJA_STATUS="%p [%f:%s/%t] %o/s, %es" ninja
- name: Test file size of binaries is sane
working-directory: ${{github.workspace}}/build
run: "ls -l func-first_operation-fast ; [ $(ls -l func-first_operation-fast | awk '{ print $5}') -lt 10000000 ]"
run: "ls -l libsnmallocshim.* ; [ $(ls -l libsnmallocshim.* | awk '{ print $5}') -lt 10000000 ]"
# If the tests are enabled for this job, run them
- name: Test
if: ${{ matrix.build-only != 'yes' }}
@@ -102,12 +109,9 @@ jobs:
if: ${{ matrix.self-host }}
working-directory: ${{github.workspace}}/build
run: |
sudo cp libsnmallocshim.so libsnmallocshim-checks.so /usr/local/lib/
ninja clean
LD_PRELOAD=/usr/local/lib/libsnmallocshim.so ninja
ninja clean
LD_PRELOAD=/usr/local/lib/libsnmallocshim-checks.so ninja
mkdir libs
cp libsnmallocshim*.so libs
for lib in `ls libs`; do echo; echo Testing $lib; ninja clean; LD_PRELOAD=libs/$lib ninja libsnmallocshim.so; done
# GitHub doesn't natively support *BSD, but we can run them in VMs on Mac /
# Linux runners
freebsd:

View File

@@ -25,6 +25,7 @@ option(SNMALLOC_NO_REALLOCARRAY "Build without reallocarray exported" ON)
option(SNMALLOC_NO_REALLOCARR "Build without reallocarr exported" ON)
option(SNMALLOC_LINK_ICF "Link with Identical Code Folding" ON)
option(SNMALLOC_IPO "Link with IPO/LTO support" OFF)
option(SNMALLOC_BENCHMARK_INDIVIDUAL_MITIGATIONS "Build tests and ld_preload for individual mitigations" OFF)
# Options that apply only if we're not building the header-only library
cmake_dependent_option(SNMALLOC_RUST_SUPPORT "Build static library for rust" OFF "NOT SNMALLOC_HEADER_ONLY_LIBRARY" OFF)
cmake_dependent_option(SNMALLOC_STATIC_LIBRARY "Build static libraries" ON "NOT SNMALLOC_HEADER_ONLY_LIBRARY" OFF)
@@ -271,7 +272,6 @@ function(add_warning_flags name)
$<$<PLATFORM_ID:Windows>:$<${ci_or_debug}:/DEBUG>>)
endfunction()
# To build with just the header library target define SNMALLOC_HEADER_ONLY_LIBRARY
if(NOT SNMALLOC_HEADER_ONLY_LIBRARY)
@@ -286,6 +286,78 @@ if(NOT SNMALLOC_HEADER_ONLY_LIBRARY)
set(${result} ${dirlist} PARENT_SCOPE)
endfunction()
set(TESTDIR ${CMAKE_CURRENT_SOURCE_DIR}/src/test)
if(BUILD_TESTING)
enable_testing()
subdirlist(TEST_CATEGORIES ${TESTDIR})
else()
set(TEST_CATEGORIES "")
endif()
list(REVERSE TEST_CATEGORIES)
if (${SNMALLOC_CLEANUP} STREQUAL THREAD_CLEANUP)
set(TEST_CLEANUP PTHREAD_DESTRUCTORS)
else ()
set(TEST_CLEANUP ${SNMALLOC_CLEANUP})
endif()
function(make_tests TAG DEFINES)
foreach(TEST_CATEGORY ${TEST_CATEGORIES})
message(STATUS "Adding ${TAG}/${TEST_CATEGORY} tests")
subdirlist(TESTS ${TESTDIR}/${TEST_CATEGORY})
foreach(TEST ${TESTS})
unset(SRC)
aux_source_directory(${TESTDIR}/${TEST_CATEGORY}/${TEST} SRC)
set(TESTNAME "${TEST_CATEGORY}-${TEST}-${TAG}")
add_executable(${TESTNAME} ${SRC})
if(SNMALLOC_SANITIZER)
target_compile_options(${TESTNAME} PRIVATE -g -fsanitize=${SNMALLOC_SANITIZER} -fno-omit-frame-pointer)
target_link_libraries(${TESTNAME} -fsanitize=${SNMALLOC_SANITIZER})
endif()
add_warning_flags(${TESTNAME})
target_link_libraries(${TESTNAME} snmalloc)
target_compile_definitions(${TESTNAME} PRIVATE "SNMALLOC_USE_${TEST_CLEANUP}")
if (NOT DEFINES STREQUAL " ")
target_compile_definitions(${TESTNAME} PRIVATE ${DEFINES})
endif()
if (${TEST} MATCHES "release-.*")
message(VERBOSE "Adding test: ${TESTNAME} only for release configs")
add_test(NAME ${TESTNAME} COMMAND ${TESTNAME} CONFIGURATIONS "Release")
else()
message(VERBOSE "Adding test: ${TESTNAME}")
add_test(${TESTNAME} ${TESTNAME})
endif()
if (${TEST_CATEGORY} MATCHES "perf")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
if(WIN32)
# On Windows these tests use a lot of memory as it doesn't support
# lazy commit.
if (${TEST} MATCHES "two_alloc_types")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
if (${TEST} MATCHES "fixed_region")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
if (${TEST} MATCHES "memory")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
endif()
endforeach()
endforeach()
endfunction()
if(NOT (DEFINED SNMALLOC_LINKER_FLAVOUR) OR ("${SNMALLOC_LINKER_FLAVOUR}" MATCHES "^$"))
# Linker not specified externally; probe to see if we can make lld work
set(CMAKE_REQUIRED_LINK_OPTIONS -fuse-ld=lld -Wl,--icf=all)
@@ -376,89 +448,75 @@ if(NOT SNMALLOC_HEADER_ONLY_LIBRARY)
target_compile_definitions(snmallocshim-checks-rust PRIVATE SNMALLOC_CHECK_CLIENT)
endif()
set(TESTDIR ${CMAKE_CURRENT_SOURCE_DIR}/src/test)
if (BUILD_TESTING)
if (WIN32
OR (CMAKE_SYSTEM_NAME STREQUAL NetBSD)
OR (CMAKE_SYSTEM_NAME STREQUAL OpenBSD)
OR (CMAKE_SYSTEM_NAME STREQUAL DragonFly)
OR (CMAKE_SYSTEM_NAME STREQUAL SunOS))
# Windows does not support aligned allocation well enough
# for pass through.
# NetBSD, OpenBSD and DragonFlyBSD do not support malloc*size calls.
set(FLAVOURS fast;check)
else()
set(FLAVOURS fast;check;malloc)
endif()
if(BUILD_TESTING)
enable_testing()
subdirlist(TEST_CATEGORIES ${TESTDIR})
else()
set(TEST_CATEGORIES "")
endif()
list(REVERSE TEST_CATEGORIES)
if (${SNMALLOC_CLEANUP} STREQUAL THREAD_CLEANUP)
set(TEST_CLEANUP PTHREAD_DESTRUCTORS)
else ()
set(TEST_CLEANUP ${SNMALLOC_CLEANUP})
endif()
foreach(TEST_CATEGORY ${TEST_CATEGORIES})
message(STATUS "Adding ${TEST_CATEGORY} tests")
subdirlist(TESTS ${TESTDIR}/${TEST_CATEGORY})
foreach(TEST ${TESTS})
if (WIN32
OR (CMAKE_SYSTEM_NAME STREQUAL NetBSD)
OR (CMAKE_SYSTEM_NAME STREQUAL OpenBSD)
OR (CMAKE_SYSTEM_NAME STREQUAL DragonFly)
OR (CMAKE_SYSTEM_NAME STREQUAL SunOS))
# Windows does not support aligned allocation well enough
# for pass through.
# NetBSD, OpenBSD and DragonFlyBSD do not support malloc*size calls.
set(FLAVOURS fast;check)
else()
set(FLAVOURS fast;check;malloc)
foreach(FLAVOUR ${FLAVOURS})
if (${FLAVOUR} STREQUAL "malloc")
set(DEFINES SNMALLOC_PASS_THROUGH)
endif()
if (${FLAVOUR} STREQUAL "check")
set(DEFINES SNMALLOC_CHECK_CLIENT)
endif()
if (${FLAVOUR} STREQUAL "fast")
set(DEFINES " ")
endif()
foreach(FLAVOUR ${FLAVOURS})
unset(SRC)
aux_source_directory(${TESTDIR}/${TEST_CATEGORY}/${TEST} SRC)
set(TESTNAME "${TEST_CATEGORY}-${TEST}-${FLAVOUR}")
add_executable(${TESTNAME} ${SRC})
if(SNMALLOC_SANITIZER)
target_compile_options(${TESTNAME} PRIVATE -g -fsanitize=${SNMALLOC_SANITIZER} -fno-omit-frame-pointer)
target_link_libraries(${TESTNAME} -fsanitize=${SNMALLOC_SANITIZER})
endif()
add_warning_flags(${TESTNAME})
if (${FLAVOUR} STREQUAL "malloc")
target_compile_definitions(${TESTNAME} PRIVATE SNMALLOC_PASS_THROUGH)
endif()
if (${FLAVOUR} STREQUAL "check")
target_compile_definitions(${TESTNAME} PRIVATE SNMALLOC_CHECK_CLIENT)
endif()
target_link_libraries(${TESTNAME} snmalloc)
target_compile_definitions(${TESTNAME} PRIVATE "SNMALLOC_USE_${TEST_CLEANUP}")
if (${TEST} MATCHES "release-.*")
message(VERBOSE "Adding test: ${TESTNAME} only for release configs")
add_test(NAME ${TESTNAME} COMMAND ${TESTNAME} CONFIGURATIONS "Release")
else()
message(VERBOSE "Adding test: ${TESTNAME}")
add_test(${TESTNAME} ${TESTNAME})
endif()
if (${TEST_CATEGORY} MATCHES "perf")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
if(WIN32)
# On Windows these tests use a lot of memory as it doesn't support
# lazy commit.
if (${TEST} MATCHES "two_alloc_types")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
if (${TEST} MATCHES "fixed_region")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
if (${TEST} MATCHES "memory")
message(VERBOSE "Single threaded test: ${TESTNAME}")
set_tests_properties(${TESTNAME} PROPERTIES PROCESSORS 4)
endif()
endif()
endforeach()
make_tests(${FLAVOUR} ${DEFINES})
endforeach()
endforeach()
endif()
if (SNMALLOC_BENCHMARK_INDIVIDUAL_MITIGATIONS)
set (MITIGATIONS
metadata_protection;
pal_enforce_access;
random_pagemap;
sanity_checks;
freelist_forward_edge;
freelist_backward_edge;
freelist_teardown_validate;
reuse_LIFO;
random_larger_thresholds;
random_initial;
random_preserve;
random_extra_slab)
foreach (MITIGATION ${MITIGATIONS})
set(DEFINES "SNMALLOC_CHECK_CLIENT_MITIGATIONS=${MITIGATION}")
add_shim(snmallocshim-${MITIGATION} SHARED ${SHIM_FILES})
target_compile_definitions(snmallocshim-${MITIGATION} PRIVATE ${DEFINES})
if (BUILD_TESTING)
make_tests(${MITIGATION} ${DEFINES})
endif()
endforeach()
set(MITIGATIONSET "no_checks")
set(COUNT 0)
foreach (MITIGATION ${MITIGATIONS})
MATH(EXPR COUNT "${COUNT} + 1")
set(MITIGATIONNAME "mitigations-${COUNT}")
set(MITIGATIONSET "${MITIGATIONSET}+${MITIGATION}")
message(STATUS "MITIGATIONSET: ${COUNT} -> ${MITIGATIONSET}")
set(DEFINES "-DSNMALLOC_CHECK_CLIENT_MITIGATIONS=${MITIGATIONSET}")
add_shim(snmallocshim-${MITIGATIONNAME} SHARED ${SHIM_FILES})
target_compile_definitions(snmallocshim-${MITIGATIONNAME} PRIVATE ${DEFINES})
if (BUILD_TESTING)
make_tests(${MITIGATIONNAME} ${DEFINES})
endif()
endforeach()
endif()
clangformat_targets()
endif()

View File

@@ -30,7 +30,7 @@ If memory is not required any more, then `snmalloc` will change the state to
`not using`, and will ensure that it notifies the `Pal` again
before it every accesses that memory again.
The `not using` state allows the `Pal` to recycle the memory for other purposes.
If `PalEnforceAccess` is set to true, then accessing that has not been notified
If `pal_enforce_access` is set as a mitigation, then accessing memory that has not been notified
correctly should trigger an exception/segfault.
The state for a particular region of memory is set with

View File

@@ -9,19 +9,6 @@
# include "meta_protected_range.h"
# include "standard_range.h"
# if defined(SNMALLOC_CHECK_CLIENT) && !defined(OPEN_ENCLAVE)
/**
* Protect meta data blocks by allocating separate from chunks for
* user allocations. This involves leaving gaps in address space.
* This is less efficient, so should only be applied for the checked
* build.
*
* On Open Enclave the address space is limited, so we disable this
* feature.
*/
# define SNMALLOC_META_PROTECTED
# endif
namespace snmalloc
{
// Forward reference to thread local cleanup.
@@ -79,11 +66,10 @@ namespace snmalloc
/**
* Use one of the default range configurations
*/
# ifdef SNMALLOC_META_PROTECTED
using LocalState = MetaProtectedRangeLocalState<Pal, Pagemap, Base>;
# else
using LocalState = StandardLocalState<Pal, Pagemap, Base>;
# endif
using LocalState = std::conditional_t<
mitigations(metadata_protection),
MetaProtectedRangeLocalState<Pal, Pagemap, Base>,
StandardLocalState<Pal, Pagemap, Base>>;
/**
* Use the default backend.
@@ -136,14 +122,11 @@ namespace snmalloc
// Initialise key for remote deallocation lists
key_global = FreeListKey(entropy.get_free_list_key());
// Need to initialise pagemap. If SNMALLOC_CHECK_CLIENT is set and this
// isn't a StrictProvenance architecture, randomize its table's location
// within a significantly larger address space allocation.
# if defined(SNMALLOC_CHECK_CLIENT)
static constexpr bool pagemap_randomize = !aal_supports<StrictProvenance>;
# else
static constexpr bool pagemap_randomize = false;
# endif
// Need to randomise pagemap location. If requested and not a
// StrictProvenance architecture, randomize its table's location within a
// significantly larger address space allocation.
static constexpr bool pagemap_randomize =
mitigations(random_pagemap) && !aal_supports<StrictProvenance>;
Pagemap::concretePagemap.template init<pagemap_randomize>();

View File

@@ -49,11 +49,8 @@ namespace snmalloc
static constexpr size_t MIN_CHUNK_SIZE = bits::one_at_bit(MIN_CHUNK_BITS);
// Minimum number of objects on a slab
#ifdef SNMALLOC_CHECK_CLIENT
static constexpr size_t MIN_OBJECT_COUNT = 13;
#else
static constexpr size_t MIN_OBJECT_COUNT = 4;
#endif
static constexpr size_t MIN_OBJECT_COUNT =
mitigations(random_larger_thresholds) ? 13 : 4;
// Maximum size of an object that uses sizeclasses.
#if defined(SNMALLOC_QEMU_WORKAROUND) && defined(SNMALLOC_VA_BITS_64)

View File

@@ -213,6 +213,10 @@ namespace snmalloc
}
else
{
if constexpr (pal_supports<LazyCommit, PAL>)
{
PAL::notify_using_readonly(new_body_untyped, REQUIRED_SIZE);
}
new_body = static_cast<T*>(new_body_untyped);
}
// Ensure bottom page is committed

View File

@@ -27,6 +27,11 @@
# define SNMALLOC_REQUIRE_CONSTINIT
# define SNMALLOC_UNUSED_FUNCTION
# define SNMALLOC_USED_FUNCTION
# ifdef SNMALLOC_USE_CXX17
# define SNMALLOC_NO_UNIQUE_ADDRESS
# else
# define SNMALLOC_NO_UNIQUE_ADDRESS [[msvc::no_unique_address]]
# endif
#else
# define SNMALLOC_FAST_FAIL() __builtin_trap()
# define SNMALLOC_LIKELY(x) __builtin_expect(!!(x), 1)
@@ -50,6 +55,11 @@
# define SNMALLOC_COLD __attribute__((cold))
# define SNMALLOC_UNUSED_FUNCTION __attribute((unused))
# define SNMALLOC_USED_FUNCTION __attribute((used))
# ifdef SNMALLOC_USE_CXX17
# define SNMALLOC_NO_UNIQUE_ADDRESS
# else
# define SNMALLOC_NO_UNIQUE_ADDRESS [[no_unique_address]]
# endif
# ifdef __clang__
# define SNMALLOC_REQUIRE_CONSTINIT \
[[clang::require_constant_initialization]]
@@ -192,43 +202,4 @@ namespace snmalloc
template<typename... Args>
SNMALLOC_FAST_PATH_INLINE void UNUSED(Args&&...)
{}
template<typename... Args>
inline SNMALLOC_FAST_PATH void
check_client_error(const char* const str, Args... args)
{
//[[clang::musttail]]
return snmalloc::report_fatal_error(str, args...);
}
template<typename... Args>
inline SNMALLOC_FAST_PATH void
check_client_impl(bool test, const char* const str, Args... args)
{
if (SNMALLOC_UNLIKELY(!test))
{
if constexpr (!DEBUG)
{
UNUSED(str, args...);
SNMALLOC_FAST_FAIL();
}
else
{
check_client_error(str, args...);
}
}
}
#ifdef SNMALLOC_CHECK_CLIENT
static constexpr bool CHECK_CLIENT = true;
#else
static constexpr bool CHECK_CLIENT = false;
#endif
} // namespace snmalloc
#ifdef SNMALLOC_CHECK_CLIENT
# define snmalloc_check_client(test, str, ...) \
snmalloc::check_client_impl(test, str, ##__VA_ARGS__)
#else
# define snmalloc_check_client(test, str, ...)
#endif

View File

@@ -11,6 +11,7 @@
#include "concept.h"
#include "defines.h"
#include "helpers.h"
#include "mitigations.h"
#include "ptrwrap.h"
#include "redblacktree.h"
#include "seqset.h"

View File

@@ -37,7 +37,7 @@ namespace snmalloc
}
};
#ifdef SNMALLOC_CHECK_CLIENT
#ifdef SNMALLOC_CHECK_CLIENT // TODO is this used/helpful?
template<size_t length, typename T>
class ModArray
{

View File

@@ -0,0 +1,263 @@
#pragma once
#include "defines.h"
#include <cstddef>
namespace snmalloc
{
template<typename... Args>
inline SNMALLOC_FAST_PATH void
check_client_error(const char* const str, Args... args)
{
//[[clang::musttail]]
return snmalloc::report_fatal_error(str, args...);
}
template<typename... Args>
inline SNMALLOC_FAST_PATH void
check_client_impl(bool test, const char* const str, Args... args)
{
if (SNMALLOC_UNLIKELY(!test))
{
if constexpr (!DEBUG)
{
UNUSED(str, args...);
SNMALLOC_FAST_FAIL();
}
else
{
check_client_error(str, args...);
}
}
}
#ifdef SNMALLOC_CHECK_CLIENT
static constexpr bool CHECK_CLIENT = true;
#else
static constexpr bool CHECK_CLIENT = false;
#endif
namespace mitigation
{
class type
{
size_t mask;
public:
constexpr type(size_t f) : mask(f){};
constexpr type(const type& t) = default;
constexpr type operator+(const type b) const
{
return {mask | b.mask};
}
constexpr type operator-(const type b) const
{
return {mask & ~(b.mask)};
}
constexpr bool operator()(const type a) const
{
return (mask & a.mask) != 0;
}
};
} // namespace mitigation
/**
* Randomize the location of the pagemap within a larger address space
* allocation. The other pages in that allocation may fault if accessed, on
* platforms that can efficiently express such configurations.
*
* This guards against adversarial attempts to access the pagemap.
*
* This is unnecessary on StrictProvenance architectures.
*/
constexpr mitigation::type random_pagemap{1 << 0};
/**
* Ensure that every slab (especially slabs used for larger "small" size
* classes) has a larger minimum number of objects and that a larger
* percentage of objects in a slab must be free to awaken the slab.
*
* This should frustrate use-after-reallocation attacks by delaying reuse.
* When combined with random_preserve, below, it additionally ensures that at
* least some shuffling of free objects is possible, and, hence, that there
* is at least some unpredictability of reuse.
*
* TODO: should this be split? mjp: Would require changing some thresholds.
* The min waking count needs to be ensure we have enough objects on a slab,
* hence is related to the min count on a slab. Currently we without this, we
* have min count of slab of 16, and a min waking count with this enabled
* of 32. So we would leak memory.
*/
constexpr mitigation::type random_larger_thresholds{1 << 1};
/**
*
* Obfuscate forward-edge pointers in intra-slab free lists.
*
* This helps prevent a UAF write from re-pointing the free list arbitrarily,
* as the de-obfuscation of a corrupted pointer will generate a wild address.
*
* This is not available on StrictProvenance architectures.
*/
constexpr mitigation::type freelist_forward_edge{1 << 2};
/**
* Store obfuscated backward-edge addresses in intra-slab free lists.
*
* Ordinarily, these lists are singly-linked. Storing backward-edges allows
* the allocator to verify the well-formedness of the links and, importantly,
* the acyclicity of the list itself. These backward-edges are also
* obfuscated in an attempt to frustrate an attacker armed with UAF
* attempting to construct a new well-formed list.
*
* Because the backward-edges are not traversed, this is available on
* StrictProvenance architectures, unlike freelist_forward_edge.
*
* This is required to detect double frees as it will break the doubly linked
* nature of the free list.
*/
constexpr mitigation::type freelist_backward_edge{1 << 3};
/**
* When de-purposing a slab (releasing its address space for reuse at a
* different size class or allocation), walk the free list and validate the
* domestication of all nodes along it.
*
* If freelist_forward_edge is also enabled, this will probe the
* domestication status of the de-obfuscated pointers before traversal.
* Each of domestication and traversal may probabilistically catch UAF
* corruption of the free list.
*
* If freelist_backward_edge is also enabled, this will verify the integrity
* of the free list links.
*
* This gives the allocator "one last chance" to catch UAF corruption of a
* slab's free list before the slab is de-purposed.
*
* This is required to comprehensively detect double free.
*/
constexpr mitigation::type freelist_teardown_validate{1 << 4};
/**
* When initializing a slab, shuffle its free list.
*
* This guards against attackers relying on object-adjacency or address-reuse
* properties of the allocation stream.
*/
constexpr mitigation::type random_initial{1 << 5};
/**
* When a slab is operating, randomly assign freed objects to one of two
* intra-slab free lists. When selecting a slab's free list for allocations,
* select the longer of the two.
*
* This guards against attackers relying on object-adjacency or address-reuse
* properties of the allocation stream.
*/
constexpr mitigation::type random_preserve{1 << 6};
/**
* Randomly introduce another slab for a given size-class, rather than use
* the last available to an allocator.
*
* This guards against attackers relying on address-reuse, especially in the
* pathological case of a size-class having only one slab with free entries.
*/
constexpr mitigation::type random_extra_slab{1 << 7};
/**
* Use a LIFO queue, rather than a stack, of slabs with free elements.
*
* This generally increases the time between address reuse.
*/
constexpr mitigation::type reuse_LIFO{1 << 8};
/**
* This performs a variety of inexpensive "sanity" tests throughout the
* allocator:
*
* - Requests to free objects must
* - not be interior pointers
* - be of allocated address space
* - Requests to free objects which also specify the size must specify a size
* that agrees with the current allocation.
*
* This guards gainst various forms of client misbehavior.
*
* TODO: Should this be split? mjp: It could, but let's not do this until
* we have performance numbers to see what this costs.
*/
constexpr mitigation::type sanity_checks{1 << 9};
/**
* On CHERI, perform a series of well-formedness tests on capabilities given
* when requesting to free an object.
*/
constexpr mitigation::type cheri_checks{1 << 10};
/**
* Erase intra-slab free list metadata before completing an allocation.
*
* This mitigates information disclosure.
*/
constexpr mitigation::type clear_meta{1 << 11};
/**
* Protect meta data blocks by allocating separate from chunks for
* user allocations. This involves leaving gaps in address space.
* This is less efficient, so should only be applied for the checked
* build.
*/
constexpr mitigation::type metadata_protection{1 << 12};
/**
* If this mitigation is enabled, then Pal implementations should provide
* exceptions/segfaults if accesses do not obey the
* - using
* - using_readonly
* - not_using
* model.
*/
static constexpr mitigation::type pal_enforce_access{1 << 13};
constexpr mitigation::type full_checks = random_pagemap +
random_larger_thresholds + freelist_forward_edge + freelist_backward_edge +
freelist_teardown_validate + random_initial + random_preserve +
metadata_protection + random_extra_slab + reuse_LIFO + sanity_checks +
clear_meta + pal_enforce_access;
constexpr mitigation::type no_checks{0};
using namespace mitigation;
constexpr mitigation::type mitigations =
#ifdef SNMALLOC_CHECK_CLIENT_MITIGATIONS
no_checks + SNMALLOC_CHECK_CLIENT_MITIGATIONS;
#elif defined(OPEN_ENCLAVE)
/**
* On Open Enclave the address space is limited, so we disable
* metadata-protection feature.
*/
CHECK_CLIENT ? full_checks - metadata_protection - random_pagemap :
no_checks;
#elif defined(__NetBSD__)
/**
* pal_enforce_access was failing on NetBSD, so we disable it.
*/
CHECK_CLIENT ? full_checks - pal_enforce_access : no_checks;
#elif defined(__CHERI_PURE_CAPABILITY__)
CHECK_CLIENT ?
/**
* freelist_forward_edge should not be used on CHERI as we cannot encode
* pointers as the tag will be destroyed.
*
* TODO: There is a known bug in CheriBSD that means round-tripping through
* PROT_NONE sheds capability load and store permissions (while restoring
* data read/write, for added excitement). For the moment, just force this
* down on CHERI.
*/
full_checks + cheri_checks + clear_meta - freelist_forward_edge -
pal_enforce_access :
/**
* clear_meta is important on CHERI to avoid leaking capabilities.
*/
sanity_checks + cheri_checks + clear_meta;
#else
CHECK_CLIENT ? full_checks : no_checks;
#endif
} // namespace snmalloc
#define snmalloc_check_client(mitigation, test, str, ...) \
if constexpr (mitigation) \
{ \
snmalloc::check_client_impl(test, str, ##__VA_ARGS__); \
}

View File

@@ -220,73 +220,78 @@ namespace snmalloc
auto& b = meta->free_queue;
#ifdef SNMALLOC_CHECK_CLIENT
// Structure to represent the temporary list elements
struct PreAllocObject
if constexpr (mitigations(random_initial))
{
capptr::AllocFull<PreAllocObject> next;
};
// The following code implements Sattolo's algorithm for generating
// random cyclic permutations. This implementation is in the opposite
// direction, so that the original space does not need initialising. This
// is described as outside-in without citation on Wikipedia, appears to be
// Folklore algorithm.
// Structure to represent the temporary list elements
struct PreAllocObject
{
capptr::AllocFull<PreAllocObject> next;
};
// The following code implements Sattolo's algorithm for generating
// random cyclic permutations. This implementation is in the opposite
// direction, so that the original space does not need initialising.
// This is described as outside-in without citation on Wikipedia,
// appears to be Folklore algorithm.
// Note the wide bounds on curr relative to each of the ->next fields;
// curr is not persisted once the list is built.
capptr::Chunk<PreAllocObject> curr =
pointer_offset(bumpptr, 0).template as_static<PreAllocObject>();
curr->next = Aal::capptr_bound<PreAllocObject, capptr::bounds::AllocFull>(
curr, rsize);
uint16_t count = 1;
for (curr =
pointer_offset(curr, rsize).template as_static<PreAllocObject>();
curr.as_void() < slab_end;
curr =
pointer_offset(curr, rsize).template as_static<PreAllocObject>())
{
size_t insert_index = entropy.sample(count);
curr->next = std::exchange(
pointer_offset(bumpptr, insert_index * rsize)
.template as_static<PreAllocObject>()
->next,
// Note the wide bounds on curr relative to each of the ->next fields;
// curr is not persisted once the list is built.
capptr::Chunk<PreAllocObject> curr =
pointer_offset(bumpptr, 0).template as_static<PreAllocObject>();
curr->next =
Aal::capptr_bound<PreAllocObject, capptr::bounds::AllocFull>(
curr, rsize));
count++;
}
curr, rsize);
// Pick entry into space, and then build linked list by traversing cycle
// to the start. Use ->next to jump from Chunk to Alloc.
auto start_index = entropy.sample(count);
auto start_ptr = pointer_offset(bumpptr, start_index * rsize)
.template as_static<PreAllocObject>()
->next;
auto curr_ptr = start_ptr;
do
uint16_t count = 1;
for (curr =
pointer_offset(curr, rsize).template as_static<PreAllocObject>();
curr.as_void() < slab_end;
curr =
pointer_offset(curr, rsize).template as_static<PreAllocObject>())
{
size_t insert_index = entropy.sample(count);
curr->next = std::exchange(
pointer_offset(bumpptr, insert_index * rsize)
.template as_static<PreAllocObject>()
->next,
Aal::capptr_bound<PreAllocObject, capptr::bounds::AllocFull>(
curr, rsize));
count++;
}
// Pick entry into space, and then build linked list by traversing cycle
// to the start. Use ->next to jump from Chunk to Alloc.
auto start_index = entropy.sample(count);
auto start_ptr = pointer_offset(bumpptr, start_index * rsize)
.template as_static<PreAllocObject>()
->next;
auto curr_ptr = start_ptr;
do
{
b.add(
// Here begins our treatment of the heap as containing Wild pointers
freelist::Object::make<capptr::bounds::AllocWild>(
capptr_to_user_address_control(curr_ptr.as_void())),
key,
entropy);
curr_ptr = curr_ptr->next;
} while (curr_ptr != start_ptr);
}
else
{
b.add(
// Here begins our treatment of the heap as containing Wild pointers
freelist::Object::make<capptr::bounds::AllocWild>(
capptr_to_user_address_control(curr_ptr.as_void())),
key,
entropy);
curr_ptr = curr_ptr->next;
} while (curr_ptr != start_ptr);
#else
auto p = bumpptr;
do
{
b.add(
// Here begins our treatment of the heap as containing Wild pointers
freelist::Object::make<capptr::bounds::AllocWild>(
capptr_to_user_address_control(
Aal::capptr_bound<void, capptr::bounds::AllocFull>(
p.as_void(), rsize))),
key);
p = pointer_offset(p, rsize);
} while (p < slab_end);
#endif
auto p = bumpptr;
do
{
b.add(
// Here begins our treatment of the heap as containing Wild pointers
freelist::Object::make<capptr::bounds::AllocWild>(
capptr_to_user_address_control(
Aal::capptr_bound<void, capptr::bounds::AllocFull>(
p.as_void(), rsize))),
key,
entropy);
p = pointer_offset(p, rsize);
} while (p < slab_end);
}
// This code consumes everything up to slab_end.
bumpptr = slab_end;
}
@@ -306,48 +311,44 @@ namespace snmalloc
capptr::Alloc<void> p =
finish_alloc_no_zero(fl.take(key, domesticate), sizeclass);
#ifdef SNMALLOC_CHECK_CLIENT
// Check free list is well-formed on platforms with
// integers as pointers.
size_t count = 1; // Already taken one above.
while (!fl.empty())
// If clear_meta is requested, we should also walk the free list to clear
// it.
// TODO: we could optimise the clear_meta case to not walk the free list
// and instead just clear the whole slab, but that requires amplification.
if constexpr (
mitigations(freelist_teardown_validate) || mitigations(clear_meta))
{
fl.take(key, domesticate);
count++;
}
// Check the list contains all the elements
SNMALLOC_CHECK(
(count + more) == snmalloc::sizeclass_to_slab_object_count(sizeclass));
if (more > 0)
{
auto no_more = meta->free_queue.close(fl, key);
SNMALLOC_ASSERT(no_more == 0);
UNUSED(no_more);
// Check free list is well-formed on platforms with
// integers as pointers.
size_t count = 1; // Already taken one above.
while (!fl.empty())
{
fl.take(key, domesticate);
count++;
}
// Check the list contains all the elements
SNMALLOC_CHECK(
(count + more) ==
snmalloc::sizeclass_to_slab_object_count(sizeclass));
if (more > 0)
{
auto no_more = meta->free_queue.close(fl, key);
SNMALLOC_ASSERT(no_more == 0);
UNUSED(no_more);
while (!fl.empty())
{
fl.take(key, domesticate);
count++;
}
}
SNMALLOC_CHECK(
count == snmalloc::sizeclass_to_slab_object_count(sizeclass));
}
SNMALLOC_CHECK(
count == snmalloc::sizeclass_to_slab_object_count(sizeclass));
#endif
// TODO: This is a capability amplification as we are saying we
// have the whole chunk.
auto start_of_slab = pointer_align_down<void>(
p, snmalloc::sizeclass_to_slab_size(sizeclass));
#if defined(__CHERI_PURE_CAPABILITY__) && !defined(SNMALLOC_CHECK_CLIENT)
// Zero the whole slab. For CHERI we at least need to clear the freelist
// pointers to avoid leaking capabilities but we do not need to do it in
// the freelist order as for SNMALLOC_CHECK_CLIENT. Zeroing the whole slab
// may be more friendly to hw because it does not involve pointer chasing
// and is amenable to prefetching.
// FIXME: This should be a back-end method guarded on a feature flag.
#endif
#ifdef SNMALLOC_TRACING
message<1024>(
"Slab {} is unused, Object sizeclass {}",
@@ -701,6 +702,7 @@ namespace snmalloc
SNMALLOC_ASSERT(!meta->is_unused());
snmalloc_check_client(
mitigations(sanity_checks),
is_start_of_object(entry.get_sizeclass(), address_cast(p)),
"Not deallocating start of an object");
@@ -722,23 +724,18 @@ namespace snmalloc
auto& sl = alloc_classes[sizeclass].available;
if (SNMALLOC_LIKELY(alloc_classes[sizeclass].length > 0))
{
#ifdef SNMALLOC_CHECK_CLIENT
// Occassionally don't use the last list.
if (SNMALLOC_UNLIKELY(alloc_classes[sizeclass].length == 1))
if constexpr (mitigations(random_extra_slab))
{
// If the slab has a lot of free space, then we shouldn't allocate a
// new slab.
auto min = alloc_classes[sizeclass]
.available.peek()
->free_queue.min_list_length();
if ((min * 2) < threshold_for_waking_slab(sizeclass))
// Occassionally don't use the last list.
if (SNMALLOC_UNLIKELY(alloc_classes[sizeclass].length == 1))
{
if (entropy.next_bit() == 0)
return small_alloc_slow<zero_mem>(sizeclass, fast_free_list);
}
}
#endif
// If CHECK_CLIENT, we use FIFO operations on the list. This reduces
// perf slightly, but increases randomness.
auto meta = sl.template pop<!CHECK_CLIENT>();
// Mitigations use LIFO to increase time to reuse.
auto meta = sl.template pop<!mitigations(reuse_LIFO)>();
// Drop length of sl, and empty count if it was empty.
alloc_classes[sizeclass].length--;
if (meta->needed() == 0)

View File

@@ -122,18 +122,47 @@ namespace snmalloc
friend class Object;
class Empty
{
public:
void check_prev(address_t) {}
void set_prev(address_t) {}
};
class Prev
{
address_t prev_encoded;
public:
/**
* Check the signature of this free Object
*/
void check_prev(address_t signed_prev)
{
snmalloc_check_client(
mitigations(freelist_backward_edge),
signed_prev == prev_encoded,
"Heap corruption - free list corrupted!");
UNUSED(signed_prev);
}
void set_prev(address_t signed_prev)
{
prev_encoded = signed_prev;
}
};
union
{
BQueuePtr<BQueue> next_object;
// TODO: Should really use C++20 atomic_ref rather than a union.
BAtomicQueuePtr<BQueue> atomic_next_object;
};
#ifdef SNMALLOC_CHECK_CLIENT
// Encoded representation of a back pointer.
// Hard to fake, and provides consistency on
// the next pointers.
address_t prev_encoded;
#endif
SNMALLOC_NO_UNIQUE_ADDRESS
std::conditional_t<mitigations(freelist_backward_edge), Prev, Empty>
prev;
public:
template<
@@ -148,13 +177,14 @@ namespace snmalloc
this->atomic_next_object.load(std::memory_order_acquire),
key);
auto n_tame = domesticate(n_wild);
#ifdef SNMALLOC_CHECK_CLIENT
if (n_tame != nullptr)
if constexpr (mitigations(freelist_backward_edge))
{
n_tame->check_prev(
signed_prev(address_cast(this), address_cast(n_tame), key));
if (n_tame != nullptr)
{
n_tame->prev.check_prev(
signed_prev(address_cast(this), address_cast(n_tame), key));
}
}
#endif
return n_tame;
}
@@ -177,25 +207,22 @@ namespace snmalloc
*/
void check_prev(address_t signed_prev)
{
UNUSED(signed_prev);
snmalloc_check_client(
signed_prev == this->prev_encoded,
"Heap corruption - free list corrupted!");
prev.check_prev(signed_prev);
}
/**
* Clean up this object when removing it from the list. This is
* important on CHERI to avoid leaking capabilities. On CHECK_CLIENT
* builds it might increase the difficulty to bypass the checks.
* Clean up this object when removing it from the list.
*/
void cleanup()
{
#if defined(__CHERI_PURE_CAPABILITY__) || defined(SNMALLOC_CHECK_CLIENT)
this->next_object = nullptr;
# ifdef SNMALLOC_CHECK_CLIENT
this->prev_encoded = 0;
# endif
#endif
if constexpr (mitigations(clear_meta))
{
this->next_object = nullptr;
if constexpr (mitigations(freelist_backward_edge))
{
this->prev.set_prev(0);
}
}
}
};
@@ -233,7 +260,8 @@ namespace snmalloc
// Curr is not used in the current encoding scheme.
UNUSED(curr);
if constexpr (CHECK_CLIENT && !aal_supports<StrictProvenance>)
if constexpr (
mitigations(freelist_forward_edge) && !aal_supports<StrictProvenance>)
{
return unsafe_from_uintptr<Object::T<BQueue>>(
unsafe_to_uintptr<Object::T<BQueue>>(next) ^ key.key_next);
@@ -331,12 +359,14 @@ namespace snmalloc
{
assert_view_queue_bounds<BView, BQueue>();
#ifdef SNMALLOC_CHECK_CLIENT
next->prev_encoded =
signed_prev(address_cast(curr), address_cast(next), key);
#else
UNUSED(key);
#endif
if constexpr (mitigations(freelist_backward_edge))
{
next->prev.set_prev(
signed_prev(address_cast(curr), address_cast(next), key));
}
else
UNUSED(key);
*curr = encode_next(address_cast(curr), next, key);
return &(next->next_object);
}
@@ -363,12 +393,14 @@ namespace snmalloc
{
static_assert(BView::wildness == capptr::dimension::Wildness::Tame);
#ifdef SNMALLOC_CHECK_CLIENT
next->prev_encoded =
signed_prev(address_cast(curr), address_cast(next), key);
#else
UNUSED(key);
#endif
if constexpr (mitigations(freelist_backward_edge))
{
next->prev.set_prev(
signed_prev(address_cast(curr), address_cast(next), key));
}
else
UNUSED(key);
// Signature needs to be visible before item is linked in
// so requires release semantics.
curr->atomic_next_object.store(
@@ -418,6 +450,39 @@ namespace snmalloc
*/
using AtomicQueuePtr = Object::BAtomicQueuePtr<capptr::bounds::AllocWild>;
class Prev
{
address_t prev{0};
protected:
constexpr Prev(address_t prev) : prev(prev) {}
constexpr Prev() = default;
address_t replace(address_t next)
{
auto p = prev;
prev = next;
return p;
}
};
class NoPrev
{
protected:
constexpr NoPrev(address_t){};
constexpr NoPrev() = default;
address_t replace(address_t t)
{
// This should never be called.
SNMALLOC_CHECK(false);
return t;
}
};
using IterBase =
std::conditional_t<mitigations(freelist_backward_edge), Prev, NoPrev>;
/**
* Used to iterate a free list in object space.
*
@@ -426,20 +491,14 @@ namespace snmalloc
template<
SNMALLOC_CONCEPT(capptr::IsBound) BView = capptr::bounds::Alloc,
SNMALLOC_CONCEPT(capptr::IsBound) BQueue = capptr::bounds::AllocWild>
class Iter
class Iter : IterBase
{
Object::BHeadPtr<BView, BQueue> curr{nullptr};
#ifdef SNMALLOC_CHECK_CLIENT
address_t prev{0};
#endif
public:
constexpr Iter(Object::BHeadPtr<BView, BQueue> head, address_t prev_value)
: curr(head)
: IterBase(prev_value), curr(head)
{
#ifdef SNMALLOC_CHECK_CLIENT
prev = prev_value;
#endif
UNUSED(prev_value);
}
@@ -473,12 +532,16 @@ namespace snmalloc
Aal::prefetch(next.unsafe_ptr());
curr = next;
#ifdef SNMALLOC_CHECK_CLIENT
c->check_prev(prev);
prev = signed_prev(address_cast(c), address_cast(next), key);
#else
UNUSED(key);
#endif
if constexpr (mitigations(freelist_backward_edge))
{
auto p =
replace(signed_prev(address_cast(c), address_cast(next), key));
c->check_prev(p);
}
else
UNUSED(key);
c->cleanup();
return c;
}
@@ -544,7 +607,7 @@ namespace snmalloc
static_cast<Object::T<BQueue>*>(head[ix]));
}
std::array<uint16_t, RANDOM ? 2 : 0> length{};
SNMALLOC_NO_UNIQUE_ADDRESS std::array<uint16_t, RANDOM ? 2 : 0> length{};
public:
constexpr Builder() = default;
@@ -683,7 +746,7 @@ namespace snmalloc
for (size_t i = 0; i < LENGTH; i++)
{
end[i] = &head[i];
if (RANDOM)
if constexpr (RANDOM)
{
length[i] = 0;
}
@@ -726,48 +789,37 @@ namespace snmalloc
SNMALLOC_FAST_PATH void
validate(const FreeListKey& key, Domesticator domesticate)
{
#ifdef SNMALLOC_CHECK_CLIENT
for (uint32_t i = 0; i < LENGTH; i++)
if constexpr (mitigations(freelist_teardown_validate))
{
if (&head[i] == end[i])
for (uint32_t i = 0; i < LENGTH; i++)
{
SNMALLOC_CHECK(length[i] == 0);
continue;
}
if (&head[i] == end[i])
{
SNMALLOC_CHECK(!RANDOM || (length[i] == 0));
continue;
}
size_t count = 1;
auto curr = read_head(i, key);
auto prev = get_fake_signed_prev(i, key);
while (true)
{
curr->check_prev(prev);
if (address_cast(&(curr->next_object)) == address_cast(end[i]))
break;
count++;
auto next = curr->read_next(key, domesticate);
prev = signed_prev(address_cast(curr), address_cast(next), key);
curr = next;
size_t count = 1;
auto curr = read_head(i, key);
auto prev = get_fake_signed_prev(i, key);
while (true)
{
curr->check_prev(prev);
if (address_cast(&(curr->next_object)) == address_cast(end[i]))
break;
count++;
auto next = curr->read_next(key, domesticate);
prev = signed_prev(address_cast(curr), address_cast(next), key);
curr = next;
}
SNMALLOC_CHECK(!RANDOM || (count == length[i]));
}
SNMALLOC_CHECK(count == length[i]);
}
#else
UNUSED(key);
UNUSED(domesticate);
#endif
}
/**
* Returns length of the shorter free list.
*
* This method is only usable if the free list is adding randomisation
* as that is when it has two lists.
*/
template<bool RANDOM_ = RANDOM>
[[nodiscard]] std::enable_if_t<RANDOM_, size_t> min_list_length() const
{
static_assert(RANDOM_ == RANDOM, "Don't set SFINAE parameter!");
return length[0] < length[1] ? length[0] : length[1];
else
{
UNUSED(key);
UNUSED(domesticate);
}
}
};
} // namespace freelist

View File

@@ -467,9 +467,9 @@ namespace snmalloc
* point where we know, from the pagemap, or by explicitly testing, that the
* pointer under test is not nullptr.
*/
#if defined(__CHERI_PURE_CAPABILITY__) && defined(SNMALLOC_CHECK_CLIENT)
SNMALLOC_SLOW_PATH void dealloc_cheri_checks(void* p)
SNMALLOC_FAST_PATH void dealloc_cheri_checks(void* p)
{
#if defined(__CHERI_PURE_CAPABILITY__)
/*
* Enforce the use of an unsealed capability.
*
@@ -477,7 +477,9 @@ namespace snmalloc
* elide this test in that world.
*/
snmalloc_check_client(
!__builtin_cheri_sealed_get(p), "Sealed capability in deallocation");
mitigations(cheri_checks),
!__builtin_cheri_sealed_get(p),
"Sealed capability in deallocation");
/*
* Enforce permissions on the returned pointer. These pointers end up in
@@ -496,6 +498,7 @@ namespace snmalloc
static const size_t reqperm = CHERI_PERM_LOAD | CHERI_PERM_STORE |
CHERI_PERM_LOAD_CAP | CHERI_PERM_STORE_CAP;
snmalloc_check_client(
mitigations(cheri_checks),
(__builtin_cheri_perms_get(p) & reqperm) == reqperm,
"Insufficient permissions on capability in deallocation");
@@ -510,13 +513,16 @@ namespace snmalloc
* elide this test.
*/
snmalloc_check_client(
__builtin_cheri_tag_get(p), "Untagged capability in deallocation");
mitigations(cheri_checks),
__builtin_cheri_tag_get(p),
"Untagged capability in deallocation");
/*
* Verify that the capability is not zero-length, ruling out the other
* edge case around monotonicity.
*/
snmalloc_check_client(
mitigations(cheri_checks),
__builtin_cheri_length_get(p) > 0,
"Zero-length capability in deallocation");
@@ -585,8 +591,10 @@ namespace snmalloc
* acceptable security posture for the allocator and between clients;
* misbehavior is confined to the misbehaving client.
*/
}
#else
UNUSED(p);
#endif
}
SNMALLOC_FAST_PATH void dealloc(void* p_raw)
{
@@ -638,9 +646,8 @@ namespace snmalloc
if (SNMALLOC_LIKELY(local_cache.remote_allocator == entry.get_remote()))
{
# if defined(__CHERI_PURE_CAPABILITY__) && defined(SNMALLOC_CHECK_CLIENT)
dealloc_cheri_checks(p_tame.unsafe_ptr());
# endif
if (SNMALLOC_LIKELY(CoreAlloc::dealloc_local_object_fast(
entry, p_tame, local_cache.entropy)))
return;
@@ -651,13 +658,13 @@ namespace snmalloc
RemoteAllocator* remote = entry.get_remote();
if (SNMALLOC_LIKELY(remote != nullptr))
{
# if defined(__CHERI_PURE_CAPABILITY__) && defined(SNMALLOC_CHECK_CLIENT)
dealloc_cheri_checks(p_tame.unsafe_ptr());
# endif
// Detect double free of large allocations here.
snmalloc_check_client(
!entry.is_backend_owned(), "Memory corruption detected");
mitigations(sanity_checks),
!entry.is_backend_owned(),
"Memory corruption detected");
// Check if we have space for the remote deallocation
if (local_cache.remote_dealloc_cache.reserve_space(entry))
@@ -678,7 +685,10 @@ namespace snmalloc
// 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(p_tame == nullptr, "Not allocated by snmalloc.");
snmalloc_check_client(
mitigations(sanity_checks),
p_tame == nullptr,
"Not allocated by snmalloc.");
# ifdef SNMALLOC_TRACING
message<1024>("nullptr deallocation");
@@ -689,17 +699,26 @@ namespace snmalloc
void check_size(void* p, size_t size)
{
#ifdef SNMALLOC_CHECK_CLIENT
size = size == 0 ? 1 : size;
auto sc = size_to_sizeclass_full(size);
auto pm_sc =
Config::Backend::get_metaentry(address_cast(p)).get_sizeclass();
auto rsize = sizeclass_full_to_size(sc);
auto pm_size = sizeclass_full_to_size(pm_sc);
snmalloc_check_client(
sc == pm_sc, "Dealloc rounded size mismatch: {} != {}", rsize, pm_size);
#else
#ifdef SNMALLOC_PASS_THROUGH
UNUSED(p, size);
#else
if constexpr (mitigations(sanity_checks))
{
size = size == 0 ? 1 : size;
auto sc = size_to_sizeclass_full(size);
auto pm_sc =
Config::Backend::get_metaentry(address_cast(p)).get_sizeclass();
auto rsize = sizeclass_full_to_size(sc);
auto pm_size = sizeclass_full_to_size(pm_sc);
snmalloc_check_client(
mitigations(sanity_checks),
sc == pm_sc,
"Dealloc rounded size mismatch: {} != {}",
rsize,
pm_size);
}
else
UNUSED(p, size);
#endif
}

View File

@@ -396,11 +396,8 @@ namespace snmalloc
/**
* Data-structure for building the free list for this slab.
*/
#ifdef SNMALLOC_CHECK_CLIENT
freelist::Builder<true> free_queue;
#else
freelist::Builder<false> free_queue;
#endif
SNMALLOC_NO_UNIQUE_ADDRESS freelist::Builder<mitigations(random_preserve)>
free_queue;
/**
* The number of deallocation required until we hit a slow path. This
@@ -566,11 +563,10 @@ namespace snmalloc
auto p = tmp_fl.take(key, domesticate);
fast_free_list = tmp_fl;
#ifdef SNMALLOC_CHECK_CLIENT
entropy.refresh_bits();
#else
UNUSED(entropy);
#endif
if constexpr (mitigations(random_preserve))
entropy.refresh_bits();
else
UNUSED(entropy);
// This marks the slab as sleeping, and sets a wakeup
// when sufficient deallocations have occurred to this slab.

View File

@@ -110,6 +110,7 @@ namespace snmalloc
// set implies this is used by the backend, and we should not be
// deallocating memory here.
snmalloc_check_client(
mitigations(sanity_checks),
!entry.is_backend_owned(),
"Delayed detection of attempt to free internal structure.");
if constexpr (Config::Options.QueueHeadsAreTame)

View File

@@ -225,12 +225,9 @@ namespace snmalloc
meta_slow.capacity =
static_cast<uint16_t>((meta.slab_mask + 1) / rsize);
meta_slow.waking =
#ifdef SNMALLOC_CHECK_CLIENT
static_cast<uint16_t>(meta_slow.capacity / 4);
#else
meta_slow.waking = mitigations(random_larger_thresholds) ?
static_cast<uint16_t>(meta_slow.capacity / 4) :
static_cast<uint16_t>(bits::min((meta_slow.capacity / 4), 32));
#endif
if (meta_slow.capacity > max_capacity)
{

View File

@@ -125,7 +125,7 @@ namespace snmalloc
while (madvise(p, size, MADV_FREE_REUSABLE) == -1 && errno == EAGAIN)
;
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
{
// This must occur after `MADV_FREE_REUSABLE`.
//
@@ -180,7 +180,7 @@ namespace snmalloc
}
}
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
{
// Mark pages as writable for `madvise` below.
//
@@ -220,7 +220,7 @@ namespace snmalloc
// must be initialized to 0 or addr is interepreted as a lower-bound.
mach_vm_address_t addr = 0;
vm_prot_t prot = (state_using || !PalEnforceAccess) ?
vm_prot_t prot = (state_using || !mitigations(pal_enforce_access)) ?
VM_PROT_READ | VM_PROT_WRITE :
VM_PROT_NONE;

View File

@@ -40,7 +40,7 @@ namespace snmalloc
madvise(p, size, MADV_FREE);
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
{
mprotect(p, size, PROT_NONE);
}

View File

@@ -38,8 +38,9 @@ namespace snmalloc
int log2align = static_cast<int>(bits::next_pow2_bits(size));
auto prot =
state_using || !PalEnforceAccess ? PROT_READ | PROT_WRITE : PROT_NONE;
auto prot = state_using || !mitigations(pal_enforce_access) ?
PROT_READ | PROT_WRITE :
PROT_NONE;
void* p = mmap(
nullptr,

View File

@@ -7,27 +7,6 @@
namespace snmalloc
{
/**
* Pal implementations should query this flag to see whether they
* are allowed to optimise memory access, or that they must provide
* exceptions/segfaults if accesses do not obey the
* - using
* - using_readonly
* - not_using
* model.
*
* TODO: There is a known bug in CheriBSD that means round-tripping through
* PROT_NONE sheds capability load and store permissions (while restoring data
* read/write, for added excitement). For the moment, just force this down on
* CHERI.
*/
#if defined(SNMALLOC_CHECK_CLIENT) && !defined(__CHERI_PURE_CAPABILITY__) && \
!defined(__NetBSD__)
static constexpr bool PalEnforceAccess = true;
#else
static constexpr bool PalEnforceAccess = false;
#endif
/**
* Flags in a bitfield of optional features that a PAL may support. These
* should be set in the PAL's `pal_features` static constexpr field.

View File

@@ -83,7 +83,7 @@ namespace snmalloc
madvise(p, size, MADV_NOCORE);
madvise(p, size, MADV_FREE);
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
{
mprotect(p, size, PROT_NONE);
}

View File

@@ -128,7 +128,7 @@ namespace snmalloc
madvise(p, size, MADV_DONTDUMP);
madvise(p, size, madvise_free_flags);
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
{
mprotect(p, size, PROT_NONE);
}

View File

@@ -204,7 +204,7 @@ namespace snmalloc
{
SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size));
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
{
// Fill memory so that when we switch the pages back on we don't make
// assumptions on the content.
@@ -232,7 +232,7 @@ namespace snmalloc
SNMALLOC_ASSERT(
is_aligned_block<OS::page_size>(p, size) || (zero_mem == NoZero));
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
mprotect(p, size, PROT_READ | PROT_WRITE);
else
{
@@ -253,7 +253,7 @@ namespace snmalloc
{
SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size));
if constexpr (PalEnforceAccess)
if constexpr (mitigations(pal_enforce_access))
mprotect(p, size, PROT_READ);
else
{
@@ -326,7 +326,8 @@ namespace snmalloc
// If enforcing access, map pages initially as None, and then
// add permissions as required. Otherwise, immediately give all
// access as this is the most efficient to implement.
auto prot = PalEnforceAccess ? PROT_NONE : PROT_READ | PROT_WRITE;
auto prot =
mitigations(pal_enforce_access) ? PROT_NONE : PROT_READ | PROT_WRITE;
void* p = mmap(
nullptr,

View File

@@ -129,11 +129,8 @@ namespace snmalloc
int main()
{
# if defined(SNMALLOC_CHECK_CLIENT)
static constexpr bool pagemap_randomize = !aal_supports<StrictProvenance>;
# else
static constexpr bool pagemap_randomize = false;
# endif
static constexpr bool pagemap_randomize =
mitigations(random_pagemap) & !aal_supports<StrictProvenance>;
snmalloc::CustomConfig::Pagemap::concretePagemap.init<pagemap_randomize>();
snmalloc::CustomConfig::Authmap::init();

View File

@@ -81,11 +81,8 @@ void test_pagemap(bool bounded)
}
else
{
#if defined(SNMALLOC_CHECK_CLIENT)
static constexpr bool pagemap_randomize = !aal_supports<StrictProvenance>;
#else
static constexpr bool pagemap_randomize = false;
#endif
static constexpr bool pagemap_randomize =
mitigations(random_pagemap) && !aal_supports<StrictProvenance>;
pagemap_test_unbound.init<pagemap_randomize>();
pagemap_test_unbound.register_range(low, high - low);