Skip to content

Commit 0c6633e

Browse files
committed
test: add stack allocation tests for normal and garbage scenarios
1 parent d53190d commit 0c6633e

2 files changed

Lines changed: 382 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ tests/validation_test_*
5858
tests/bump_alloc_test_*
5959
tests/em_nested_test_*
6060
tests/slab_alloc_test_*
61+
tests/stack_alloc_test_*
6162

6263
build_matrix
6364

tests/stack_alloc_test.c

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
#define EASY_MEMORY_IMPLEMENTATION
2+
#define EM_NO_ATTRIBUTES
3+
#include "easy_memory.h"
4+
#include "test_utils.h"
5+
#include <limits.h>
6+
#include <stdint.h>
7+
8+
static void test_stack_lifecycle_normal(void) {
9+
TEST_PHASE("Stack Lifecycle - Normal Path");
10+
11+
EM *em = em_create(2048);
12+
size_t initial_free = free_size_in_tail(em);
13+
14+
TEST_CASE("Standard Stack initialization");
15+
size_t stack_size = 128;
16+
Stack *stack = em_stack_create(em, stack_size);
17+
18+
ASSERT(stack != NULL, "Stack pointer should not be NULL");
19+
ASSERT(stack_get_capacity(stack) >= stack_size, "Capacity should meet requested size");
20+
ASSERT(stack_get_em(stack) == em, "Parent EM should be correctly stored");
21+
ASSERT(stack_get_meta_index(stack) == 0, "Initial metadata index should be 0");
22+
23+
ASSERT(stack_get_meta_type(stack) == 0, "Metadata type should be 0 (uint8_t) for small capacity");
24+
25+
em_stack_destroy(stack);
26+
ASSERT(free_size_in_tail(em) == initial_free, "Parent EM should reclaim memory after Stack destruction");
27+
28+
TEST_CASE("Stack Metadata Type Scaling");
29+
Stack *medium_stack = em_stack_create(em, 512);
30+
ASSERT(medium_stack != NULL, "Medium stack pointer should not be NULL");
31+
ASSERT(stack_get_meta_type(medium_stack) == 1, "Metadata type should be 1 (uint16_t) for medium capacity");
32+
33+
em_stack_destroy(medium_stack);
34+
ASSERT(free_size_in_tail(em) == initial_free, "Parent EM should reclaim memory after medium Stack destruction");
35+
36+
TEST_CASE("Scratch Stack initialization");
37+
Stack *scratch_stack = em_stack_create_scratch(em, stack_size);
38+
ASSERT(scratch_stack != NULL, "Scratch Stack should not be NULL");
39+
ASSERT(stack_get_em(scratch_stack) == em, "Parent EM of scratch stack should be correctly stored");
40+
ASSERT(em_get_has_scratch(em) == true, "Parent EM scratch flag should be active");
41+
42+
em_stack_destroy(scratch_stack);
43+
ASSERT(em_get_has_scratch(em) == false, "Parent EM scratch flag should be inactive");
44+
ASSERT(free_size_in_tail(em) == initial_free, "Scratch tail should be restored");
45+
46+
em_destroy(em);
47+
}
48+
49+
static void test_stack_lifecycle_garbage(void) {
50+
TEST_PHASE("Stack Lifecycle - Sad Path & Garbage");
51+
52+
EM *em = em_create(1024);
53+
54+
#if EM_SAFETY_POLICY == EM_POLICY_DEFENSIVE
55+
TEST_CASE("Creation with NULL parent EM");
56+
ASSERT(em_stack_create(NULL, 128) == NULL, "Should fail on NULL parent");
57+
ASSERT(em_stack_create_scratch(NULL, 128) == NULL, "Should fail on NULL parent for scratch creation");
58+
59+
TEST_CASE("Creation with zero size");
60+
ASSERT(em_stack_create(em, 0) == NULL, "Should fail on zero stack size");
61+
62+
TEST_CASE("Creation with size too small");
63+
ASSERT(em_stack_create(em, 2) == NULL, "Should fail if requested size is below EM_MIN_BUFFER_SIZE");
64+
65+
TEST_CASE("Creation with extreme OOM size");
66+
ASSERT(em_stack_create(em, 4096) == NULL, "Should return NULL if parent EM is exhausted");
67+
ASSERT(em_stack_create(em, SIZE_MAX) == NULL, "Should return NULL on size integer overflow");
68+
69+
TEST_CASE("Scratch Stack conflict");
70+
Stack *scratch1 = em_stack_create_scratch(em, 128);
71+
ASSERT(scratch1 != NULL, "First scratch allocation should succeed");
72+
73+
Stack *scratch2 = em_stack_create_scratch(em, 128);
74+
ASSERT(scratch2 == NULL, "Second scratch allocation must fail while another scratch is active");
75+
76+
em_stack_destroy(scratch1);
77+
78+
TEST_CASE("Destroy NULL Stack");
79+
em_stack_destroy(NULL);
80+
ASSERT(true, "Destroying NULL Stack should not crash");
81+
#endif
82+
83+
em_destroy(em);
84+
}
85+
86+
static void test_stack_operations_normal(void) {
87+
TEST_PHASE("Stack Operations - Normal Path");
88+
89+
EM *em = em_create(2048);
90+
size_t stack_size = 512;
91+
Stack *stack = em_stack_create(em, stack_size);
92+
93+
void *ptrs[8];
94+
size_t count = 0;
95+
96+
TEST_CASE("Sequential allocations with LIFO ordering");
97+
while (count < 4) {
98+
void *p = em_stack_alloc(stack, 64);
99+
ASSERT(p != NULL, "Allocation should succeed");
100+
ptrs[count++] = p;
101+
102+
ASSERT_QUIET(((uintptr_t)p % EMMIN_ALIGNMENT) == 0, "Payload must be word-aligned");
103+
fill_memory_pattern(p, 64, (int)count);
104+
}
105+
106+
// Since metadata grows from start and payloads grow backward from the end,
107+
// subsequent allocations should return strictly decreasing memory addresses.
108+
for (size_t i = 1; i < count; i++) {
109+
ASSERT_QUIET((uintptr_t)ptrs[i] < (uintptr_t)ptrs[i - 1], "Addresses must decrease sequentially");
110+
}
111+
112+
// Verify written data remains valid and untouched
113+
for (size_t i = 0; i < count; i++) {
114+
ASSERT_QUIET(verify_memory_pattern(ptrs[i], 64, (int)(i + 1)), "Data integrity check failed");
115+
}
116+
117+
TEST_CASE("Strict LIFO deallocation (popping)");
118+
// Pop elements in exact reverse order of allocation
119+
for (size_t i = count; i-- > 0;) {
120+
size_t prev_index = stack_get_meta_index(stack);
121+
em_stack_free(stack, ptrs[i]);
122+
ASSERT_QUIET(stack_get_meta_index(stack) == prev_index - 1, "Meta index must decrement after free");
123+
124+
#ifdef EM_POISONING
125+
// Check if the memory was poisoned upon deallocation
126+
ASSERT_QUIET(verify_memory_pattern(ptrs[i], 64, EM_POISON_BYTE), "Freed memory must be poisoned");
127+
#endif
128+
}
129+
ASSERT(stack_get_meta_index(stack) == 0, "Stack must be empty after popping all elements");
130+
131+
// Reset the stack to start aligned allocation tests
132+
em_stack_reset(stack);
133+
134+
TEST_CASE("Custom alignment allocations");
135+
size_t alignments[] = {16, 32, 64, 128};
136+
for (size_t i = 0; i < 4; i++) {
137+
size_t align = alignments[i];
138+
void *p = em_stack_alloc_aligned(stack, 32, align);
139+
ASSERT(p != NULL, "Aligned allocation should succeed");
140+
ASSERT_QUIET(((uintptr_t)p % align) == 0, "Payload must satisfy requested custom alignment");
141+
}
142+
143+
// Reset the stack to perform exhaustion test
144+
em_stack_reset(stack);
145+
146+
TEST_CASE("Stack exhaustion");
147+
size_t capacity = stack_get_capacity(stack);
148+
size_t allocated_total = 0;
149+
150+
while (true) {
151+
void *p = em_stack_alloc(stack, 32);
152+
if (!p) {
153+
break;
154+
}
155+
allocated_total += 32;
156+
ASSERT_QUIET(allocated_total <= capacity, "Allocated size cannot exceed capacity");
157+
}
158+
159+
// Once exhausted, any further allocations must return NULL
160+
ASSERT(em_stack_alloc(stack, 1) == NULL, "Stack allocation should return NULL when exhausted");
161+
162+
em_stack_destroy(stack);
163+
em_destroy(em);
164+
}
165+
166+
static void test_stack_operations_garbage(void) {
167+
#if EM_SAFETY_POLICY == EM_POLICY_DEFENSIVE
168+
TEST_PHASE("Stack Operations - Sad Path & Garbage");
169+
170+
EM *em = em_create(1024);
171+
Stack *stack = em_stack_create(em, 512);
172+
173+
void *valid_ptr = em_stack_alloc(stack, 32);
174+
175+
TEST_CASE("Allocation on NULL stack");
176+
ASSERT(em_stack_alloc(NULL, 16) == NULL, "Should return NULL on NULL stack");
177+
ASSERT(em_stack_alloc_aligned(NULL, 16, 16) == NULL, "Should return NULL on NULL stack with custom alignment");
178+
179+
TEST_CASE("Allocation of zero size");
180+
ASSERT(em_stack_alloc(stack, 0) == NULL, "Should return NULL on zero size");
181+
ASSERT(em_stack_alloc_aligned(stack, 0, 16) == NULL, "Should return NULL on zero size with custom alignment");
182+
183+
TEST_CASE("Allocation with invalid custom alignments");
184+
// Alignments must be powers of two
185+
ASSERT(em_stack_alloc_aligned(stack, 16, 3) == NULL, "Should fail on non-power-of-two alignment");
186+
ASSERT(em_stack_alloc_aligned(stack, 16, 15) == NULL, "Should fail on non-power-of-two alignment");
187+
188+
// Check below minimum limit
189+
if (EMMIN_ALIGNMENT > 1) {
190+
ASSERT(em_stack_alloc_aligned(stack, 16, EMMIN_ALIGNMENT / 2) == NULL, "Should fail if alignment is too small");
191+
}
192+
193+
// Check above maximum limit
194+
ASSERT(em_stack_alloc_aligned(stack, 16, EMMAX_ALIGNMENT * 2) == NULL, "Should fail if alignment is too large");
195+
196+
TEST_CASE("Allocation with size larger than capacity");
197+
size_t capacity = stack_get_capacity(stack);
198+
ASSERT(em_stack_alloc(stack, capacity + 1) == NULL, "Should return NULL on allocation larger than capacity");
199+
ASSERT(em_stack_alloc(stack, SIZE_MAX) == NULL, "Should return NULL on overflow size");
200+
201+
TEST_CASE("Freeing on NULL inputs");
202+
em_stack_free(NULL, valid_ptr);
203+
em_stack_free(stack, NULL);
204+
ASSERT(true, "Deallocating on NULL inputs should not crash");
205+
206+
TEST_CASE("Freeing on empty stack");
207+
em_stack_reset(stack);
208+
em_stack_free(stack, valid_ptr);
209+
ASSERT(stack_get_meta_index(stack) == 0, "Meta index must remain 0 after illegal pop on empty stack");
210+
211+
TEST_CASE("LIFO violation detection");
212+
void *p1 = em_stack_alloc(stack, 32);
213+
void *p2 = em_stack_alloc(stack, 32);
214+
215+
// Attempting to free p1 first (which violates LIFO as p2 is the current head)
216+
size_t prev_index = stack_get_meta_index(stack);
217+
em_stack_free(stack, p1);
218+
ASSERT(stack_get_meta_index(stack) == prev_index, "Deallocating non-head pointer must be ignored");
219+
220+
// Clean up correctly
221+
em_stack_free(stack, p2);
222+
em_stack_free(stack, p1);
223+
ASSERT(stack_get_meta_index(stack) == 0, "Stack must be successfully emptied using correct LIFO order");
224+
225+
em_stack_destroy(stack);
226+
em_destroy(em);
227+
#endif
228+
}
229+
230+
static void test_stack_markers_normal(void) {
231+
TEST_PHASE("Stack Markers - Normal Path");
232+
233+
EM *em = em_create(2048);
234+
Stack *stack = em_stack_create(em, 512);
235+
236+
void *p1 = em_stack_alloc(stack, 32);
237+
fill_memory_pattern(p1, 32, 0x11);
238+
239+
TEST_CASE("Get and rollback to stack markers");
240+
// Snapshot state after first allocation
241+
StackMarker marker1 = em_stack_get_marker(stack);
242+
243+
void *p2 = em_stack_alloc(stack, 32);
244+
fill_memory_pattern(p2, 32, 0x22);
245+
void *p3 = em_stack_alloc(stack, 32);
246+
fill_memory_pattern(p3, 32, 0x33);
247+
248+
// Snapshot state after three allocations
249+
StackMarker marker2 = em_stack_get_marker(stack);
250+
251+
void *p4 = em_stack_alloc(stack, 32);
252+
fill_memory_pattern(p4, 32, 0x44);
253+
254+
// Roll back to marker2 (this should release p4)
255+
em_stack_free_to_marker(stack, marker2);
256+
ASSERT(stack_get_meta_index(stack) == 3, "Stack index should revert to 3");
257+
258+
#ifdef EM_POISONING
259+
// Ensure the rolled-back region is poisoned
260+
ASSERT_QUIET(verify_memory_pattern(p4, 32, EM_POISON_BYTE), "Rolled back block must be poisoned");
261+
#endif
262+
263+
// Verify we can re-allocate on the freed space
264+
void *p4_retry = em_stack_alloc(stack, 32);
265+
ASSERT(p4_retry == p4, "Re-allocation must reclaim the freed space");
266+
267+
// Roll back to marker1 (releasing p2, p3, p4_retry)
268+
em_stack_free_to_marker(stack, marker1);
269+
ASSERT(stack_get_meta_index(stack) == 1, "Stack index should revert to 1");
270+
271+
#ifdef EM_POISONING
272+
// Verify both released blocks are poisoned
273+
ASSERT_QUIET(verify_memory_pattern(p2, 32, EM_POISON_BYTE), "Rolled back blocks must be poisoned");
274+
ASSERT_QUIET(verify_memory_pattern(p3, 32, EM_POISON_BYTE), "Rolled back blocks must be poisoned");
275+
#endif
276+
277+
em_stack_destroy(stack);
278+
em_destroy(em);
279+
}
280+
281+
static void test_stack_markers_garbage(void) {
282+
#if EM_SAFETY_POLICY == EM_POLICY_DEFENSIVE
283+
TEST_PHASE("Stack Markers - Sad Path & Garbage");
284+
285+
EM *em = em_create(1024);
286+
Stack *stackA = em_stack_create(em, 256);
287+
Stack *stackB = em_stack_create(em, 256);
288+
289+
TEST_CASE("NULL stack or NULL marker operations");
290+
// Requesting marker on NULL stack should return a zeroed marker structure
291+
StackMarker null_marker = em_stack_get_marker(NULL);
292+
ASSERT(null_marker.index == 0 && null_marker.magic == 0, "NULL stack marker must be zeroed");
293+
294+
// Reverting with NULL should safely do nothing
295+
em_stack_free_to_marker(NULL, null_marker);
296+
ASSERT(true, "Rollback on NULL stack should safely return");
297+
298+
TEST_CASE("Alien marker protection");
299+
em_stack_alloc(stackA, 32);
300+
StackMarker markerA = em_stack_get_marker(stackA);
301+
302+
em_stack_alloc(stackB, 32);
303+
em_stack_alloc(stackB, 32);
304+
size_t index_before = stack_get_meta_index(stackB);
305+
306+
// Attempting to apply Stack A's marker to Stack B (cross-contamination check)
307+
em_stack_free_to_marker(stackB, markerA);
308+
ASSERT(stack_get_meta_index(stackB) == index_before, "Alien marker must be detected and ignored");
309+
310+
TEST_CASE("Corrupted marker magic protection");
311+
StackMarker corrupt_marker = em_stack_get_marker(stackB);
312+
corrupt_marker.magic ^= 0xDEAD; // corrupt the cryptographic validation signature
313+
314+
em_stack_free_to_marker(stackB, corrupt_marker);
315+
ASSERT(stack_get_meta_index(stackB) == index_before, "Corrupted marker signature must be ignored");
316+
317+
TEST_CASE("Forward marker index protection");
318+
StackMarker future_marker = em_stack_get_marker(stackB);
319+
// Artificially modify index to point to a future state (index > current_index)
320+
future_marker.index ^= (size_t)10;
321+
322+
em_stack_free_to_marker(stackB, future_marker);
323+
ASSERT(stack_get_meta_index(stackB) == index_before, "Rollback to future index must be ignored");
324+
325+
em_stack_destroy(stackB);
326+
em_stack_destroy(stackA);
327+
em_destroy(em);
328+
#endif
329+
}
330+
331+
static void test_stack_resets(void) {
332+
TEST_PHASE("Stack Resets - Standard & Zero");
333+
334+
EM *em = em_create(1024);
335+
size_t capacity = 256;
336+
Stack *stack = em_stack_create(em, capacity);
337+
338+
TEST_CASE("Standard stack reset");
339+
void *p1 = em_stack_alloc(stack, 32);
340+
void *p2 = em_stack_alloc(stack, 32);
341+
fill_memory_pattern(p1, 32, 0xAA);
342+
fill_memory_pattern(p2, 32, 0xBB);
343+
344+
em_stack_reset(stack);
345+
ASSERT(stack_get_meta_index(stack) == 0, "Reset must set metadata index to 0");
346+
347+
// Standard reset only resets metadata index, letting us overwrite dirty memory
348+
void *p1_new = em_stack_alloc(stack, 32);
349+
ASSERT(p1_new == p1, "Standard reset must allow re-allocation from the start");
350+
351+
TEST_CASE("Stack reset with zero-initialization");
352+
void *p2_new = em_stack_alloc(stack, 32);
353+
fill_memory_pattern(p1_new, 32, 0xCC);
354+
fill_memory_pattern(p2_new, 32, 0xDD);
355+
356+
em_stack_reset_zero(stack);
357+
ASSERT(stack_get_meta_index(stack) == 0, "Reset-zero must set metadata index to 0");
358+
359+
// Verify the entire physical payload area has been strictly cleared
360+
void *payload_start = (void *)((char *)stack + sizeof(Stack));
361+
size_t payload_capacity = stack_get_capacity(stack);
362+
ASSERT(verify_memory_pattern(payload_start, payload_capacity, 0x00), "Entire payload area must be zeroed");
363+
364+
em_stack_destroy(stack);
365+
em_destroy(em);
366+
}
367+
368+
int main(void) {
369+
setvbuf(stdout, NULL, _IONBF, 0);
370+
371+
test_stack_lifecycle_normal();
372+
test_stack_lifecycle_garbage();
373+
test_stack_operations_normal();
374+
test_stack_operations_garbage();
375+
test_stack_markers_normal();
376+
test_stack_markers_garbage();
377+
test_stack_resets();
378+
379+
print_test_summary();
380+
return tests_failed > 0 ? 1 : 0;
381+
}

0 commit comments

Comments
 (0)