Skip to content

[lld][WebAssembly] Always initialize fixed __tls_base in single threaded mode#193563

Open
sbc100 wants to merge 1 commit intollvm:mainfrom
sbc100:always_init_tls_base
Open

[lld][WebAssembly] Always initialize fixed __tls_base in single threaded mode#193563
sbc100 wants to merge 1 commit intollvm:mainfrom
sbc100:always_init_tls_base

Conversation

@sbc100
Copy link
Copy Markdown
Collaborator

@sbc100 sbc100 commented Apr 22, 2026

Without this fix __tls_base can remain set to zero which leads __builtin_thread_pointer to return NULL, which is should not.

See emscripten-core/emscripten#26747

@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Apr 22, 2026

@llvm/pr-subscribers-lld-wasm

@llvm/pr-subscribers-lld

Author: Sam Clegg (sbc100)

Changes

Without this fix __tls_base can remain set to zero which leads __builtin_thread_pointer to return NULL, which is should not.

See emscripten-core/emscripten#26747


Full diff: https://github.com/llvm/llvm-project/pull/193563.diff

2 Files Affected:

  • (added) lld/test/wasm/tls-base-none-shared-memory.s (+37)
  • (modified) lld/wasm/Writer.cpp (+11-4)
diff --git a/lld/test/wasm/tls-base-none-shared-memory.s b/lld/test/wasm/tls-base-none-shared-memory.s
new file mode 100644
index 0000000000000..df027e520a649
--- /dev/null
+++ b/lld/test/wasm/tls-base-none-shared-memory.s
@@ -0,0 +1,37 @@
+# Test that linking without shared memory causes __tls_base to be
+# internalized, and initialized to a non-zero value, even without any TLS data
+# present.
+
+# RUN: llvm-mc -filetype=obj -triple=wasm32-unknown-unknown -o %t.o %s
+# RUN: wasm-ld -o %t.wasm %t.o
+# RUN: obj2yaml %t.wasm | FileCheck %s
+
+.globaltype __tls_base, i32
+
+.globl _start
+_start:
+  .functype _start () -> ()
+  global.get __tls_base
+  drop
+  end_function
+
+# CHECK:        - Type:            GLOBAL
+# CHECK-NEXT:     Globals:
+# CHECK-NEXT:       - Index:           0
+# CHECK-NEXT:         Type:            I32
+# CHECK-NEXT:         Mutable:         true
+# CHECK-NEXT:         InitExpr:
+# CHECK-NEXT:           Opcode:          I32_CONST
+# CHECK-NEXT:           Value:           65536
+# CHECK-NEXT:       - Index:           1
+# CHECK-NEXT:         Type:            I32
+# CHECK-NEXT:         Mutable:         false
+# CHECK-NEXT:         InitExpr:
+# CHECK-NEXT:           Opcode:          I32_CONST
+# CHECK-NEXT:           Value:           65536
+
+# CHECK:          GlobalNames:
+# CHECK-NEXT:       - Index:           0
+# CHECK-NEXT:         Name:            __stack_pointer
+# CHECK-NEXT:       - Index:           1
+# CHECK-NEXT:         Name:            __tls_base
diff --git a/lld/wasm/Writer.cpp b/lld/wasm/Writer.cpp
index 128931513b215..038a8b3f5417d 100644
--- a/lld/wasm/Writer.cpp
+++ b/lld/wasm/Writer.cpp
@@ -381,6 +381,7 @@ void Writer::layoutMemory() {
     ctx.sym.dsoHandle->setVA(dataStart);
 
   out.dylinkSec->memAlign = 0;
+  uint64_t fixedTLSBase = memoryPtr;
   for (OutputSegment *seg : segments) {
     out.dylinkSec->memAlign = std::max(out.dylinkSec->memAlign, seg->alignment);
     memoryPtr = alignTo(memoryPtr, 1ULL << seg->alignment);
@@ -397,10 +398,7 @@ void Writer::layoutMemory() {
         auto *tlsAlign = cast<DefinedGlobal>(ctx.sym.tlsAlign);
         setGlobalPtr(tlsAlign, int64_t{1} << seg->alignment);
       }
-      if (!ctx.arg.sharedMemory && ctx.sym.tlsBase) {
-        auto *tlsBase = cast<DefinedGlobal>(ctx.sym.tlsBase);
-        setGlobalPtr(tlsBase, memoryPtr);
-      }
+      fixedTLSBase = memoryPtr;
     }
 
     if (ctx.sym.rodataStart && seg->name.starts_with(".rodata") &&
@@ -414,6 +412,15 @@ void Writer::layoutMemory() {
       ctx.sym.rodataEnd->setVA(memoryPtr);
   }
 
+  // In single-threaded builds we set __tls_base statically.
+  // Even in the absense of any actual TLS data, this symbol can still be
+  // referenced (for example by __builtin_thread_pointer, which should not
+  // return NULL).
+  if (!ctx.arg.sharedMemory && ctx.sym.tlsBase) {
+    auto *tlsBase = cast<DefinedGlobal>(ctx.sym.tlsBase);
+    setGlobalPtr(tlsBase, fixedTLSBase);
+  }
+
   // Make space for the memory initialization flag
   if (ctx.arg.sharedMemory && hasPassiveInitializedSegments()) {
     memoryPtr = alignTo(memoryPtr, 4);

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

🪟 Windows x64 Test Results

  • 3130 tests passed
  • 61 tests skipped

✅ The build succeeded and all tests passed.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

🐧 Linux x64 Test Results

  • 3837 tests passed
  • 212 tests skipped

✅ The build succeeded and all tests passed.

@sbc100 sbc100 force-pushed the always_init_tls_base branch from e732601 to 6d92055 Compare April 22, 2026 19:00
@sbc100 sbc100 requested a review from tlively April 23, 2026 02:12
Comment thread lld/test/wasm/tls-base-non-shared-memory.s
…ilds

Without this fix `__tls_base` can remain set to zero which leads
`__builtin_thread_pointer` to return NULL, which is should not.

See emscripten-core/emscripten#26747
@sbc100 sbc100 force-pushed the always_init_tls_base branch from 6d92055 to 4f464a4 Compare April 23, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants