[WebAssembly] Fix __init_tls_base global in coop threading
Project: llvm/llvm-project | Language: C++ | Concepts: memory-management, data-structures | Difficulty: mid
Link: https://github.com/llvm/llvm-project/pull/208597
The problem
In WebAssembly modules using cooperative threading, the __init_tls_base global was not being properly initialized during module instantiation. It was always left at 0, which is incorrect. This global is supposed to hold the base address for Thread-Local Storage (TLS) initialization, which is critical for correct thread-local data handling.
Why it's non-trivial
A naive fix might just set the global to a constant or try to initialize it statically. However, the correct value for __init_tls_base depends on whether the build is position-independent code (PIC) or not. In PIC mode, the address must be computed dynamically at runtime, because the actual memory base is not known until instantiation. In non-PIC mode, it can be set statically. The fix must handle both cases and update the global at the right point in the initialization sequence.
The approach
1. Always create __init_tls_base as a mutable global and set its value appropriately:
- ctx.sym.tlsBase =
- createGlobalVariable(tls_base_name, !ctx.arg.libcallThreadContext,
- WASM_SYMBOL_VISIBILITY_HIDDEN);
+ ctx.sym.tlsBase = createGlobalVariable(tls_base_name, true,
+ WASM_SYMBOL_VISIBILITY_HIDDEN);
This change ensures that __init_tls_base is always created as a mutable global (the second argument to createGlobalVariable is now always true), so its value can be set at runtime if needed.
2. In the memory initialization function, set the global correctly for both PIC and non-PIC:
+ if (ctx.arg.libcallThreadContext) {
+ writeU8(os, WASM_OPCODE_LOCAL_GET, "local.get");
+ writeUleb128(os, tlsAddressLocal, "tls address local");
+ writeU8(os, WASM_OPCODE_GLOBAL_SET, "global.set");
+ writeUleb128(os, ctx.sym.tlsBase->getGlobalIndex(),
+ "__init_tls_base");
+ }
In PIC mode, after calculating the TLS address and storing it in a local, this code writes WebAssembly instructions to store the calculated value into the __init_tls_base global.
+ if (ctx.arg.libcallThreadContext)
+ ctx.sym.tlsBase->global->setPointerValue(s->startVA);
In non-PIC mode, the global is set statically to the known start address of the TLS segment.
3. Test updates to check that the global is set and used correctly:
+# Ensure __init_stack_pointer, __init_tls_base, and __tls_size are all correct.
+# CHECK: - Type: GLOBAL
+# CHECK-NEXT: Globals:
+# CHECK-NEXT: - Index: 0
+# CHECK-NEXT: Type: I32
+# CHECK-NEXT: Mutable: false
+# CHECK-NEXT: InitExpr:
+# CHECK-NEXT: Opcode: I32_CONST
+# CHECK-NEXT: Value: 65536
+# CHECK-NEXT: - Index: 1
+# CHECK-NEXT: Type: I32
+# CHECK-NEXT: Mutable: true
+# CHECK-NEXT: InitExpr:
+# CHECK-NEXT: Opcode: I32_CONST
+# CHECK-NEXT: Value: 65544
+# CHECK-NEXT: - Index: 2
+# CHECK-NEXT: Type: I32
+# CHECK-NEXT: Mutable: false
+# CHECK-NEXT: InitExpr:
+# CHECK-NEXT: Opcode: I32_CONST
+# CHECK-NEXT: Value: 8
The tests now check that __init_tls_base is present, mutable, and has the correct value depending on the build mode.
4. In the disassembly checks, verify the global is set at runtime in PIC:
+# PIC-DIS-NEXT: global.get 1
+# PIC-DIS-NEXT: i32.add
+# PIC-DIS-NEXT: local.tee 0
+# PIC-DIS-NEXT: local.get 0
+# PIC-DIS-NEXT: global.set 3
This confirms that the generated WebAssembly code stores the computed TLS base into the correct global.
From the review thread
No notable review discussion on this PR.
Takeaways
- When a global variable's value depends on runtime information (like a memory base in PIC), make it mutable and set it during initialization, not at compile time.
- Ensure initialization code paths for both PIC and non-PIC builds correctly handle global state, especially for low-level constructs like TLS.
- Always update and extend tests to verify that globals and memory are initialized as expected in all supported modes.