From 934f3f09b91dd902cadb4d06f63933a6c65c752b Mon Sep 17 00:00:00 2001 From: Valentin Churavy Date: Mon, 29 Jun 2026 20:04:15 +0200 Subject: [PATCH] Fix Julia 1.13 segfault on undef/partially-initialized struct return (#3087) On Julia 1.13 the augmented-forward pass of an inner function returning a small mixed tuple (e.g. `Tuple{Float64,Int64}` -> sret `{double,i64}`) is emitted by the core with the active scalar returned directly and the primal struct plus its shadow delivered through `enzyme_sret` output pointers. The caller never reads the primal cache and passes `undef` for the shadow sret. `propagate_returned!`'s interprocedural constant propagation treated "every caller passes undef" as license to replace the argument with `undef` inside the body. For an sret-like *output* pointer the body stores into the argument, so this produced `store i64 2, ptr undef`. Downstream InstCombine/SROA then treat that store-to-undef as UB and collapse the whole augmented function to `ret poison` / `unreachable` and strip every argument. At runtime the caller invokes the now-empty function, uses the undefined result, and falls through to garbage, segfaulting. Guard the constant-propagation so it never replaces an `sret`/`enzyme_sret`/`enzyme_sret_v` output pointer with `undef` when there is no concrete value to propagate. Also fix a latent typo in `delete_writes_into_removed_args` where erasing a store into a removed argument referenced the undefined `nphi` instead of `cur`. The `Undef sret` regression test in `test/core/abi.jl` now passes on 1.13; results match 1.12 (finite-difference verified). Co-Authored-By: Claude Opus 4.8 --- src/llvm/transforms.jl | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/llvm/transforms.jl b/src/llvm/transforms.jl index 21a89bf988..b6d977e481 100644 --- a/src/llvm/transforms.jl +++ b/src/llvm/transforms.jl @@ -1901,6 +1901,11 @@ function remove_readonly_unused_calls!(fn::LLVM.Function, next::Set{String}) end function propagate_returned!(mod::LLVM.Module) + sretkind = LLVM.kind(if LLVM.version().major >= 12 + LLVM.TypeAttribute("sret", LLVM.Int32Type()) + else + LLVM.EnumAttribute("sret") + end) globs = LLVM.GlobalVariable[] for g in globals(mod) if linkage(g) == LLVM.API.LLVMInternalLinkage || @@ -2133,6 +2138,24 @@ function propagate_returned!(mod::LLVM.Module) illegalUse = true break end + # If every caller passes undef/poison for this argument (val === nothing), + # the interprocedural const-prop below would replace the argument with undef + # inside the body. That is only sound for arguments used purely as values. + # For an sret-like output pointer (`sret`/`enzyme_sret`/`enzyme_sret_v`) the + # body *stores into* the argument, so replacing it with undef turns those + # stores into stores-to-undef. Downstream InstCombine/SROA then treat that as + # UB and collapse the whole function to `unreachable`/`poison`, miscompiling + # the augmented forward pass (segfault, issue #3087). Skip the replacement for + # such write-only output pointers when there is no concrete value to propagate. + if !illegalUse && val === nothing && any( + ( + kind(attr) == sretkind || + kind(attr) == kind(StringAttribute("enzyme_sret")) || + kind(attr) == kind(StringAttribute("enzyme_sret_v")) + ) for attr in collect(parameter_attributes(fn, i)) + ) + illegalUse = true + end if !illegalUse if val === nothing val = LLVM.UndefValue(value_type(arg)) @@ -2332,7 +2355,7 @@ function delete_writes_into_removed_args(fn::LLVM.Function, toremove::Vector{Int cur, cval = pop!(todorep) if isa(cur, LLVM.StoreInst) if operands(cur)[2] == cval - LLVM.API.LLVMInstructionEraseFromParent(nphi) + LLVM.API.LLVMInstructionEraseFromParent(cur) continue end end