Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ jobs:
test_4_24_32bit, test_4_24_32bit_debug,
test_8_24_32bit, test_8_24_32bit_debug,
test_4_24_64bit, test_4_24_64bit_debug,
test_8_24_64bit, test_8_24_64bit_debug
test_8_24_64bit, test_8_24_64bit_debug,
test_4_24_arm64_32, test_4_24_arm64_32_debug,
test_8_24_arm64_32, test_8_24_arm64_32_debug
]

steps:
Expand Down
30 changes: 28 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
CC = gcc
CFLAGS_64 = -Wall -Wextra -g -O0
CFLAGS_32 = -Wall -Wextra -g -O0 -m32
LDFLAGS =
# arm64_32 (Apple Watch) is AArch64 with 4-byte pointers. Forcing
# PLATFORM_64BIT on an ILP32 build reproduces its struct layout, including the
# pool header whose sizeof is not a multiple of 8. See issue #2.
CFLAGS_ILP32_P64 = -Wall -Wextra -g -O0 -m32 -DPLATFORM_64BIT
LDFLAGS =

# Debug flags for different test configurations
DEBUG_FLAGS = -DESTALLOC_DEBUG -DESTALLOC_PRINT_DEBUG
Expand All @@ -31,7 +35,11 @@ CONFIGS = $(OUTDIR)/test_4_16_32bit \
$(OUTDIR)/test_4_24_64bit \
$(OUTDIR)/test_4_24_64bit_debug \
$(OUTDIR)/test_8_24_64bit \
$(OUTDIR)/test_8_24_64bit_debug
$(OUTDIR)/test_8_24_64bit_debug \
$(OUTDIR)/test_4_24_arm64_32 \
$(OUTDIR)/test_4_24_arm64_32_debug \
$(OUTDIR)/test_8_24_arm64_32 \
$(OUTDIR)/test_8_24_arm64_32_debug

# Source files
SRCS = estalloc.h estalloc.c test/test.c
Expand Down Expand Up @@ -95,6 +103,24 @@ $(OUTDIR)/test_8_24_64bit_debug: $(SRCS)
@mkdir -p $(OUTDIR)
$(CC) $(CFLAGS_64) $(DEBUG_FLAGS) -DESTALLOC_ALIGNMENT=8 -DESTALLOC_ADDRESS_24BIT $^ -o $@ $(LDFLAGS)

# ILP32 with PLATFORM_64BIT forced: reproduces the arm64_32 layout of issue #2.
# ESTALLOC_ADDRESS_16BIT is rejected by estalloc.h in this combination.
$(OUTDIR)/test_4_24_arm64_32: $(SRCS)
@mkdir -p $(OUTDIR)
$(CC) $(CFLAGS_ILP32_P64) -DESTALLOC_ALIGNMENT=4 -DESTALLOC_ADDRESS_24BIT $^ -o $@ $(LDFLAGS)

$(OUTDIR)/test_8_24_arm64_32: $(SRCS)
@mkdir -p $(OUTDIR)
$(CC) $(CFLAGS_ILP32_P64) -DESTALLOC_ALIGNMENT=8 -DESTALLOC_ADDRESS_24BIT $^ -o $@ $(LDFLAGS)

$(OUTDIR)/test_4_24_arm64_32_debug: $(SRCS)
@mkdir -p $(OUTDIR)
$(CC) $(CFLAGS_ILP32_P64) $(DEBUG_FLAGS) -DESTALLOC_ALIGNMENT=4 -DESTALLOC_ADDRESS_24BIT $^ -o $@ $(LDFLAGS)

$(OUTDIR)/test_8_24_arm64_32_debug: $(SRCS)
@mkdir -p $(OUTDIR)
$(CC) $(CFLAGS_ILP32_P64) $(DEBUG_FLAGS) -DESTALLOC_ALIGNMENT=8 -DESTALLOC_ADDRESS_24BIT $^ -o $@ $(LDFLAGS)

# Run all tests
test: $(CONFIGS)
@mkdir -p $(LOGDIR)
Expand Down
56 changes: 42 additions & 14 deletions estalloc.c
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,18 @@ typedef struct MEMORY_POOL {
FREE_BLOCK *free_blocks[SIZE_FREE_BLOCKS +1]; // +1=sentinel
} MEMORY_POOL;

#define BPOOL_TOP(memory_pool) ((void *)((uint8_t *)(memory_pool) + sizeof(MEMORY_POOL)))
/*
Size of the pool header, rounded up to ESTALLOC_ALIGNMENT.

sizeof(MEMORY_POOL) is not guaranteed to be a multiple of the alignment on
every ABI, so it must never be used to locate the first memory block. Round
up here instead of hand-padding ESTALLOC for each known ABI: the few wasted
bytes buy an allocator that lays out correctly wherever it is compiled.
*/
#define POOL_HEADER_SIZE \
((sizeof(MEMORY_POOL) + ALIGNMENT_MASK) & ~(size_t)ALIGNMENT_MASK)

#define BPOOL_TOP(memory_pool) ((void *)((uint8_t *)(memory_pool) + POOL_HEADER_SIZE))
#define BPOOL_END(memory_pool) ((void *)((uint8_t *)(memory_pool) + ((MEMORY_POOL *)(memory_pool))->size))
#define BLOCK_ADRS(p) ((void *)((uint8_t *)(p) - sizeof(USED_BLOCK)))

