[Mlir-commits] [mlir] 5e814e2 - [mlir][llvm] Fix crash in LLVM inliner when callee has no recognized terminator (#183949)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Tue Mar 3 02:35:54 PST 2026
Author: Mehdi Amini
Date: 2026-03-03T11:35:49+01:00
New Revision: 5e814e26dd72d14ac1118a647210294d38c8d01e
URL: https://github.com/llvm/llvm-project/commit/5e814e26dd72d14ac1118a647210294d38c8d01e
DIFF: https://github.com/llvm/llvm-project/commit/5e814e26dd72d14ac1118a647210294d38c8d01e.diff
LOG: [mlir][llvm] Fix crash in LLVM inliner when callee has no recognized terminator (#183949)
When the callee of an llvm.call has a body block ending with an
unregistered op (rather than a recognized LLVM terminator like
llvm.return), the LLVM inliner's handleTerminator method was called with
that unregistered op and crashed via a cast<LLVM::ReturnOp>() assertion
or use-after-erase due to unresolved call result uses.
The root cause is that the generic MLIR verifier conservatively treats
unregistered ops as potential terminators (using mightHaveTrait), so
malformed IR of this shape passes verification. The inliner, however,
assumes that the callee's terminator is a recognized LLVM op.
Fix by adding a guard in LLVMInlinerInterface::isLegalToInline() that
refuses to inline a callee containing any block whose last operation
does not have the IsTerminator trait. This prevents the crash and leaves
the call site intact without any IR mutation.
Fixes #108363
Fixes #118766
Added:
Modified:
mlir/lib/Dialect/LLVMIR/Transforms/InlinerInterfaceImpl.cpp
mlir/test/Dialect/LLVMIR/inlining.mlir
Removed:
################################################################################
diff --git a/mlir/lib/Dialect/LLVMIR/Transforms/InlinerInterfaceImpl.cpp b/mlir/lib/Dialect/LLVMIR/Transforms/InlinerInterfaceImpl.cpp
index 06c0af5445d5d..0e43480e82926 100644
--- a/mlir/lib/Dialect/LLVMIR/Transforms/InlinerInterfaceImpl.cpp
+++ b/mlir/lib/Dialect/LLVMIR/Transforms/InlinerInterfaceImpl.cpp
@@ -725,6 +725,20 @@ struct LLVMInlinerInterface : public DialectInlinerInterface {
}))
return false;
}
+ // Refuse to inline if any block in the callee ends with an op that does
+ // not have the terminator trait. The MLIR verifier conservatively accepts
+ // unregistered ops as potential terminators (via mightHaveTrait), but
+ // handleTerminator uses cast<LLVM::ReturnOp> in the single-block path and
+ // would crash on such ops. Registered terminators from other dialects
+ // (e.g. cf.br) are safe: the multi-block path uses dyn_cast and skips
+ // non-llvm.return ops gracefully.
+ for (Block &block : funcOp.getBody()) {
+ if (!block.empty() && !block.back().hasTrait<OpTrait::IsTerminator>()) {
+ LDBG() << "Cannot inline " << funcOp.getSymName()
+ << ": block ends with non-terminator op";
+ return false;
+ }
+ }
return true;
}
diff --git a/mlir/test/Dialect/LLVMIR/inlining.mlir b/mlir/test/Dialect/LLVMIR/inlining.mlir
index 9a77c5e223110..70ce7ca20986b 100644
--- a/mlir/test/Dialect/LLVMIR/inlining.mlir
+++ b/mlir/test/Dialect/LLVMIR/inlining.mlir
@@ -709,3 +709,42 @@ func.func @llvm_ret(%arg0 : i32) -> i32 {
// CHECK: return %[[R]]
return %res : i32
}
+
+// -----
+// Regression test for https://github.com/llvm/llvm-project/issues/118766
+// A callee whose body block ends with an unregistered (non-terminator) op used
+// to crash the inliner. The inliner should skip such callees instead.
+
+llvm.func @callee_malformed_terminator() -> i64 attributes {llvm.emit_c_interface} {
+ // Unregistered op acting as a fake terminator -- the function has no llvm.return.
+ "test.foo"() : () -> ()
+}
+
+// CHECK-LABEL: @caller_malformed_terminator
+llvm.func @caller_malformed_terminator() -> i64 attributes {llvm.emit_c_interface} {
+ // CHECK: llvm.call @callee_malformed_terminator
+ %0 = llvm.call @callee_malformed_terminator() : () -> i64
+ llvm.return %0 : i64
+}
+
+// -----
+// Complement to the above: a callee whose block ends with a registered
+// non-LLVM terminator that genuinely has the IsTerminator trait (cf.br) does
+// NOT trigger the guard. The call is inlined normally via the multi-block path
+// (handleTerminator uses dyn_cast for non-llvm.return ops, so cf.br is left
+// as-is and control flow reaches the llvm.return in the successor block).
+
+llvm.func @callee_cf_br_terminator(%arg0 : i64) -> i64 {
+ cf.br ^exit(%arg0 : i64)
+^exit(%val : i64):
+ llvm.return %val : i64
+}
+
+// CHECK-LABEL: @caller_cf_br_terminator
+llvm.func @caller_cf_br_terminator(%arg0 : i64) -> i64 {
+ // cf.br has IsTerminator, so isLegalToInline allows inlining.
+ // CHECK-NOT: llvm.call @callee_cf_br_terminator
+ // CHECK: llvm.return %arg0
+ %0 = llvm.call @callee_cf_br_terminator(%arg0) : (i64) -> i64
+ llvm.return %0 : i64
+}
More information about the Mlir-commits
mailing list