Snmalloc2 API cleanups for sandbox use. (#359)

This is the set of changes required for snmalloc2 to be usable by the
process sandboxing code and incorporates some API changes that reduce
the amount of code required to embed snmalloc.  Highlights:

 - Merge the config and back-end classes.
 - Everything in config is now global (all methods are static)
 - The GlobalState class is gone (all global state is managed by global
   methods on the config class)
 - LocalState is now a member of the config class, all methods are
   instance methods.
 - Not every configuration needs to use the lazy initialisation hooks.
   They now need to be provided only if they are used.  If the
   configuration does not provide an `ensure_init` method, it is not
   called.  If it does not provide an `is_initialised` method then the
   global initialisation state is not checked.
 - There is now an `snmalloc::Options` class that default initialises
   itself to the default behaviour.  Every configuration must provide a
   `constexpr` instance of this class.  Each flag can be separately
   overridden and new flags can be added without breaking any existing
   API consumers.

The config classes are moved into the backend directory.
This commit is contained in:
David Chisnall
2021-08-05 15:08:12 +01:00
committed by GitHub
parent 2ef1eea3ba
commit e8374479f4
29 changed files with 676 additions and 380 deletions

View File

@@ -1,6 +1,8 @@
#pragma once
#include "../ds/address.h"
#include "../ds/flaglock.h"
#include "../mem/allocconfig.h"
#include "../mem/metaslab.h"
#include "../pal/pal.h"
#include <array>

View File

@@ -3,6 +3,7 @@
#include "../mem/metaslab.h"
#include "../pal/pal.h"
#include "address_space.h"
#include "commonconfig.h"
#include "pagemap.h"
namespace snmalloc
@@ -15,7 +16,7 @@ namespace snmalloc
SNMALLOC_CONCEPT(ConceptPAL) PAL,
bool fixed_range,
typename PageMapEntry = MetaEntry>
class BackendAllocator
class BackendAllocator : public CommonConfig
{
// Size of local address space requests. Currently aimed at 2MiB large
// pages but should make this configurable (i.e. for OE, so we don't need as
@@ -54,55 +55,31 @@ namespace snmalloc
#endif
};
/**
* Global state for the backend allocator
*
* This contains the various global datastructures required to store
* meta-data for each chunk of memory, and to provide well aligned chunks
* of memory.
*
* This type is required by snmalloc to exist as part of the Backend.
*/
class GlobalState
SNMALLOC_REQUIRE_CONSTINIT
static inline AddressSpaceManager<PAL> address_space;
SNMALLOC_REQUIRE_CONSTINIT
static inline FlatPagemap<MIN_CHUNK_BITS, PageMapEntry, PAL, fixed_range>
pagemap;
public:
template<bool fixed_range_ = fixed_range>
static std::enable_if_t<!fixed_range_> init()
{
friend BackendAllocator;
static_assert(fixed_range_ == fixed_range, "Don't set SFINAE parameter!");
protected:
AddressSpaceManager<PAL> address_space;
pagemap.init();
}
FlatPagemap<MIN_CHUNK_BITS, PageMapEntry, PAL, fixed_range> pagemap;
template<bool fixed_range_ = fixed_range>
static std::enable_if_t<fixed_range_> init(void* base, size_t length)
{
static_assert(fixed_range_ == fixed_range, "Don't set SFINAE parameter!");
public:
template<bool fixed_range_ = fixed_range>
std::enable_if_t<!fixed_range_> init()
{
static_assert(
fixed_range_ == fixed_range, "Don't set SFINAE parameter!");
pagemap.init();
if constexpr (fixed_range)
{
abort();
}
}
template<bool fixed_range_ = fixed_range>
std::enable_if_t<fixed_range_> init(void* base, size_t length)
{
static_assert(
fixed_range_ == fixed_range, "Don't set SFINAE parameter!");
auto [heap_base, heap_length] = pagemap.init(base, length);
address_space.add_range(
CapPtr<void, CBChunk>(heap_base), heap_length, pagemap);
if constexpr (!fixed_range)
{
abort();
}
}
};
auto [heap_base, heap_length] = pagemap.init(base, length);
address_space.add_range(
CapPtr<void, CBChunk>(heap_base), heap_length, pagemap);
}
private:
#ifdef SNMALLOC_CHECK_CLIENT
@@ -138,8 +115,7 @@ namespace snmalloc
* space managers.
*/
template<bool is_meta>
static CapPtr<void, CBChunk>
reserve(GlobalState& h, LocalState* local_state, size_t size)
static CapPtr<void, CBChunk> reserve(LocalState* local_state, size_t size)
{
#ifdef SNMALLOC_CHECK_CLIENT
constexpr auto MAX_CACHED_SIZE =
@@ -148,7 +124,7 @@ namespace snmalloc
constexpr auto MAX_CACHED_SIZE = LOCAL_CACHE_BLOCK;
#endif
auto& global = h.address_space;
auto& global = address_space;
CapPtr<void, CBChunk> p;
if ((local_state != nullptr) && (size <= MAX_CACHED_SIZE))
@@ -160,14 +136,14 @@ namespace snmalloc
auto& local = local_state->local_address_space;
#endif
p = local.template reserve_with_left_over<PAL>(size, h.pagemap);
p = local.template reserve_with_left_over<PAL>(size, pagemap);
if (p != nullptr)
{
return p;
}
auto refill_size = LOCAL_CACHE_BLOCK;
auto refill = global.template reserve<false>(refill_size, h.pagemap);
auto refill = global.template reserve<false>(refill_size, pagemap);
if (refill == nullptr)
return nullptr;
@@ -179,10 +155,10 @@ namespace snmalloc
}
#endif
PAL::template notify_using<NoZero>(refill.unsafe_ptr(), refill_size);
local.template add_range<PAL>(refill, refill_size, h.pagemap);
local.template add_range<PAL>(refill, refill_size, pagemap);
// This should succeed
return local.template reserve_with_left_over<PAL>(size, h.pagemap);
return local.template reserve_with_left_over<PAL>(size, pagemap);
}
#ifdef SNMALLOC_CHECK_CLIENT
@@ -193,7 +169,7 @@ namespace snmalloc
size_t rsize = bits::max(OS_PAGE_SIZE, bits::next_pow2(size));
size_t size_request = rsize * 64;
p = global.template reserve<false>(size_request, h.pagemap);
p = global.template reserve<false>(size_request, pagemap);
if (p == nullptr)
return nullptr;
@@ -209,7 +185,7 @@ namespace snmalloc
SNMALLOC_ASSERT(!is_meta);
#endif
p = global.template reserve_with_left_over<true>(size, h.pagemap);
p = global.template reserve_with_left_over<true>(size, pagemap);
return p;
}
@@ -219,11 +195,16 @@ namespace snmalloc
*
* Backend allocator may use guard pages and separate area of
* address space to protect this from corruption.
*
* The template argument is the type of the metadata being allocated. This
* allows the backend to allocate different types of metadata in different
* places or with different policies.
*/
template<typename T>
static CapPtr<void, CBChunk>
alloc_meta_data(GlobalState& h, LocalState* local_state, size_t size)
alloc_meta_data(LocalState* local_state, size_t size)
{
return reserve<true>(h, local_state, size);
return reserve<true>(local_state, size);
}
/**
@@ -236,7 +217,6 @@ namespace snmalloc
* where metaslab, is the second element of the pair return.
*/
static std::pair<CapPtr<void, CBChunk>, Metaslab*> alloc_chunk(
GlobalState& h,
LocalState* local_state,
size_t size,
RemoteAllocator* remote,
@@ -246,12 +226,12 @@ namespace snmalloc
SNMALLOC_ASSERT(size >= MIN_CHUNK_SIZE);
auto meta = reinterpret_cast<Metaslab*>(
reserve<true>(h, local_state, sizeof(Metaslab)).unsafe_ptr());
reserve<true>(local_state, sizeof(Metaslab)).unsafe_ptr());
if (meta == nullptr)
return {nullptr, nullptr};
CapPtr<void, CBChunk> p = reserve<false>(h, local_state, size);
CapPtr<void, CBChunk> p = reserve<false>(local_state, size);
#ifdef SNMALLOC_TRACING
std::cout << "Alloc chunk: " << p.unsafe_ptr() << " (" << size << ")"
@@ -274,7 +254,7 @@ namespace snmalloc
a < address_cast(pointer_offset(p, size));
a += MIN_CHUNK_SIZE)
{
h.pagemap.set(a, t);
pagemap.set(a, t);
}
return {p, meta};
}
@@ -286,20 +266,19 @@ namespace snmalloc
* to access a location that is not backed by a chunk.
*/
template<bool potentially_out_of_range = false>
static const MetaEntry& get_meta_data(GlobalState& h, address_t p)
static const MetaEntry& get_meta_data(address_t p)
{
return h.pagemap.template get<potentially_out_of_range>(p);
return pagemap.template get<potentially_out_of_range>(p);
}
/**
* Set the metadata associated with a chunk.
*/
static void
set_meta_data(GlobalState& h, address_t p, size_t size, MetaEntry t)
static void set_meta_data(address_t p, size_t size, MetaEntry t)
{
for (address_t a = p; a < p + size; a += MIN_CHUNK_SIZE)
{
h.pagemap.set(a, t);
pagemap.set(a, t);
}
}
};

