Skip to content

Commit 1297982

Browse files
committed
Add a replay allocator for ABA
Asan delays reallocation to increase the likelyhood of catching a UAF. This adds a simple free list allocator that tries to reuse allocations quickly to increase the chance of an ABA problem being detected.
1 parent db95caf commit 1297982

File tree

5 files changed

+237
-2
lines changed

5 files changed

+237
-2
lines changed

.github/workflows/cmake.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ jobs:
3434
cmake-flags: "-G Ninja -DSANITIZER=address,undefined -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang"
3535
dependencies: "sudo apt install ninja-build"
3636

37+
- variant: "Sanitizer with replay"
38+
platform: "ubuntu-latest"
39+
build-type: "Release"
40+
cmake-flags: "-G Ninja -DUSE_REPLAY_ALLOCATOR -DSANITIZER=address,undefined -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang"
41+
dependencies: "sudo apt install ninja-build"
42+
43+
3744
- platform: "windows-2019"
3845
build-type: "Debug"
3946
# Exclude perf tests in Debug on Windows as they take too long.

src/rt/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ if(USE_SYSTEMATIC_TESTING)
3737
target_compile_definitions(verona_rt INTERFACE USE_SYSTEMATIC_TESTING)
3838
endif()
3939

40+
if(USE_REPLAY_ALLOCATOR)
41+
target_compile_definitions(verona_rt INTERFACE USE_REPLAY_ALLOCATOR)
42+
endif()
43+
4044
if(USE_CRASH_LOGGING)
4145
target_compile_definitions(verona_rt INTERFACE USE_FLIGHT_RECORDER)
4246
endif()

