Introduce PalEnforceAccess

The various Pals were given different meanings in CHECK_CLIENT and
non-CHECK_CLIENT builds.  This was because it is essential
that in the CHECK_CLIENT builds access is prevented, when not requested.

This PR separates the CHECK_CLIENT concept from how the Pal should be
implemented.
This commit is contained in:
Matthew Parkinson
2021-09-27 13:32:46 +01:00
committed by Matthew Parkinson
parent b4efc40aa6
commit 55a7ad2d58
8 changed files with 108 additions and 84 deletions

View File

@@ -16,31 +16,37 @@ The PAL must implement the following methods:
``` ```
Report a fatal error and exit. Report a fatal error and exit.
The memory that snmalloc is supplied from the Pal should be in one of three
states
* `using`
* `using readonly`
* `not using`
Before accessing the memory for a read, `snmalloc` will change the state to
either `using` or `using readonly`,
and before a write by it will change the state to `using`.
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
correctly should trigger an exception/segfault.
The state for a particular region of memory is set with
```c++ ```c++
static void notify_not_using(void* p, size_t size) noexcept; static void notify_not_using(void* p, size_t size) noexcept;
```
Notify the system that the range of memory from `p` to `p` + `size` is no
longer in use, allowing the underlying physical pages to recycled for other
purposes.
```c++
template<ZeroMem zero_mem> template<ZeroMem zero_mem>
static void notify_using(void* p, size_t size) noexcept; static void notify_using(void* p, size_t size) noexcept;
```
Notify the system that the range of memory from `p` to `p` + `size` is now in use.
On systems that lazily provide physical memory to virtual mappings, this
function may not be required to do anything.
If the template parameter is set to `YesZero` then this function is also
responsible for ensuring that the newly requested memory is full of zeros.
```c++
static void notify_using_readonly(void* p, size_t size) noexcept; static void notify_using_readonly(void* p, size_t size) noexcept;
``` ```
Notify the system that the range of memory from `p` to `p` + `size` is now in use These functions notify the system that the range of memory from `p` to `p` +
for read-only access. This is currently only requried on platforms that support `size` is in the relevant state.
`LazyCommit`.
On systems that lazily provide physical memory to virtual mappings, this If the template parameter is set to `YesZero` then `notify_using` must ensure
function may not be required to do anything. the range is full of zeros.
```c++ ```c++
template<bool page_aligned = false> template<bool page_aligned = false>
@@ -54,18 +60,17 @@ efficient to request that the operating system provides background-zeroed
pages, rather than zeroing them synchronously in this call pages, rather than zeroing them synchronously in this call
```c++ ```c++
template<bool committed> template<bool state_using>
static void* reserve_aligned(size_t size) noexcept; static void* reserve_aligned(size_t size) noexcept;
static void* reserve(size_t size) noexcept; static void* reserve(size_t size) noexcept;
``` ```
All platforms should provide `reserve` and can optionally provide All platforms should provide `reserve` and can optionally provide
`reserve_aligned` if the underlying system can provide strongly aligned `reserve_aligned` if the underlying system can provide strongly aligned
memory regions. memory regions.
If the system guarantees only page alignment, implement only the second. The Pal is If the system guarantees only page alignment, implement only the second. `snmalloc` will
free to overallocate based on the platform's desire and snmalloc overallocate to ensure it can find suitably aligned blocks inside the region.
will find suitably aligned blocks inside the region. `reserve` should `reserve` should consider memory initially as `not_using`, and `snmalloc` will notify when it
not commit memory as snmalloc will commit the range of memory it requires of what needs the range of memory.
is returned.
If the system provides strong alignment, implement the first to return memory If the system provides strong alignment, implement the first to return memory
at the desired alignment. If providing the first, then the `Pal` should also at the desired alignment. If providing the first, then the `Pal` should also

View File

@@ -113,7 +113,7 @@ namespace snmalloc
{ {
SNMALLOC_ASSERT(is_aligned_block<page_size>(p, size)); SNMALLOC_ASSERT(is_aligned_block<page_size>(p, size));
# if defined(SNMALLOC_CHECK_CLIENT) && !defined(NDEBUG) # if !defined(NDEBUG)
memset(p, 0x5a, size); memset(p, 0x5a, size);
# endif # endif
@@ -126,12 +126,13 @@ namespace snmalloc
while (madvise(p, size, MADV_FREE_REUSABLE) == -1 && errno == EAGAIN) while (madvise(p, size, MADV_FREE_REUSABLE) == -1 && errno == EAGAIN)
; ;
# ifdef SNMALLOC_CHECK_CLIENT if constexpr (PalEnforceAccess)
// This must occur after `MADV_FREE_REUSABLE`. {
// // This must occur after `MADV_FREE_REUSABLE`.
// `mach_vm_protect` is observably slower in benchmarks. //
mprotect(p, size, PROT_NONE); // `mach_vm_protect` is observably slower in benchmarks.
# endif mprotect(p, size, PROT_NONE);
}
} }
/** /**
@@ -180,12 +181,13 @@ namespace snmalloc
} }
} }
# ifdef SNMALLOC_CHECK_CLIENT if constexpr (PalEnforceAccess)
// Mark pages as writable for `madvise` below. {
// // Mark pages as writable for `madvise` below.
// `mach_vm_protect` is observably slower in benchmarks. //
mprotect(p, size, PROT_READ | PROT_WRITE); // `mach_vm_protect` is observably slower in benchmarks.
# endif mprotect(p, size, PROT_READ | PROT_WRITE);
}
// `MADV_FREE_REUSE` can only be applied to writable pages, // `MADV_FREE_REUSE` can only be applied to writable pages,
// otherwise it's an error. // otherwise it's an error.
@@ -205,7 +207,7 @@ namespace snmalloc
// Apple's `mmap` doesn't support user-specified alignment and only // Apple's `mmap` doesn't support user-specified alignment and only
// guarantees mappings are aligned to the system page size, so we use // guarantees mappings are aligned to the system page size, so we use
// `mach_vm_map` instead. // `mach_vm_map` instead.
template<bool committed> template<bool state_using>
static void* reserve_aligned(size_t size) noexcept static void* reserve_aligned(size_t size) noexcept
{ {
SNMALLOC_ASSERT(bits::is_pow2(size)); SNMALLOC_ASSERT(bits::is_pow2(size));
@@ -219,11 +221,9 @@ namespace snmalloc
// must be initialized to 0 or addr is interepreted as a lower-bound. // must be initialized to 0 or addr is interepreted as a lower-bound.
mach_vm_address_t addr = 0; mach_vm_address_t addr = 0;
# ifdef SNMALLOC_CHECK_CLIENT vm_prot_t prot = (state_using || !PalEnforceAccess) ?
vm_prot_t prot = committed ? VM_PROT_READ | VM_PROT_WRITE : VM_PROT_NONE; VM_PROT_READ | VM_PROT_WRITE :
# else VM_PROT_NONE;
vm_prot_t prot = VM_PROT_READ | VM_PROT_WRITE;
# endif
kern_return_t kr = mach_vm_map( kern_return_t kr = mach_vm_map(
mach_task_self(), mach_task_self(),

View File

@@ -34,14 +34,16 @@ namespace snmalloc
static void notify_not_using(void* p, size_t size) noexcept static void notify_not_using(void* p, size_t size) noexcept
{ {
SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size)); SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size));
// Call this Pal to simulate the Windows decommit in CI.
#if defined(SNMALLOC_CHECK_CLIENT) && !defined(NDEBUG) #if !defined(NDEBUG)
memset(p, 0x5a, size); memset(p, 0x5a, size);
#endif #endif
madvise(p, size, MADV_FREE); madvise(p, size, MADV_FREE);
#ifdef SNMALLOC_CHECK_CLIENT
mprotect(p, size, PROT_NONE); if constexpr (PalEnforceAccess)
#endif {
mprotect(p, size, PROT_NONE);
}
} }
}; };
} // namespace snmalloc } // namespace snmalloc

View File

@@ -28,7 +28,7 @@ namespace snmalloc
/** /**
* Reserve memory at a specific alignment. * Reserve memory at a specific alignment.
*/ */
template<bool committed> template<bool state_using>
static void* reserve_aligned(size_t size) noexcept static void* reserve_aligned(size_t size) noexcept
{ {
// Alignment must be a power of 2. // Alignment must be a power of 2.
@@ -37,11 +37,8 @@ namespace snmalloc
int log2align = static_cast<int>(bits::next_pow2_bits(size)); int log2align = static_cast<int>(bits::next_pow2_bits(size));
#ifdef SNMALLOC_CHECK_CLIENT auto prot =
auto prot = committed ? PROT_READ | PROT_WRITE : PROT_NONE; state_using || !PalEnforceAccess ? PROT_READ | PROT_WRITE : PROT_NONE;
#else
auto prot = PROT_READ | PROT_WRITE;
#endif
void* p = mmap( void* p = mmap(
nullptr, nullptr,

View File

@@ -6,6 +6,21 @@
namespace snmalloc 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.
*/
#ifdef SNMALLOC_CHECK_CLIENT
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 * 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. * should be set in the PAL's `pal_features` static constexpr field.

View File

@@ -59,7 +59,7 @@ namespace snmalloc
::bzero(p, size); ::bzero(p, size);
} }
template<bool committed> template<bool state_using>
static void* reserve_aligned(size_t size) noexcept static void* reserve_aligned(size_t size) noexcept
{ {
SNMALLOC_ASSERT(bits::is_pow2(size)); SNMALLOC_ASSERT(bits::is_pow2(size));
@@ -80,7 +80,7 @@ namespace snmalloc
{ {
return nullptr; return nullptr;
} }
if (committed) if (state_using)
{ {
if ( if (
kmem_back(kernel_object, addr, size, M_ZERO | M_WAITOK) != kmem_back(kernel_object, addr, size, M_ZERO | M_WAITOK) !=

View File

@@ -178,17 +178,21 @@ namespace snmalloc
static void notify_not_using(void* p, size_t size) noexcept static void notify_not_using(void* p, size_t size) noexcept
{ {
SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size)); SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size));
#ifdef SNMALLOC_CHECK_CLIENT
// Fill memory so that when we switch the pages back on we don't make if constexpr (PalEnforceAccess)
// assumptions on the content. {
# if !defined(NDEBUG) #if !defined(NDEBUG)
memset(p, 0x5a, size); // Fill memory so that when we switch the pages back on we don't make
# endif // assumptions on the content.
mprotect(p, size, PROT_NONE); memset(p, 0x5a, size);
#else
UNUSED(p);
UNUSED(size);
#endif #endif
mprotect(p, size, PROT_NONE);
}
else
{
UNUSED(p);
UNUSED(size);
}
} }
/** /**
@@ -204,12 +208,13 @@ namespace snmalloc
SNMALLOC_ASSERT( SNMALLOC_ASSERT(
is_aligned_block<OS::page_size>(p, size) || (zero_mem == NoZero)); is_aligned_block<OS::page_size>(p, size) || (zero_mem == NoZero));
#ifdef SNMALLOC_CHECK_CLIENT if constexpr (PalEnforceAccess)
mprotect(p, size, PROT_READ | PROT_WRITE); mprotect(p, size, PROT_READ | PROT_WRITE);
#else else
UNUSED(p); {
UNUSED(size); UNUSED(p);
#endif UNUSED(size);
}
if constexpr (zero_mem == YesZero) if constexpr (zero_mem == YesZero)
zero<true>(p, size); zero<true>(p, size);
@@ -225,12 +230,13 @@ namespace snmalloc
{ {
SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size)); SNMALLOC_ASSERT(is_aligned_block<OS::page_size>(p, size));
#ifdef SNMALLOC_CHECK_CLIENT if constexpr (PalEnforceAccess)
mprotect(p, size, PROT_READ); mprotect(p, size, PROT_READ);
#else else
UNUSED(p); {
UNUSED(size); UNUSED(p);
#endif UNUSED(size);
}
} }
/** /**
@@ -286,11 +292,10 @@ namespace snmalloc
*/ */
static void* reserve(size_t size) noexcept static void* reserve(size_t size) noexcept
{ {
#ifdef SNMALLOC_CHECK_CLIENT // If enforcing access, map pages initially as None, and then
auto prot = PROT_NONE; // add permissions as required. Otherwise, immediately give all
#else // access as this is the most efficient to implement.
auto prot = PROT_READ | PROT_WRITE; auto prot = PalEnforceAccess ? PROT_NONE : PROT_READ | PROT_WRITE;
#endif
void* p = mmap( void* p = mmap(
nullptr, nullptr,

View File

@@ -158,7 +158,7 @@ namespace snmalloc
} }
# ifdef PLATFORM_HAS_VIRTUALALLOC2 # ifdef PLATFORM_HAS_VIRTUALALLOC2
template<bool committed> template<bool state_using>
static void* reserve_aligned(size_t size) noexcept static void* reserve_aligned(size_t size) noexcept
{ {
SNMALLOC_ASSERT(bits::is_pow2(size)); SNMALLOC_ASSERT(bits::is_pow2(size));
@@ -166,7 +166,7 @@ namespace snmalloc
DWORD flags = MEM_RESERVE; DWORD flags = MEM_RESERVE;
if (committed) if (state_using)
flags |= MEM_COMMIT; flags |= MEM_COMMIT;
// If we're on Windows 10 or newer, we can use the VirtualAlloc2 // If we're on Windows 10 or newer, we can use the VirtualAlloc2