118
src/backend/commonconfig.h Normal file
View File

@@ -0,0 +1,118 @@
#pragma once
#include "../ds/defines.h"
#include "../mem/remotecache.h"
namespace snmalloc
{
// Forward reference to thread local cleanup.
void register_clean_up();
/**
* Options for a specific snmalloc configuration. Every globals object must
* have one `constexpr` instance of this class called `Options`. This should
* be constructed to explicitly override any of the defaults. A
* configuration that does not need to override anything would simply declare
* this as a field of the global object:
*
* ```c++
* constexpr static snmalloc::Flags Options{};
* ```
*
* A global configuration that wished to use out-of-line message queues but
* accept the defaults for everything else would instead do this:
*
* ```c++
* constexpr static snmalloc::Flags Options{.IsQueueInline = false};
* ```
*
* To maintain backwards source compatibility in future versions, any new
* option added here should have its default set to be whatever snmalloc was
* doing before the new option was added.
*/
struct Flags
{
/**
* Should allocators have inline message queues? If this is true then
* the `CoreAllocator` is responsible for allocating the
* `RemoteAllocator` that contains its message queue. If this is false
* then the `RemoteAllocator` must be separately allocated and provided
* to the `CoreAllocator` before it is used.
*
* Setting this to `false` currently requires also setting
* `LocalAllocSupportsLazyInit` to false so that the `CoreAllocator` can
* be provided to the `LocalAllocator` fully initialised but in the
* future it may be possible to allocate the `RemoteAllocator` via
* `alloc_meta_data` or a similar API in the back end.
*/
bool IsQueueInline = true;
/**
* Does the `CoreAllocator` own a `Backend::LocalState` object? If this is
* true then the `CoreAllocator` is responsible for allocating and
* deallocating a local state object, otherwise the surrounding code is
* responsible for creating it.
*
* Use cases that set this to false will probably also need to set
* `LocalAllocSupportsLazyInit` to false so that they can provide the local
* state explicitly during allocator creation.
*/
bool CoreAllocOwnsLocalState = true;
/**
* Are `CoreAllocator` allocated by the pool allocator? If not then the
* code embedding this snmalloc configuration is responsible for allocating
* `CoreAllocator` instances.
*
* Users setting this flag must also set `LocalAllocSupportsLazyInit` to
* false currently because there is no alternative mechanism for allocating
* core allocators. This may change in future versions.
*/
bool CoreAllocIsPoolAllocated = true;
/**
* Do `LocalAllocator` instances in this configuration support lazy
* initialisation? If so, then the first exit from a fast path will
* trigger allocation of a `CoreAllocator` and associated state. If not
* then the code embedding this configuration of snmalloc is responsible
* for allocating core allocators.
*/
bool LocalAllocSupportsLazyInit = true;
};
/**
* Class containing definitions that are likely to be used by all except for
* the most unusual back-end implementations. This can be subclassed as a
* convenience for back-end implementers, but is not required.
*/
class CommonConfig
{
public:
/**
* Special remote that should never be used as a real remote.
* This is used to initialise allocators that should always hit the
* remote path for deallocation. Hence moving a branch off the critical
* path.
*/
SNMALLOC_REQUIRE_CONSTINIT
inline static RemoteAllocator unused_remote;
/**
* Special remote that is used in meta-data for large allocations.
*
* nullptr is considered a large allocations for this purpose to move
* of the critical path.
*
* Bottom bits of the remote pointer are used for a sizeclass, we need
* size bits to represent the non-large sizeclasses, we can then get
* the large sizeclass by having the fake large_remote considerably
* more aligned.
*/
SNMALLOC_REQUIRE_CONSTINIT
inline static constexpr RemoteAllocator* fake_large_remote{nullptr};
static_assert(
&unused_remote != fake_large_remote,
"Compilation should ensure these are different");
};
} // namespace snmalloc

