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 fields configured for Bug.

    Projects

    Status

    Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions