From cc90ad6c306c734701477e92bcc33477d079bc27 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 23 Jul 2026 02:20:12 -0500 Subject: [PATCH] [wasm] Emit terminal `end` for EH funclets ending in a no-return call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crossgen2 emitted a malformed wasm function body for certain exception-handling funclets: the required terminal `end` opcode (0x0b) was dropped, so the module failed validation (V8 / `wasm-tools validate`: "function body must end with end opcode"). Observed on `System.Data.Common` (`DataColumn.set_Expression` funclet 4 and `DataTable.set_Locale` funclet 3) — both non-SIMD `BBJ_THROW` funclets whose last block ends in a no-return `call_indirect` throw-helper tail. The wasm end-emission guard `block->IsLast() || bbIsFuncletBeg(block->Next())` uses the GLOBAL block order (`bbNext`), in which a funclet's last block can be followed by an interspersed root throw-helper block (`fgIsThrowHlpBlk`, no handler index) that is not a funclet begin. Codegen iterates per-funclet, so the guard fails to recognize the funclet boundary and the terminal `end` is never emitted. This same unsound predicate was duplicated across four wasm end-emission sites (the `BBJ_THROW` and `BBJ_ALWAYS` arms of `genEmitEndBlock`, the retless `BBJ_CALLFINALLY` path in `genCallFinally`, and the `end`/`return` discriminator in `genFuncletEpilog`), which is what let this gap — and the incompleteness of the prior fix #129335 — arise. Consolidate the predicate into a single wasm helper `CodeGen::genIsLastBlockOfCurrentFunc` that also compares against the current func/funclet's authoritative last block (`funCurrentFunc()->GetLastBlock()`), and route all four sites through it. The added term is an additive OR, so each block still emits at most one terminal `end` (no double-`end` regression on the `BBJ_RETURN` / funclet-epilog paths). Validated by recrossgenning standalone `System.Data.Common` to wasm R2R: both funclets now emit their terminal `end` and the module passes `wasm-tools validate`, with no double-`end` regression across all functions. Related: #129335 (incomplete predecessor), #129449 (`genCallFinally` shares the predicate). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2e627b94-2658-41ce-a880-d9c27b7febd9 --- src/coreclr/jit/codegen.h | 1 + src/coreclr/jit/codegenlinear.cpp | 4 ++-- src/coreclr/jit/codegenwasm.cpp | 36 +++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/codegen.h b/src/coreclr/jit/codegen.h index f7d9192cb588ec..1869b94ed33b8c 100644 --- a/src/coreclr/jit/codegen.h +++ b/src/coreclr/jit/codegen.h @@ -229,6 +229,7 @@ class CodeGen final : public CodeGenInterface void genEmitBeginBlock(WasmValueType blockType = WasmValueType::Invalid); void genEmitEndBlock(); void genEmitFunctionEnd(bool emitTerminalUnreachable = true); + bool genIsLastBlockOfCurrentFunc(BasicBlock* block); #endif void genEmitStartBlock(BasicBlock* block); diff --git a/src/coreclr/jit/codegenlinear.cpp b/src/coreclr/jit/codegenlinear.cpp index 49f67585225024..2ca527c2722ed7 100644 --- a/src/coreclr/jit/codegenlinear.cpp +++ b/src/coreclr/jit/codegenlinear.cpp @@ -808,7 +808,7 @@ void CodeGen::genEmitEndBlock(BasicBlock* block) // At function/funclet end, close open intervals and emit `end` // (we've already emitted the terminating `unreachable` above). // - if (block->IsLast() || m_compiler->bbIsFuncletBeg(block->Next())) + if (genIsLastBlockOfCurrentFunc(block)) { genEmitFunctionEnd(/* emitTerminalUnreachable */ false); } @@ -910,7 +910,7 @@ void CodeGen::genEmitEndBlock(BasicBlock* block) // (e.g., backedge of an infinite loop), close any still-open // wasm intervals and emit the function-body terminator. // - if (block->IsLast() || m_compiler->bbIsFuncletBeg(block->Next())) + if (genIsLastBlockOfCurrentFunc(block)) { genEmitFunctionEnd(); } diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 75b6403dec8fb9..b8032de1f3f7df 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -471,6 +471,35 @@ void CodeGen::genFuncletProlog(BasicBlock* block) } } +//------------------------------------------------------------------------ +// genIsLastBlockOfCurrentFunc: determine whether `block` is the last block of the +// wasm function or funclet currently being generated -- i.e. the block after +// which the function body's terminal `end` opcode must be emitted. +// +// Arguments: +// block - the block being generated +// +// Return Value: +// true if `block` is the current func/funclet's last block. +// +// Notes: +// A wasm function body is structurally required to end in `end`, even when its +// last block is a no-return call (e.g. a throw-helper tail). `block->Next()` +// walks the GLOBAL block order (bbNext), in which a funclet's last block can be +// followed by an interspersed root throw-helper block (`fgIsThrowHlpBlk`, no +// handler index) that is not a funclet begin, so `bbIsFuncletBeg(block->Next())` +// fails to recognize the funclet boundary. Codegen iterates per-funclet +// (`genCodeForFunclet` over `funcInfo->Blocks()`), so also compare against the +// current func/funclet's authoritative last block (`FuncInfoDsc::GetLastBlock`: +// `ebdHndLast` for handlers, `BBFilterLast` for filters, and +// `fgLastBBInMainFunction` for the root). +// +bool CodeGen::genIsLastBlockOfCurrentFunc(BasicBlock* block) +{ + return block->IsLast() || m_compiler->bbIsFuncletBeg(block->Next()) || + (block == m_compiler->funCurrentFunc()->GetLastBlock(m_compiler)); +} + //------------------------------------------------------------------------ // genFuncletEpilog: codegen for funclet epilogs. // @@ -479,7 +508,10 @@ void CodeGen::genFuncletProlog(BasicBlock* block) // void CodeGen::genFuncletEpilog(BasicBlock* block) { - if (block->IsLast() || m_compiler->bbIsFuncletBeg(block->Next())) + // Emit the function-body terminator (`end`) when this epilog block is the last + // block of the funclet; otherwise emit `return` to unwind to the caller. + // + if (genIsLastBlockOfCurrentFunc(block)) { instGen(INS_end); } @@ -3781,7 +3813,7 @@ void CodeGen::genCallFinally(BasicBlock* block) // A retless BBJ_CALLFINALLY can be the last block of a wasm function/funclet; // every wasm function body must end with `end`. // - if (block->IsLast() || m_compiler->bbIsFuncletBeg(block->Next())) + if (genIsLastBlockOfCurrentFunc(block)) { GetEmitter()->emitIns(INS_end); }