Protect free list pointers stored in object space

Free list pointers can be exploited by attackers. This commit implements
a simple encoding scheme to detect corruption of the pointers.  This can
be used to detect UAF and double free.

This does not currently address anything for Medium or Large
allocations.  It also does not address cross thread deallocations.

Co-authored-by: Nathaniel Wesley Filardo <nfilardo@microsoft.com>
This commit is contained in:
Matthew Parkinson
2021-03-13 20:46:49 +00:00
committed by Matthew Parkinson
parent afe53e71af
commit 50f412157f
6 changed files with 382 additions and 173 deletions

View File

@@ -266,9 +266,11 @@ if(NOT DEFINED SNMALLOC_ONLY_HEADER_LIBRARY)
if(NOT WIN32)
set(SHARED_FILES src/override/new.cc src/override/malloc.cc)
add_shim(snmallocshim SHARED ${SHARED_FILES})
add_shim(snmallocshim-checks SHARED ${SHARED_FILES})
add_shim(snmallocshim-1mib SHARED ${SHARED_FILES})
add_shim(snmallocshim-16mib SHARED ${SHARED_FILES})
target_compile_definitions(snmallocshim-16mib PRIVATE SNMALLOC_USE_LARGE_CHUNKS)
target_compile_definitions(snmallocshim-checks PRIVATE CHECK_CLIENT)
# Build a shim with some settings from oe.
add_shim(snmallocshim-oe SHARED ${SHARED_FILES})
oe_simulate(snmallocshim-oe)

View File

@@ -47,7 +47,7 @@ namespace snmalloc
class FastFreeLists
{
protected:
FreeListHead small_fast_free_lists[NUM_SMALL_CLASSES];
FreeListIter small_fast_free_lists[NUM_SMALL_CLASSES];
public:
FastFreeLists() : small_fast_free_lists() {}
@@ -747,35 +747,29 @@ namespace snmalloc
{
auto& bp = bump_ptrs[i];
auto rsize = sizeclass_to_size(i);
FreeListHead ffl;
FreeListIter ffl;
while (pointer_align_up(bp, SLAB_SIZE) != bp)
{
Slab::alloc_new_list(bp, ffl, rsize);
SlabNext* prev = ffl.value;
while (prev != nullptr)
while (!ffl.empty())
{
auto n = Metaslab::follow_next(prev);
Superslab* super = Superslab::get(prev);
Slab* slab = Metaslab::get_slab(prev);
small_dealloc_offseted_inner(super, slab, prev, i);
prev = n;
auto curr = ffl.take();
Superslab* super = Superslab::get(curr);
Slab* slab = Metaslab::get_slab(curr);
small_dealloc_offseted_inner(super, slab, curr, i);
}
}
}
for (size_t i = 0; i < NUM_SMALL_CLASSES; i++)
{
auto prev = small_fast_free_lists[i].value;
small_fast_free_lists[i].value = nullptr;
while (prev != nullptr)
while (!small_fast_free_lists[i].empty())
{
auto n = Metaslab::follow_next(prev);
auto curr = small_fast_free_lists[i].take();
Superslab* super = Superslab::get(prev);
Slab* slab = Metaslab::get_slab(prev);
small_dealloc_offseted_inner(super, slab, prev, i);
prev = n;
Superslab* super = Superslab::get(curr);
Slab* slab = Metaslab::get_slab(curr);
small_dealloc_offseted_inner(super, slab, curr, i);
}
test(small_classes[i]);
@@ -1035,15 +1029,11 @@ namespace snmalloc
{
SNMALLOC_ASSUME(sizeclass < NUM_SMALL_CLASSES);
auto& fl = small_fast_free_lists[sizeclass];
SlabNext* head = fl.value;
if (likely(head != nullptr))
if (likely(!fl.empty()))
{
stats().alloc_request(size);
stats().sizeclass_alloc(sizeclass);
// Read the next slot from the memory that's about to be allocated.
fl.value = Metaslab::follow_next(head);
void* p = remove_cache_friendly_offset(head, sizeclass);
void* p = remove_cache_friendly_offset(fl.take(), sizeclass);
if constexpr (zero_mem == YesZero)
{
MemoryProvider::Pal::zero(p, sizeclass_to_size(sizeclass));
@@ -1154,12 +1144,10 @@ namespace snmalloc
auto& bp = bump_ptrs[sizeclass];
auto rsize = sizeclass_to_size(sizeclass);
auto& ffl = small_fast_free_lists[sizeclass];
SNMALLOC_ASSERT(ffl.value == nullptr);
SNMALLOC_ASSERT(ffl.empty());
Slab::alloc_new_list(bp, ffl, rsize);
SlabNext* p = static_cast<SlabNext*>(
remove_cache_friendly_offset(ffl.value, sizeclass));
ffl.value = Metaslab::follow_next(p);
auto p = remove_cache_friendly_offset(ffl.take(), sizeclass);
if constexpr (zero_mem == YesZero)
{
@@ -1181,7 +1169,7 @@ namespace snmalloc
Slab* slab = alloc_slab<allow_reserve>(sizeclass);
if (slab == nullptr)
return nullptr;
bp = pointer_offset<SlabNext>(
bp = pointer_offset<void>(
slab, get_initial_offset(sizeclass, Metaslab::is_short(slab)));
return small_alloc_build_free_list<zero_mem, allow_reserve>(sizeclass);

317
src/mem/freelist.h Normal file
View File

@@ -0,0 +1,317 @@
#pragma once
/**
* This file encapsulates the in disused object free lists
* that are used per slab of small objects.
*/
#include "../ds/address.h"
#include "../ds/cdllist.h"
#include "../ds/dllist.h"
#include "../ds/helpers.h"
#include "allocconfig.h"
#include <iostream>
namespace snmalloc
{
#ifdef CHECK_CLIENT
/**
* The key that is used to encode free list pointers.
* This should be randomised at startup in the future.
*/
inline static address_t global_key =
static_cast<size_t>(bits::is64() ? 0x9999'9999'9999'9999 : 0x9999'9999);
/**
* Used to turn a location into a key. This is currently
* just the value of the previous location + 1.
*/
inline static uintptr_t initial_key(void* p)
{
return address_cast(p) + 1;
}
#endif
static inline bool different_slab(uintptr_t p1, uintptr_t p2)
{
return ((p1 ^ p2) >= SLAB_SIZE);
}
static inline bool different_slab(uintptr_t p1, void* p2)
{
return different_slab(p1, address_cast(p2));
}
static inline bool different_slab(void* p1, void* p2)
{
return different_slab(address_cast(p1), address_cast(p2));
}
/**
* Free objects within each slab point directly to the next.
* The next_object pointer can be encoded to detect
* corruption caused by writes in a UAF or a double free.
*
* If cache-friendly offsets are used, then the FreeObject is
* potentially offset from the start of the object.
*/
class FreeObject
{
FreeObject* next_object;
static FreeObject* encode(uintptr_t local_key, FreeObject* next_object)
{
#ifdef CHECK_CLIENT
if constexpr (aal_supports<IntegerPointers>)
{
// Simple involutional encoding. The bottom half of each word is
// multiplied by a function of both global and local keys (the latter,
// in practice, being the address of the previous list entry) and the
// resulting word's top half is XORed into the pointer value before it
// is stored.
auto next = address_cast(next_object);
constexpr uintptr_t MASK = bits::one_at_bit(bits::BITS / 2) - 1;
// Mix in local_key
auto key = local_key ^ global_key;
next ^= (((next & MASK) + 1) * key) & ~MASK;
next_object = reinterpret_cast<FreeObject*>(next);
}
#else
UNUSED(local_key);
#endif
return next_object;
}
public:
static FreeObject* make(void* p)
{
return static_cast<FreeObject*>(p);
}
/**
* Read the next pointer handling any required decoding of the pointer
*/
FreeObject* read_next(uintptr_t key)
{
auto next = encode(key, next_object);
return next;
}
/**
* Store the next pointer handling any required encoding of the pointer
*/
void store_next(FreeObject* next, uintptr_t key)
{
next_object = encode(key, next);
SNMALLOC_ASSERT(next == read_next(key));
}
};
/**
* Wrapper class that allows the keys for pointer encoding to be
* conditionally compiled.
*/
class FreeObjectCursor
{
FreeObject* curr = nullptr;
#ifdef CHECK_CLIENT
uintptr_t prev = 0;
#endif
uintptr_t get_prev()
{
#ifdef CHECK_CLIENT
return prev;
#else
return 0;
#endif
}
/**
* Updates the cursor to the new value,
* importantly this updates the key being used.
* Currently this is just the value of current before this call.
* Other schemes could be used.
*/
void update_cursor(FreeObject* next)
{
#ifdef CHECK_CLIENT
# ifndef NDEBUG
if (next != nullptr)
{
if (unlikely(different_slab(prev, next)))
{
error("Heap corruption - free list corrupted!");
}
}
# endif
prev = address_cast(curr);
#endif
curr = next;
}
public:
FreeObject* get_curr()
{
return curr;
}
/**
* Advance the cursor through the list
*/
void move_next()
{
#ifdef CHECK_CLIENT
if (unlikely(different_slab(prev, curr)))
{
error("Heap corruption - free list corrupted!");
}
#endif
update_cursor(curr->read_next(get_prev()));
}
/**
* Update the next pointer at the location in the list pointed to
* by the cursor.
*/
void set_next(FreeObject* next)
{
curr->store_next(next, get_prev());
}
/**
* Update the next pointer at the location in the list pointed to
* by the cursor, and move the cursor to that new value.
*/
void set_next_and_move(FreeObject* next)
{
set_next(next);
update_cursor(next);
}
/**
* Resets the key to an initial value. So the cursor can be used
* on a new sequence.
*/
void reset_cursor(FreeObject* next)
{
#ifdef CHECK_CLIENT
prev = initial_key(next);
#endif
curr = next;
}
};
/**
* Used to iterate a free list in object space.
*
* Checks signing of pointers
*/
class FreeListIter
{
protected:
FreeObjectCursor front;
public:
/**
* Checks if there are any more values to iterate.
*/
bool empty()
{
return front.get_curr() == nullptr;
}
/**
* Moves the iterator on, and returns the current value.
*/
void* take()
{
auto c = front.get_curr();
front.move_next();
return c;
}
};
/**
* Used to build a free list in object space.
*
* Checks signing of pointers
*/
class FreeListBuilder : FreeListIter
{
FreeObjectCursor end;
public:
/**
* Start building a new free list.
*/
void open(void* n)
{
SNMALLOC_ASSERT(empty());
FreeObject* next = FreeObject::make(n);
end.reset_cursor(next);
front.reset_cursor(next);
}
/**
* Returns current head without affecting the builder.
*/
void* peek_head()
{
return front.get_curr();
}
/**
* Checks if there are any more values to iterate.
*/
bool empty()
{
return FreeListIter::empty();
}
/**
* Adds an element to the free list
*/
void add(void* n)
{
SNMALLOC_ASSERT(!different_slab(end.get_curr(), n));
FreeObject* next = FreeObject::make(n);
end.set_next_and_move(next);
}
/**
* Adds a terminator at the end of a free list,
* but does not close the builder. Thus new elements
* can still be added. It returns a new iterator to the
* list.
*
* This is used to iterate an list that is being constructed.
* It is currently only used to check invariants in Debug builds.
*/
FreeListIter terminate()
{
if (!empty())
end.set_next(nullptr);
return *this;
}
/**
* Close a free list, and set the iterator parameter
* to iterate it.
*/
void close(FreeListIter& dst)
{
terminate();
dst = *this;
init();
}
/**
* Set the builder to a not building state.
*/
void init()
{
front.reset_cursor(nullptr);
}
};
} // namespace snmalloc

View File

@@ -3,30 +3,13 @@
#include "../ds/cdllist.h"
#include "../ds/dllist.h"
#include "../ds/helpers.h"
#include "freelist.h"
#include "sizeclass.h"
namespace snmalloc
{
class Slab;
/**
* Free objects within each slab point directly to the next (contrast
* SlabLink, which chain different Slabs of the same sizeclass together).
*/
struct SlabNext
{
struct SlabNext* next;
#ifdef CHECK_CLIENT
uintptr_t guard;
#endif
};
struct FreeListHead
{
// Use a value with bottom bit set for empty list.
SlabNext* value = nullptr;
};
using SlabList = CDLLNode<>;
using SlabLink = CDLLNode<>;
@@ -49,7 +32,7 @@ namespace snmalloc
*
* The list will be (allocated - needed) long.
*/
SlabNext* head = nullptr;
FreeListBuilder free_queue;
/**
* How many entries are not in the free list of slab, i.e.
@@ -90,57 +73,25 @@ namespace snmalloc
bool is_full()
{
auto result = get_prev() == nullptr;
SNMALLOC_ASSERT(!result || head == nullptr);
SNMALLOC_ASSERT(!result || free_queue.empty());
return result;
}
SNMALLOC_FAST_PATH void set_full()
{
SNMALLOC_ASSERT(head == nullptr);
SNMALLOC_ASSERT(free_queue.empty());
// Set needed to 1, so that "return_object" will return true after calling
// set_full
needed = 1;
null_prev();
}
/// Value used to check for corruptions in a block
static constexpr size_t POISON =
static_cast<size_t>(bits::is64() ? 0xDEADBEEFDEADBEEF : 0xDEADBEEF);
/// Store next pointer in a block. In Debug using magic value to detect some
/// simple corruptions.
static SNMALLOC_FAST_PATH void store_next(SlabNext* p, SlabNext* head)
{
p->next = head;
#if defined(CHECK_CLIENT)
if constexpr (aal_supports<IntegerPointers>)
{
p->guard = address_cast(head) ^ POISON;
}
#endif
}
/// Accessor function for the next pointer in a block.
/// In Debug checks for simple corruptions.
static SNMALLOC_FAST_PATH SlabNext* follow_next(SlabNext* node)
{
#if defined(CHECK_CLIENT)
if constexpr (aal_supports<IntegerPointers>)
{
uintptr_t next = address_cast(node->next);
if ((next ^ node->guard) != POISON)
error("Detected memory corruption. Use-after-free.");
}
#endif
return node->next;
}
bool valid_head()
{
size_t size = sizeclass_to_size(sizeclass);
size_t slab_end = (address_cast(head) | ~SLAB_MASK) + 1;
uintptr_t allocation_start =
remove_cache_friendly_offset(address_cast(head), sizeclass);
auto h = address_cast(free_queue.peek_head());
address_t slab_end = (h | ~SLAB_MASK) + 1;
address_t allocation_start = remove_cache_friendly_offset(h, sizeclass);
return (slab_end - allocation_start) % size == 0;
}
@@ -155,7 +106,7 @@ namespace snmalloc
return pointer_align_down<SUPERSLAB_SIZE>(p) == p;
}
static bool is_start_of_object(Metaslab* self, void* p)
SNMALLOC_FAST_PATH static bool is_start_of_object(Metaslab* self, void* p)
{
return is_multiple_of_sizeclass(
sizeclass_to_size(self->sizeclass),
@@ -172,26 +123,24 @@ namespace snmalloc
*/
template<ZeroMem zero_mem, SNMALLOC_CONCEPT(ConceptPAL) PAL>
static SNMALLOC_FAST_PATH void*
alloc(Metaslab* self, FreeListHead& fast_free_list, size_t rsize)
alloc(Metaslab* self, FreeListIter& fast_free_list, size_t rsize)
{
SNMALLOC_ASSERT(rsize == sizeclass_to_size(self->sizeclass));
SNMALLOC_ASSERT(!self->is_full());
auto slab = get_slab(self->head);
auto slab = get_slab(self->free_queue.peek_head());
self->debug_slab_invariant(slab);
// Use first element as the allocation
SlabNext* h = self->head;
// Put the rest in allocators small_class fast free list.
fast_free_list.value = Metaslab::follow_next(h);
self->head = nullptr;
self->free_queue.close(fast_free_list);
void* n = fast_free_list.take();
// Treat stealing the free list as allocating it all.
self->needed = self->allocated;
self->remove();
self->set_full();
void* p = remove_cache_friendly_offset(h, self->sizeclass);
void* p = remove_cache_friendly_offset(n, self->sizeclass);
SNMALLOC_ASSERT(is_start_of_object(self, p));
self->debug_slab_invariant(slab);
@@ -211,48 +160,6 @@ namespace snmalloc
return p;
}
/**
* Check bump-free-list-segment for cycles
*
* Using
* https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_Tortoise_and_Hare
* We don't expect a cycle, so worst case is only followed by a crash, so
* slow doesn't mater.
*/
size_t debug_slab_acyclic_free_list(Slab* slab)
{
#ifndef NDEBUG
size_t length = 0;
SlabNext* curr = head;
SlabNext* curr_slow = head;
bool both = false;
while (curr != nullptr)
{
if (get_slab(curr) != slab)
{
error("Free list corruption, not correct slab.");
}
curr = follow_next(curr);
if (both)
{
curr_slow = follow_next(curr_slow);
}
if (curr == curr_slow)
{
error("Free list contains a cycle, typically indicates double free.");
}
both = !both;
length++;
}
return length;
#else
UNUSED(slab);
return 0;
#endif
}
void debug_slab_invariant(Slab* slab)
{
#if !defined(NDEBUG) && !defined(SNMALLOC_CHEAP_CHECKS)
@@ -274,24 +181,18 @@ namespace snmalloc
// Block is not full
SNMALLOC_ASSERT(SLAB_SIZE > accounted_for);
// Keep variable so it appears in debugger.
size_t length = debug_slab_acyclic_free_list(slab);
UNUSED(length);
// Walk bump-free-list-segment accounting for unused space
SlabNext* curr = head;
while (curr != nullptr)
FreeListIter fl = free_queue.terminate();
while (!fl.empty())
{
// Check we are looking at a correctly aligned block
void* start = remove_cache_friendly_offset(curr, sizeclass);
void* start = remove_cache_friendly_offset(fl.take(), sizeclass);
SNMALLOC_ASSERT(((pointer_diff(slab, start) - offset) % size) == 0);
// Account for free elements in free list
accounted_for += size;
SNMALLOC_ASSERT(SLAB_SIZE >= accounted_for);
// Iterate bump/free list segment
curr = follow_next(curr);
}
auto bumpptr = (allocated * size) + offset;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "freelist.h"
#include "superslab.h"
namespace snmalloc
@@ -27,10 +28,10 @@ namespace snmalloc
* page.
*/
static SNMALLOC_FAST_PATH void
alloc_new_list(void*& bumpptr, FreeListHead& fast_free_list, size_t rsize)
alloc_new_list(void*& bumpptr, FreeListIter& fast_free_list, size_t rsize)
{
auto snbumpptr = static_cast<SlabNext*>(bumpptr);
fast_free_list.value = snbumpptr;
FreeListBuilder b;
b.open(bumpptr);
void* newbumpptr = pointer_offset(bumpptr, rsize);
void* slab_end = pointer_align_up<SLAB_SIZE>(newbumpptr);
@@ -39,17 +40,14 @@ namespace snmalloc
if (slab_end2 < slab_end)
slab_end = slab_end2;
while (newbumpptr < slab_end)
bumpptr = newbumpptr;
while (bumpptr < slab_end)
{
auto newsnbumpptr = static_cast<SlabNext*>(newbumpptr);
Metaslab::store_next(snbumpptr, newsnbumpptr);
snbumpptr = newsnbumpptr;
bumpptr = newbumpptr;
newbumpptr = pointer_offset(bumpptr, rsize);
b.add(bumpptr);
bumpptr = pointer_offset(bumpptr, rsize);
}
Metaslab::store_next(snbumpptr, nullptr);
bumpptr = newbumpptr;
b.close(fast_free_list);
}
// Returns true, if it deallocation can proceed without changing any status
@@ -71,17 +69,10 @@ namespace snmalloc
return false;
// Update the head and the next pointer in the free list.
SlabNext* head = meta.head;
meta.free_queue.add(p);
SlabNext* psn = static_cast<SlabNext*>(p);
// Set the head to the memory being deallocated.
meta.head = psn;
SNMALLOC_ASSERT(meta.valid_head());
// Set the next pointer to the previous head.
Metaslab::store_next(psn, head);
return true;
}
@@ -109,10 +100,8 @@ namespace snmalloc
return super->dealloc_slab(self);
}
SNMALLOC_ASSERT(meta.head == nullptr);
SlabNext* psn = static_cast<SlabNext*>(p);
meta.head = psn;
Metaslab::store_next(psn, nullptr);
SNMALLOC_ASSERT(meta.free_queue.empty());
meta.free_queue.open(p);
meta.needed = meta.allocated - 1;
// Push on the list of slabs for this sizeclass.
@@ -121,12 +110,24 @@ namespace snmalloc
return Superslab::NoSlabReturn;
}
// Remove from the sizeclass list and dealloc on the superslab.
#ifdef CHECK_CLIENT
size_t count = 1;
// Check free list is well-formed on platforms with
// integers as pointers.
FreeListIter fl;
meta.free_queue.close(fl);
while (!fl.empty())
{
fl.take();
count++;
}
#endif
meta.remove();
if (Metaslab::is_short(self))
return super->dealloc_short_slab();
return super->dealloc_slab(self);
}
};

View File

@@ -164,7 +164,7 @@ namespace snmalloc
if ((used & 1) == 1)
return alloc_slab(sizeclass);
meta[0].head = nullptr;
meta[0].free_queue.init();
// Set up meta data as if the entire slab has been turned into a free
// list. This means we don't have to check for special cases where we have
// returned all the elements, but this is a slab that is still being bump
@@ -188,7 +188,7 @@ namespace snmalloc
uint8_t n = meta[h].next;
meta[h].head = nullptr;
meta[h].free_queue.init();
// Set up meta data as if the entire slab has been turned into a free
// list. This means we don't have to check for special cases where we have
// returned all the elements, but this is a slab that is still being bump