rust-lang / rust-lang/rust

Incorrect codegen (instead of error) since 1.87.0, when all but one register is reserved on RISC-V.

Open
#146,393 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

A-codegen A-LLVM C-bug O-riscv P-medium regression-from-stable-to-stable T-compiler
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Code

Note: This is very convoluted and does not reflect any at-all reasonable real-world use-case (i.e. reserving every register except one).

I tried this code:

#[inline(never)]
pub fn foo(x: &[u32; 2]) -> u32 {
  x[0] + x[1]
}

Compiler invocation:

rustc  lib.rs --crate-type=lib --target=riscv64gc-unknown-linux-gnu -Copt-level=3 -Ctarget-feature=+reserve-x1,+reserve-x3,+reserve-x4,+reserve-x5,+reserve-x6,+reserve-x7,+reserve-x8,+reserve-x9,+reserve-x11,+reserve-x12,+reserve-x13,+reserve-x14,+reserve-x15,+reserve-x16,+reserve-x17,+reserve-x18,+reserve-x19,+reserve-x20,+reserve-x21,+reserve-x22,+reserve-x23,+reserve-x24,+reserve-x25,+reserve-x26,+reserve-x27,+reserve-x28,+reserve-x29,+reserve-x30,+reserve-x31

Probably reproducible with any riscv32 or riscv64 target (I tried riscv32i-unknown-none-elf and riscv64gc-unknown-linux-gnu). The main requirements are: -Copt-level=3, and we tell LLVM that all general-purpose registers except zero/x0, sp/x2, and a0/x10 are reserved.

I expected to see this happen: Compiler error due to register allocation failure[^1]. This used to be the case in Rust 1.86.0 and prior, before the LLVM 20 bump.

Instead, this happened: Successful compilation with incorrect assembly code.

0000000000000000 <_ZN3lib3foo17hc3dadf2c88cc8d04E>:
   0:	1141                	addi	sp,sp,-16 # 61302ff0 <_ZN3lib3foo17hc3dadf2c88cc8d04E+0x61302ff0>
   2:	e42a                	sd	a0,8(sp) # save x into a stack slot
   4:	6522                	ld	a0,8(sp) # load x from stack slot (inefficient, but not *wrong*)
   6:	4108                	lw	a0,0(a0) # load x[0] signext to i64 into a0
   8:	e02a                	sd	a0,0(sp) # save x[0] signext to i64 into a stack slot
   a:	6522                	ld	a0,8(sp) # load x from stack slot
   c:	4148                	lw	a0,4(a0) # load x[1] signext to i64 into a0
   e:	6502                	ld	a0,0(sp) # load x[0] signext to i64 from stack slot, *overwriting a0's previous value of x[1]*
  10:	9d29                	addw	a0,a0,a0 # this tries to add x[0] and x[1], but actually adds x[0] and x[0]
  12:	0141                	addi	sp,sp,16
  14:	8082                	ret # a0 is implicitly the return register
Version it worked on

It most recently worked on: Rust 1.86.0 and nightly-2025-02-17 (just before the LLVM 20 bump)

Version with regression

Rust 1.87.0 and nightly-2025-02-18 (just after the LLVM 20 bump), as well as Rust 1.89.0 stable, and nightly-2025-09-08

rustc --version --verbose:

rustc 1.87.0-nightly (ce36a966c 2025-02-17)
binary: rustc
commit-hash: ce36a966c79e109dabeef7a47fe68e5294c6d71e
commit-date: 2025-02-17
host: x86_64-unknown-linux-gnu
release: 1.87.0-nightly
LLVM version: 20.1.0
Backtrace

N/A

@rustbot modify labels: +regression-from-stable-to-stable -regression-untriaged A-LLVM A-codegen O-riscv


I assume this is an LLVM issue, but I could not reproduce it using local LLVM. I.e. I got the LLVM IR from affected rustc using --emit-llvm-ir and passed it to llc-20 (version 20.1.2), but that did give an error: error: <unknown>:0:0: ran out of registers during register allocation in function '_ZN13bullying_llvm3foo17h2329acc782955056E', so IDK what's different there. Maybe there was a bugfix between LLVM 20.1.0 and 20.1.2?