src/rt/debug/harness.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ class SystematicTestHarness
146146
Scheduler& sched = Scheduler::get();
147147
#ifdef USE_SYSTEMATIC_TESTING
148148
Systematic::set_seed(seed);
149+
heap::set_seed(seed);
149150
if (seed % 2 == 1)
150151
{
151152
sched.set_fair(true);

src/rt/ds/heap.h

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,166 @@
11
#pragma once
22

3+
#include <array>
34
#include <snmalloc/snmalloc.h>
5+
#include <test/xoroshiro.h>
6+
7+
#if __has_feature(address_sanitizer)
8+
# include <sanitizer/asan_interface.h>
9+
// Asan does not appear to support a __asan_update_deallocation_context
10+
# define VERONA_TRACK_FREE(ptr, size) __asan_poison_memory_region(ptr, size);
11+
# define VERONA_TRACK_ALLOC(ptr, size) __asan_unpoison_memory_region(ptr, size); __asan_update_allocation_context(ptr);
12+
# define VERONA_NO_SANITIZE_ADDRESS __attribute__((no_sanitize_address))
13+
#else
14+
# define VERONA_POISON_MEMORY(ptr, size)
15+
# define VERONA_UNPOISON_MEMORY(ptr, size)
16+
# define VERONA_NO_SANITIZE_ADDRESS
17+
#endif
418

519
namespace verona::rt::heap
620
{
21+
#ifdef USE_REPLAY_ALLOCATOR
22+
class ReplayAllocator
23+
{
24+
struct Node
25+
{
26+
Node* next;
27+
};
28+
29+
static inline std::array<Node*, 256> allocs;
30+
static inline std::array<size_t, 256> lengths;
31+
static inline snmalloc::FlagWord lock;
32+
static inline xoroshiro::p128r32 rng;
33+
34+
public:
35+
static void set_seed(uint64_t seed)
36+
{
37+
rng.set_state(seed);
38+
}
39+
40+
VERONA_NO_SANITIZE_ADDRESS
41+
static void* alloc(size_t size)
42+
{
43+
auto sc = snmalloc::size_to_sizeclass_full(size);
44+
auto idx = sc.index();
45+
{
46+
snmalloc::FlagLock l(lock);
47+
if (lengths[idx] > 0)
48+
{
49+
// Reuse an element if there are at least 16, or
50+
// the randomisation says to.
51+
auto r = rng.next() & 0xf;
52+
auto reuse =
53+
lengths[idx] > 16 || (r == 0);
54+
if (reuse)
55+
{
56+
auto r = rng.next();
57+
Node** prev = &allocs[idx];
58+
for (size_t i = 0; i < r % lengths[idx]; i++)
59+
{
60+
prev = &(*prev)->next;
61+
}
62+
63+
auto curr = *prev;
64+
auto next = curr->next;
65+
*prev = next;
66+
lengths[idx]--;
67+
VERONA_TRACK_ALLOC(curr, size);
68+
return curr;
69+
}
70+
}
71+
}
72+
73+
return snmalloc::ThreadAlloc::get().alloc(size);
74+
}
75+
76+
static void dealloc(void* ptr, size_t size)
77+
{
78+
auto sc = snmalloc::size_to_sizeclass_full(size);
79+
auto idx = sc.index();
80+
snmalloc::FlagLock l(lock);
81+
auto hd = reinterpret_cast<Node*>(ptr);
82+
hd->next = allocs[idx];
83+
allocs[idx] = hd;
84+
lengths[idx]++;
85+
VERONA_TRACK_FREE(ptr, size);
86+
}
87+
88+
VERONA_NO_SANITIZE_ADDRESS
89+
static void flush()
90+
{
91+
for (size_t i = 0; i < allocs.size(); i++)
92+
{
93+
auto hd = allocs[i];
94+
size_t count = 0;
95+
while (hd != nullptr)
96+
{
97+
auto next = hd->next;
98+
snmalloc::ThreadAlloc::get().dealloc(
99+
hd, snmalloc::sizeclass_to_size(i));
100+
hd = next;
101+
count++;
102+
}
103+
assert(count == lengths[i]);
104+
lengths[i] = 0;
105+
allocs[i] = nullptr;
106+
}
107+
}
108+
};
109+
110+
inline void* alloc(size_t size)
111+
{
112+
return ReplayAllocator::alloc(size);
113+
}
114+
115+
template<size_t size>
116+
inline void* alloc()
117+
{
118+
return ReplayAllocator::alloc(size);
119+
}
120+
121+
inline void* calloc(size_t size)
122+
{
123+
auto obj = ReplayAllocator::alloc(size);
124+
memset(obj, 0, size);
125+
return obj;
126+
}
127+
128+
template<size_t size>
129+
inline void* calloc()
130+
{
131+
auto obj = ReplayAllocator::alloc(size);
132+
memset(obj, 0, size);
133+
return obj;
134+
}
135+
136+
inline void dealloc(void* ptr, size_t size)
137+
{
138+
ReplayAllocator::dealloc(ptr, size);
139+
}
140+
141+
inline void dealloc(void* ptr)
142+
{
143+
auto size = snmalloc::ThreadAlloc::get().alloc_size(ptr);
144+
dealloc(ptr, size);
145+
}
146+
147+
template<size_t size>
148+
inline void dealloc(void* ptr)
149+
{
150+
ReplayAllocator::dealloc(ptr, size);
151+
}
152+
153+
inline void debug_check_empty()
154+
{
155+
ReplayAllocator::flush();
156+
snmalloc::debug_check_empty<snmalloc::Alloc::Config>();
157+
}
158+
159+
inline void set_seed(uint64_t seed)
160+
{
161+
ReplayAllocator::set_seed(seed);
162+
}
163+
#else
7164
inline void* alloc(size_t size)
8165
{
9166
return snmalloc::ThreadAlloc::get().alloc(size);
@@ -26,7 +183,6 @@ namespace verona::rt::heap
26183
return snmalloc::ThreadAlloc::get().alloc<size, snmalloc::YesZero>();
27184
}
28185

29-
30186
inline void dealloc(void* ptr)
31187
{
32188
return snmalloc::ThreadAlloc::get().dealloc(ptr);
@@ -37,7 +193,8 @@ namespace verona::rt::heap
37193
return snmalloc::ThreadAlloc::get().dealloc(ptr, size);
38194
}
39195

40-
template <size_t size> inline void dealloc(void* ptr)
196+
template<size_t size>
197+
inline void dealloc(void* ptr)
41198
{
42199
return snmalloc::ThreadAlloc::get().dealloc<size>(ptr);
43200
}
@@ -46,4 +203,10 @@ namespace verona::rt::heap
46203
{
47204
snmalloc::debug_check_empty<snmalloc::Alloc::Config>();
48205
}
206+
207+
inline void set_seed(uint64_t seed)
208+
{
209+
// Do nothing
210+
}
211+
#endif
49212
} // namespace verona::rt::heap
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright Microsoft and Project Verona Contributors.
2+
// SPDX-License-Identifier: MIT
3+
4+
#define USE_REPLAY_ALLOCATOR
5+
6+
#include <debug/harness.h>
7+
#include <test/opt.h>
8+
9+
/**
10+
* Tests heap replay functionality with systematic testing.
11+
*
12+
* We count each time alloc returns the value just freed, this should
13+
* be possible, but not guaranteed.
14+
**/
15+
16+
size_t count = 0;
17+
18+
void test_replay()
19+
{
20+
size_t size = 16;
21+
void* p = heap::alloc(size);
22+
uintptr_t p_addr = (uintptr_t)p;
23+
heap::dealloc(p, size);
24+
25+
void* p2 = heap::alloc(size);
26+
uintptr_t p2_addr = (uintptr_t)p2;
27+
heap::dealloc(p2, size);
28+
29+
if (p_addr == p2_addr)
30+
{
31+
std::cout << "*" << std::flush;
32+
count ++;
33+
}
34+
else
35+
{
36+
std::cout << "." << std::flush;
37+
}
38+
}
39+
40+
41+
int main(int argc, char** argv)
42+
{
43+
auto t = Aal::tick();
44+
size_t repeats = 1000;
45+
for (size_t i = 0; i < repeats; i++)
46+
{
47+
heap::set_seed(i + t);
48+
test_replay();
49+
heap::debug_check_empty();
50+
}
51+
52+
std::cout << std::endl << "count: " << count << std::endl;
53+
54+
// We should have at least one replay
55+
check(count != 0);
56+
// We should not have all replays
57+
check(count != repeats);
58+
59+
return 0;
60+
}

0 commit comments

Comments
 (0)