diff --git a/.github/openclaw/artifacts.py b/.github/openclaw/artifacts.py index 0e745538e8882..1712e3c7f47ad 100644 --- a/.github/openclaw/artifacts.py +++ b/.github/openclaw/artifacts.py @@ -104,6 +104,8 @@ def build(label, destination): if label == 'bun-webkit-linux-amd64': run(str(artifact/'bin/jsc'), '-e', 'print("OpenClaw WebKit ready")') run(str(artifact/'bin/testFFI')) + run(str(artifact/'bin/testMimallocExit')) + run(str(artifact/'bin/testMimallocExitInFlight')) shutil.rmtree(artifact) def main(): diff --git a/.github/openclaw/qualify-arm64.sh b/.github/openclaw/qualify-arm64.sh index 62d790b490714..a6cfd8fd1dceb 100644 --- a/.github/openclaw/qualify-arm64.sh +++ b/.github/openclaw/qualify-arm64.sh @@ -7,6 +7,8 @@ QUALIFICATION_DIR=$(cd "$QUALIFICATION_DIR" && pwd) jsc="$ENGINE_BUILD_DIR/bin/jsc" "$ENGINE_BUILD_DIR/bin/testmasm" > "$QUALIFICATION_DIR/testmasm.log" 2>&1 "$ENGINE_BUILD_DIR/bin/testFFI" > "$QUALIFICATION_DIR/testffi.log" 2>&1 +"$ENGINE_BUILD_DIR/bin/testMimallocExit" > "$QUALIFICATION_DIR/mimalloc-exit.log" 2>&1 +"$ENGINE_BUILD_DIR/bin/testMimallocExitInFlight" > "$QUALIFICATION_DIR/mimalloc-exit-in-flight.log" 2>&1 Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$jsc" --child-processes "${ENGINE_TEST_JOBS:-4}" \ --filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter|callsite-syntax-positions|stack-position-regressions|error-stack' \ --output-dir "$QUALIFICATION_DIR/jsc-stress" --no-retry > "$QUALIFICATION_DIR/jsc-stress.log" 2>&1 diff --git a/OPENCLAW.md b/OPENCLAW.md index 082b4d44fafd2..9a57972e004f8 100644 --- a/OPENCLAW.md +++ b/OPENCLAW.md @@ -150,6 +150,8 @@ baseline. Engine and Bun headers and libraries must be rebuilt together. ## Unreleased +- Prevent late JSC helper-thread allocation from overwriting a recycled pthread TLS key during process exit; add a deterministic native allocator-exit regression. Ports the allocator correction from oven-sh/WebKit#698, thanks @dylan-conway. + ## ARM64 arithmetic and Windows artifacts (2026-10-05) - Fix ARM64 register-to-memory 64-bit addition so DFG typed-array allocations update their accounting counter without corrupting the source register; cover the operation in testmasm and qualify native ARM64 JSC across interpreter and JIT tiers. diff --git a/Source/JavaScriptCore/shell/CMakeLists.txt b/Source/JavaScriptCore/shell/CMakeLists.txt index fe6073d65a08e..08f2dd02aed34 100644 --- a/Source/JavaScriptCore/shell/CMakeLists.txt +++ b/Source/JavaScriptCore/shell/CMakeLists.txt @@ -71,6 +71,24 @@ endif () WEBKIT_EXECUTABLE_DECLARE(jsc) +if (USE_MIMALLOC AND UNIX) + find_package(Threads REQUIRED) + add_executable(testMimallocExit ../../bmalloc/mimalloc/test-exit.c) + target_link_libraries(testMimallocExit PRIVATE mimalloc-obj Threads::Threads ${CMAKE_DL_LIBS}) + add_library(mimallocExitTestHooks OBJECT ../../bmalloc/mimalloc/mimalloc/src/static.c) + target_include_directories(mimallocExitTestHooks PUBLIC $) + target_compile_options(mimallocExitTestHooks PRIVATE $) + target_compile_definitions(mimallocExitTestHooks PRIVATE + $ + MI_TEST_THREAD_KEY_WAIT=1 + pthread_setspecific=mimalloc_test_setspecific + pthread_key_delete=mimalloc_test_key_delete + ) + add_executable(testMimallocExitInFlight ../../bmalloc/mimalloc/test-exit-in-flight.c) + target_link_libraries(testMimallocExitInFlight PRIVATE mimallocExitTestHooks Threads::Threads ${CMAKE_DL_LIBS}) + add_dependencies(jsc testMimallocExit testMimallocExitInFlight) +endif () + if (USE_BUN_JSC_ADDITIONS) # bun:ffi (JSC FFI) C++ unit / ABI test executable, modeled on the # testmasm_* variables below. Build and run it with: diff --git a/Source/bmalloc/mimalloc/mimalloc/src/prim/unix/prim.c b/Source/bmalloc/mimalloc/mimalloc/src/prim/unix/prim.c index ba8dfb1f87d7e..af61e9b780a5b 100644 --- a/Source/bmalloc/mimalloc/mimalloc/src/prim/unix/prim.c +++ b/Source/bmalloc/mimalloc/mimalloc/src/prim/unix/prim.c @@ -946,6 +946,9 @@ bool _mi_prim_random_buf(void* buf, size_t buf_len) { // use pthread local storage keys to detect thread ending // (and used with MI_TLS_PTHREADS for the default theap) pthread_key_t _mi_heap_default_key = (pthread_key_t)(-1); +// The high bit closes admission; the remaining bits count setters using the key. +#define MI_THREAD_KEY_CLOSED ((uintptr_t)1 << (8 * sizeof(uintptr_t) - 1)) +static _Atomic(uintptr_t) mi_thread_key_users; static void mi_pthread_done(void* value) { if (value!=NULL) { @@ -959,15 +962,34 @@ void _mi_prim_thread_init_auto_done(void) { } void _mi_prim_thread_done_auto_done(void) { - if (_mi_heap_default_key != (pthread_key_t)(-1)) { // do not leak the key, see issue #809 - pthread_key_delete(_mi_heap_default_key); + // Stop new setters and drain those that already loaded the key before its slot + // can be reused by another component (for example, ParkingLot at process exit). + uintptr_t users = mi_atomic_or_acq_rel(&mi_thread_key_users, MI_THREAD_KEY_CLOSED); + if (users & MI_THREAD_KEY_CLOSED) return; + while (mi_atomic_load_acquire(&mi_thread_key_users) != MI_THREAD_KEY_CLOSED) { + #if defined(MI_TEST_THREAD_KEY_WAIT) + extern void mi_test_thread_key_wait(void); + mi_test_thread_key_wait(); + #endif + mi_atomic_yield(); + } + pthread_key_t key = _mi_heap_default_key; + if (key != (pthread_key_t)(-1)) { // do not leak the key, see issue #809 + _mi_heap_default_key = (pthread_key_t)(-1); + pthread_key_delete(key); } } void _mi_prim_thread_associate_default_theap(mi_theap_t* theap) { + uintptr_t users = mi_atomic_load_acquire(&mi_thread_key_users); + do { + if (users & MI_THREAD_KEY_CLOSED) return; + } while (!mi_atomic_cas_weak_acq_rel(&mi_thread_key_users, &users, users + 1)); + if (_mi_heap_default_key != (pthread_key_t)(-1)) { // can happen during recursive invocation on freeBSD pthread_setspecific(_mi_heap_default_key, theap); } + mi_atomic_decrement_acq_rel(&mi_thread_key_users); } #else diff --git a/Source/bmalloc/mimalloc/test-exit-in-flight.c b/Source/bmalloc/mimalloc/test-exit-in-flight.c new file mode 100644 index 0000000000000..93a3abe1212d9 --- /dev/null +++ b/Source/bmalloc/mimalloc/test-exit-in-flight.c @@ -0,0 +1,97 @@ +/* Copyright (C) 2026 Peter Steinberger. See test-exit.c for license terms. */ + +#include +#include +#include +#include +#include + +#define CHECK(expression) do { \ + if (!(expression)) { \ + fprintf(stderr, "check failed: %s\n", #expression); \ + _exit(EXIT_FAILURE); \ + } \ +} while (0) + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t condition = PTHREAD_COND_INITIALIZER; +static pthread_t worker; +static _Thread_local int pauseNextAssociation; +static pthread_key_t pausedKey; +static int associationPaused; +static int releaseAssociation; +static int deletedWhilePaused; + +// Only the test's allocator object renames these calls. All synchronization and +// the real pthread operations in this file use the platform functions directly. +int mimalloc_test_setspecific(pthread_key_t key, const void* value) +{ + if (!pauseNextAssociation) + return pthread_setspecific(key, value); + pauseNextAssociation = 0; + CHECK(!pthread_mutex_lock(&lock)); + pausedKey = key; + associationPaused = 1; + CHECK(!pthread_cond_signal(&condition)); + while (!releaseAssociation) + CHECK(!pthread_cond_wait(&condition, &lock)); + int result = pthread_setspecific(key, value); + associationPaused = 0; + CHECK(!pthread_mutex_unlock(&lock)); + return result; +} + +int mimalloc_test_key_delete(pthread_key_t key) +{ + CHECK(!pthread_mutex_lock(&lock)); + if (associationPaused && key == pausedKey) { + deletedWhilePaused = 1; + releaseAssociation = 1; + CHECK(!pthread_cond_signal(&condition)); + } + CHECK(!pthread_mutex_unlock(&lock)); + return pthread_key_delete(key); +} + +static void* allocate(void* unused) +{ + (void)unused; + pauseNextAssociation = 1; + void* allocation = mi_malloc(64); + CHECK(allocation); + mi_free(allocation); + return NULL; +} + +// The guarded allocator calls this only after it has closed admission and +// observed an active setter. An unguarded allocator reaches the deletion hook +// instead, which records the violation and releases the setter there. +void mi_test_thread_key_wait(void) +{ + CHECK(!pthread_mutex_lock(&lock)); + releaseAssociation = 1; + CHECK(!pthread_cond_signal(&condition)); + CHECK(!pthread_mutex_unlock(&lock)); +} + +static void checkExit(void) +{ + mi_process_done(); + CHECK(!pthread_join(worker, NULL)); + CHECK(!deletedWhilePaused); + puts("mimalloc in-flight exit TLS ownership: PASS"); +} + +int main(void) +{ + void* allocation = mi_malloc(16); + CHECK(allocation); + mi_free(allocation); + CHECK(!pthread_mutex_lock(&lock)); + CHECK(!pthread_create(&worker, NULL, allocate, NULL)); + while (!associationPaused) + CHECK(!pthread_cond_wait(&condition, &lock)); + CHECK(!pthread_mutex_unlock(&lock)); + CHECK(!atexit(checkExit)); + return 0; +} diff --git a/Source/bmalloc/mimalloc/test-exit.c b/Source/bmalloc/mimalloc/test-exit.c new file mode 100644 index 0000000000000..6fb7b757478ac --- /dev/null +++ b/Source/bmalloc/mimalloc/test-exit.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2026 Peter Steinberger. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the above copyright notice, + * this condition and the following disclaimer are retained. + * THIS SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND. + */ + +// Regression for the pthread-key collision described in oven-sh/WebKit#698. +// Run after main returns so allocator teardown and a live thread overlap. +#include +#include +#include +#include +#include + +#define CHECK(expression) do { \ + if (!(expression)) { \ + fprintf(stderr, "check failed: %s\n", #expression); \ + _exit(EXIT_FAILURE); \ + } \ +} while (0) + +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t condition = PTHREAD_COND_INITIALIZER; +static pthread_t thread; +static int allocatorStopped; +static int sentinel; + +static void* allocateAfterShutdown(void* unused) +{ + (void)unused; + CHECK(!pthread_mutex_lock(&lock)); + while (!allocatorStopped) + CHECK(!pthread_cond_wait(&condition, &lock)); + CHECK(!pthread_mutex_unlock(&lock)); + + // The first new key may reuse mimalloc's deleted key, just as ParkingLot does. + pthread_key_t key; + CHECK(!pthread_key_create(&key, NULL)); + CHECK(!pthread_setspecific(key, &sentinel)); + void* allocation = mi_malloc(64); + CHECK(allocation); + CHECK(pthread_getspecific(key) == &sentinel); + mi_free(allocation); + CHECK(pthread_getspecific(key) == &sentinel); + CHECK(!pthread_key_delete(key)); + return NULL; +} + +static void checkExit(void) +{ + // Calling the same public shutdown function here avoids relying on link-order + // dependent destructor priorities. The automatic allocator destructor is idempotent. + mi_process_done(); + CHECK(!pthread_mutex_lock(&lock)); + allocatorStopped = 1; + CHECK(!pthread_cond_signal(&condition)); + CHECK(!pthread_mutex_unlock(&lock)); + CHECK(!pthread_join(thread, NULL)); + puts("mimalloc exit TLS ownership: PASS"); +} + +int main(void) +{ + // Initialize the allocator on the main thread, leaving the worker's first + // allocation until after shutdown has deleted the allocator's pthread key. + void* allocation = mi_malloc(16); + CHECK(allocation); + mi_free(allocation); + CHECK(!pthread_create(&thread, NULL, allocateAfterShutdown, NULL)); + CHECK(!atexit(checkExit)); + return 0; +}