From b60a39e3c223624577d475f278bbc26540adca9c Mon Sep 17 00:00:00 2001 From: Derek Schuff Date: Tue, 24 Mar 2026 17:30:39 -0700 Subject: [PATCH] [lld][WebAssembly] Propagate +atomics for ThinLTO when using --shared-memory (#188381) When compiling WebAssembly with ThinLTO, functions are partitioned into isolated `.bc` modules and dispatched to individual LTO backend threads. During code generation, the `CoalesceFeaturesAndStripAtomics` pass iterates over the module to gather the union of target features (like `+atomics`) attached to defined functions. In particular when not using threads, it lowers away atomics and TLS variables to their single-threaded equivalents. However, if a partitioned module only contains globally defined TLS variables (e.g. there are no functions, or all functions were fully inlined or stripped by dropDeadSymbols before ThinLTO optimization), the module becomes completely devoid of function definitions. The coalescing pass then falls back to fetching features from the `TargetMachine`. Because in LTO the `TargetMachine` defaults to a generic target without atomics enabled, the TLS is lowered away and the `wasm-feature-atomics` flag is omitted from the resulting ThinLTO object partition, causing `wasm-ld` to immediately reject it. To fix this we take advantage of the fact that the linker always knows whether threads are being used (via the --shared-memory flag). When using shared memory, we enable +atomics and +bulk-memory in the TargetMachine that is used for the backend, and the feature coalescing pass will correctly detect the use of therads. This only makes sense for atomics because of the global linker configuration; for other features we wouldn't be able to do this, but we don't rewrite away any other features anyway. --- .../wasm/lto/thinlto-shared-memory-atomics.ll | 40 +++++++++++++++++++ lld/wasm/LTO.cpp | 13 ++++++ 2 files changed, 53 insertions(+) create mode 100644 lld/test/wasm/lto/thinlto-shared-memory-atomics.ll diff --git a/lld/test/wasm/lto/thinlto-shared-memory-atomics.ll b/lld/test/wasm/lto/thinlto-shared-memory-atomics.ll new file mode 100644 index 000000000000..0eb54034511d --- /dev/null +++ b/lld/test/wasm/lto/thinlto-shared-memory-atomics.ll @@ -0,0 +1,40 @@ +; RUN: split-file %s %t +; RUN: opt -module-summary %t/main.ll -o %t.main.o +; RUN: opt -module-summary %t/empty.ll -o %t.empty.o +; RUN: wasm-ld --shared-memory %t.main.o %t.empty.o -o %t.wasm +; RUN: llvm-nm %t.wasm | FileCheck %s + +; This test ensures that when an unused function with target-features="+atomics,+bulk-memory" +; is stripped during ThinLTO, the resulting empty module does not fall back to generic TargetMachine +; features and incorrectly omit the atomics/bulk-memory features from the generated custom sections. +; +; Since --shared-memory is passed to wasm-ld, `+atomics` and `+bulk-memory` are propagated +; to the LTO Config via MAttrs, bypassing the functional absence of attributes. + +; CHECK-NOT: error: +; CHECK: _start + +;--- main.ll +target triple = "wasm32-unknown-emscripten" + +define void @_start() #0 { +entry: + ret void +} + +attributes #0 = { "target-features"="+atomics,+bulk-memory,+mutable-globals,+sign-ext" } + +;--- empty.ll +target triple = "wasm32-unknown-emscripten" + +; Thread-local variable that forces generation of TLS layout +@my_tls = thread_local global i32 42, align 4 + +; This function will be removed by dropDeadSymbols because it's unused, +; taking its target-features attribute block along with it. +define void @unused() #0 { +entry: + ret void +} + +attributes #0 = { "target-features"="+atomics,+bulk-memory,+mutable-globals,+sign-ext" } diff --git a/lld/wasm/LTO.cpp b/lld/wasm/LTO.cpp index 668cdf21ea3e..9fcb95b84b54 100644 --- a/lld/wasm/LTO.cpp +++ b/lld/wasm/LTO.cpp @@ -53,6 +53,19 @@ static lto::Config createConfig() { c.OptLevel = ctx.arg.ltoo; c.CPU = getCPUStr(); c.MAttrs = getMAttrs(); + + // If shared memory is enabled, ensure the TargetMachine backend is + // instantiated with atomics and bulk-memory features so that empty + // partitioned ThinLTO modules don't incorrectly strip TLS variables or fall + // back to defaults. This bypasses a bug where deleted functions take their + // target-features away. This is only necessary for atomics (and not other + // features) because Atomics and TLS are the only features we lower away + // during codegen. + if (ctx.arg.sharedMemory) { + c.MAttrs.push_back("+atomics"); + c.MAttrs.push_back("+bulk-memory"); + } + c.CGOptLevel = ctx.arg.ltoCgo; c.DebugPassManager = ctx.arg.ltoDebugPassManager; c.AlwaysEmitRegularLTOObj = !ctx.arg.ltoObjPath.empty();