Skip to content

[RISCV] Do not tail call optimize if arguments are passed indirectly#184972

Closed
xroche wants to merge 1 commit intollvm:mainfrom
xroche:riscv-tail-call-indirect-fix
Closed

[RISCV] Do not tail call optimize if arguments are passed indirectly#184972
xroche wants to merge 1 commit intollvm:mainfrom
xroche:riscv-tail-call-indirect-fix

Conversation

@xroche
Copy link
Copy Markdown
Contributor

@xroche xroche commented Mar 6, 2026

When an argument is passed indirectly (CCValAssign::Indirect), the caller allocates stack space and passes a pointer. A tail call deallocates the caller's frame before the callee executes, leaving the pointer dangling.

X86 already guards against this in isEligibleForSiblingCallOpt. This adds the same check to the RISC-V backend. The musttail case is handled separately in #185094.

The existing tail-calls.ll CHECK lines were asserting the buggy behavior; regenerated via update_llc_test_checks.py.

AI Disclosure

This patch was developed with assistance from Claude (Anthropic). All code has been reviewed and validated by the author.

When a function argument is passed indirectly (CCValAssign::Indirect),
the caller allocates stack space for the value and passes a pointer to
the callee. If the call is tail-called, the caller's stack frame is
deallocated before the callee executes, leaving the pointer dangling
(use-after-free on the stack).

X86 already guards against this case in isEligibleForSiblingCallOpt.
The RISC-V backend was missing this check in
isEligibleForTailCallOptimization.

The existing tail-calls.ll test had a comment stating "Do not tail call
optimize if parameters need to be passed indirectly" but the CHECK
lines were asserting the incorrect (buggy) behavior, where a tail call
was generated despite the dangling pointer. Regenerated via
update_llc_test_checks.py.

Assisted-by: Claude (Anthropic)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Mar 6, 2026

@llvm/pr-subscribers-backend-risc-v

Author: Xavier Roche (xroche)

Changes

When a function argument is passed indirectly (CCValAssign::Indirect), the caller allocates stack space for the value and passes a pointer to the callee. If the call is tail-called, the caller's stack frame is deallocated before the callee executes, leaving the pointer dangling (use-after-free on the stack).

X86 already guards against this case in isEligibleForSiblingCallOpt (X86ISelLoweringCall.cpp:3070). The RISC-V backend was missing this check in isEligibleForTailCallOptimization.

The fix adds a check that rejects tail call optimization when any argument location has CCValAssign::Indirect.

The existing tail-calls.ll test had a comment stating "Do not tail call optimize if parameters need to be passed indirectly" but the CHECK lines were asserting the incorrect (buggy) behavior, where a tail instruction was generated despite the dangling pointer. CHECK lines regenerated via update_llc_test_checks.py.

Note: musttail with indirect args on RV32 (e.g., fp128) also has the same dangling-pointer issue in the current codebase -- the data is copied to a local stack slot that is deallocated before the tail jump. This is a pre-existing problem and is out of scope for this patch; properly supporting musttail with indirect arguments would require forwarding the original pointer rather than copying data to a new stack slot.

AI Disclosure

This patch was developed with assistance from Claude (Anthropic). All code has been reviewed and validated by the author.


Full diff: https://github.com/llvm/llvm-project/pull/184972.diff

2 Files Affected:

  • (modified) llvm/lib/Target/RISCV/RISCVISelLowering.cpp (+9)
  • (modified) llvm/test/CodeGen/RISCV/tail-calls.ll (+12-6)
