mirror of
https://github.com/jemalloc/jemalloc.git
synced 2026-07-23 09:07:18 +03:00
test: add tcache_fiber_migration standalone LTO reproducer
Exercises the tsd thread-pointer hoisting bug: ucontext fibers do free/swapcontext/malloc in one frame on a worker pool, so a fiber routinely resumes on a different OS thread; under whole-program LTO the inlined fastpath then frees/allocs against the previous thread's tcache and crashes. It is a standalone program (no test harness, so it can be static-linked without symbol clashes), calling jemalloc via JEMALLOC_MANGLE and linked statically with --whole-archive so the allocator inlines next to the swapcontext. Built only when -flto is in the build flags -- without LTO the allocator is not inlined and the bug cannot reproduce, so the test would be a meaningless always-pass -- and gated on ELF + static + have_ucontext (a configure link test, since musl declares but does not implement getcontext/makecontext/swapcontext). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9f37c70826
commit
c68b7b1cac
3 changed files with 181 additions and 0 deletions
135
test/integration/tcache_fiber_migration.c
Normal file
135
test/integration/tcache_fiber_migration.c
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* Regression test for issue #2890: under whole-program LTO the inlined allocator
|
||||
* fastpath caches the TLS-derived tcache base in a register across a
|
||||
* swapcontext, so a fiber resumed on another OS thread uses the previous
|
||||
* thread's tcache -> heap corruption. See JEMALLOC_TLS_ADDR in tsd_internals.h.
|
||||
*
|
||||
* Standalone (no test harness, so it can be static-linked), calling jemalloc via
|
||||
* JEMALLOC_MANGLE -- it rewrites malloc/free to jemalloc's configured-prefix
|
||||
* symbols, so the calls bind to and inline the static allocator, not libc's
|
||||
* wrappers. Reproduces only with the allocator inlined next to the swapcontext
|
||||
* (a static --whole-archive link plus whole-program LTO), so the Makefile builds
|
||||
* it only when -flto is in the build flags.
|
||||
*/
|
||||
#include <pthread.h>
|
||||
#include <stdbool.h>
|
||||
#include <ucontext.h>
|
||||
|
||||
#define JEMALLOC_MANGLE
|
||||
#include <jemalloc/jemalloc.h>
|
||||
|
||||
enum {
|
||||
num_fibers = 128,
|
||||
num_workers = 8,
|
||||
ops_per_fiber = 20 * 1000,
|
||||
fiber_stack_size = 1 << 16,
|
||||
ready_queue_capacity = 512 /* power of two, > num_fibers */
|
||||
};
|
||||
|
||||
static ucontext_t fiber_context[num_fibers];
|
||||
/* Scheduler context to switch back to when a fiber yields; the resuming worker
|
||||
* writes its own context here, so this changes when the fiber migrates. */
|
||||
static ucontext_t *return_context[num_fibers];
|
||||
static unsigned fiber_remaining_ops[num_fibers];
|
||||
static bool fiber_done[num_fibers];
|
||||
|
||||
static unsigned ready_queue[ready_queue_capacity];
|
||||
static unsigned ready_queue_head;
|
||||
static unsigned ready_queue_tail;
|
||||
static unsigned live_fibers;
|
||||
static pthread_mutex_t queue_mtx = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static void
|
||||
push_ready_fiber(unsigned id) {
|
||||
pthread_mutex_lock(&queue_mtx);
|
||||
ready_queue[ready_queue_head++ & (ready_queue_capacity - 1)] = id;
|
||||
pthread_mutex_unlock(&queue_mtx);
|
||||
}
|
||||
|
||||
/* Returns a ready fiber id, -1 if none ready now, or -2 if all have finished. */
|
||||
static int
|
||||
pop_ready_fiber(void) {
|
||||
int id;
|
||||
pthread_mutex_lock(&queue_mtx);
|
||||
if (live_fibers == 0) {
|
||||
id = -2;
|
||||
} else if (ready_queue_tail != ready_queue_head) {
|
||||
id = (int)ready_queue[ready_queue_tail++ & (ready_queue_capacity - 1)];
|
||||
} else {
|
||||
id = -1;
|
||||
}
|
||||
pthread_mutex_unlock(&queue_mtx);
|
||||
return id;
|
||||
}
|
||||
|
||||
static void
|
||||
fiber_run(int id) {
|
||||
void *p = malloc(64);
|
||||
while (fiber_remaining_ops[id] > 0) {
|
||||
fiber_remaining_ops[id]--;
|
||||
free(p); /* push to the CURRENT thread's tcache */
|
||||
/* Yield; may be resumed on a different worker thread. */
|
||||
swapcontext(&fiber_context[id], return_context[id]);
|
||||
p = malloc(64); /* pop; must come from the NEW thread's tcache */
|
||||
*(char *)p = (char)id;
|
||||
}
|
||||
free(p);
|
||||
pthread_mutex_lock(&queue_mtx);
|
||||
fiber_done[id] = true;
|
||||
live_fibers--;
|
||||
pthread_mutex_unlock(&queue_mtx);
|
||||
swapcontext(&fiber_context[id], return_context[id]);
|
||||
}
|
||||
|
||||
static void *
|
||||
worker_thread(void *arg) {
|
||||
ucontext_t scheduler_context;
|
||||
(void)arg;
|
||||
for (;;) {
|
||||
int id = pop_ready_fiber();
|
||||
if (id == -2) {
|
||||
break;
|
||||
}
|
||||
if (id < 0) {
|
||||
continue;
|
||||
}
|
||||
return_context[id] = &scheduler_context;
|
||||
swapcontext(&scheduler_context, &fiber_context[id]);
|
||||
if (!fiber_done[id]) {
|
||||
push_ready_fiber((unsigned)id);
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int
|
||||
main(void) {
|
||||
void *stacks[num_fibers];
|
||||
pthread_t threads[num_workers];
|
||||
unsigned i;
|
||||
|
||||
live_fibers = num_fibers;
|
||||
for (i = 0; i < num_fibers; i++) {
|
||||
stacks[i] = malloc(fiber_stack_size);
|
||||
fiber_remaining_ops[i] = ops_per_fiber;
|
||||
getcontext(&fiber_context[i]);
|
||||
fiber_context[i].uc_stack.ss_sp = stacks[i];
|
||||
fiber_context[i].uc_stack.ss_size = fiber_stack_size;
|
||||
fiber_context[i].uc_link = NULL;
|
||||
makecontext(&fiber_context[i], (void (*)(void))fiber_run, 1, (int)i);
|
||||
push_ready_fiber(i);
|
||||
}
|
||||
|
||||
for (i = 0; i < num_workers; i++) {
|
||||
pthread_create(&threads[i], NULL, worker_thread, NULL);
|
||||
}
|
||||
for (i = 0; i < num_workers; i++) {
|
||||
pthread_join(threads[i], NULL);
|
||||
}
|
||||
for (i = 0; i < num_fibers; i++) {
|
||||
free(stacks[i]);
|
||||
}
|
||||
|
||||
/* Completing without a tcache-corruption crash is success. */
|
||||
return live_fibers == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue