llvm / llvm/llvm-project

[WebAssembly] ISel emits invalid call x@GOT instead of call x when devirtualizing indirect calls in PIC mode

Open
#215,664 1 comment 0 reactions 0 assignees View on GitHub
backend:WebAssembly miscompilation
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

Note: This bug was produced using an AI agent on a downstream emscripten issue: https://github.com/emscripten-core/emscripten/issues/27525

In WebAssembly PIC mode (`-relocation-model=pic`), devirtualized indirect function calls (e.g. indirect calls through function pointers loaded from memory that `DAGCombiner` optimizes to direct function calls) cause the ISel backend to emit `call x@GOT` with relocation `R_WASM_GLOBAL_INDEX_LEB` instead of `call x` with `R_WASM_FUNCTION_INDEX_LEB`.

When linked by `wasm-ld`, `R_WASM_GLOBAL_INDEX_LEB` resolves to a WebAssembly Global Index rather than a Function Index. At runtime or during post-link `wasm-opt` passes, calling a global index causes evaluation stack corruption (`popping from empty stack` error).

*Relates to Emscripten issue:* https://github.com/emscripten-core/emscripten/issues/27525

---

## Minimal Repro (`repro.ll`)

```llvm
target triple = "wasm32-unknown-emscripten"

@t = global ptr null, align 4

define void @o() {
entry:
store ptr @x, ptr @t, align 4
%0 = load ptr, ptr @t, align 4
tail call void %0()
ret void
}

declare void @x()
```

## Command
```bash
llc -march=wasm32 -relocation-model=pic -O1 repro.ll -o -
```

## Actual Output

```wasm
o:
.functype o () -> ()
.local i32
global.get x@GOT
local.set 0
global.get t@GOT
local.get 0
i32.store 0
call x@GOT # <--- BUG: invalid call target!
end_function
```

## Expected Output

```wasm
o:
.functype o () -> ()
.local i32
global.get x@GOT
local.set 0
global.get t@GOT
local.get 0
i32.store 0
call x # <--- Correct direct call target
end_function
```

---

## Root Cause Details

1. Initial call lowering creates `WebAssemblyISD::CALL` wrapping a `load` node.
2. `DAGCombiner` devirtualizes the `load` node to `GlobalAddressSDNode` `@x`.
3. During DAG Legalization in `-fPIC` mode, `LowerGlobalAddress` lowers `GlobalAddressSDNode` `@x` into `WebAssemblyISD::Wrapper` wrapping `TargetGlobalAddressSDNode` with `TargetFlags = WebAssemblyII::MO_GOT`.
4. In `WebAssemblyISelDAGToDAG.cpp`, when matching `WebAssemblyISD::CALL` / `RET_CALL`, the instruction selector strips the `WebAssemblyISD::Wrapper` node for function targets, but retains the underlying `TargetGlobalAddressSDNode` which still carries `TargetFlags = MO_GOT`.
5. `MCInstLower` sees `MO_GOT` on the call target operand of `CALL_PARAMS` and lowers it to `call x@GOT`, attaching `MCSymbolRefExpr::VK_WASM_GOT` and generating relocation `R_WASM_GLOBAL_INDEX_LEB`.

---

## Suggested Fix

In `llvm/lib/Target/WebAssembly/WebAssemblyISelDAGToDAG.cpp`, when stripping `WebAssemblyISD::Wrapper` from operand 1 (the call target) of a `WebAssemblyISD::CALL` or `WebAssemblyISD::RET_CALL` node, reset/clear the target flags to `WebAssemblyII::MO_NO_FLAG` if it points to a `Function`:

```diff
--- a/llvm/lib/Target/WebAssembly/WebAssemblyISelDAGToDAG.cpp
+++ b/llvm/lib/Target/WebAssembly/WebAssemblyISelDAGToDAG.cpp
@@ -441,8 +441,16 @@ void WebAssemblyDAGToDAGISel::Select(SDNode *Node) {
SDValue NewOp = Op->getOperand(0);
if (auto *GlobalOp = dyn_cast(NewOp.getNode())) {
if (isa(
- GlobalOp->getGlobal()->stripPointerCastsAndAliases()))
- Op = NewOp;
+ GlobalOp->getGlobal()->stripPointerCastsAndAliases())) {
+ if (GlobalOp->getTargetFlags() != WebAssemblyII::MO_NO_FLAG) {
+ Op = CurDAG->getTargetGlobalAddress(
+ GlobalOp->getGlobal(), SDLoc(GlobalOp),
+ GlobalOp->getValueType(0), GlobalOp->getOffset(),
+ WebAssemblyII::MO_NO_FLAG);
+ } else {
+ Op = NewOp;
+ }
+ }
} else if (isa(NewOp.getNode())) {
Op = NewOp;
}
```

Contributor guide

Open the contributing guide

Research direction

Start in llvm/lib/Target/WebAssembly/WebAssemblyISelDAGToDAG.cpp, focusing on CALL and RET_CALL selection and how Wrapper operands are stripped. Run llc -march=wasm32 -relocation-model=pic -O1 repro.ll -o - using the provided minimal repro, then verify the call target uses the function relocation and emits call x rather than call x@GOT.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, wasm
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.