diff --git a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
index 891bc22a7463d..2cbe7722726f4 100644
--- a/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
+++ b/llvm/lib/Target/RISCV/RISCVISelLowering.cpp
@@ -24578,6 +24578,15 @@ bool RISCVTargetLowering::isEligibleForTailCallOptimization(
   if (CCInfo.getStackSize() > RVFI->getArgumentStackSize())
     return false;
 
+  // Do not tail call optimize if any argument needs to be passed indirectly.
+  // The caller allocates stack space and passes a pointer to the callee. On a
+  // tail call the caller's stack frame is deallocated before the callee
+  // executes, invalidating the pointer (use-after-free).
+  for (const auto &VA : ArgLocs) {
+    if (VA.getLocInfo() == CCValAssign::Indirect)
+      return false;
+  }
+
   // Do not tail call opt if either caller or callee uses struct return
   // semantics.
   auto IsCallerStructRet = Caller.hasStructRetAttr();
diff --git a/llvm/test/CodeGen/RISCV/tail-calls.ll b/llvm/test/CodeGen/RISCV/tail-calls.ll
index 33feba3c6fba1..79855aa03adcf 100644
--- a/llvm/test/CodeGen/RISCV/tail-calls.ll
+++ b/llvm/test/CodeGen/RISCV/tail-calls.ll
@@ -247,20 +247,24 @@ declare i32 @callee_indirect_args(fp128 %a)
 define void @caller_indirect_args() nounwind {
 ; CHECK-LABEL: caller_indirect_args:
 ; CHECK:       # %bb.0: # %entry
-; CHECK-NEXT:    addi sp, sp, -16
+; CHECK-NEXT:    addi sp, sp, -32
+; CHECK-NEXT:    sw ra, 28(sp) # 4-byte Folded Spill
 ; CHECK-NEXT:    lui a1, 262128
 ; CHECK-NEXT:    mv a0, sp
 ; CHECK-NEXT:    sw zero, 0(sp)
 ; CHECK-NEXT:    sw zero, 4(sp)
 ; CHECK-NEXT:    sw zero, 8(sp)
 ; CHECK-NEXT:    sw a1, 12(sp)
-; CHECK-NEXT:    addi sp, sp, 16
-; CHECK-NEXT:    tail callee_indirect_args
+; CHECK-NEXT:    call callee_indirect_args
+; CHECK-NEXT:    lw ra, 28(sp) # 4-byte Folded Reload
+; CHECK-NEXT:    addi sp, sp, 32
+; CHECK-NEXT:    ret
 ;
 ; CHECK-LARGE-ZICFILP-LABEL: caller_indirect_args:
 ; CHECK-LARGE-ZICFILP:       # %bb.0: # %entry
 ; CHECK-LARGE-ZICFILP-NEXT:    lpad 0
-; CHECK-LARGE-ZICFILP-NEXT:    addi sp, sp, -16
+; CHECK-LARGE-ZICFILP-NEXT:    addi sp, sp, -32
+; CHECK-LARGE-ZICFILP-NEXT:    sw ra, 28(sp) # 4-byte Folded Spill
 ; CHECK-LARGE-ZICFILP-NEXT:    lui a1, 262128
 ; CHECK-LARGE-ZICFILP-NEXT:  .Lpcrel_hi9:
 ; CHECK-LARGE-ZICFILP-NEXT:    auipc a0, %pcrel_hi(.LCPI7_0)
@@ -270,8 +274,10 @@ define void @caller_indirect_args() nounwind {
 ; CHECK-LARGE-ZICFILP-NEXT:    sw zero, 4(sp)
 ; CHECK-LARGE-ZICFILP-NEXT:    sw zero, 8(sp)
 ; CHECK-LARGE-ZICFILP-NEXT:    sw a1, 12(sp)
-; CHECK-LARGE-ZICFILP-NEXT:    addi sp, sp, 16
-; CHECK-LARGE-ZICFILP-NEXT:    jr t2
+; CHECK-LARGE-ZICFILP-NEXT:    jalr t2
+; CHECK-LARGE-ZICFILP-NEXT:    lw ra, 28(sp) # 4-byte Folded Reload
+; CHECK-LARGE-ZICFILP-NEXT:    addi sp, sp, 32
+; CHECK-LARGE-ZICFILP-NEXT:    ret
 entry:
   %call = tail call i32 @callee_indirect_args(fp128 0xL00000000000000003FFF000000000000)
   ret void

Copy link
Copy Markdown
Contributor

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a testcase for a musttail call. (For musttail, we know the signatures match, so we have a place to store the value: the memory allocated by the caller.)

@efriedma-quic
Copy link
Copy Markdown
Contributor

Oh, wait, I see, you added a comment about musttail... please file a bug with your findings.

@xroche
Copy link
Copy Markdown
Contributor Author

xroche commented Mar 6, 2026

Oh, wait, I see, you added a comment about musttail... please file a bug with your findings.

Tentative PR with associated bug:
#185094

Copy link
Copy Markdown
Contributor

@efriedma-quic efriedma-quic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ... but please wait for @folkertdev to comment, since he modified this test recently in #170547. (CC @topperc )

@topperc topperc requested a review from lenary March 7, 2026 06:55
@xroche
Copy link
Copy Markdown
Contributor Author

xroche commented Mar 14, 2026

Should this PR be closed ? #185094 includes this fix too, and it might make more sense to include all in one.

@xroche xroche closed this Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants