Skip to content

[WebAssembly] eh regression from llvm 20 to 21 with nested catchpads #187302

@hoodmane

Description

@hoodmane

Compiling and linking the following llvm ir behaves differently between llvm 20 and llvm 21. I think the llvm 21 behavior is a miscompilation. Causes this downstream issue: rust-lang/rust#153948

Gist: https://gist.github.com/hoodmane/50aac5028f24352fa59685acbea6b6e6

wasm_eh_regression.ll

target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-i128:128-f128:64-n32:64-S128-ni:1:10:20"
target triple = "wasm32-unknown-emscripten"

declare ptr @llvm.wasm.get.exception(token)
declare i32 @llvm.wasm.get.ehselector(token)
declare i32 @__gxx_wasm_personality_v0(...)

declare void @resume_unwind(i32) noreturn
declare void @do_catch(ptr, i32) nounwind
declare void @start_cleanup()
declare void @end_cleanup()
declare void @panic_in_cleanup() noreturn nounwind
declare void @panic_cannot_unwind() noreturn nounwind

define void @catch_unwind_in_cleanup() personality ptr @__gxx_wasm_personality_v0 {
start:
  ; Outer resume_unwind
  invoke void @resume_unwind(i32 1)
          to label %unreachable unwind label %outer_cleanuppad

outer_cleanuppad:
  %outer_pad = cleanuppad within none []

  call void @start_cleanup() [ "funclet"(token %outer_pad) ]
  ; Inner resume_unwind inside of drop
  invoke void @resume_unwind(i32 2) [ "funclet"(token %outer_pad) ]
          to label %unreachable unwind label %inner_catchswitch

; inner catch_unwind
inner_catchswitch:
  %inner_cs = catchswitch within %outer_pad [label %inner_catchpad] unwind label %terminate_catchswitch

inner_catchpad:
  %inner_cp = catchpad within %inner_cs [ptr null]
  %exn = call ptr @llvm.wasm.get.exception(token %inner_cp)
  %sel = call i32 @llvm.wasm.get.ehselector(token %inner_cp)
  invoke void @do_catch(ptr %exn, i32 2) [ "funclet"(token %inner_cp) ]
          to label %inner_catchret unwind label %terminate_catchswitch

; Call panic_in_cleanup if exception propagates out of inner_catchpad
; we catch everything so %terminate_catchswitch should be unreachable
terminate_catchswitch:
  %term_cs = catchswitch within %outer_pad [label %terminate_catchpad] unwind label %outer_catchswitch
terminate_catchpad:
  %term_pad = catchpad within %term_cs [ptr null]
  call void @panic_in_cleanup() [ "funclet"(token %term_pad) ]
  unreachable

inner_catchret:
  ; catchret exits the inner catchpad — back in the outer cleanup funclet
  catchret from %inner_cp to label %outer_cleanupret

; propagate original exception
outer_cleanupret:
  call void @end_cleanup() [ "funclet"(token %outer_pad) ]
  cleanupret from %outer_pad unwind label %outer_catchswitch

; catch original exception
outer_catchswitch:
  %outer_cs = catchswitch within none [label %outer_catchpad] unwind to caller
outer_catchpad:
  %outer_cp = catchpad within %outer_cs [ptr null]
  %exn2 = call ptr @llvm.wasm.get.exception(token %outer_cp)
  %sel2 = call i32 @llvm.wasm.get.ehselector(token %outer_cp)
  call void @do_catch(ptr %exn2, i32 1) [ "funclet"(token %outer_cp) ]
  catchret from %outer_cp to label %done

done:
  ret void
unreachable:
  unreachable
}

main.cpp

#include <stdexcept>
#include <stdio.h>

extern "C" void resume_unwind(int id) {
    printf("resume_unwind %d\n", id);
    throw std::runtime_error("panic");
}

extern "C" void do_catch(void* ptr, int id) {
    printf("do_catch %d\n", id);
}

extern "C" void start_cleanup() {
    printf("start cleanup\n");
}

extern "C" void end_cleanup() {
    printf("end cleanup\n");
}

extern "C" void panic_in_cleanup() {
    printf("panic_in_cleanup\n");
    abort();
}

// Defined in the .ll file
extern "C" void catch_unwind_in_cleanup();

int main() {
    catch_unwind_in_cleanup();
    printf("returned successfully!\n");
    return 0;
}

run.sh

mkdir -p out

llc-20 \
    -mtriple=wasm32-unknown-emscripten \
    -exception-model=wasm \
    -wasm-enable-eh \
    -mattr=+exception-handling \
    -filetype=obj \
    wasm_eh_regression.ll \
    -o out/wasm_eh_regression_llvm_20.o

llc-21 \
    -mtriple=wasm32-unknown-emscripten \
    -exception-model=wasm \
    -wasm-enable-eh \
    -mattr=+exception-handling \
    -filetype=obj \
    wasm_eh_regression.ll \
    -o out/wasm_eh_regression_llvm_21.o

em++ -fwasm-exceptions -o out/test_llvm_20.js main.cpp out/wasm_eh_regression_llvm_20.o
em++ -fwasm-exceptions -o out/test_llvm_21.js main.cpp out/wasm_eh_regression_llvm_21.o

echo "llvm 20"
echo "--------"

node out/test_llvm_20.js

echo ""
echo ""

echo "llvm 21"
echo "--------"

node out/test_llvm_21.js

em++ --version

Output:

Details

$ bash run.sh 
llvm 20
--------
resume_unwind 1
start cleanup
resume_unwind 2
do_catch 2
end cleanup
do_catch 1
returned successfully!


llvm 21
--------
resume_unwind 1
start cleanup
resume_unwind 2
do_catch 2
end cleanup
panic_in_cleanup
Aborted(native code called abort())
wasm://wasm/0006d99e:1


RuntimeError: unreachable
    at wasm://wasm/0006d99e:wasm-function[520]:0x17111
    at abort (/home/hood/Documents/programming/tmp/llvm-wasm-eh-bug/out/test_llvm_21.js:587:5)
    at __abort_js (/home/hood/Documents/programming/tmp/llvm-wasm-eh-bug/out/test_llvm_21.js:1053:7)
    at wasm://wasm/0006d99e:wasm-function[16]:0x995
    at wasm://wasm/0006d99e:wasm-function[12]:0x8a2
    at wasm://wasm/0006d99e:wasm-function[15]:0x988
    at wasm://wasm/0006d99e:wasm-function[13]:0x8c8
    at wasm://wasm/0006d99e:wasm-function[14]:0x8f2
    at /home/hood/Documents/programming/tmp/llvm-wasm-eh-bug/out/test_llvm_21.js:623:12
    at callMain (/home/hood/Documents/programming/tmp/llvm-wasm-eh-bug/out/test_llvm_21.js:1747:15)

Node.js v24.14.0
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 5.0.3 (285c424dfa9e83b03cf8490c65ceadb7c45f28eb)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Needs Fix

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions