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
2 changes: 2 additions & 0 deletions .github/openclaw/artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 2 additions & 0 deletions .github/openclaw/qualify-arm64.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions OPENCLAW.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ baseline. Engine and Bun headers and libraries must be rebuilt together.

## Unreleased

- Preserve mimalloc pthread TLS-key ownership during shell exit by draining active setters before deletion; cover both late and in-flight allocations with native regressions. Builds on oven-sh/WebKit#698, thanks @dylan-conway.

- Fix ARM64 MacroAssembler register-to-memory `add64` operand order, restoring DFG typed-array allocation accounting and preserving the source register; cover the operation in testmasm and native ARM64 qualification.

- Cache source-relative stack line and column lookups on demand, including syntax-selected positions.
Expand Down
18 changes: 18 additions & 0 deletions Source/JavaScriptCore/shell/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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_PROPERTY:mimalloc-obj,INCLUDE_DIRECTORIES>)
target_compile_options(mimallocExitTestHooks PRIVATE $<TARGET_PROPERTY:mimalloc-obj,COMPILE_OPTIONS>)
target_compile_definitions(mimallocExitTestHooks PRIVATE
$<TARGET_PROPERTY:mimalloc-obj,COMPILE_DEFINITIONS>
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:
Expand Down
26 changes: 24 additions & 2 deletions Source/bmalloc/mimalloc/mimalloc/src/prim/unix/prim.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
97 changes: 97 additions & 0 deletions Source/bmalloc/mimalloc/test-exit-in-flight.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* Copyright (C) 2026 Peter Steinberger. See test-exit.c for license terms. */

#include <mimalloc.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#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;
}
75 changes: 75 additions & 0 deletions Source/bmalloc/mimalloc/test-exit.c
Original file line number Diff line number Diff line change
@@ -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 <mimalloc.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#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;
}