From 47b53b9c055d89ca7631569720675f6af6142c3c Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Tue, 23 Jun 2026 19:23:17 -0700 Subject: [PATCH 1/2] JIT/wasm: emit branch in BBJ_CALLFINALLY for non-fallthrough callfinallyret BBJ_CALLFINALLYRET emits no code via genCodeForBlock's early return, relying on fall-through to the continuation. On wasm the layout may place the continuation elsewhere (e.g., a loop header reached via back-edge from a catch handler that leaves to a label outside its enclosing try-finally), in which case fall-through skips past the target. Emit an explicit branch in genCallFinally when the CALLFINALLYRET's continuation is not the linear next block. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 7a0bbb5efeb440..610c84f30db879 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -3587,6 +3587,29 @@ void CodeGen::genCallFinally(BasicBlock* block) { GetEmitter()->emitIns(INS_end); } + + return; + } + + // After call_indirect returns, control falls through to the next wasm instruction. + // The paired BBJ_CALLFINALLYRET has no code emitted (see genCodeForBlock), so the + // CALLFINALLYRET's continuation must be reached either by fall-through (when it's + // the next block in linear order) or by an explicit branch emitted here. + // + // This is wasm-specific: other targets rely on the native `call`/`ret` to land at + // the instruction after the call, and the JIT layout places the CALLFINALLYRET's + // continuation immediately after. On wasm the layout may place the continuation + // elsewhere (e.g. a loop header reached via back-edge, as produced by a leave from + // a catch handler that targets a label outside its enclosing try-finally). + // + assert(block->isBBCallFinallyPair()); + BasicBlock* const callFinallyRet = block->Next(); + assert(callFinallyRet->KindIs(BBJ_CALLFINALLYRET)); + BasicBlock* const continuation = callFinallyRet->GetTarget(); + + if (continuation->bbPreorderNum != callFinallyRet->bbPreorderNum + 1) + { + inst_JMP(EJ_jmp, continuation); } } From 40434fb630e11eb8bf88c408a4db4b9532e43ceb Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Wed, 24 Jun 2026 09:42:51 -0700 Subject: [PATCH 2/2] JIT/wasm: null check faulting GT_IND loads On wasm a load from a null address does not fault, so the array-length read in a decoupled bounds check returned 0 and threw IndexOutOfRange instead of NullReference. Emit an explicit null check for faulting loads, matching GT_STOREIND (lower marks the address multiply-used, regalloc reserves the temp, codegen emits the check using the same predicate as the throw-helper reservation phase). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 10 ++++++++++ src/coreclr/jit/lowerwasm.cpp | 7 +++++++ src/coreclr/jit/regallocwasm.cpp | 9 +++++++++ 3 files changed, 26 insertions(+) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 610c84f30db879..22badc840cac9c 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2652,6 +2652,16 @@ void CodeGen::genCodeForIndir(GenTreeIndir* tree) genConsumeAddress(tree->Addr()); + // On wasm a load from a null address does not fault, so emit an explicit null check + // for faulting loads (mirrors genCodeForStoreInd). Use the same predicate the throw- + // helper reservation phase uses (StackLevelSetter), so we never demand an unreserved + // throw helper block here. + if (((tree->gtFlags & GTF_EXCEPT) != 0) && tree->OperMayThrow(m_compiler)) + { + regNumber addrReg = GetMultiUseOperandReg(tree->Addr()); + genEmitNullCheck(addrReg); + } + // TODO-WASM: Memory barriers GetEmitter()->emitIns_I(ins, emitActualTypeSize(type), 0); diff --git a/src/coreclr/jit/lowerwasm.cpp b/src/coreclr/jit/lowerwasm.cpp index 78cda8482f64d6..1932e39b99aa42 100644 --- a/src/coreclr/jit/lowerwasm.cpp +++ b/src/coreclr/jit/lowerwasm.cpp @@ -458,6 +458,13 @@ void Lowering::ContainCheckIndir(GenTreeIndir* indirNode) return; } + if (indirNode->OperIs(GT_IND) && ((indirNode->gtFlags & GTF_IND_NONFAULTING) == 0)) + { + // On wasm a load from a null address does not fault, so a faulting load needs an + // explicit null check, which requires multiple uses of the address operand. + SetMultiplyUsed(indirNode->Addr() DEBUGARG("ContainCheckIndir faulting load Addr")); + } + // TODO-WASM-CQ: contain suitable LEAs here. Take note of the fact that for this to be correct we must prove the // LEA doesn't overflow. It will involve creating a new frontend node to represent "nuw" (offset) addition. } diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index f8cac164887670..485d2c63cfe841 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -490,6 +490,15 @@ void WasmRegAlloc::CollectReferencesForNode(GenTree* node) CollectReferencesForStoreInd(node->AsStoreInd()); break; + case GT_IND: + if (node->AsIndir()->Addr()->gtLIRFlags & LIR::Flags::MultiplyUsed) + { + // Faulting loads are null checked on wasm; consume the temporary register + // holding the address so it is available for both the check and the load. + ConsumeTemporaryRegForOperand(node->AsIndir()->Addr() DEBUGARG("indir load null check")); + } + break; + case GT_STORE_BLK: CollectReferencesForBlockStore(node->AsBlk()); break;