FFI + release mode + passing pointer to temporary value to C function produces an invalid result
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Playing around with SDL3 crate I've met behavior which is interestingly even caught in C++: foo(&bar()) where bar returns struct in C gives error like "Taking the address of a temporary object of type 'T'". When rust calls C function, it allows similar code and bad things might happen in release mode:
The code (optionally) creates C struct and calls C func passing a pointer to it.
use std::ffi::c_float;
#[repr(C)]
struct DataFFI { pub a: c_float, pub b: c_float }
fn make_data(x:f32) -> DataFFI { DataFFI { a: x, b: x } }
extern "C" { fn sum_me(dat: *const DataFFI) -> c_float; }
fn do_sum(dat: Option<f32>) -> f32
{
unsafe {
sum_me(
match dat { Some(x) => &make_data(x), None => std::ptr::null() },
)
}
}
fn main()
{
println!("expect: 0, got: {}", do_sum(None));
println!("expect: 8, got: {}", do_sum(Some(4.0)));
}
build.rs is done like this
fn main()
{
println!("cargo::rustc-flags=-L/home/fella/src/memtest/c");
println!("cargo::rustc-flags=-lbackend");
}
C function is
//gcc -shared -o libbackend.so -fPIC backend.c , compiled inside `c` folder
#include <stdio.h>
typedef struct Data { float a,b; } Data;
const Data nul_data={0.0};
float sum_me(const Data* dat)
{
float res=0.0;
printf("DAT(%p):\n", dat);
dat = dat ? dat : &nul_data;
res += dat->a; printf("%f ", res); res += dat->b; printf("%f\n", res);
return res;
}
I expected to see this happen: explanation
2 ╰ LD_LIBRARY_PATH=/home/fella/src/memtest/c/ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/memtest`
DAT((nil)):
0.000000 0.000000
expect: 0, got: 0
DAT(0x7ffef9cfdb3c):
4.000000 8.000000
expect: 8, got: 8
And this happens in debug mode: when dat is passed it contains what we want. But in release mode
Instead, this happened:
✦2 ╰ LD_LIBRARY_PATH=/home/fella/src/memtest/c/ cargo run --release
Finished `release` profile [optimized] target(s) in 0.00s
Running `target/release/memtest`
DAT((nil)):
0.000000 0.000000
expect: 0, got: 0
DAT(0x7fff5f15e0c0):
-1833370141065216.000000 -1833370141065216.000000
expect: 8, got: -1833370100000000
(Also interestingly in this run we have -1833370100000000 without 41065216)
It relies on both using address from return value and match. If we use
fn do_sum(dat: Option<f32>) -> f32
{
unsafe {
let real_dat;
sum_me(
match dat { Some(x) => {
real_dat = make_data(x);
&real_dat
},
None => std::ptr::null() },
)
}
}
or sum_dat(&make_data()) it works.
Meta
rustc --version --verbose:
rustc 1.84.1 (e71f9a9a9 2025-01-27)
binary: rustc
commit-hash: e71f9a9a98b0faf423844bf0ba7438f29dc27d58
commit-date: 2025-01-27
host: x86_64-unknown-linux-gnu
release: 1.84.1
LLVM version: 19.1.5
Backtrace
<backtrace>
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the provided Rust and C reproducer, running both cargo run and cargo run --release with rustc 1.84.1. Compare the temporary-value pointer behavior with the shown local-variable workaround, then determine whether the result matches Rust's temporary lifetime and FFI rules. Done means the behavior is explained and, if confirmed as a compiler bug, covered by an appropriate regression test.
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
- 35/100