Fix lives in: ESP-IDF startup / FreeRTOS-Xtensa port
What the workaround does
We run a constructor very early in the static-init sequence that points the Xtensa THREADPTR register at a static 4 KB TLS area, so subsequent thread_local accesses succeed even before vTaskStartScheduler:
// src/esp_posix_shims.c:20-65
static __attribute__((aligned(16))) uint8_t s_pre_main_tls[4096];
__attribute__((constructor(101)))
static void esp_posix_shims_init_threadptr(void) {
asm volatile ("wsr.threadptr %0" :: "a"(s_pre_main_tls));
}
We additionally inject -DNDEBUG on Abseil/protobuf to disable Mutex deadlock detection:
# cmake/protobuf_setup.cmake:170-178
target_compile_definitions(absl_synchronization PRIVATE NDEBUG)
target_compile_definitions(libprotobuf PRIVATE NDEBUG)
# ...
Root cause
protobuf's global static constructors build descriptor tables via Abseil hash maps. Two TLS-touching paths fire before vTaskStartScheduler:
- Abseil hash tables use
thread_local uint16_t seed without an initialization guard. Reading the variable accesses the Xtensa THREADPTR register, which is 0 at static-init time → LoadProhibited exception.
- Abseil
Mutex deadlock detection (DebugOnlyDeadlockCheck) calls GetOrCreateCurrentThreadIdentity → pthread_key_create → pvTaskGetThreadLocalStoragePointer. FreeRTOS TLS is not initialized at this point.
The C++ standard guarantees thread_local works during dynamic initialization. The Xtensa port is the odd one out — most architectures initialize their thread pointer in C runtime startup (_init / crt0) before global constructors run.
Proposed fix
ESP-IDF should initialize THREADPTR (and FreeRTOS TLS) before C++ global constructors. Concretely:
- ESP-IDF's
start_app_cpu / start_cpu0 / startup hooks should set THREADPTR to a valid address (a per-app static TLS area, or the bootstrap idle task's TLS) before invoking __libc_init_array.
- Once
THREADPTR is valid pre-main, both paths above stop crashing — and -DNDEBUG becomes unnecessary because Mutex deadlock detection's TLS path works.
What we delete once fixed
- The
THREADPTR pre-init constructor and the s_pre_main_tls buffer in src/esp_posix_shims.c:20-65
- The
-DNDEBUG injection on Abseil/protobuf targets in cmake/protobuf_setup.cmake:170-178
Fix lives in: ESP-IDF startup / FreeRTOS-Xtensa port
What the workaround does
We run a constructor very early in the static-init sequence that points the Xtensa
THREADPTRregister at a static 4 KB TLS area, so subsequentthread_localaccesses succeed even beforevTaskStartScheduler:We additionally inject
-DNDEBUGon Abseil/protobuf to disable Mutex deadlock detection:Root cause
protobuf's global static constructors build descriptor tables via Abseil hash maps. Two TLS-touching paths fire before
vTaskStartScheduler:thread_local uint16_t seedwithout an initialization guard. Reading the variable accesses the XtensaTHREADPTRregister, which is 0 at static-init time → LoadProhibited exception.Mutexdeadlock detection (DebugOnlyDeadlockCheck) callsGetOrCreateCurrentThreadIdentity→pthread_key_create→pvTaskGetThreadLocalStoragePointer. FreeRTOS TLS is not initialized at this point.The C++ standard guarantees
thread_localworks during dynamic initialization. The Xtensa port is the odd one out — most architectures initialize their thread pointer in C runtime startup (_init/ crt0) before global constructors run.Proposed fix
ESP-IDF should initialize
THREADPTR(and FreeRTOS TLS) before C++ global constructors. Concretely:start_app_cpu/start_cpu0/ startup hooks should setTHREADPTRto a valid address (a per-app static TLS area, or the bootstrap idle task's TLS) before invoking__libc_init_array.THREADPTRis valid pre-main, both paths above stop crashing — and-DNDEBUGbecomes unnecessary becauseMutexdeadlock detection's TLS path works.What we delete once fixed
THREADPTRpre-init constructor and thes_pre_main_tlsbuffer insrc/esp_posix_shims.c:20-65-DNDEBUGinjection on Abseil/protobuf targets incmake/protobuf_setup.cmake:170-178