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/2] 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 47752950f..3d9d4aa5a 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/2] 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 000000000..1f573eb65 --- /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 000000000..aaefb9c06 --- /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; +}