; ModuleID = 'e3s1mjt1cx7h2uznpt1s0mib7'
source_filename = "e3s1mjt1cx7h2uznpt1s0mib7"
target datalayout = "e-m:e-p:64:64-i64:64-i128:128-n32:64-S128"
target triple = "riscv64-unknown-linux-gnu"

; repro::foo
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind willreturn memory(argmem: read) uwtable
define noundef i32 @_ZN5repro3foo17h8e0ccca3a760da02E(ptr noalias noundef readonly align 4 captures(none) dereferenceable(8) %x) unnamed_addr #0 {
start:
  %_2 = load i32, ptr %x, align 4, !noundef !4
  %0 = getelementptr inbounds nuw i8, ptr %x, i64 4
  %_3 = load i32, ptr %0, align 4, !noundef !4
  %_0 = add i32 %_3, %_2
  ret i32 %_0
}

attributes #0 = { mustprogress nofree noinline norecurse nosync nounwind willreturn memory(argmem: read) uwtable "target-cpu"="generic-rv64" "target-features"="+m,+a,+f,+d,+c,+zicsr,+zifencei,+reserve-x1,+reserve-x3,+reserve-x4,+reserve-x5,+reserve-x6,+reserve-x7,+reserve-x8,+reserve-x9,+reserve-x11,+reserve-x12,+reserve-x13,+reserve-x14,+reserve-x15,+reserve-x16,+reserve-x17,+reserve-x18,+reserve-x19,+reserve-x20,+reserve-x21,+reserve-x22,+reserve-x23,+reserve-x24,+reserve-x25,+reserve-x26,+reserve-x27,+reserve-x28,+reserve-x29,+reserve-x30,+reserve-x31" }

!llvm.module.flags = !{!0, !1, !2}
!llvm.ident = !{!3}

!0 = !{i32 8, !"PIC Level", i32 2}
!1 = !{i32 1, !"Code Model", i32 3}
!2 = !{i32 1, !"target-abi", !"lp64d"}
!3 = !{!"rustc version 1.91.0-nightly (9c27f27ea 2025-09-08)"}
!4 = !{}
$ cargo +nightly rustc -- --emit=llvm-ir -Copt-level=3 -o nightly4.ll -Cdebuginfo=none
$ llc-20 nightly3-688d8f82623f4b82.ll -o out-20
error: <unknown>:0:0: ran out of registers during register allocation in function '_ZN5repro3foo17h8e0ccca3a760da02E'
# .cargo/config.toml
[build]
target = "riscv64gc-unknown-linux-gnu"

[target.riscv64gc-unknown-linux-gnu]
rustflags = ["-Ctarget-feature=+reserve-x1,+reserve-x3,+reserve-x4,+reserve-x5,+reserve-x6,+reserve-x7,+reserve-x8,+reserve-x9,+reserve-x11,+reserve-x12,+reserve-x13,+reserve-x14,+reserve-x15,+reserve-x16,+reserve-x17,+reserve-x18,+reserve-x19,+reserve-x20,+reserve-x21,+reserve-x22,+reserve-x23,+reserve-x24,+reserve-x25,+reserve-x26,+reserve-x27,+reserve-x28,+reserve-x29,+reserve-x30,+reserve-x31"]

[^1]: we need at least two GPRs to do an addition on RISC-V, and we have only one usable one, a0 (x0 is hardwired to zero, and sp is the stack pointer), so there's no reasonable way to codegen this.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the issue with the provided rustc RISC-V command and reserved-register flags, then inspect the emitted LLVM IR and generated assembly. Compare the bundled LLVM 20.1.0 behavior with llc-20 using LLVM 20.1.2, and determine why register allocation succeeds incorrectly instead of reporting an error. Done means compilation rejects the insufficient-register case or produces correct assembly.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.