[lld][WebAssembly] Fix spurious signature mismatch under LTO (#136197)

When generating C++ vtables, Clang declares virtual functions as
`void(void)` when their signature is not known (e.g.parameter types are
forward-declared). As WASM type checks imports, this would conflict with
the real definition during linking. Commit 59f959ff introduced a
workaround for this by deferring signature assignment until a definition
or direct call is seen.

When performing LTO, LLD first scans the bitcode files and creates
`DefinedFunction` symbol table entries for their contents. After LTO
codegen, they are replaced with `UndefinedFunction`s (so that the
definitions will be pulled in from the native LTO-d files when they are
added). At this point, if a function is only referenced in bitcode, its
signature remains `nullptr`.

From here, it should have behaved like in the non-LTO case: the first
direct call sets the signature. However, as the `isCalledDirectly` flag
was set to true, the missing signature was filled in by the type of the
first reference to the function, which could be a `void(void)` vtable
entry, which would then conflict with the real definition.

This commit sets `isCalledDirectly` to false so that the signature will
only be populated when a direct call is found.

See godotengine/godot#104497 and
emscripten-core/emscripten#10831
This commit is contained in:
Daniel Bertalan 2025-04-18 18:36:54 +02:00 committed by GitHub
parent f4a47b4003
commit bc48b3f8b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 1 deletions

View File

@ -0,0 +1,45 @@
; RUN: rm -rf %t && split-file %s %t && cd %t
; RUN: opt -thinlto-bc a.ll -o a.o
; RUN: opt -thinlto-bc b.ll -o b.o
; RUN: llvm-ar rcs b.a b.o
; RUN: opt -thinlto-bc c.ll -o c.o
;; Taking the address of the incorrectly declared @foo should not generate a warning.
; RUN: wasm-ld --fatal-warnings --no-entry --export-all a.o b.a -o a.out \
; RUN: | FileCheck %s --implicit-check-not 'warning' --allow-empty
;; But we should still warn if we call the function with the wrong signature.
; RUN: not wasm-ld --fatal-warnings --no-entry --export-all a.o b.a c.o -o b.out 2>&1 \
; RUN: | FileCheck %s --check-prefix=INVALID
; INVALID: error: function signature mismatch: foo
; INVALID: >>> defined as () -> void
; INVALID: >>> defined as () -> i32
;--- a.ll
target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20"
target triple = "wasm32-unknown-unknown"
@ptr = constant ptr @foo
declare void @foo()
;--- b.ll
target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20"
target triple = "wasm32-unknown-unknown"
define i32 @foo() noinline {
entry:
ret i32 42
}
;--- c.ll
target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20"
target triple = "wasm32-unknown-unknown"
declare void @foo()
define void @invalid() {
entry:
call void @foo()
ret void
}

View File

@ -108,9 +108,10 @@ BitcodeCompiler::~BitcodeCompiler() = default;
static void undefine(Symbol *s) {
if (auto f = dyn_cast<DefinedFunction>(s))
// If the signature is null, there were no calls from non-bitcode objects.
replaceSymbol<UndefinedFunction>(f, f->getName(), std::nullopt,
std::nullopt, 0, f->getFile(),
f->signature);
f->signature, f->signature != nullptr);
else if (isa<DefinedData>(s))
replaceSymbol<UndefinedData>(s, s->getName(), 0, s->getFile());
else