From 628d4110a4758b02e65e023ef7a08ccb9df63fa5 Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Mon, 29 Dec 2025 13:13:12 -0500 Subject: [PATCH 1/3] fix: clear exec_env_tls when destroying exec_env When an exec_env is destroyed, check if it matches the current thread's exec_env_tls and clear it to avoid dangling pointer issues. Without this fix, in daemon-style execution where the same thread runs multiple WASM modules sequentially (like Cloudflare Workers), the exec_env_tls can point to freed memory after an exec_env is destroyed, causing crashes on subsequent executions when the signal handler tries to access it. This is critical for AOT mode with hardware bounds checking enabled, where signal handlers rely on exec_env_tls to handle SIGSEGV properly. --- core/iwasm/common/wasm_exec_env.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/iwasm/common/wasm_exec_env.c b/core/iwasm/common/wasm_exec_env.c index 47752950f2..3d9d4aa5a0 100644 --- a/core/iwasm/common/wasm_exec_env.c +++ b/core/iwasm/common/wasm_exec_env.c @@ -199,6 +199,20 @@ wasm_exec_env_create(struct WASMModuleInstanceCommon *module_inst, void wasm_exec_env_destroy(WASMExecEnv *exec_env) { +#ifdef OS_ENABLE_HW_BOUND_CHECK + /* + * Clear exec_env_tls if it points to this exec_env to avoid dangling + * pointer after destruction. This is critical for daemon-style execution + * where the same thread runs multiple WASM modules sequentially. + * Without this, the signal handler may access freed memory on subsequent + * executions, causing crashes. + */ + WASMExecEnv *current_tls = wasm_runtime_get_exec_env_tls(); + if (current_tls == exec_env) { + wasm_runtime_set_exec_env_tls(NULL); + } +#endif + #if WASM_ENABLE_THREAD_MGR != 0 /* Wait for all sub-threads */ WASMCluster *cluster = wasm_exec_env_get_cluster(exec_env); From 9f73f598709e136aec0be165ea9c4b5052cb91dd Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Thu, 8 Jan 2026 07:25:42 -0500 Subject: [PATCH 2/3] test(exec_env): add reproducer for exec_env_tls dangling pointer bug Add test case that reproduces the bug where exec_env_tls is not cleared on early return paths in invoke_native_with_hw_bound_check. The test triggers native stack overflow check failure, which causes wasm_runtime_call_wasm to return early after setting exec_env_tls but without clearing it. This leaves exec_env_tls pointing to a destroyed exec_env, causing subsequent calls to fail with "invalid exec env". Test confirms the fix in wasm_exec_env_destroy correctly clears exec_env_tls when destroying the exec_env it points to. --- .../test-exec-env-tls/CMakeLists.txt | 70 +++ .../test-exec-env-tls/test_exec_env_tls.c | 407 ++++++++++++++++++ 2 files changed, 477 insertions(+) create mode 100644 tests/standalone/test-exec-env-tls/CMakeLists.txt create mode 100644 tests/standalone/test-exec-env-tls/test_exec_env_tls.c diff --git a/tests/standalone/test-exec-env-tls/CMakeLists.txt b/tests/standalone/test-exec-env-tls/CMakeLists.txt new file mode 100644 index 0000000000..1f573eb65f --- /dev/null +++ b/tests/standalone/test-exec-env-tls/CMakeLists.txt @@ -0,0 +1,70 @@ +# Copyright (C) 2024 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +cmake_minimum_required(VERSION 3.14) + +project(test_exec_env_tls) + +################ runtime settings ############## +string (TOLOWER ${CMAKE_HOST_SYSTEM_NAME} WAMR_BUILD_PLATFORM) +if (APPLE) + add_definitions(-DBH_PLATFORM_DARWIN) +endif () + +# Reset default linker flags +set (CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") +set (CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") + +# Set WAMR_BUILD_TARGET +if (NOT DEFINED WAMR_BUILD_TARGET) + if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)") + set (WAMR_BUILD_TARGET "AARCH64") + elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "riscv64") + set (WAMR_BUILD_TARGET "RISCV64") + elseif (CMAKE_SIZEOF_VOID_P EQUAL 8) + set (WAMR_BUILD_TARGET "X86_64") + elseif (CMAKE_SIZEOF_VOID_P EQUAL 4) + set (WAMR_BUILD_TARGET "X86_32") + else () + message(SEND_ERROR "Unsupported build target platform!") + endif () +endif () + +if (NOT CMAKE_BUILD_TYPE) + set (CMAKE_BUILD_TYPE Debug) +endif () + +# WAMR features - enable HW bound check for this test +set (WAMR_BUILD_INTERP 1) +set (WAMR_BUILD_AOT 1) +set (WAMR_BUILD_LIBC_BUILTIN 1) + +# compiling and linking flags +if (NOT (CMAKE_C_COMPILER MATCHES ".*clang.*" OR CMAKE_C_COMPILER_ID MATCHES ".*Clang")) + set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") +endif () + +set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wformat -Wformat-security") + +# build out libiwasm +set (WAMR_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../../..) +include (${WAMR_ROOT_DIR}/build-scripts/runtime_lib.cmake) + +add_library(libiwasm STATIC ${WAMR_RUNTIME_LIB_SOURCE}) +set_target_properties (libiwasm PROPERTIES OUTPUT_NAME iwasm) + +################ test executable ################ +add_executable (test_exec_env_tls test_exec_env_tls.c) + +target_include_directories(test_exec_env_tls PRIVATE + ${WAMR_ROOT_DIR}/core/iwasm/include + ${WAMR_ROOT_DIR}/core/iwasm/common + ${SHARED_DIR}/include + ${PLATFORM_SHARED_DIR} +) + +target_link_libraries(test_exec_env_tls libiwasm -lpthread -lm) + +if (NOT APPLE) + target_link_libraries(test_exec_env_tls -ldl) +endif () diff --git a/tests/standalone/test-exec-env-tls/test_exec_env_tls.c b/tests/standalone/test-exec-env-tls/test_exec_env_tls.c new file mode 100644 index 0000000000..aaefb9c066 --- /dev/null +++ b/tests/standalone/test-exec-env-tls/test_exec_env_tls.c @@ -0,0 +1,407 @@ +/* + * Copyright (C) 2024 Intel Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + */ + +/** + * Test case for exec_env_tls dangling pointer issue. + * + * This test reproduces a real bug in WAMR where exec_env_tls is not cleared + * on early return paths, causing "invalid exec env" errors in subsequent calls. + * + * BUG LOCATION: aot_runtime.c and wasm_runtime.c in + * invoke_native_with_hw_bound_check + * + * // Line ~2475: TLS is SET + * wasm_runtime_set_exec_env_tls(exec_env); + * + * // Line ~2487-2489: Early return WITHOUT clearing TLS! + * if (!wasm_runtime_detect_native_stack_overflow(exec_env)) { + * return false; // BUG: TLS never cleared! + * } + * + * SCENARIO: Native stack overflow triggers early return + * 1. Create exec_env_A + * 2. Set native_stack_boundary to trigger overflow check failure + * 3. Call WASM -> fails with "native stack overflow" + * 4. TLS still points to exec_env_A (never cleared!) + * 5. Destroy exec_env_A -> TLS is now dangling pointer + * 6. Create exec_env_B + * 7. Call WASM -> "invalid exec env" because TLS != exec_env_B + * + * The fix: Clear exec_env_tls in wasm_exec_env_destroy() if it points to + * the exec_env being destroyed. + */ + +#include +#include +#include +#include "wasm_export.h" + +/* Include internal header for exec_env_tls APIs */ +#include "wasm_runtime_common.h" + +/* Minimal WASM module that just returns 42 + * Generated with: echo '(module (func (export "test") (result i32) i32.const + * 42))' | wat2wasm - + */ +static const unsigned char wasm_test_file[] = { + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, + 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, + 0x08, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x0a, + 0x06, 0x01, 0x04, 0x00, 0x41, 0x2a, 0x0b +}; +#define WASM_FILE_SIZE sizeof(wasm_test_file) + +/* Helper to load module with a copy of the buffer */ +static wasm_module_t +load_test_module(char *error_buf, uint32_t error_buf_size) +{ + unsigned char *buf_copy = malloc(WASM_FILE_SIZE); + if (!buf_copy) { + snprintf(error_buf, error_buf_size, "Failed to allocate buffer"); + return NULL; + } + memcpy(buf_copy, wasm_test_file, WASM_FILE_SIZE); + wasm_module_t module = + wasm_runtime_load(buf_copy, WASM_FILE_SIZE, error_buf, error_buf_size); + if (!module) { + free(buf_copy); + } + return module; +} + +static char global_heap_buf[4 * 1024 * 1024]; + +/* ============================================================================ + * TEST: Native stack overflow early return leaves TLS dangling + * ============================================================================ + * This is the REAL bug scenario - not artificial TLS manipulation. + */ +static int +test_native_stack_overflow_early_return(void) +{ + char error_buf[128]; + wasm_module_t module = NULL; + wasm_module_inst_t module_inst_a = NULL, module_inst_b = NULL; + wasm_exec_env_t exec_env_a = NULL, exec_env_b = NULL; + wasm_function_inst_t func = NULL; + uint32_t stack_size = 8192, heap_size = 8192; + uint32_t argv[1]; + int test_passed = 0; + uint8_t *high_boundary; + + printf("=== TEST: Native stack overflow early return leaves TLS dangling " + "===\n"); + printf("\n"); + printf("This test reproduces a real bug where:\n"); + printf(" 1. wasm_runtime_call_wasm sets exec_env_tls\n"); + printf(" 2. Native stack overflow check fails\n"); + printf(" 3. Function returns early WITHOUT clearing exec_env_tls\n"); + printf(" 4. exec_env is destroyed, TLS becomes dangling pointer\n"); + printf(" 5. Next call fails with 'invalid exec env'\n"); + printf("\n"); + +#ifndef OS_ENABLE_HW_BOUND_CHECK + printf("SKIP: Hardware bound check is disabled, test not applicable.\n"); + printf("=== TEST: SKIPPED ===\n\n"); + return 1; /* Consider pass when not applicable */ +#else + + module = load_test_module(error_buf, sizeof(error_buf)); + if (!module) { + printf("FAIL: Load module failed: %s\n", error_buf); + return 0; + } + + /* Step 1: Create exec_env_A */ + printf("Step 1: Create exec_env_A\n"); + module_inst_a = wasm_runtime_instantiate(module, stack_size, heap_size, + error_buf, sizeof(error_buf)); + if (!module_inst_a) { + printf("FAIL: Instantiate A failed: %s\n", error_buf); + goto cleanup; + } + + exec_env_a = wasm_runtime_create_exec_env(module_inst_a, stack_size); + if (!exec_env_a) { + printf("FAIL: Create exec_env A failed\n"); + wasm_runtime_deinstantiate(module_inst_a); + goto cleanup; + } + printf(" exec_env_A = %p\n", (void *)exec_env_a); + + /* Step 2: Set native_stack_boundary to trigger overflow check failure */ + printf("Step 2: Set native_stack_boundary high to trigger overflow\n"); + /* Set boundary to a very high address (above current stack) */ + high_boundary = (uint8_t *)((uintptr_t)&high_boundary + 0x100000); + wasm_runtime_set_native_stack_boundary(exec_env_a, high_boundary); + printf(" Set boundary to %p (current stack ~%p)\n", (void *)high_boundary, + (void *)&high_boundary); + + /* Step 3: Call WASM - should fail with native stack overflow */ + printf("Step 3: Call WASM (expect 'native stack overflow')\n"); + func = wasm_runtime_lookup_function(module_inst_a, "test"); + argv[0] = 0; + if (wasm_runtime_call_wasm(exec_env_a, func, 0, argv)) { + printf( + " UNEXPECTED: Call succeeded (expected stack overflow failure)\n"); + printf(" This means we couldn't trigger the bug condition.\n"); + wasm_runtime_destroy_exec_env(exec_env_a); + wasm_runtime_deinstantiate(module_inst_a); + test_passed = 1; /* Not a test failure, just couldn't trigger */ + goto cleanup; + } + + const char *exception = wasm_runtime_get_exception(module_inst_a); + printf(" Call failed as expected: %s\n", exception ? exception : "(null)"); + + if (!exception || !strstr(exception, "native stack overflow")) { + printf(" WARNING: Expected 'native stack overflow', got different " + "error.\n"); + } + + /* Check exec_env_tls state - this is the bug! */ + WASMExecEnv *tls_after_fail = wasm_runtime_get_exec_env_tls(); + printf(" exec_env_tls after failed call = %p\n", (void *)tls_after_fail); + if (tls_after_fail == exec_env_a) { + printf(" BUG CONFIRMED: TLS still points to exec_env_A (not cleared " + "on early return)\n"); + } + else if (tls_after_fail == NULL) { + printf(" TLS is NULL (bug may be fixed or different code path)\n"); + } + + /* Clear exception for next operations */ + wasm_runtime_clear_exception(module_inst_a); + + /* Step 4: Destroy exec_env_A */ + printf("Step 4: Destroy exec_env_A\n"); + void *exec_env_a_addr = exec_env_a; + wasm_runtime_destroy_exec_env(exec_env_a); + exec_env_a = NULL; + wasm_runtime_deinstantiate(module_inst_a); + module_inst_a = NULL; + + /* Check if fix cleared TLS */ + WASMExecEnv *tls_after_destroy = wasm_runtime_get_exec_env_tls(); + printf(" exec_env_tls after destroy = %p\n", (void *)tls_after_destroy); + if (tls_after_destroy == NULL) { + printf(" GOOD: TLS was cleared (fix is working)\n"); + } + else if (tls_after_destroy == exec_env_a_addr) { + printf(" BAD: TLS still points to destroyed exec_env (fix needed)\n"); + } + + /* Allocate dummy to prevent address reuse */ + wasm_module_inst_t dummy_inst = wasm_runtime_instantiate( + module, stack_size, heap_size, error_buf, sizeof(error_buf)); + wasm_exec_env_t dummy_env = NULL; + if (dummy_inst) { + dummy_env = wasm_runtime_create_exec_env(dummy_inst, stack_size); + } + + /* Step 5: Create exec_env_B */ + printf("Step 5: Create exec_env_B\n"); + module_inst_b = wasm_runtime_instantiate(module, stack_size, heap_size, + error_buf, sizeof(error_buf)); + if (!module_inst_b) { + printf("FAIL: Instantiate B failed: %s\n", error_buf); + if (dummy_env) + wasm_runtime_destroy_exec_env(dummy_env); + if (dummy_inst) + wasm_runtime_deinstantiate(dummy_inst); + goto cleanup; + } + + exec_env_b = wasm_runtime_create_exec_env(module_inst_b, stack_size); + if (!exec_env_b) { + printf("FAIL: Create exec_env B failed\n"); + wasm_runtime_deinstantiate(module_inst_b); + if (dummy_env) + wasm_runtime_destroy_exec_env(dummy_env); + if (dummy_inst) + wasm_runtime_deinstantiate(dummy_inst); + goto cleanup; + } + printf(" exec_env_B = %p\n", (void *)exec_env_b); + + if (exec_env_b == exec_env_a_addr) { + printf( + " WARNING: Same address as destroyed exec_env_A (may mask bug)\n"); + } + + /* Clean up dummy */ + if (dummy_env) + wasm_runtime_destroy_exec_env(dummy_env); + if (dummy_inst) + wasm_runtime_deinstantiate(dummy_inst); + + /* Step 6: Call WASM with exec_env_B */ + printf("Step 6: Call WASM with exec_env_B\n"); + func = wasm_runtime_lookup_function(module_inst_b, "test"); + argv[0] = 0; + if (!wasm_runtime_call_wasm(exec_env_b, func, 0, argv)) { + exception = wasm_runtime_get_exception(module_inst_b); + printf(" FAIL: Call failed: %s\n", exception ? exception : "(null)"); + if (exception && strstr(exception, "invalid exec env")) { + printf("\n"); + printf(" >>> THIS IS THE BUG! <<<\n"); + printf(" exec_env_tls was not cleared on early return,\n"); + printf(" so it still pointed to destroyed exec_env_A.\n"); + printf(" When calling with exec_env_B, the check\n"); + printf(" 'exec_env_tls != exec_env' failed.\n"); + printf("\n"); + } + wasm_runtime_destroy_exec_env(exec_env_b); + wasm_runtime_deinstantiate(module_inst_b); + goto cleanup; + } + + printf(" SUCCESS: Call returned %u\n", argv[0]); + test_passed = (argv[0] == 42); + + wasm_runtime_destroy_exec_env(exec_env_b); + wasm_runtime_deinstantiate(module_inst_b); + +cleanup: + if (module) + wasm_runtime_unload(module); + + if (test_passed) { + printf("=== TEST: PASSED ===\n\n"); + } + else { + printf("=== TEST: FAILED ===\n\n"); + } + return test_passed; +#endif /* OS_ENABLE_HW_BOUND_CHECK */ +} + +/* ============================================================================ + * TEST: Sequential daemon pattern (stress test) + * ============================================================================ + */ +static int +test_sequential_daemon_pattern(void) +{ + char error_buf[128]; + wasm_module_t module = NULL; + wasm_module_inst_t module_inst = NULL; + wasm_exec_env_t exec_env = NULL; + wasm_function_inst_t func = NULL; + uint32_t stack_size = 8192, heap_size = 8192; + uint32_t argv[1]; + int i; + int success_count = 0; + + printf("=== TEST: Sequential daemon pattern (100 iterations) ===\n"); + + module = load_test_module(error_buf, sizeof(error_buf)); + if (!module) { + printf("FAIL: Load module failed: %s\n", error_buf); + return 0; + } + + for (i = 0; i < 100; i++) { + /* Simulate handleRequest: grab instance, execute, destroy */ + module_inst = wasm_runtime_instantiate(module, stack_size, heap_size, + error_buf, sizeof(error_buf)); + if (!module_inst) { + printf("FAIL: Iteration %d: Instantiate failed: %s\n", i, + error_buf); + break; + } + + exec_env = wasm_runtime_create_exec_env(module_inst, stack_size); + if (!exec_env) { + printf("FAIL: Iteration %d: Create exec_env failed\n", i); + wasm_runtime_deinstantiate(module_inst); + break; + } + + func = wasm_runtime_lookup_function(module_inst, "test"); + argv[0] = 0; + if (!wasm_runtime_call_wasm(exec_env, func, 0, argv)) { + printf("FAIL: Iteration %d: Call failed: %s\n", i, + wasm_runtime_get_exception(module_inst)); + wasm_runtime_destroy_exec_env(exec_env); + wasm_runtime_deinstantiate(module_inst); + break; + } + + if (argv[0] != 42) { + printf("FAIL: Iteration %d: Wrong result %u\n", i, argv[0]); + wasm_runtime_destroy_exec_env(exec_env); + wasm_runtime_deinstantiate(module_inst); + break; + } + + wasm_runtime_destroy_exec_env(exec_env); + wasm_runtime_deinstantiate(module_inst); + success_count++; + } + + wasm_runtime_unload(module); + + if (success_count == 100) { + printf(" All 100 iterations succeeded\n"); + printf("=== TEST: PASSED ===\n\n"); + return 1; + } + else { + printf(" Only %d/100 iterations succeeded\n", success_count); + printf("=== TEST: FAILED ===\n\n"); + return 0; + } +} + +int +main(int argc, char *argv[]) +{ + RuntimeInitArgs init_args; + int tests_passed = 0; + int tests_total = 0; + + memset(&init_args, 0, sizeof(RuntimeInitArgs)); + init_args.mem_alloc_type = Alloc_With_Pool; + init_args.mem_alloc_option.pool.heap_buf = global_heap_buf; + init_args.mem_alloc_option.pool.heap_size = sizeof(global_heap_buf); + + if (!wasm_runtime_full_init(&init_args)) { + printf("Init runtime failed.\n"); + return 1; + } + + printf("\n"); + printf("==========================================================\n"); + printf("Testing exec_env_tls cleanup on destroy\n"); + printf("==========================================================\n\n"); + +#ifdef OS_ENABLE_HW_BOUND_CHECK + printf("Hardware bound check: ENABLED\n\n"); +#else + printf("Hardware bound check: DISABLED\n"); + printf("(Some tests will be skipped)\n\n"); +#endif + + /* Test 1: Native stack overflow early return */ + tests_total++; + if (test_native_stack_overflow_early_return()) { + tests_passed++; + } + + /* Test 2: Sequential daemon pattern */ + tests_total++; + if (test_sequential_daemon_pattern()) { + tests_passed++; + } + + wasm_runtime_destroy(); + + printf("==========================================================\n"); + printf("Results: %d/%d tests passed\n", tests_passed, tests_total); + printf("==========================================================\n"); + + return (tests_passed == tests_total) ? 0 : 1; +} From c0d3e1e2c07845363b606a97dfb784da6d04f5e9 Mon Sep 17 00:00:00 2001 From: teamchong <25894545+teamchong@users.noreply.github.com> Date: Mon, 12 Jan 2026 17:55:13 -0500 Subject: [PATCH 3/3] fix(runtime): clear exec_env_tls on early return from stack overflow check Move the fix to clear exec_env_tls at the source - in the early return path of invoke_native_with_hw_bound_check when native stack overflow check fails. Changes: - aot_runtime.c: Clear exec_env_tls before early return on stack overflow - wasm_runtime.c: Clear exec_env_tls before early return on stack overflow - Remove defensive fix from wasm_exec_env_destroy (no longer needed) - Move test from standalone to unit tests (runtime-common) The bug: When wasm_runtime_call_wasm sets exec_env_tls but returns early due to native stack overflow check failure, TLS was not cleared. This caused subsequent calls with a different exec_env to fail with "invalid exec env" error. --- core/iwasm/aot/aot_runtime.c | 1 + core/iwasm/common/wasm_exec_env.c | 14 - core/iwasm/interpreter/wasm_runtime.c | 1 + .../test-exec-env-tls/CMakeLists.txt | 70 --- .../test-exec-env-tls/test_exec_env_tls.c | 407 ------------------ .../unit/runtime-common/wasm_exec_env_test.cc | 90 ++++ 6 files changed, 92 insertions(+), 491 deletions(-) delete mode 100644 tests/standalone/test-exec-env-tls/CMakeLists.txt delete mode 100644 tests/standalone/test-exec-env-tls/test_exec_env_tls.c diff --git a/core/iwasm/aot/aot_runtime.c b/core/iwasm/aot/aot_runtime.c index 4368886a27..31611f1565 100644 --- a/core/iwasm/aot/aot_runtime.c +++ b/core/iwasm/aot/aot_runtime.c @@ -2485,6 +2485,7 @@ invoke_native_with_hw_bound_check(WASMExecEnv *exec_env, void *func_ptr, native stack to run the following codes before actually calling the aot function in invokeNative function. */ if (!wasm_runtime_detect_native_stack_overflow(exec_env)) { + wasm_runtime_set_exec_env_tls(NULL); return false; } diff --git a/core/iwasm/common/wasm_exec_env.c b/core/iwasm/common/wasm_exec_env.c index 3d9d4aa5a0..47752950f2 100644 --- a/core/iwasm/common/wasm_exec_env.c +++ b/core/iwasm/common/wasm_exec_env.c @@ -199,20 +199,6 @@ wasm_exec_env_create(struct WASMModuleInstanceCommon *module_inst, void wasm_exec_env_destroy(WASMExecEnv *exec_env) { -#ifdef OS_ENABLE_HW_BOUND_CHECK - /* - * Clear exec_env_tls if it points to this exec_env to avoid dangling - * pointer after destruction. This is critical for daemon-style execution - * where the same thread runs multiple WASM modules sequentially. - * Without this, the signal handler may access freed memory on subsequent - * executions, causing crashes. - */ - WASMExecEnv *current_tls = wasm_runtime_get_exec_env_tls(); - if (current_tls == exec_env) { - wasm_runtime_set_exec_env_tls(NULL); - } -#endif - #if WASM_ENABLE_THREAD_MGR != 0 /* Wait for all sub-threads */ WASMCluster *cluster = wasm_exec_env_get_cluster(exec_env); diff --git a/core/iwasm/interpreter/wasm_runtime.c b/core/iwasm/interpreter/wasm_runtime.c index a59bc9257b..cac3730bfe 100644 --- a/core/iwasm/interpreter/wasm_runtime.c +++ b/core/iwasm/interpreter/wasm_runtime.c @@ -3618,6 +3618,7 @@ call_wasm_with_hw_bound_check(WASMModuleInstance *module_inst, native stack to run the following codes before actually calling the aot function in invokeNative function. */ if (!wasm_runtime_detect_native_stack_overflow(exec_env)) { + wasm_runtime_set_exec_env_tls(NULL); return; } diff --git a/tests/standalone/test-exec-env-tls/CMakeLists.txt b/tests/standalone/test-exec-env-tls/CMakeLists.txt deleted file mode 100644 index 1f573eb65f..0000000000 --- a/tests/standalone/test-exec-env-tls/CMakeLists.txt +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (C) 2024 Intel Corporation. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -cmake_minimum_required(VERSION 3.14) - -project(test_exec_env_tls) - -################ runtime settings ############## -string (TOLOWER ${CMAKE_HOST_SYSTEM_NAME} WAMR_BUILD_PLATFORM) -if (APPLE) - add_definitions(-DBH_PLATFORM_DARWIN) -endif () - -# Reset default linker flags -set (CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") -set (CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "") - -# Set WAMR_BUILD_TARGET -if (NOT DEFINED WAMR_BUILD_TARGET) - if (CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm64|aarch64)") - set (WAMR_BUILD_TARGET "AARCH64") - elseif (CMAKE_SYSTEM_PROCESSOR STREQUAL "riscv64") - set (WAMR_BUILD_TARGET "RISCV64") - elseif (CMAKE_SIZEOF_VOID_P EQUAL 8) - set (WAMR_BUILD_TARGET "X86_64") - elseif (CMAKE_SIZEOF_VOID_P EQUAL 4) - set (WAMR_BUILD_TARGET "X86_32") - else () - message(SEND_ERROR "Unsupported build target platform!") - endif () -endif () - -if (NOT CMAKE_BUILD_TYPE) - set (CMAKE_BUILD_TYPE Debug) -endif () - -# WAMR features - enable HW bound check for this test -set (WAMR_BUILD_INTERP 1) -set (WAMR_BUILD_AOT 1) -set (WAMR_BUILD_LIBC_BUILTIN 1) - -# compiling and linking flags -if (NOT (CMAKE_C_COMPILER MATCHES ".*clang.*" OR CMAKE_C_COMPILER_ID MATCHES ".*Clang")) - set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections") -endif () - -set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wformat -Wformat-security") - -# build out libiwasm -set (WAMR_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/../../..) -include (${WAMR_ROOT_DIR}/build-scripts/runtime_lib.cmake) - -add_library(libiwasm STATIC ${WAMR_RUNTIME_LIB_SOURCE}) -set_target_properties (libiwasm PROPERTIES OUTPUT_NAME iwasm) - -################ test executable ################ -add_executable (test_exec_env_tls test_exec_env_tls.c) - -target_include_directories(test_exec_env_tls PRIVATE - ${WAMR_ROOT_DIR}/core/iwasm/include - ${WAMR_ROOT_DIR}/core/iwasm/common - ${SHARED_DIR}/include - ${PLATFORM_SHARED_DIR} -) - -target_link_libraries(test_exec_env_tls libiwasm -lpthread -lm) - -if (NOT APPLE) - target_link_libraries(test_exec_env_tls -ldl) -endif () diff --git a/tests/standalone/test-exec-env-tls/test_exec_env_tls.c b/tests/standalone/test-exec-env-tls/test_exec_env_tls.c deleted file mode 100644 index aaefb9c066..0000000000 --- a/tests/standalone/test-exec-env-tls/test_exec_env_tls.c +++ /dev/null @@ -1,407 +0,0 @@ -/* - * Copyright (C) 2024 Intel Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - */ - -/** - * Test case for exec_env_tls dangling pointer issue. - * - * This test reproduces a real bug in WAMR where exec_env_tls is not cleared - * on early return paths, causing "invalid exec env" errors in subsequent calls. - * - * BUG LOCATION: aot_runtime.c and wasm_runtime.c in - * invoke_native_with_hw_bound_check - * - * // Line ~2475: TLS is SET - * wasm_runtime_set_exec_env_tls(exec_env); - * - * // Line ~2487-2489: Early return WITHOUT clearing TLS! - * if (!wasm_runtime_detect_native_stack_overflow(exec_env)) { - * return false; // BUG: TLS never cleared! - * } - * - * SCENARIO: Native stack overflow triggers early return - * 1. Create exec_env_A - * 2. Set native_stack_boundary to trigger overflow check failure - * 3. Call WASM -> fails with "native stack overflow" - * 4. TLS still points to exec_env_A (never cleared!) - * 5. Destroy exec_env_A -> TLS is now dangling pointer - * 6. Create exec_env_B - * 7. Call WASM -> "invalid exec env" because TLS != exec_env_B - * - * The fix: Clear exec_env_tls in wasm_exec_env_destroy() if it points to - * the exec_env being destroyed. - */ - -#include -#include -#include -#include "wasm_export.h" - -/* Include internal header for exec_env_tls APIs */ -#include "wasm_runtime_common.h" - -/* Minimal WASM module that just returns 42 - * Generated with: echo '(module (func (export "test") (result i32) i32.const - * 42))' | wat2wasm - - */ -static const unsigned char wasm_test_file[] = { - 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, - 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, - 0x08, 0x01, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x00, 0x0a, - 0x06, 0x01, 0x04, 0x00, 0x41, 0x2a, 0x0b -}; -#define WASM_FILE_SIZE sizeof(wasm_test_file) - -/* Helper to load module with a copy of the buffer */ -static wasm_module_t -load_test_module(char *error_buf, uint32_t error_buf_size) -{ - unsigned char *buf_copy = malloc(WASM_FILE_SIZE); - if (!buf_copy) { - snprintf(error_buf, error_buf_size, "Failed to allocate buffer"); - return NULL; - } - memcpy(buf_copy, wasm_test_file, WASM_FILE_SIZE); - wasm_module_t module = - wasm_runtime_load(buf_copy, WASM_FILE_SIZE, error_buf, error_buf_size); - if (!module) { - free(buf_copy); - } - return module; -} - -static char global_heap_buf[4 * 1024 * 1024]; - -/* ============================================================================ - * TEST: Native stack overflow early return leaves TLS dangling - * ============================================================================ - * This is the REAL bug scenario - not artificial TLS manipulation. - */ -static int -test_native_stack_overflow_early_return(void) -{ - char error_buf[128]; - wasm_module_t module = NULL; - wasm_module_inst_t module_inst_a = NULL, module_inst_b = NULL; - wasm_exec_env_t exec_env_a = NULL, exec_env_b = NULL; - wasm_function_inst_t func = NULL; - uint32_t stack_size = 8192, heap_size = 8192; - uint32_t argv[1]; - int test_passed = 0; - uint8_t *high_boundary; - - printf("=== TEST: Native stack overflow early return leaves TLS dangling " - "===\n"); - printf("\n"); - printf("This test reproduces a real bug where:\n"); - printf(" 1. wasm_runtime_call_wasm sets exec_env_tls\n"); - printf(" 2. Native stack overflow check fails\n"); - printf(" 3. Function returns early WITHOUT clearing exec_env_tls\n"); - printf(" 4. exec_env is destroyed, TLS becomes dangling pointer\n"); - printf(" 5. Next call fails with 'invalid exec env'\n"); - printf("\n"); - -#ifndef OS_ENABLE_HW_BOUND_CHECK - printf("SKIP: Hardware bound check is disabled, test not applicable.\n"); - printf("=== TEST: SKIPPED ===\n\n"); - return 1; /* Consider pass when not applicable */ -#else - - module = load_test_module(error_buf, sizeof(error_buf)); - if (!module) { - printf("FAIL: Load module failed: %s\n", error_buf); - return 0; - } - - /* Step 1: Create exec_env_A */ - printf("Step 1: Create exec_env_A\n"); - module_inst_a = wasm_runtime_instantiate(module, stack_size, heap_size, - error_buf, sizeof(error_buf)); - if (!module_inst_a) { - printf("FAIL: Instantiate A failed: %s\n", error_buf); - goto cleanup; - } - - exec_env_a = wasm_runtime_create_exec_env(module_inst_a, stack_size); - if (!exec_env_a) { - printf("FAIL: Create exec_env A failed\n"); - wasm_runtime_deinstantiate(module_inst_a); - goto cleanup; - } - printf(" exec_env_A = %p\n", (void *)exec_env_a); - - /* Step 2: Set native_stack_boundary to trigger overflow check failure */ - printf("Step 2: Set native_stack_boundary high to trigger overflow\n"); - /* Set boundary to a very high address (above current stack) */ - high_boundary = (uint8_t *)((uintptr_t)&high_boundary + 0x100000); - wasm_runtime_set_native_stack_boundary(exec_env_a, high_boundary); - printf(" Set boundary to %p (current stack ~%p)\n", (void *)high_boundary, - (void *)&high_boundary); - - /* Step 3: Call WASM - should fail with native stack overflow */ - printf("Step 3: Call WASM (expect 'native stack overflow')\n"); - func = wasm_runtime_lookup_function(module_inst_a, "test"); - argv[0] = 0; - if (wasm_runtime_call_wasm(exec_env_a, func, 0, argv)) { - printf( - " UNEXPECTED: Call succeeded (expected stack overflow failure)\n"); - printf(" This means we couldn't trigger the bug condition.\n"); - wasm_runtime_destroy_exec_env(exec_env_a); - wasm_runtime_deinstantiate(module_inst_a); - test_passed = 1; /* Not a test failure, just couldn't trigger */ - goto cleanup; - } - - const char *exception = wasm_runtime_get_exception(module_inst_a); - printf(" Call failed as expected: %s\n", exception ? exception : "(null)"); - - if (!exception || !strstr(exception, "native stack overflow")) { - printf(" WARNING: Expected 'native stack overflow', got different " - "error.\n"); - } - - /* Check exec_env_tls state - this is the bug! */ - WASMExecEnv *tls_after_fail = wasm_runtime_get_exec_env_tls(); - printf(" exec_env_tls after failed call = %p\n", (void *)tls_after_fail); - if (tls_after_fail == exec_env_a) { - printf(" BUG CONFIRMED: TLS still points to exec_env_A (not cleared " - "on early return)\n"); - } - else if (tls_after_fail == NULL) { - printf(" TLS is NULL (bug may be fixed or different code path)\n"); - } - - /* Clear exception for next operations */ - wasm_runtime_clear_exception(module_inst_a); - - /* Step 4: Destroy exec_env_A */ - printf("Step 4: Destroy exec_env_A\n"); - void *exec_env_a_addr = exec_env_a; - wasm_runtime_destroy_exec_env(exec_env_a); - exec_env_a = NULL; - wasm_runtime_deinstantiate(module_inst_a); - module_inst_a = NULL; - - /* Check if fix cleared TLS */ - WASMExecEnv *tls_after_destroy = wasm_runtime_get_exec_env_tls(); - printf(" exec_env_tls after destroy = %p\n", (void *)tls_after_destroy); - if (tls_after_destroy == NULL) { - printf(" GOOD: TLS was cleared (fix is working)\n"); - } - else if (tls_after_destroy == exec_env_a_addr) { - printf(" BAD: TLS still points to destroyed exec_env (fix needed)\n"); - } - - /* Allocate dummy to prevent address reuse */ - wasm_module_inst_t dummy_inst = wasm_runtime_instantiate( - module, stack_size, heap_size, error_buf, sizeof(error_buf)); - wasm_exec_env_t dummy_env = NULL; - if (dummy_inst) { - dummy_env = wasm_runtime_create_exec_env(dummy_inst, stack_size); - } - - /* Step 5: Create exec_env_B */ - printf("Step 5: Create exec_env_B\n"); - module_inst_b = wasm_runtime_instantiate(module, stack_size, heap_size, - error_buf, sizeof(error_buf)); - if (!module_inst_b) { - printf("FAIL: Instantiate B failed: %s\n", error_buf); - if (dummy_env) - wasm_runtime_destroy_exec_env(dummy_env); - if (dummy_inst) - wasm_runtime_deinstantiate(dummy_inst); - goto cleanup; - } - - exec_env_b = wasm_runtime_create_exec_env(module_inst_b, stack_size); - if (!exec_env_b) { - printf("FAIL: Create exec_env B failed\n"); - wasm_runtime_deinstantiate(module_inst_b); - if (dummy_env) - wasm_runtime_destroy_exec_env(dummy_env); - if (dummy_inst) - wasm_runtime_deinstantiate(dummy_inst); - goto cleanup; - } - printf(" exec_env_B = %p\n", (void *)exec_env_b); - - if (exec_env_b == exec_env_a_addr) { - printf( - " WARNING: Same address as destroyed exec_env_A (may mask bug)\n"); - } - - /* Clean up dummy */ - if (dummy_env) - wasm_runtime_destroy_exec_env(dummy_env); - if (dummy_inst) - wasm_runtime_deinstantiate(dummy_inst); - - /* Step 6: Call WASM with exec_env_B */ - printf("Step 6: Call WASM with exec_env_B\n"); - func = wasm_runtime_lookup_function(module_inst_b, "test"); - argv[0] = 0; - if (!wasm_runtime_call_wasm(exec_env_b, func, 0, argv)) { - exception = wasm_runtime_get_exception(module_inst_b); - printf(" FAIL: Call failed: %s\n", exception ? exception : "(null)"); - if (exception && strstr(exception, "invalid exec env")) { - printf("\n"); - printf(" >>> THIS IS THE BUG! <<<\n"); - printf(" exec_env_tls was not cleared on early return,\n"); - printf(" so it still pointed to destroyed exec_env_A.\n"); - printf(" When calling with exec_env_B, the check\n"); - printf(" 'exec_env_tls != exec_env' failed.\n"); - printf("\n"); - } - wasm_runtime_destroy_exec_env(exec_env_b); - wasm_runtime_deinstantiate(module_inst_b); - goto cleanup; - } - - printf(" SUCCESS: Call returned %u\n", argv[0]); - test_passed = (argv[0] == 42); - - wasm_runtime_destroy_exec_env(exec_env_b); - wasm_runtime_deinstantiate(module_inst_b); - -cleanup: - if (module) - wasm_runtime_unload(module); - - if (test_passed) { - printf("=== TEST: PASSED ===\n\n"); - } - else { - printf("=== TEST: FAILED ===\n\n"); - } - return test_passed; -#endif /* OS_ENABLE_HW_BOUND_CHECK */ -} - -/* ============================================================================ - * TEST: Sequential daemon pattern (stress test) - * ============================================================================ - */ -static int -test_sequential_daemon_pattern(void) -{ - char error_buf[128]; - wasm_module_t module = NULL; - wasm_module_inst_t module_inst = NULL; - wasm_exec_env_t exec_env = NULL; - wasm_function_inst_t func = NULL; - uint32_t stack_size = 8192, heap_size = 8192; - uint32_t argv[1]; - int i; - int success_count = 0; - - printf("=== TEST: Sequential daemon pattern (100 iterations) ===\n"); - - module = load_test_module(error_buf, sizeof(error_buf)); - if (!module) { - printf("FAIL: Load module failed: %s\n", error_buf); - return 0; - } - - for (i = 0; i < 100; i++) { - /* Simulate handleRequest: grab instance, execute, destroy */ - module_inst = wasm_runtime_instantiate(module, stack_size, heap_size, - error_buf, sizeof(error_buf)); - if (!module_inst) { - printf("FAIL: Iteration %d: Instantiate failed: %s\n", i, - error_buf); - break; - } - - exec_env = wasm_runtime_create_exec_env(module_inst, stack_size); - if (!exec_env) { - printf("FAIL: Iteration %d: Create exec_env failed\n", i); - wasm_runtime_deinstantiate(module_inst); - break; - } - - func = wasm_runtime_lookup_function(module_inst, "test"); - argv[0] = 0; - if (!wasm_runtime_call_wasm(exec_env, func, 0, argv)) { - printf("FAIL: Iteration %d: Call failed: %s\n", i, - wasm_runtime_get_exception(module_inst)); - wasm_runtime_destroy_exec_env(exec_env); - wasm_runtime_deinstantiate(module_inst); - break; - } - - if (argv[0] != 42) { - printf("FAIL: Iteration %d: Wrong result %u\n", i, argv[0]); - wasm_runtime_destroy_exec_env(exec_env); - wasm_runtime_deinstantiate(module_inst); - break; - } - - wasm_runtime_destroy_exec_env(exec_env); - wasm_runtime_deinstantiate(module_inst); - success_count++; - } - - wasm_runtime_unload(module); - - if (success_count == 100) { - printf(" All 100 iterations succeeded\n"); - printf("=== TEST: PASSED ===\n\n"); - return 1; - } - else { - printf(" Only %d/100 iterations succeeded\n", success_count); - printf("=== TEST: FAILED ===\n\n"); - return 0; - } -} - -int -main(int argc, char *argv[]) -{ - RuntimeInitArgs init_args; - int tests_passed = 0; - int tests_total = 0; - - memset(&init_args, 0, sizeof(RuntimeInitArgs)); - init_args.mem_alloc_type = Alloc_With_Pool; - init_args.mem_alloc_option.pool.heap_buf = global_heap_buf; - init_args.mem_alloc_option.pool.heap_size = sizeof(global_heap_buf); - - if (!wasm_runtime_full_init(&init_args)) { - printf("Init runtime failed.\n"); - return 1; - } - - printf("\n"); - printf("==========================================================\n"); - printf("Testing exec_env_tls cleanup on destroy\n"); - printf("==========================================================\n\n"); - -#ifdef OS_ENABLE_HW_BOUND_CHECK - printf("Hardware bound check: ENABLED\n\n"); -#else - printf("Hardware bound check: DISABLED\n"); - printf("(Some tests will be skipped)\n\n"); -#endif - - /* Test 1: Native stack overflow early return */ - tests_total++; - if (test_native_stack_overflow_early_return()) { - tests_passed++; - } - - /* Test 2: Sequential daemon pattern */ - tests_total++; - if (test_sequential_daemon_pattern()) { - tests_passed++; - } - - wasm_runtime_destroy(); - - printf("==========================================================\n"); - printf("Results: %d/%d tests passed\n", tests_passed, tests_total); - printf("==========================================================\n"); - - return (tests_passed == tests_total) ? 0 : 1; -} diff --git a/tests/unit/runtime-common/wasm_exec_env_test.cc b/tests/unit/runtime-common/wasm_exec_env_test.cc index 5c5b464997..8ee83053a6 100644 --- a/tests/unit/runtime-common/wasm_exec_env_test.cc +++ b/tests/unit/runtime-common/wasm_exec_env_test.cc @@ -7,6 +7,7 @@ #include "gtest/gtest.h" #include "wasm_exec_env.h" +#include "wasm_runtime_common.h" class wasm_exec_env_test_suite : public testing::Test { @@ -43,3 +44,92 @@ TEST_F(wasm_exec_env_test_suite, wasm_exec_env_pop_jmpbuf) exec_env.jmpbuf_stack_top = nullptr; EXPECT_EQ(nullptr, wasm_exec_env_pop_jmpbuf(&exec_env)); } + +/* + * Test: exec_env_tls is cleared on early return from native stack overflow + * + * This test verifies that when wasm_runtime_call_wasm fails early due to + * native stack overflow check, exec_env_tls is properly cleared. Without + * this fix, subsequent calls with a different exec_env would fail with + * "invalid exec env" error. + * + * Bug scenario: + * 1. Call WASM with exec_env_A, TLS set to exec_env_A + * 2. Native stack overflow check fails, early return + * 3. TLS still points to exec_env_A (BUG: not cleared) + * 4. Destroy exec_env_A + * 5. Create exec_env_B, call WASM + * 6. Fails with "invalid exec env" because TLS != exec_env_B + */ +#ifdef OS_ENABLE_HW_BOUND_CHECK +/* Minimal WASM module: (module (func (export "test") (result i32) i32.const + * 42)) */ +static uint8_t test_wasm[] = { 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f, 0x03, + 0x02, 0x01, 0x00, 0x07, 0x08, 0x01, 0x04, 0x74, + 0x65, 0x73, 0x74, 0x00, 0x00, 0x0a, 0x06, 0x01, + 0x04, 0x00, 0x41, 0x2a, 0x0b }; + +TEST_F(wasm_exec_env_test_suite, exec_env_tls_cleared_on_stack_overflow) +{ + WAMRRuntimeRAII<512 * 1024> runtime; + + /* Load and instantiate module */ + wasm_module_t module = + wasm_runtime_load(test_wasm, sizeof(test_wasm), nullptr, 0); + ASSERT_NE(module, nullptr); + + wasm_module_inst_t inst_a = + wasm_runtime_instantiate(module, 8192, 8192, nullptr, 0); + ASSERT_NE(inst_a, nullptr); + + wasm_exec_env_t exec_env_a = + wasm_runtime_create_exec_env(inst_a, 8192); + ASSERT_NE(exec_env_a, nullptr); + + /* Set native stack boundary high to trigger overflow check failure */ + uint8_t stack_var; + uint8_t *high_boundary = &stack_var + 0x100000; + wasm_runtime_set_native_stack_boundary(exec_env_a, high_boundary); + + /* Call should fail with native stack overflow */ + wasm_function_inst_t func = + wasm_runtime_lookup_function(inst_a, "test"); + ASSERT_NE(func, nullptr); + + uint32_t argv[1] = { 0 }; + bool result = wasm_runtime_call_wasm(exec_env_a, func, 0, argv); + EXPECT_FALSE(result); + + /* Verify TLS is cleared after failed call (this is the fix) */ + WASMExecEnv *tls = wasm_runtime_get_exec_env_tls(); + EXPECT_EQ(tls, nullptr); + + /* Clean up first instance */ + wasm_runtime_destroy_exec_env(exec_env_a); + wasm_runtime_deinstantiate(inst_a); + + /* Create second instance and exec_env */ + wasm_module_inst_t inst_b = + wasm_runtime_instantiate(module, 8192, 8192, nullptr, 0); + ASSERT_NE(inst_b, nullptr); + + wasm_exec_env_t exec_env_b = + wasm_runtime_create_exec_env(inst_b, 8192); + ASSERT_NE(exec_env_b, nullptr); + + /* This call should succeed (would fail without the fix) */ + func = wasm_runtime_lookup_function(inst_b, "test"); + ASSERT_NE(func, nullptr); + + argv[0] = 0; + result = wasm_runtime_call_wasm(exec_env_b, func, 0, argv); + EXPECT_TRUE(result); + EXPECT_EQ(argv[0], 42u); + + /* Clean up */ + wasm_runtime_destroy_exec_env(exec_env_b); + wasm_runtime_deinstantiate(inst_b); + wasm_runtime_unload(module); +} +#endif /* OS_ENABLE_HW_BOUND_CHECK */