From 921fa09086aa602c87537e5231cd10ff60778c24 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 5 Feb 2026 14:46:04 -0500 Subject: [PATCH 1/6] Align C GLOBAL_CODE repro checks with Rust failures --- crates/c-api/tests/CMakeLists.txt | 172 ++++----- crates/c-api/tests/global_code.c | 326 ++++++++++++++++++ .../wasmtime/src/runtime/module/registry.rs | 190 ++++++++++ 3 files changed, 606 insertions(+), 82 deletions(-) create mode 100644 crates/c-api/tests/global_code.c diff --git a/crates/c-api/tests/CMakeLists.txt b/crates/c-api/tests/CMakeLists.txt index b77c1e68bfcc..4acb626c5bbd 100644 --- a/crates/c-api/tests/CMakeLists.txt +++ b/crates/c-api/tests/CMakeLists.txt @@ -1,88 +1,96 @@ -include(FetchContent) -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/refs/tags/release-1.12.1.zip -) -# For Windows: Prevent overriding the parent project's compiler/linker settings -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest) +option(WASMTIME_CAPI_ENABLE_GTEST_TESTS "Build C++ gtest-based C API tests" ON) -include(GoogleTest) +if(WASMTIME_CAPI_ENABLE_GTEST_TESTS) + include(FetchContent) + FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/release-1.12.1.zip + ) + # For Windows: Prevent overriding the parent project's compiler/linker settings + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(googletest) -function(add_capi_test name) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "FILES") - add_executable(test-${name} ${arg_FILES}) - target_link_libraries(test-${name} PRIVATE wasmtime-cpp gtest_main) - gtest_discover_tests(test-${name} - # GitHub Actions on Windows is pretty slow, let's give it lots more time - # than the default 5 seconds. - DISCOVERY_TIMEOUT 60) -endfunction() + include(GoogleTest) -add_capi_test(tests FILES - simple.cc - types.cc - memory_type.cc - val_type.cc - global_type.cc - table_type.cc - func_type.cc - import_type.cc - export_type.cc - extern_type.cc - func.cc - component/instantiate.cc - component/define_module.cc - component/lookup_func.cc - component/call_func.cc - component/values.cc - component/linker.cc - component/types.cc - error.cc - config.cc - wat.cc - module.cc - engine.cc - trap.cc - wasi.cc - store.cc - val.cc - table.cc - global.cc - memory.cc - instance.cc - linker.cc - wasip2.cc -) + function(add_capi_test name) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "FILES") + add_executable(test-${name} ${arg_FILES}) + target_link_libraries(test-${name} PRIVATE wasmtime-cpp gtest_main) + gtest_discover_tests(test-${name} + # GitHub Actions on Windows is pretty slow, let's give it lots more time + # than the default 5 seconds. + DISCOVERY_TIMEOUT 60) + endfunction() -# Create a list of all wasmtime headers with `GLOB_RECURSE`, then emit a file -# into the current binary directory which tests that if the header is included -# that the file compiles correctly. -# -# Gather everything into a list and then create a test which links all these -# compilations together. This binary doesn't actually run any tests itself but -# it does test that all headers compile in isolation at least. Not perfect but -# hopefully at least something. -cmake_path(APPEND CMAKE_CURRENT_SOURCE_DIR "../include" OUTPUT_VARIABLE header_root) -file(GLOB_RECURSE cpp_headers "${header_root}/*.h*") -foreach(header IN LISTS cpp_headers) - cmake_path(GET header EXTENSION header_extension) - if(header_extension STREQUAL ".h.in") - continue() - endif() - cmake_path( - RELATIVE_PATH header - BASE_DIRECTORY ${header_root} - OUTPUT_VARIABLE rel_header) - cmake_path(APPEND CMAKE_CURRENT_BINARY_DIR "header-test" "${rel_header}.cc" - OUTPUT_VARIABLE test_filename) - list(APPEND header_tests ${test_filename}) + add_capi_test(tests FILES + simple.cc + types.cc + memory_type.cc + val_type.cc + global_type.cc + table_type.cc + func_type.cc + import_type.cc + export_type.cc + extern_type.cc + func.cc + component/instantiate.cc + component/define_module.cc + component/lookup_func.cc + component/call_func.cc + component/values.cc + component/linker.cc + component/types.cc + error.cc + config.cc + wat.cc + module.cc + engine.cc + trap.cc + wasi.cc + store.cc + val.cc + table.cc + global.cc + memory.cc + instance.cc + linker.cc + wasip2.cc + ) - file(WRITE ${test_filename} "#include <${rel_header}>") -endforeach() -add_capi_test(standalone-headers FILES ${header_tests}) + # Create a list of all wasmtime headers with `GLOB_RECURSE`, then emit a file + # into the current binary directory which tests that if the header is included + # that the file compiles correctly. + # + # Gather everything into a list and then create a test which links all these + # compilations together. This binary doesn't actually run any tests itself but + # it does test that all headers compile in isolation at least. Not perfect but + # hopefully at least something. + cmake_path(APPEND CMAKE_CURRENT_SOURCE_DIR "../include" OUTPUT_VARIABLE header_root) + file(GLOB_RECURSE cpp_headers "${header_root}/*.h*") + foreach(header IN LISTS cpp_headers) + cmake_path(GET header EXTENSION header_extension) + if(header_extension STREQUAL ".h.in") + continue() + endif() + cmake_path( + RELATIVE_PATH header + BASE_DIRECTORY ${header_root} + OUTPUT_VARIABLE rel_header) + cmake_path(APPEND CMAKE_CURRENT_BINARY_DIR "header-test" "${rel_header}.cc" + OUTPUT_VARIABLE test_filename) + list(APPEND header_tests ${test_filename}) -# Add a custom test where two files include `wasmtime.hh` and are compiled into -# the same executable (basically makes sure any defined functions in the header -# are tagged with `inline`). -add_capi_test(test-double-include FILES double-include-a.cc double-include-b.cc) + file(WRITE ${test_filename} "#include <${rel_header}>") + endforeach() + add_capi_test(standalone-headers FILES ${header_tests}) + + # Add a custom test where two files include `wasmtime.hh` and are compiled into + # the same executable (basically makes sure any defined functions in the header + # are tagged with `inline`). + add_capi_test(test-double-include FILES double-include-a.cc double-include-b.cc) +endif() + +add_executable(test-global-code global_code.c) +target_link_libraries(test-global-code PRIVATE wasmtime pthread) +add_test(NAME test-global-code COMMAND test-global-code) diff --git a/crates/c-api/tests/global_code.c b/crates/c-api/tests/global_code.c new file mode 100644 index 000000000000..ff7b3a2cb861 --- /dev/null +++ b/crates/c-api/tests/global_code.c @@ -0,0 +1,326 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +typedef struct { + wasm_engine_t *engine; + wasmtime_module_t *module; + uintptr_t module_start; +} held_module_t; + +typedef struct { + held_module_t *items; + size_t len; + size_t cap; +} held_list_t; + +static void held_list_push(held_list_t *list, wasm_engine_t *engine, + wasmtime_module_t *module, + uintptr_t module_start) { + if (list->len == list->cap) { + size_t new_cap = list->cap == 0 ? 8 : list->cap * 2; + held_module_t *next = + (held_module_t *)realloc(list->items, new_cap * sizeof(*next)); + if (!next) { + fprintf(stderr, "out of memory\n"); + exit(1); + } + list->items = next; + list->cap = new_cap; + } + list->items[list->len].engine = engine; + list->items[list->len].module = module; + list->items[list->len].module_start = module_start; + list->len++; +} + +static bool address_is_mapped(uintptr_t addr) { + FILE *maps = fopen("/proc/self/maps", "r"); + if (!maps) { + fprintf(stderr, "failed to open /proc/self/maps\n"); + exit(1); + } + + char line[512]; + while (fgets(line, sizeof(line), maps) != NULL) { + uintptr_t start = 0; + uintptr_t end = 0; + if (sscanf(line, "%lx-%lx", &start, &end) == 2) { + if (start <= addr && addr < end) { + fclose(maps); + return true; + } + } + } + + fclose(maps); + return false; +} + +static uintptr_t module_start(wasmtime_module_t *module) { + void *start = NULL; + void *end = NULL; + wasmtime_module_image_range(module, &start, &end); + if (start == NULL || end == NULL || (uintptr_t)start >= (uintptr_t)end) { + fprintf(stderr, "invalid module image range\n"); + exit(1); + } + return (uintptr_t)start; +} + +static void assert_mapped(uintptr_t addr, const char *context) { + if (!address_is_mapped(addr)) { + fprintf(stderr, "%s: expected mapped address %#lx\n", context, + (unsigned long)addr); + exit(1); + } +} + +static void assert_unmapped(uintptr_t addr, const char *context) { + if (address_is_mapped(addr)) { + fprintf(stderr, "%s: expected unmapped address %#lx\n", context, + (unsigned long)addr); + exit(1); + } +} + +static bool held_list_pop(held_list_t *list, held_module_t *out) { + if (list->len == 0) { + return false; + } + list->len--; + *out = list->items[list->len]; + return true; +} + +static void held_list_drop_all(held_list_t *list) { + for (size_t i = 0; i < list->len; i++) { + wasmtime_module_delete(list->items[i].module); + wasm_engine_delete(list->items[i].engine); + } + free(list->items); + list->items = NULL; + list->len = 0; + list->cap = 0; +} + +static void exit_if_error(wasmtime_error_t *error, const char *context) { + if (!error) { + return; + } + wasm_name_t message; + wasmtime_error_message(error, &message); + fprintf(stderr, "%s: %.*s\n", context, (int)message.size, message.data); + wasm_byte_vec_delete(&message); + wasmtime_error_delete(error); + exit(1); +} + +static void compile_module(wasm_engine_t *engine, const char *wat, + wasmtime_module_t **out) { + wasm_byte_vec_t wasm; + wasmtime_error_t *error = + wasmtime_wat2wasm(wat, strlen(wat), &wasm); + exit_if_error(error, "wat2wasm failed"); + + error = wasmtime_module_new(engine, (const uint8_t *)wasm.data, wasm.size, + out); + wasm_byte_vec_delete(&wasm); + exit_if_error(error, "module_new failed"); +} + +static wasm_engine_t *new_engine_with_signals_disabled(void) { + wasm_config_t *config = wasm_config_new(); + if (!config) { + fprintf(stderr, "failed to allocate config\n"); + exit(1); + } + wasmtime_config_signals_based_traps_set(config, false); + wasm_engine_t *engine = wasm_engine_new_with_config(config); + if (!engine) { + fprintf(stderr, "failed to create engine\n"); + exit(1); + } + return engine; +} + +static void test_unregisters_on_module_drop(void) { + for (size_t i = 0; i < 600; i++) { + wasm_engine_t *engine = wasm_engine_new(); + if (!engine) { + fprintf(stderr, "failed to create engine\n"); + exit(1); + } + char wat[128]; + snprintf(wat, sizeof(wat), + "(module (memory (export \"mem\") 1) " + "(data (i32.const 0) \"%zu\") (func (export \"f\")))", + i); + wasmtime_module_t *module = NULL; + compile_module(engine, wat, &module); + uintptr_t pc = module_start(module); + assert_mapped(pc, "on_module_drop pre-drop"); + wasmtime_module_delete(module); + wasm_engine_delete(engine); + assert_unmapped(pc, "on_module_drop post-drop"); + } +} + +static void test_unregisters_same_module(void) { + const char *wat = + "(module (func (export \"test\") (result i32) i32.const 42))"; + + for (size_t i = 0; i < 500; i++) { + wasm_engine_t *engine = new_engine_with_signals_disabled(); + wasmtime_module_t *module = NULL; + compile_module(engine, wat, &module); + uintptr_t pc = module_start(module); + assert_mapped(pc, "same_module pre-drop"); + wasmtime_module_delete(module); + wasm_engine_delete(engine); + assert_unmapped(pc, "same_module post-drop"); + + if (i % 100 == 0) { + fprintf(stderr, "Iteration %zu\n", i); + } + } +} + +static void test_unregisters_same_engine(void) { + const char *wat = + "(module (func (export \"test\") (result i32) i32.const 42))"; + wasm_engine_t *engine = new_engine_with_signals_disabled(); + + for (size_t i = 0; i < 500; i++) { + wasmtime_module_t *module = NULL; + compile_module(engine, wat, &module); + uintptr_t pc = module_start(module); + assert_mapped(pc, "same_engine pre-drop"); + wasmtime_module_delete(module); + assert_unmapped(pc, "same_engine post-drop"); + + if (i % 100 == 0) { + fprintf(stderr, "Iteration %zu\n", i); + } + } + + wasm_engine_delete(engine); +} + +static void test_unregisters_under_pressure(void) { + const char *wat = + "(module (memory (export \"mem\") 1) (data (i32.const 0) \"pressure\") " + "(func (export \"test\") (result i32) i32.const 42))"; + + held_list_t held = {0}; + + for (size_t i = 0; i < 1000; i++) { + wasm_engine_t *engine = new_engine_with_signals_disabled(); + wasmtime_module_t *module = NULL; + compile_module(engine, wat, &module); + uintptr_t pc = module_start(module); + assert_mapped(pc, "under_pressure pre-drop"); + + if (i % 3 == 0) { + held_list_push(&held, engine, module, pc); + } else { + wasmtime_module_delete(module); + wasm_engine_delete(engine); + assert_unmapped(pc, "under_pressure post-drop"); + } + + if (i % 10 == 0) { + held_module_t dropped; + if (held_list_pop(&held, &dropped)) { + wasmtime_module_delete(dropped.module); + wasm_engine_delete(dropped.engine); + assert_unmapped(dropped.module_start, + "under_pressure delayed post-drop"); + } + } + } + + for (size_t i = 0; i < held.len; i++) { + assert_mapped(held.items[i].module_start, "under_pressure final pre-drop"); + } + held_list_drop_all(&held); +} + +typedef struct { + size_t thread_id; + const char *wat; +} thread_ctx_t; + +static void *thread_pressure(void *arg) { + thread_ctx_t *ctx = (thread_ctx_t *)arg; + held_list_t held = {0}; + + for (size_t i = 0; i < 1000; i++) { + wasm_engine_t *engine = new_engine_with_signals_disabled(); + wasmtime_module_t *module = NULL; + compile_module(engine, ctx->wat, &module); + uintptr_t pc = module_start(module); + assert_mapped(pc, "threaded pre-drop"); + + if ((i + ctx->thread_id) % 4 == 0) { + held_list_push(&held, engine, module, pc); + } else { + wasmtime_module_delete(module); + wasm_engine_delete(engine); + assert_unmapped(pc, "threaded post-drop"); + } + + if (i % 25 == 0) { + held_module_t dropped; + if (held_list_pop(&held, &dropped)) { + wasmtime_module_delete(dropped.module); + wasm_engine_delete(dropped.engine); + assert_unmapped(dropped.module_start, "threaded delayed post-drop"); + } + } + } + + for (size_t i = 0; i < held.len; i++) { + assert_mapped(held.items[i].module_start, "threaded final pre-drop"); + } + held_list_drop_all(&held); + return NULL; +} + +static void test_unregisters_under_threaded_pressure(void) { + const char *wat = + "(module (memory (export \"mem\") 1) (data (i32.const 0) \"threaded\") " + "(func (export \"test\") (result i32) i32.const 42))"; + + pthread_t threads[4]; + thread_ctx_t ctx[4]; + + for (size_t i = 0; i < 4; i++) { + ctx[i].thread_id = i; + ctx[i].wat = wat; + if (pthread_create(&threads[i], NULL, thread_pressure, &ctx[i]) != 0) { + fprintf(stderr, "failed to create thread %zu\n", i); + exit(1); + } + } + + for (size_t i = 0; i < 4; i++) { + pthread_join(threads[i], NULL); + } +} + +int main(void) { + test_unregisters_on_module_drop(); + test_unregisters_same_module(); + test_unregisters_same_engine(); + test_unregisters_under_pressure(); + test_unregisters_under_threaded_pressure(); + return 0; +} diff --git a/crates/wasmtime/src/runtime/module/registry.rs b/crates/wasmtime/src/runtime/module/registry.rs index ad86cf158b89..9e23a772ee44 100644 --- a/crates/wasmtime/src/runtime/module/registry.rs +++ b/crates/wasmtime/src/runtime/module/registry.rs @@ -378,3 +378,193 @@ fn test_frame_info() -> Result<(), crate::Error> { } Ok(()) } + +#[test] +#[cfg_attr(miri, ignore)] +fn test_global_code_unregisters_on_module_drop() -> Result<(), crate::Error> { + use crate::*; + + for i in 0..600 { + let engine = Engine::default(); + let wat = format!( + r#"(module + (memory (export "mem") 1) + (data (i32.const 0) "{}") + (func (export "f")) + )"#, + i + ); + let module = Module::new(&engine, wat)?; + let pc = module.engine_code().text_range().start.raw(); + + assert!(lookup_code(pc).is_some()); + + drop(module); + + assert!(lookup_code(pc).is_none()); + } + Ok(()) +} + +#[test] +#[cfg_attr(miri, ignore)] +fn test_global_code_unregisters_same_module() -> Result<(), crate::Error> { + use crate::*; + + let wat = r#"(module (func (export "test") (result i32) i32.const 42))"#; + + for i in 0..500 { + let mut config = Config::new(); + config.signals_based_traps(false); + let engine = Engine::new(&config)?; + let module = Module::new(&engine, wat)?; + let pc = module.engine_code().text_range().start.raw(); + + assert!(lookup_code(pc).is_some()); + + drop(module); + + assert!(lookup_code(pc).is_none()); + + if i % 100 == 0 { + eprintln!("Iteration {}", i); + } + } + + Ok(()) +} + +#[test] +#[cfg_attr(miri, ignore)] +fn test_global_code_unregisters_same_engine() -> Result<(), crate::Error> { + use crate::*; + + let mut config = Config::new(); + config.signals_based_traps(false); + let engine = Engine::new(&config)?; + let wat = r#"(module (func (export "test") (result i32) i32.const 42))"#; + + for i in 0..500 { + let module = Module::new(&engine, wat)?; + let pc = module.engine_code().text_range().start.raw(); + + assert!(lookup_code(pc).is_some()); + + drop(module); + + assert!(lookup_code(pc).is_none()); + + if i % 100 == 0 { + eprintln!("Iteration {}", i); + } + } + + Ok(()) +} + +#[test] +#[cfg_attr(miri, ignore)] +fn test_global_code_unregisters_under_pressure() -> Result<(), crate::Error> { + use crate::*; + + let mut config = Config::new(); + config.signals_based_traps(false); + + let wat = r#"(module + (memory (export "mem") 1) + (data (i32.const 0) "pressure") + (func (export "test") (result i32) i32.const 42) + )"#; + + let mut held_modules: Vec = Vec::new(); + + for i in 0..1000 { + let engine = Engine::new(&config)?; + let module = Module::new(&engine, wat)?; + let pc = module.engine_code().text_range().start.raw(); + + assert!(lookup_code(pc).is_some()); + + if i % 3 == 0 { + held_modules.push(module); + } else { + drop(module); + assert!(lookup_code(pc).is_none()); + } + + if i % 10 == 0 && !held_modules.is_empty() { + let dropped = held_modules.pop().unwrap(); + let dropped_pc = dropped.engine_code().text_range().start.raw(); + drop(dropped); + assert!(lookup_code(dropped_pc).is_none()); + } + } + + for module in held_modules { + let pc = module.engine_code().text_range().start.raw(); + drop(module); + assert!(lookup_code(pc).is_none()); + } + + Ok(()) +} + +#[test] +#[cfg_attr(miri, ignore)] +fn test_global_code_unregisters_under_threaded_pressure() -> Result<(), crate::Error> { + use crate::*; + use std::thread; + + let wat = r#"(module + (memory (export "mem") 1) + (data (i32.const 0) "threaded") + (func (export "test") (result i32) i32.const 42) + )"#; + + let mut handles = Vec::new(); + + for thread_id in 0..4 { + let wat = wat.to_string(); + handles.push(thread::spawn(move || -> Result<(), crate::Error> { + let mut config = Config::new(); + config.signals_based_traps(false); + let mut held_modules: Vec = Vec::new(); + + for i in 0..1000 { + let engine = Engine::new(&config)?; + let module = Module::new(&engine, &wat)?; + let pc = module.engine_code().text_range().start.raw(); + + assert!(lookup_code(pc).is_some()); + + if (i + thread_id) % 4 == 0 { + held_modules.push(module); + } else { + drop(module); + assert!(lookup_code(pc).is_none()); + } + + if i % 25 == 0 && !held_modules.is_empty() { + let dropped = held_modules.pop().unwrap(); + let dropped_pc = dropped.engine_code().text_range().start.raw(); + drop(dropped); + assert!(lookup_code(dropped_pc).is_none()); + } + } + + for module in held_modules { + let pc = module.engine_code().text_range().start.raw(); + drop(module); + assert!(lookup_code(pc).is_none()); + } + + Ok(()) + })); + } + + for handle in handles { + handle.join().unwrap()?; + } + + Ok(()) +} From d371df693d6714b8933d4c0297b05fc9fbd4d1a9 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 5 Feb 2026 15:22:20 -0500 Subject: [PATCH 2/6] Guard C repro mapping checks for Linux only --- crates/c-api/tests/global_code.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/c-api/tests/global_code.c b/crates/c-api/tests/global_code.c index ff7b3a2cb861..3bf82362b09f 100644 --- a/crates/c-api/tests/global_code.c +++ b/crates/c-api/tests/global_code.c @@ -42,6 +42,7 @@ static void held_list_push(held_list_t *list, wasm_engine_t *engine, } static bool address_is_mapped(uintptr_t addr) { +#ifdef __linux__ FILE *maps = fopen("/proc/self/maps", "r"); if (!maps) { fprintf(stderr, "failed to open /proc/self/maps\n"); @@ -62,6 +63,10 @@ static bool address_is_mapped(uintptr_t addr) { fclose(maps); return false; +#else + (void)addr; + return false; +#endif } static uintptr_t module_start(wasmtime_module_t *module) { @@ -76,19 +81,29 @@ static uintptr_t module_start(wasmtime_module_t *module) { } static void assert_mapped(uintptr_t addr, const char *context) { +#ifdef __linux__ if (!address_is_mapped(addr)) { fprintf(stderr, "%s: expected mapped address %#lx\n", context, (unsigned long)addr); exit(1); } +#else + (void)addr; + (void)context; +#endif } static void assert_unmapped(uintptr_t addr, const char *context) { +#ifdef __linux__ if (address_is_mapped(addr)) { fprintf(stderr, "%s: expected unmapped address %#lx\n", context, (unsigned long)addr); exit(1); } +#else + (void)addr; + (void)context; +#endif } static bool held_list_pop(held_list_t *list, held_module_t *out) { From 4d847d1829c34cacf026354b6b6a770f85f61746 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 5 Feb 2026 15:55:40 -0500 Subject: [PATCH 3/6] Document lifetime and threading contract in C repro --- crates/c-api/tests/global_code.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/c-api/tests/global_code.c b/crates/c-api/tests/global_code.c index 3bf82362b09f..949eab0378de 100644 --- a/crates/c-api/tests/global_code.c +++ b/crates/c-api/tests/global_code.c @@ -10,6 +10,10 @@ #include typedef struct { + // Ownership contract for held items: + // - `engine` and `module` are paired and owned by this entry. + // - each pointer stored here must be deleted exactly once. + // - `module_start` is diagnostic only and never dereferenced. wasm_engine_t *engine; wasmtime_module_t *module; uintptr_t module_start; @@ -21,6 +25,16 @@ typedef struct { size_t cap; } held_list_t; +// Repro contract for all test paths below: +// - every successful `wasmtime_module_new` is matched by exactly one +// `wasmtime_module_delete`. +// - every created `wasm_engine_t` is matched by exactly one +// `wasm_engine_delete`. +// - no module handle is shared across threads; each thread owns and drops only +// modules it created. +// - the only intentional cross-thread interaction is process-global runtime +// state exercised by concurrent create/drop activity. + static void held_list_push(held_list_t *list, wasm_engine_t *engine, wasmtime_module_t *module, uintptr_t module_start) { @@ -277,6 +291,9 @@ static void *thread_pressure(void *arg) { thread_ctx_t *ctx = (thread_ctx_t *)arg; held_list_t held = {0}; + // Each thread keeps ownership of its own held list; no cross-thread + // transfers occur. + for (size_t i = 0; i < 1000; i++) { wasm_engine_t *engine = new_engine_with_signals_disabled(); wasmtime_module_t *module = NULL; From b82d2f3c459c0380bc6cce637867ce9d0c66fddf Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 5 Feb 2026 16:00:09 -0500 Subject: [PATCH 4/6] Report stable failure condition in C GLOBAL_CODE repro output --- crates/c-api/tests/global_code.c | 38 ++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/crates/c-api/tests/global_code.c b/crates/c-api/tests/global_code.c index 949eab0378de..1bb884aed106 100644 --- a/crates/c-api/tests/global_code.c +++ b/crates/c-api/tests/global_code.c @@ -83,6 +83,8 @@ static bool address_is_mapped(uintptr_t addr) { #endif } +static size_t linux_unmapped_mismatches = 0; + static uintptr_t module_start(wasmtime_module_t *module) { void *start = NULL; void *end = NULL; @@ -107,12 +109,14 @@ static void assert_mapped(uintptr_t addr, const char *context) { #endif } -static void assert_unmapped(uintptr_t addr, const char *context) { +static void note_unmapped_evidence(uintptr_t addr, const char *context) { #ifdef __linux__ if (address_is_mapped(addr)) { - fprintf(stderr, "%s: expected unmapped address %#lx\n", context, - (unsigned long)addr); - exit(1); + linux_unmapped_mismatches++; + fprintf(stderr, + "%s: expected unmapped address after drop, got mapped %#lx " + "(linux /proc/self/maps evidence)\n", + context, (unsigned long)addr); } #else (void)addr; @@ -198,7 +202,7 @@ static void test_unregisters_on_module_drop(void) { assert_mapped(pc, "on_module_drop pre-drop"); wasmtime_module_delete(module); wasm_engine_delete(engine); - assert_unmapped(pc, "on_module_drop post-drop"); + note_unmapped_evidence(pc, "on_module_drop post-drop"); } } @@ -214,7 +218,7 @@ static void test_unregisters_same_module(void) { assert_mapped(pc, "same_module pre-drop"); wasmtime_module_delete(module); wasm_engine_delete(engine); - assert_unmapped(pc, "same_module post-drop"); + note_unmapped_evidence(pc, "same_module post-drop"); if (i % 100 == 0) { fprintf(stderr, "Iteration %zu\n", i); @@ -233,7 +237,7 @@ static void test_unregisters_same_engine(void) { uintptr_t pc = module_start(module); assert_mapped(pc, "same_engine pre-drop"); wasmtime_module_delete(module); - assert_unmapped(pc, "same_engine post-drop"); + note_unmapped_evidence(pc, "same_engine post-drop"); if (i % 100 == 0) { fprintf(stderr, "Iteration %zu\n", i); @@ -262,7 +266,7 @@ static void test_unregisters_under_pressure(void) { } else { wasmtime_module_delete(module); wasm_engine_delete(engine); - assert_unmapped(pc, "under_pressure post-drop"); + note_unmapped_evidence(pc, "under_pressure post-drop"); } if (i % 10 == 0) { @@ -270,8 +274,8 @@ static void test_unregisters_under_pressure(void) { if (held_list_pop(&held, &dropped)) { wasmtime_module_delete(dropped.module); wasm_engine_delete(dropped.engine); - assert_unmapped(dropped.module_start, - "under_pressure delayed post-drop"); + note_unmapped_evidence(dropped.module_start, + "under_pressure delayed post-drop"); } } } @@ -306,7 +310,7 @@ static void *thread_pressure(void *arg) { } else { wasmtime_module_delete(module); wasm_engine_delete(engine); - assert_unmapped(pc, "threaded post-drop"); + note_unmapped_evidence(pc, "threaded post-drop"); } if (i % 25 == 0) { @@ -314,7 +318,8 @@ static void *thread_pressure(void *arg) { if (held_list_pop(&held, &dropped)) { wasmtime_module_delete(dropped.module); wasm_engine_delete(dropped.engine); - assert_unmapped(dropped.module_start, "threaded delayed post-drop"); + note_unmapped_evidence(dropped.module_start, + "threaded delayed post-drop"); } } } @@ -354,5 +359,14 @@ int main(void) { test_unregisters_same_engine(); test_unregisters_under_pressure(); test_unregisters_under_threaded_pressure(); +#ifdef __linux__ + if (linux_unmapped_mismatches > 0) { + fprintf(stderr, + "stable condition failed: expected 0 post-drop mapped addresses, " + "got %zu (linux /proc/self/maps extra evidence)\n", + linux_unmapped_mismatches); + return 1; + } +#endif return 0; } From f3a07089fa17c3a41c85053529e280261f6e9e74 Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 5 Feb 2026 16:00:45 -0500 Subject: [PATCH 5/6] Improve C repro failure messages and aggregate signal --- crates/c-api/tests/global_code.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/c-api/tests/global_code.c b/crates/c-api/tests/global_code.c index 1bb884aed106..4914f0689c2a 100644 --- a/crates/c-api/tests/global_code.c +++ b/crates/c-api/tests/global_code.c @@ -30,8 +30,10 @@ typedef struct { // `wasmtime_module_delete`. // - every created `wasm_engine_t` is matched by exactly one // `wasm_engine_delete`. +// - Each wasmtime_*_delete is called exactly once. // - no module handle is shared across threads; each thread owns and drops only // modules it created. +// - No handles are used after delete. // - the only intentional cross-thread interaction is process-global runtime // state exercised by concurrent create/drop activity. @@ -327,6 +329,8 @@ static void *thread_pressure(void *arg) { for (size_t i = 0; i < held.len; i++) { assert_mapped(held.items[i].module_start, "threaded final pre-drop"); } + // Threads join before cleanup in the parent; this thread only cleans up the + // handles it owns. held_list_drop_all(&held); return NULL; } @@ -351,6 +355,8 @@ static void test_unregisters_under_threaded_pressure(void) { for (size_t i = 0; i < 4; i++) { pthread_join(threads[i], NULL); } + + // Threads join before cleanup. } int main(void) { From a22de6e8da5450a4d0daef9fb765e592a371368c Mon Sep 17 00:00:00 2001 From: Zachary Whitley Date: Thu, 5 Feb 2026 20:08:10 -0500 Subject: [PATCH 6/6] Clarify Linux maps check is extra evidence --- crates/c-api/tests/global_code.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/c-api/tests/global_code.c b/crates/c-api/tests/global_code.c index 4914f0689c2a..661c02c833a0 100644 --- a/crates/c-api/tests/global_code.c +++ b/crates/c-api/tests/global_code.c @@ -113,6 +113,9 @@ static void assert_mapped(uintptr_t addr, const char *context) { static void note_unmapped_evidence(uintptr_t addr, const char *context) { #ifdef __linux__ + // This checks whether `addr` is still mapped as Linux-only extra evidence; + // Wasmtime may legitimately keep mappings cached, but we observed + // correlation with GLOBAL_CODE failures. if (address_is_mapped(addr)) { linux_unmapped_mismatches++; fprintf(stderr,