View File

@@ -0,0 +1,51 @@
#pragma once
#include "../backend/backend.h"
#include "../mem/corealloc.h"
#include "../mem/pool.h"
#include "../mem/slaballocator.h"
#include "commonconfig.h"
namespace snmalloc
{
/**
* A single fixed address range allocator configuration
*/
template<SNMALLOC_CONCEPT(ConceptPAL) PAL>
class FixedGlobals final : public BackendAllocator<PAL, true>
{
private:
using Backend = BackendAllocator<PAL, true>;
inline static ChunkAllocatorState slab_allocator_state;
inline static PoolState<CoreAllocator<FixedGlobals>> alloc_pool;
public:
static ChunkAllocatorState&
get_slab_allocator_state(typename Backend::LocalState*)
{
return slab_allocator_state;
}
static PoolState<CoreAllocator<FixedGlobals>>& pool()
{
return alloc_pool;
}
static constexpr Flags Options{};
// This needs to be a forward reference as the
// thread local state will need to know about this.
// This may allocate, so must be called once a thread
// local allocator exists.
static void register_clean_up()
{
snmalloc::register_clean_up();
}
static void init(void* base, size_t length)
{
Backend::init(base, length);
}
};
}

103
src/backend/globalconfig.h Normal file
View File

@@ -0,0 +1,103 @@
#pragma once
#include "../backend/backend.h"
#include "../mem/corealloc.h"
#include "../mem/pool.h"
#include "../mem/slaballocator.h"
#include "commonconfig.h"
#include <iostream>
namespace snmalloc
{
// Forward reference to thread local cleanup.
void register_clean_up();
#ifdef USE_SNMALLOC_STATS
inline static void print_stats()
{
printf("No Stats yet!");
// Stats s;
// current_alloc_pool()->aggregate_stats(s);
// s.print<Alloc>(std::cout);
}
#endif
/**
* The default configuration for a global snmalloc. This allocates memory
* from the operating system and expects to manage memory anywhere in the
* address space.
*/
class Globals final : public BackendAllocator<Pal, false>
{
private:
using Backend = BackendAllocator<Pal, false>;
SNMALLOC_REQUIRE_CONSTINIT
inline static ChunkAllocatorState slab_allocator_state;
SNMALLOC_REQUIRE_CONSTINIT
inline static PoolState<CoreAllocator<Globals>> alloc_pool;
SNMALLOC_REQUIRE_CONSTINIT
inline static std::atomic<bool> initialised{false};
SNMALLOC_REQUIRE_CONSTINIT
inline static std::atomic_flag initialisation_lock{};
public:
static ChunkAllocatorState&
get_slab_allocator_state(Backend::LocalState* = nullptr)
{
return slab_allocator_state;
}
static PoolState<CoreAllocator<Globals>>& pool()
{
return alloc_pool;
}
static constexpr Flags Options{};
// Performs initialisation for this configuration
// of allocators. Needs to be idempotent,
// and concurrency safe.
static void ensure_init()
{
FlagLock lock{initialisation_lock};
#ifdef SNMALLOC_TRACING
std::cout << "Run init_impl" << std::endl;
#endif
if (initialised)
return;
LocalEntropy entropy;
entropy.init<Pal>();
// Initialise key for remote deallocation lists
key_global = FreeListKey(entropy.get_free_list_key());
// Need to initialise pagemap.
Backend::init();
#ifdef USE_SNMALLOC_STATS
atexit(snmalloc::print_stats);
#endif
initialised = true;
}
static bool is_initialised()
{
return initialised;
}
// This needs to be a forward reference as the
// thread local state will need to know about this.
// This may allocate, so should only be called once
// a thread local allocator is available.
static void register_clean_up()
{
snmalloc::register_clean_up();
}
};
} // namespace snmalloc

View File

@@ -66,6 +66,36 @@ namespace snmalloc
constexpr FlatPagemap() = default;
/**
* For pagemaps that cover an entire fixed address space, return the size
* that they must be. This allows the caller to allocate the correct
* amount of memory to be passed to `init`. This is not available for
* fixed-range pagemaps, whose size depends on dynamic configuration.
*/
template<bool has_bounds_ = has_bounds>
static constexpr std::enable_if_t<!has_bounds_, size_t> required_size()
{
static_assert(
has_bounds_ == has_bounds, "Don't set SFINAE template parameter!");
constexpr size_t COVERED_BITS = bits::ADDRESS_BITS - GRANULARITY_BITS;
constexpr size_t ENTRIES = bits::one_at_bit(COVERED_BITS);
return ENTRIES * sizeof(T);
}
/**
* Initialise with pre-allocated memory.
*
* This is currently disabled for bounded pagemaps but may be reenabled if
* `required_size` is enabled for the has-bounds case.
*/
template<bool has_bounds_ = has_bounds>
std::enable_if_t<!has_bounds_> init(T* address)
{
static_assert(
has_bounds_ == has_bounds, "Don't set SFINAE template parameter!");
body = address;
}
/**
* Initialise the pagemap with bounds.
*
@@ -117,11 +147,7 @@ namespace snmalloc
{
static_assert(
has_bounds_ == has_bounds, "Don't set SFINAE template parameter!");
static constexpr size_t COVERED_BITS =
bits::ADDRESS_BITS - GRANULARITY_BITS;
static constexpr size_t ENTRIES = bits::one_at_bit(COVERED_BITS);
static constexpr size_t REQUIRED_SIZE = ENTRIES * sizeof(T);
static constexpr size_t REQUIRED_SIZE = required_size();
#ifdef SNMALLOC_CHECK_CLIENT
// Allocate a power of two extra to allow the placement of the