Skip to content

Flexible alignment - #50

Open
ilyachastikov wants to merge 36 commits into
sysprog21:mainfrom
ilyachastikov:Flexible_alignment
Open

ilyachastikov wants to merge 36 commits into
sysprog21:mainfrom
ilyachastikov:Flexible_alignment

Conversation

@ilyachastikov

@ilyachastikov ilyachastikov commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

All mallocs for 64 bit targets in modern compilers have 16 byte alignment as default. It is done this way because all 64-bit processors have SIMD extensions and compiles use them. In other words, using 8-byte alignment would reduce performance.
But TLSF has 8-byte default alignment in 64-bit mode. There is no possibility to control default alignment. But aalloc has no partner functions for calloc and realloc.
I.e. I think is too late to change behavior of tlsf_malloc.
This PR adds such functions.
These functions are:

  • For tlsf:
  • tlsf_acalloc(t, align, nmemb, size) - analogue of tlsf_calloc with alignment
  • tlsf_arealloc(t, mem, align, size) - analogue of tlsf_realloc with ability to preserve alignment
  • For tlsf_thread:
  • tlsf_thread_arealloc(ts, ptr, align, size) - analogue of tlsf_thread_realloc with ability to preserve alignment

Also this PR adds auxiliary macros for native architecture alignment alloctions:

  • For tlsf:
  • TLSF_NATIVE_AALLOC(t, size)
  • TLSF_NATIVE_ACALLOC(t, align, nmemb, size)
  • TLSF_NATIVE_AREALLOC(t, mem, size)
  • For tlsf_thread:
  • TLSF_NATIVE_THREAD_AALLOC(t, size)
  • TLSF_NATIVE_THREAD_AREALLOC(t, mem, size)
    I think that way with using of macros is more flexible cause different modules can be compiled for different architecture targets.
    I.e. now user can get correct alignment with using of these macros. And there is no need to change behavior of tlsf_malloc.

Summary by cubic

Adds aligned calloc and realloc APIs to TLSF, plus aligned realloc for the thread-safe wrapper. Previously, aligned allocation had no calloc or realloc partners; callers can now allocate zeroed aligned arrays and resize allocations without changing the allocator’s default alignment.

Behavior notes

  • Centralizes TLSF_ARCH_ALIGNMENT in tlsf.h and adds native-alignment convenience macros.
  • tlsf_arealloc treats NULL as allocation and zero size as free; invalid alignment returns NULL without modifying the allocation.
  • tlsf_thread_arealloc validates alignment, tries the owning arena first, then falls back to another arena.
  • Failed resizing leaves the original block intact, and relocation copies only the smaller old or new size.
  • Misalignment forces relocation, including when shrinking; alignments at or below the standard alignment use plain realloc, while in-place growth merges only forward.
  • tlsf_acalloc rejects multiplication overflow and returns a minimum-sized allocation for a zero total size.
  • Tests cover in-place growth, relocation, alignment changes, overflow, failure paths, thread-safe resizing, and 32-bit alignment cases.

Written for commit c7ec9fe. Summary will update on new commits.

Review in cubic

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

@ilyachastikov
ilyachastikov requested a review from jserv September 18, 2026 00:59
@sysprog21 sysprog21 deleted a comment from ilyachastikov Sep 18, 2026
Comment thread src/tlsf.c Outdated
Comment thread src/tlsf.c Outdated
Comment thread include/tlsf.h Outdated
Comment thread README.md
tlsf_free(&t, p);
tlsf_free(&t, z);
tlsf_free(&t, q);
tlsf_free(&t, h);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

h is never declared or allocated anywhere in this snippet, so the example does not compile. The table below gains a tlsf_acalloc row that the snippet never shows, so this was probably meant to be paired with a void *h = tlsf_acalloc(&t, 4, 64, 64); above the frees.

@ilyachastikov ilyachastikov Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tlsf_acalloc for h is present in this example at line 117

Comment thread tests/test.c
assert(p_new != NULL);
assert(((uintptr_t) p_new % target_align) == 0 &&
"tlsf_arealloc lost alignment");
assert(p_new != p_align && "tlsf_arealloc didn't relocate the block");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert depends on where the linker puts pool. Falling back to target_align = 128 only helps when p_align is 64 byte aligned but not 128 byte aligned; when the static pool lands on a 128 byte boundary the pointer satisfies both, tlsf_arealloc grows in place, and p_new == p_align trips the assert. Make the layout deterministic: allocate padding so p_align is known to be misaligned for target_align, and assert that before the call.

Comment thread tests/test.c Outdated
Comment thread tests/test_thread.c
memset(p, id & 0xFF, sz);
tlsf_thread_free(&ts, p);
size_t new_sz = (size_t) (TLSF_RAND(&seed) % 512) + 1;
void *q = tlsf_thread_arealloc(&ts, p, align, new_sz);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stress loop frees q without looking at it, so tlsf_thread_arealloc passes here as long as it returns non-NULL: lost alignment or a corrupted payload goes undetected. Assert ((uintptr_t) q % align) == 0 and check that the first min(sz, new_sz) bytes still hold id & 0xFF before freeing.

Comment thread tests/test_thread.c Outdated
@jserv

jserv commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

tlsf_arealloc corrupts the heap when a caller changes alignment and shrinks in the same call: the relocation path for a misaligned pointer copies the old block size into a destination sized for the new request. tlsf_aalloc(t, 16, 4096) followed by tlsf_arealloc(t, p, 128, 32) writes 4064 bytes past the end of the new block. Detail is in the inline comment on the memcpy in src/tlsf.c.

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

@ilyachastikov
ilyachastikov requested a review from jserv September 19, 2026 23:33
Comment thread src/tlsf.c Outdated
Comment thread src/tlsf.c Outdated
Comment thread src/tlsf.c
block_merge_next(t, block);
ASAN_UNPOISON(block_payload(block), block_size(block));
block_set_prev_free(block_next(block), false);
} else {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in the suite reaches this branch. It needs a block already aligned for align whose block_next is in use, and every aligned grow in tests/test.c has a free tail behind it, while arealloc_test relocates through the misalignment branch instead. Add a case that allocates an aligned block, allocates a barrier right after it, then grows the first one, and confirm the case fails against a wrong copy length before trusting it.

Comment thread src/tlsf_thread.c
if (!ptr)
return tlsf_thread_aalloc(ts, align, size);

if (!size) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same ordering as tlsf_arealloc: a zero size frees ptr before align is looked at, so an invalid align with size == 0 returns NULL with the block already released, against the contract stated on this function that a failure leaves @ptr allocated and intact. Validate align first.

Comment thread src/tlsf_thread.c Outdated
size_t old_size;
TLSF_LOCK_ACQUIRE(&ts->arenas[idx].lock);
old_size = tlsf_usable_size(ptr);
if (!align || (align & (align - 1))) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check runs after arena_find and after the arena lock is taken, so an invalid align pays a lock acquire and release before it is rejected. It is also narrower than the one in tlsf_arealloc, which additionally rejects align > TLSF_MAX_SIZE: for such an align the in-arena call returns NULL, this function reads that as arena exhaustion, and falls through to tlsf_thread_aalloc, which try-locks every remaining arena before failing anyway. Hoist !align || (align & (align - 1)) || align > TLSF_MAX_SIZE to the top of the function and drop this copy from inside the lock.

Comment thread include/tlsf.h Outdated
#define TLSF_INIT_STATIC TLSF_INIT
#endif

/* Native alignment for target paltform architecture */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling in text that ships in the public header: paltform for platform here, alloctions for allocations on the macro block below and again in tlsf_thread.h, upto for up to in the tlsf_arealloc doc comment, and falls back o allocating in the tlsf_thread_arealloc doc comment.

Suggested change
/* Native alignment for target paltform architecture */
/* Native alignment for target platform architecture */

Comment thread include/tlsf.h
defined(_M_X64) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2)
#define TLSF_ARCH_ALIGNMENT 16
#else
#define TLSF_ARCH_ALIGNMENT (_TLSF_SIZE_WIDTH >> 2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback is 2 * sizeof(size_t), 16 on 64-bit and 8 on 32-bit, not the sizeof(void *) that tests/bench.c used before the definition moved here, so the benchmark's alignment changes on any target that misses every SIMD test above. It matches what a hosted malloc guarantees, so it looks deliberate, but the comment reads as the architecture's natural alignment and the value is twice that. On a 32-bit target without SIMD it also pushes every TLSF_NATIVE_AALLOC past the align <= ALIGN_SIZE fast path in tlsf_aalloc and into a left trim plus an extra header. Say which of the two is intended.

Comment thread tests/test.c
uint8_t *p_align = (uint8_t *) tlsf_aalloc(&t, 16, initial_size);
assert(p_align != NULL && "Allocation failed");
assert(((uintptr_t) p_align % target_align) != 0 &&
"Test setup error: p_align is accidentally aligned");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This aborts the suite when the setup happens to land aligned, rather than reporting a defect in tlsf_arealloc, and its premise rests on TLSF_GCC_ALIGN actually expanding. On a compiler that is neither MSVC nor GCC-like, all three alignment macros expand to nothing, raw_pool gets whatever the linker picks, and whether p_align lands off a 128-byte boundary is luck. Compute the offset into the pool so the misalignment is forced instead of asserted.

Comment thread tests/test.c
* there is no space available thus tlsf_aalloc will return NULL */
void *p_fail = tlsf_arealloc(&t, p_oom, 16, 2048);
assert(p_fail == NULL);
assert(p_oom != NULL);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p_oom is a local that the failing call cannot change, so this asserts nothing about the block surviving. An implementation that freed or scribbled on the old block before returning NULL passes here and surfaces later as a double free. Fill p_oom with a canary before the call, verify it after, then free it once and call tlsf_check. The allocation above is also allowed to fail (the if (p_filler) guard says so), and if it does the pool still has room and p_fail comes back non-NULL, so assert p_filler as well.

@sysprog21 sysprog21 deleted a comment from ilyachastikov Sep 20, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/test.c">

<violation number="1" location="tests/test.c:2069">
P2: When either setup allocation fails, `memset(p_adjavail, ...)` dereferences NULL and aborts the test before validating `tlsf_arealloc`. Assert both setup pointers are non-NULL before writing to them, or skip the branch when setup cannot be established.</violation>