Expand All @@ -214,6 +225,31 @@ typedef struct MEMORY_POOL {
#define NLZ_SLI(x) nlz8(x)


/***** Compile time checks **************************************************/
/*
These are checked at compile time rather than with assert(), because a
release build defines NDEBUG and would silently ship a broken layout.
*/
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
# define ESTALLOC_STATIC_ASSERT(name, cond) _Static_assert(cond, #name)
#else
# define ESTALLOC_STATIC_ASSERT(name, cond) \
typedef char estalloc_static_assert_##name[(cond) ? 1 : -1]
#endif

ESTALLOC_STATIC_ASSERT(pool_header_is_aligned,
(POOL_HEADER_SIZE & ALIGNMENT_MASK) == 0);
/*
If you get the following assertion, you can change minimum memory block size
parameter to `ESTALLOC_MIN_MEMORY_BLOCK_SIZE (1 << ESTALLOC_IGNORE_LSBS)`
and #define ESTALLOC_ADDRESS_16BIT.
*/
ESTALLOC_STATIC_ASSERT(min_block_holds_free_block,
ESTALLOC_MIN_MEMORY_BLOCK_SIZE >= sizeof(FREE_BLOCK));
ESTALLOC_STATIC_ASSERT(min_block_covers_ignored_lsbs,
ESTALLOC_MIN_MEMORY_BLOCK_SIZE >= (1 << ESTALLOC_IGNORE_LSBS));


#if defined(ESTALLOC_DEBUG)
static void take_profile(ESTALLOC *est);
# define PROFILE() do { \
Expand Down Expand Up @@ -422,15 +458,7 @@ void merge_block(FREE_BLOCK *target, FREE_BLOCK *next)
ESTALLOC *
est_init(void *ptr, unsigned int size)
{
assert(ESTALLOC_MIN_MEMORY_BLOCK_SIZE >= sizeof(FREE_BLOCK));
assert(ESTALLOC_MIN_MEMORY_BLOCK_SIZE >= (1 << ESTALLOC_IGNORE_LSBS));
/*
If you get this assertion, you can change minimum memory block size
parameter to `ESTALLOC_MIN_MEMORY_BLOCK_SIZE (1 << ESTALLOC_IGNORE_LSBS)`
and #define ESTALLOC_ADDRESS_16BIT.
*/

assert((sizeof(MEMORY_POOL) & ALIGNMENT_MASK) == 0);
/* Layout invariants are checked at compile time. See "Compile time checks". */
#if defined(UINTPTR_MAX)
assert(((uintptr_t)ptr & ALIGNMENT_MASK) == 0);
#else
Expand All @@ -450,7 +478,7 @@ est_init(void *ptr, unsigned int size)
// large free block + zero size used block (sentinel).
ESTALLOC_MEMSIZE_T sentinel_size = sizeof(USED_BLOCK);
sentinel_size += (-sentinel_size & ALIGNMENT_MASK);
ESTALLOC_MEMSIZE_T free_size = size - sizeof(MEMORY_POOL) - sentinel_size;
ESTALLOC_MEMSIZE_T free_size = size - POOL_HEADER_SIZE - sentinel_size;
FREE_BLOCK *free_block = BPOOL_TOP(memory_pool);
USED_BLOCK *used_block = (USED_BLOCK *)((uint8_t *)free_block + free_size);

Expand Down Expand Up @@ -1099,9 +1127,9 @@ est_fprint_pool_header(ESTALLOC *est, FILE *fp)

fprintf(fp, "== MEMORY POOL HEADER DUMP ==\n");
fprintf(fp, " Address:%p - %p - %p ", pool, BPOOL_TOP(pool), BPOOL_END(pool));
fprintf(fp, " Size Total:%d User:%" PRIu32 "\n", pool->size, (ESTALLOC_MEMSIZE_T)(pool->size - sizeof(MEMORY_POOL)));
fprintf(fp, " sizeof MEMORY_POOL:%" PRIu32 "(%04" PRIx32 "), USED_BLOCK:%" PRIu32 "(%02" PRIx32 "), FREE_BLOCK:%" PRIu32 "(%02" PRIx32 ")\n",
(uint32_t)sizeof(MEMORY_POOL), (uint32_t)sizeof(MEMORY_POOL),
fprintf(fp, " Size Total:%d User:%" PRIu32 "\n", pool->size, (ESTALLOC_MEMSIZE_T)(pool->size - POOL_HEADER_SIZE));
fprintf(fp, " pool header:%" PRIu32 "(%04" PRIx32 "), USED_BLOCK:%" PRIu32 "(%02" PRIx32 "), FREE_BLOCK:%" PRIu32 "(%02" PRIx32 ")\n",
(uint32_t)POOL_HEADER_SIZE, (uint32_t)POOL_HEADER_SIZE,
(uint32_t)sizeof(USED_BLOCK), (uint32_t)sizeof(USED_BLOCK),
(uint32_t)sizeof(FREE_BLOCK), (uint32_t)sizeof(FREE_BLOCK));

Expand Down
11 changes: 8 additions & 3 deletions estalloc.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@
extern "C" {
#endif

#if defined (__alpha__) || defined (__ia64__) || defined (__x86_64__) \
|| defined (_WIN64) || defined (__LP64__) || defined (__LLP64__) \
|| defined(__ppc64__) || defined(__aarch64__)
/*
PLATFORM_64BIT means "a pointer is 8 bytes wide", not "the CPU is 64-bit".
Deriving it from architecture macros breaks on ILP32 ABIs that run on 64-bit
CPUs: on arm64_32 (Apple Watch Series 4 and later) clang defines __aarch64__
while sizeof(void *) is 4, which used to give a pool header that is not a
multiple of ESTALLOC_ALIGNMENT.
*/
#if defined(UINTPTR_MAX) && UINTPTR_MAX > 0xFFFFFFFFu
# define PLATFORM_64BIT
#endif

Expand Down
114 changes: 114 additions & 0 deletions test/test.c
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,115 @@ check_memory_content(void *ptr, size_t size, unsigned char value)
return 1;
}

// ================================================================
// Layout tests
// ================================================================

/*
Regression test for issue #2 (arm64_32 crash).

A pool header whose size is not a multiple of ESTALLOC_ALIGNMENT shifts every
block off its boundary and makes the chain miss the end-of-pool sentinel. The
symptom on the device was a wild pointer inside remove_free_block(), so check
the alignment directly and walk the pool until it is exhausted.
*/
#define LAYOUT_ALLOCS 16

/*
A user pointer sits sizeof(USED_BLOCK) bytes into an ESTALLOC_ALIGNMENT
aligned block. That header is 8 bytes in 24-bit address mode but only 4 bytes
in 16-bit address mode, so ESTALLOC_ALIGNMENT=8 combined with
ESTALLOC_ADDRESS_16BIT hands out 4-byte aligned pointers by construction.
*/
#if defined(ESTALLOC_ADDRESS_16BIT)
# define EXPECTED_USER_ALIGNMENT 4
#else
# define EXPECTED_USER_ALIGNMENT ESTALLOC_ALIGNMENT
#endif
#define USER_ALIGNMENT_MASK (EXPECTED_USER_ALIGNMENT - 1)

static int
test_pool_layout(void)
{
printf("--- test_pool_layout ---\n");
const unsigned int size = 4096;
void *mem = malloc(size);
if (mem == NULL) {
printf("FAILED: could not allocate the pool\n");
return 1;
}
if (((uintptr_t)mem & ALIGNMENT_MASK) != 0) {
printf("SKIPPED: malloc() returned %p, not %d-byte aligned\n", mem, ESTALLOC_ALIGNMENT);
free(mem);
return 0;
}

int failures = 0;
ESTALLOC *est = est_init(mem, size);
void *ptrs[LAYOUT_ALLOCS] = {0};

for (int i = 0; i < LAYOUT_ALLOCS; i++) {
size_t alloc_size = (size_t)(i * 7 + 1);
ptrs[i] = est_malloc(est, (unsigned int)alloc_size);
if (ptrs[i] == NULL) {
printf("FAILED: est_malloc(%zu) returned NULL\n", alloc_size);
failures++;
break;
}
if (((uintptr_t)ptrs[i] & USER_ALIGNMENT_MASK) != 0) {
printf("FAILED: est_malloc(%zu) returned %p, not %d-byte aligned\n",
alloc_size, ptrs[i], EXPECTED_USER_ALIGNMENT);
failures++;
}
fill_memory(ptrs[i], alloc_size, (unsigned char)i);
}

// Exhaust the pool. A misaligned header makes this walk off the sentinel.
while (est_malloc(est, 64) != NULL) { /* no-op */ }

for (int i = 0; i < LAYOUT_ALLOCS; i++) {
if (ptrs[i] == NULL) continue;
size_t alloc_size = (size_t)(i * 7 + 1);
if (!check_memory_content(ptrs[i], alloc_size, (unsigned char)i)) {
printf("FAILED: block %d was corrupted by a later allocation\n", i);
failures++;
}
}

#if defined(ESTALLOC_DEBUG)
int sanity = est_sanity_check(est);
if (sanity != 0) {
printf("FAILED: est_sanity_check() returned 0x%x\n", sanity);
failures++;
}
#endif

for (int i = 0; i < LAYOUT_ALLOCS; i++) {
if (ptrs[i] != NULL) est_free(est, ptrs[i]);
}

if (failures == 0) printf("PASSED\n");
free(mem);
return failures;
}

static int
run_layout_tests(void)
{
printf("\n=== Layout Tests ===\n");
fprintf(stderr, "sizeof(void *): %zu, ESTALLOC_ALIGNMENT: %d\n",
sizeof(void *), ESTALLOC_ALIGNMENT);
#if defined(PLATFORM_64BIT)
fprintf(stderr, "PLATFORM_64BIT: defined\n");
#else
fprintf(stderr, "PLATFORM_64BIT: not defined\n");
#endif
int failures = test_pool_layout();
printf("=== Layout Tests: %s (%d failure(s)) ===\n\n",
failures == 0 ? "PASSED" : "FAILED", failures);
return failures;
}

// ================================================================
// Critical section test infrastructure
// ================================================================
Expand Down Expand Up @@ -437,6 +546,11 @@ log_operation(enum operation_type op, void *ptr, size_t size, int result)
int
main()
{
if (run_layout_tests() != 0) {
fprintf(stderr, "Layout tests failed\n");
return 1;
}

if (run_critical_section_tests() != 0) {
fprintf(stderr, "Critical section tests failed\n");
return 1;
Expand Down
Loading