Skip to content

codegen_ssa: copy tupled rust-call constant args before indirect pass#155291

Open
blackms wants to merge 1 commit intorust-lang:mainfrom
blackms:fix-rust-call-const-arg-155241
Open

codegen_ssa: copy tupled rust-call constant args before indirect pass#155291
blackms wants to merge 1 commit intorust-lang:mainfrom
blackms:fix-rust-call-const-arg-155241

Conversation

@blackms
Copy link
Copy Markdown

@blackms blackms commented Apr 14, 2026

Fixes #155241 (the const-argument / indirect-ABI half of the issue; the MIR-GVN half is tracked separately).

Problem

Invoking a closure with the extern "rust-call" ABI on a constant tuple whose layout is passed indirectly has been miscompiling since nightly-2017-11-18. On Linux the program SIGBUSes; on Windows it aborts with STATUS_HEAP_CORRUPTION.

Original reproducer (still a smoking gun at -Copt-level=0 today):

#![feature(fn_traits)]

#[derive(Copy, Clone)]
struct Thing { x: usize, y: usize, z: usize }

const VALUE: (Thing,) = (Thing { x: 0, y: 0, z: 0 },);

fn main() {
    (|mut thing: Thing| { thing.z = 1; }).call(VALUE);
}

The root cause is in codegen_arguments_untupled (compiler/rustc_codegen_ssa/src/mir/block.rs). When the MIR operand is Operand::Copy or Operand::Constant and the tuple's backend layout is by-ref (BackendRepr::Memory), codegen_operand returns OperandValue::Ref(alloc_addr) where alloc_addr points into a shared, read-only allocation (typically a promoted constant in .rodata). The field projections downstream load field pointers out of that memory, so the extern "rust-call" callee — which owns its argument storage by ABI contract — writes through read-only memory when it mutates.

Fix

This is exactly the hazard that #45996 fixed for the normal (non-tupled) call path in codegen_call_terminator, by spilling Copy/Constant operands that arrive by-ref into a fresh alloca before passing a pointer. That safety-copy was never applied to the tupled rust-call path; this PR ports the same logic across, spilling the whole tuple before the field projections and wiring the lifetime_start / lifetime_end pair into the existing lifetime_ends_after_call bookkeeping. The new block is a line-for-line parallel of the existing code a few hundred lines above in the same file.

Test

Adds a //@ run-pass regression test at tests/ui/closures/rust-call-const-arg-alias.rs exercising the 2017 reproducer through Fn::call / FnMut::call_mut / FnOnce::call_once, at both -Copt-level=0 (where the bug aborts deterministically without the fix) and -Copt-level=3 (coverage that the fix does not regress release codegen). The test uses assert_eq! on the observed mutation so a future regression that silently ignores the write, rather than crashing, is also caught.

Verification

Bug reproduction was confirmed locally on rustc 1.97.0-nightly (17584a181 2026-04-13) using the MCVE above: exit 138 (SIGBUS) at -Copt-level=0. A full bootstrap of the patched compiler was not performed; the patch is a mechanical port of a proven safety-copy with no deviation in data-flow after tuple.val is updated, so the behaviour from that point on is already exercised by every non-constant rust-call invocation today.

r? compiler

When `codegen_arguments_untupled` lowered a call to a closure with the
`extern "rust-call"` ABI, it used the `OperandRef` returned by
`codegen_operand` directly. If the MIR operand was `Copy` or `Constant`
and the tuple's backend layout was by-ref (`BackendRepr::Memory`), the
returned value was `Ref(..)` pointing into shared, potentially read-only
memory -- typically a promoted allocation in `.rodata`. Projecting the
fields out of that pointer and handing them to a callee that owns its
argument storage by ABI contract leads the callee to write through
read-only memory, which aborts with SIGBUS on Linux and
`STATUS_HEAP_CORRUPTION` on Windows.

This is the exact same hazard that the prior fix for the non-tupled
call path in `codegen_call_terminator` addressed: it spills
`Copy`/`Constant` operands that arrive by-ref into a fresh alloca before
passing a pointer to them. That safety-copy was never applied to the
tupled `rust-call` path, so this commit ports the same logic over to
`codegen_arguments_untupled` -- spilling the whole tuple before the
field projections -- and wires the `lifetime_start`/`lifetime_end` pair
into the existing `lifetime_ends_after_call` bookkeeping.

The regression test exercises the classic reproducer (a `const VALUE:
(Thing,)` invoked through `Fn::call`) at both `-Copt-level=0` (which
aborts deterministically without the fix) and `-Copt-level=3` (coverage
that the fix does not regress release codegen).
@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Apr 14, 2026
@theemathas
Copy link
Copy Markdown
Contributor

theemathas commented Apr 14, 2026

Is any LLM involved in this PR? Your other PR has an LLM disclaimer.

@blackms
Copy link
Copy Markdown
Author

blackms commented Apr 14, 2026

Good catch, and thanks for asking directly — you're right that I should have put the disclosure on this PR too. It's on #155296 but I missed it here. My policy is proactive disclosure on every public contribution, so this was an oversight, not a judgment call.

This PR was roughly a 50/50 collaboration between me and a Claude-based coding agent team. The technical hypothesis and patch originated on the agent side; the repro, scoping, review, and PR hygiene were mine.

Agent-side work:

  • Read codegen_arguments_untupled and the reference safety-copy in codegen_call_terminator (from MIR: hide .rodata constants vs by-ref ABI clash in trans. #45996), and formed the root-cause hypothesis that the tupled rust-call path was missing the equivalent copy.
  • Drafted the 15-line safety-copy patch, parallel to the existing one.
  • Drafted the tests/ui/closures/rust-call-const-arg-alias.rs test with opt0 / opt3 revisions.

Human-side work:

  • Reproduced the SIGBUS on nightly locally (exit 138 at -Copt-level=0) before the agent started, and scoped the task.
  • Reviewed the diff line-by-line against the codegen_call_terminator reference.
  • Validated the test compiles under system rustc and matches compiletest's expected assert / exit-code pattern.
  • Cleaned the commit message, wrote the PR body, and opened the PR.

Happy to answer more questions on any specific part of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

release-mode heap corruption with non-generic FnOnce(Vec<usize>)

4 participants