<violation number="2" location="tests/test.c:2083">
P2: When either setup allocation fails, `memset(p_else, ...)` dereferences NULL and the test aborts before exercising the relocation branch. Assert both setup pointers are non-NULL before writing to them.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread tests/test.c
/* adjust > avail test branch (else sub-branch) */
void *p_else = tlsf_aalloc(&t, 32, 32);
void *p_else_barrier = tlsf_aalloc(&t, 32, 32);
memset(p_else, 0xAA, 32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When either setup allocation fails, memset(p_else, ...) dereferences NULL and the test aborts before exercising the relocation branch. Assert both setup pointers are non-NULL before writing to them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test.c, line 2083:

<comment>When either setup allocation fails, `memset(p_else, ...)` dereferences NULL and the test aborts before exercising the relocation branch. Assert both setup pointers are non-NULL before writing to them.</comment>

<file context>
@@ -2063,6 +2063,34 @@ static void arealloc_test(void)
+    /* adjust > avail test branch (else sub-branch) */
+    void *p_else = tlsf_aalloc(&t, 32, 32);
+    void *p_else_barrier = tlsf_aalloc(&t, 32, 32);
+    memset(p_else, 0xAA, 32);
+    void *p_else_test = tlsf_arealloc(&t, p_else, 32, 128);
+    assert(p_else_test != NULL);
</file context>
Suggested change
memset(p_else, 0xAA, 32);
assert(p_else != NULL);
assert(p_else_barrier != NULL);
memset(p_else, 0xAA, 32);

Comment thread tests/test.c
/* adjust > avail test branch (if sub-branch) */
void *p_adjavail = tlsf_aalloc(&t, 32, 32);
void *p_adjavail_barrier = tlsf_aalloc(&t, 32, 32);
memset(p_adjavail, 0xBB, 32);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When either setup allocation fails, memset(p_adjavail, ...) dereferences NULL and aborts the test before validating tlsf_arealloc. Assert both setup pointers are non-NULL before writing to them, or skip the branch when setup cannot be established.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test.c, line 2069:

<comment>When either setup allocation fails, `memset(p_adjavail, ...)` dereferences NULL and aborts the test before validating `tlsf_arealloc`. Assert both setup pointers are non-NULL before writing to them, or skip the branch when setup cannot be established.</comment>

<file context>
@@ -2063,6 +2063,34 @@ static void arealloc_test(void)
+    /* adjust > avail test branch (if sub-branch) */
+    void *p_adjavail = tlsf_aalloc(&t, 32, 32);
+    void *p_adjavail_barrier = tlsf_aalloc(&t, 32, 32);
+    memset(p_adjavail, 0xBB, 32);
+    void *p_adjavail_test = tlsf_arealloc(&t, p_adjavail, 32, 64);
+    assert(p_adjavail_test != NULL);
</file context>
Suggested change
memset(p_adjavail, 0xBB, 32);
assert(p_adjavail != NULL);
assert(p_adjavail_barrier != NULL);
memset(p_adjavail, 0xBB, 32);

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests/test.c">

<violation number="1" location="tests/test.c:2068">
P3: The whole if-sub-branch test now depends on this allocation succeeding: if it returns NULL, `tlsf_free(NULL)` is a no-op, no hole is created, `tlsf_arealloc` falls through to the relocate path, and `assert(p_adjavail_test == p_adjavail)` at line 2074 aborts the suite with no indication that the failure was setup, not allocator behavior. Assert the setup allocation like `p_align`/`barrier` do above, so the dependency is documented and the abort message is meaningful.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread tests/test.c

/* adjust > avail test branch (if sub-branch) */
void *p_adjavail = tlsf_aalloc(&t, 32, 32);
void *p_if_free_space = tlsf_aalloc(&t, 32, 64);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The whole if-sub-branch test now depends on this allocation succeeding: if it returns NULL, tlsf_free(NULL) is a no-op, no hole is created, tlsf_arealloc falls through to the relocate path, and assert(p_adjavail_test == p_adjavail) at line 2074 aborts the suite with no indication that the failure was setup, not allocator behavior. Assert the setup allocation like p_align/barrier do above, so the dependency is documented and the abort message is meaningful.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/test.c, line 2068:

<comment>The whole if-sub-branch test now depends on this allocation succeeding: if it returns NULL, `tlsf_free(NULL)` is a no-op, no hole is created, `tlsf_arealloc` falls through to the relocate path, and `assert(p_adjavail_test == p_adjavail)` at line 2074 aborts the suite with no indication that the failure was setup, not allocator behavior. Assert the setup allocation like `p_align`/`barrier` do above, so the dependency is documented and the abort message is meaningful.</comment>

<file context>
@@ -2065,7 +2065,9 @@ static void arealloc_test(void)
 
     /* adjust > avail test branch (if sub-branch) */
     void *p_adjavail = tlsf_aalloc(&t, 32, 32);
+    void *p_if_free_space = tlsf_aalloc(&t, 32, 64);
     void *p_adjavail_barrier = tlsf_aalloc(&t, 32, 32);
+    tlsf_free(&t, p_if_free_space);
</file context>
Suggested change
void *p_if_free_space = tlsf_aalloc(&t, 32, 64);
void *p_if_free_space = tlsf_aalloc(&t, 32, 64);
assert(p_if_free_space != NULL && "Test setup failed: cannot create free space");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants