End of statement / lifetime in inline assembly blocks?

    pub fn macro_generated_function(location: &std::panic::Location) {
        unsafe {
            std::arch::asm!(
                "... {0} ...",
                in (reg) (std::ffi::CString::new(location.file()).unwrap().as_ptr()) as isize,
                options(readonly, preserves_flags, nostack, att_syntax)
            )
        };
    }

generates the warning:

warning: a dangling pointer will be produced because the temporary `CString` will be dropped
   --> src/lib.rs:498:76
    |
498 |                 in (reg) (std::ffi::CString::new(location.file()).unwrap().as_ptr()) as isize,
    |                           ------------------------------------------------ ^^^^^^ this pointer will immediately be invalid
    |                           |
    |                           this `CString` is deallocated at the end of the statement, bind it to a variable to extend its lifetime
    |
    = note: pointers do not have a lifetime; when calling `as_ptr` the `CString` will be deallocated at the end of the statement because nothing is referencing it as far as the type system is concerned
    = help: you must make sure that the variable you bind the `CString` to lives at least as long as the pointer returned by the call to `as_ptr`
    = help: in particular, if this pointer is returned from the current function, binding the `CString` inside the function will not suffice
    = help: for more information, see <https://doc.rust-lang.org/reference/destructors.html>
    = note: `#[warn(dangling_pointers_from_temporaries)]` on by default

However, what is actually the "statement" here? Is the statement the whole asm!() (in which case it is fine for my use case), or does the lifetime end before the assembly code is executed? Wouldn't lifetime extension extend this lifetime to the end of the asm!()?

The code does work in practise, but this might be because the code is accessing the newly freed memory. This would be hard to tell as it is the kernel doing the access on my behalf (I'm defining user space static trace points where the kernel will replace some NOPs with a trap into the kernel when the tracing is enabled).

So I would like to know exactly when that lifetime ends. It is hard for me to move the code outside the assembly in (reg) list (without major changes to the probe crate).

EDIT: Looking at the inline assembly it certainly looks like the destructor is called after the inline assembly. Is this just dumb luck or guaranteed? See Compiler Explorer

IMO, this should be (but is not) specified in the documentation for inline assembly.

I can indeed not find anything in the reference. Would the correct next step be to open a bug on the Rust repo about this?

For this, rust-lang/reference. You'd open an issue on rust-lang/rust if the actual behavior, or standard library documentation, was wrong.

I would ideally like to know if my code has UB or not. It works, but the insidious thing about UB is that it can work and then "mysteriously" stop working.

I'm unsure if this is a docs bug, UB, or just "we haven't decided yet / thought about this before". And it seems to me that which of those it is would change where I submit an issue.

In order to determine whether the drop order is such that your code would be UB, you can replace the CString::new() and as_ptr() with similar functions that operate on a type of your own that has a Drop implementation that writes something the assembly can read but doesn’t actually free the memory — something like struct Checker(&AtomicU8);. Then, if the assembly doesn’t observe the value the drop writes, you know the drop didn’t run before the assembly did, and wouldn’t be reading a freed CString.

I filed Inline assembly lifetime extension / statement · Issue #1976 · rust-lang/reference · GitHub

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.