Skip to content

The sparc64, riscv64, loongarch64 extern "C" fn ABIs are all wrong when aligned/packed structs are involved #115609

Closed

Description

@RalfJung

View all comments

All of these ABIs have in common that they are looking at layout.fields to compute the ABI for the function, and they are all getting it wrong, in various different ways. (I previously reported bugs against all of these because repr(transparent) is not properly respected; those bugs have the same cause -- relying on layout.fields -- but they are orthogonal.)

For instance, this type

#[repr(packed)]
struct P(i8, f32);

on riscv64 gets a PassMode::Cast of

                           prefix: [
                               Some(
                                   Reg {
                                       kind: Integer,
                                       size: Size(1 bytes),
                                   },
                               ),
                               None,
                               None,
                               None,
                               None,
                               None,
                               None,
                               None,
                           ],
                           rest: Uniform {
                               unit: Reg {
                                   kind: Float,
                                   size: Size(4 bytes),
                               },
                               total: Size(4 bytes),
                           },

which gets translated to the LLVM type { i8, f32 }, and that's a non-packed LLVM struct. On a call, P gets transmuted to that LLVM type, which obviously produces garbage since the fields are at different offsets.

On sparc64, the type translates into { i32, f32 } which is wrong as well.

As another example, this type

#[repr(align(8))]
struct Aligned8Int(i32);

#[repr(C)]
struct S2(Aligned8Int, f32);

on loongarch64 gets a PassMode::Cast of

                           prefix: [
                               Some(
                                   Reg {
                                       kind: Integer,
                                       size: Size(4 bytes),
                                   },
                               ),
                               None,
                               None,
                               None,
                               None,
                               None,
                               None,
                               None,
                           ],
                           rest: Uniform {
                               unit: Reg {
                                   kind: Float,
                                   size: Size(4 bytes),
                               },
                               total: Size(4 bytes),
                           },


which is the LLVM type { i32, f32 }, and that's obviously not the right type for S2.

(Caveat: I can't actually run code on these targets, so there's a small chance that I am completely misinterpreting what these PassMode mean. I hope that's not the case...)

I don't know what the C ABI on those platforms has to say about packed and aligned structs. Currently, we don't even provide ABIs a way to generate a packed LLVM struct for the arguments -- PassMode::Cast always uses an un-packed LLVM struct.

Updates:

Activity

  1. added
    O-SPARCTarget: SPARC processors
    O-riscvTarget: RISC-V architecture
    A-ABIArea: Concerning the application binary interface (ABI)
    O-loongarchTarget: LoongArch (LA32R, LA32S, LA64)
    on Sep 6, 2023
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Sep 6, 2023
  3. bjorn3 commented on Sep 6, 2023

    @bjorn3
    Member

    Looks like clang will split

    struct P {
        int8_t a;
        float b;
    } __packed__;

    into two arguments on rv64gc with i8 and float respectively as types. Also I think clang will tell LLVM that the struct is packed if necessary on various targets. I also found this function: https://github.com/llvm/llvm-project/blob/d1487670ee1caaf1762c341f9f96cfb07540b5c1/clang/include/clang/CodeGen/CGFunctionInfo.h#L247-L250

  4. RalfJung commented on Sep 6, 2023

    @RalfJung
    MemberAuthor

    into two arguments

    So that's <{ i8, f32 }>? Or really two separate arguments to the function -- like Rust's ScalarPair? Or does that not even make a difference for the ABI?

  5. bjorn3 commented on Sep 6, 2023

    @bjorn3
    Member

    In this case it seemed like two separate args were emitted. AFAIK LLVM will internally recursively split structs into separate arguments anyway though if byval is not used. At least that is what it does for Webassembly.

  6. heiher commented on Sep 7, 2023

    @heiher
    Contributor

    The #[repr(packed)] attribute explicitly defines and constrains the memory layout for types, while the extern "C" for functions requires the function's calling convention to be the same as in the C language. On some architectures with only 32-bit and 64-bit register widths, separating the passing of i8 and f32 can improve access performance. So, as long as parameter passing matches the C calling convention of the target architecture, it is correct. Isn't it?

  7. bjorn3 commented on Sep 7, 2023

    @bjorn3
    Member

    The issue is with how we turn the value as represented in memory into an argument that can be passed in registers. Currently we assume that the value as represented in memory and the llvm type we use for argument passing have the same layout, which is not the case for aligned/packed structs as the llvm type has a different amount of padding from the real type.

  8. RalfJung commented on Sep 7, 2023

    @RalfJung
    MemberAuthor

    The #[repr(packed)] attribute explicitly defines and constrains the memory layout for types, while the extern "C" for functions requires the function's calling convention to be the same as in the C language.

    The issue is: when I declare a packed type in C, and a packed type in Rust, then they should be ABI-compatible. But currently they are not. If you take the type P from my example, declare an equivalent type in C, have a C function that takes a struct P as argument, and call it from Rust, then things will explode in the most amazing ways.

  9. heiher commented on Sep 7, 2023

    @heiher
    Contributor

    Thanks for your explanation. ❤️ I tried the test cases you mentioned, and indeed there is an issue when accessing packed structure fields through memory addresses. If I didn't make any mistakes, is x86_64 also wrong?

    t.rs:

    #[repr(packed, C)]
    struct P {
        i: i8,
        f: f32,
    }
    
    #[no_mangle]
    extern "C" fn test(p: P) {
        let ip = std::ptr::addr_of!(p.i);
        let fp = std::ptr::addr_of!(p.f);
        let i = unsafe { std::ptr::read_unaligned(ip) };
        let f = unsafe { std::ptr::read_unaligned(fp) };
        println!("{:p} {:p} {} {}", ip, fp, i, f);
    }

    t.c:

    struct P
    {
        char i;
        float f;
    } __attribute__((packed));
    
    extern void test(struct P p);
    
    int
    main (int argc, char *argv[])
    {
        struct P p;
        
        p.i = 16;
        p.f = 128.0;
    
        test(p);
    
        return 0;
    }

    run:

    rustc --crate-type=cdylib -o libt.so t.rs; gcc -o t t.c -L . -l t; LD_LIBRARY_PATH=`pwd` ./t

    x86_64 with #[repr(packed)] and __attribute__((packed)):

    0x7ffc31f02520 0x7ffc31f02521 16 128
    

    x86_64 without #[repr(packed)] and __attribute__((packed)):

    0x7ffc5a60eaa0 0x7ffc5a60eaa4 16 128
    

    loongarch64 with #[repr(packed)] and __attribute__((packed)):

    0x7ffffbf10720 0x7ffffbf10721 16 0.000000000000000000000000000000000000023509886
    

    loongarch64 without #[repr(packed)] and __attribute__((packed)):

    0x7ffffba12f90 0x7ffffba12f94 16 128
    
  10. RalfJung commented on Sep 7, 2023

    @RalfJung
    MemberAuthor

    Interesting. x86_64 uses indirect pass mode for packed structs, so... that can't really be an ABI difference. Not sure what is happening. Do the field offsets match on both sides?

  11. heiher commented on Sep 8, 2023

    @heiher
  12. RalfJung commented on Sep 8, 2023

    @RalfJung
    MemberAuthor

    I can't read assembly so I can't quite figure out what that gdb snippet at the end tells me.
    Could you open a new issue for the x86_64 problem? It's different in nature from this one. This issue leads to data loss even when passing packed values between two extern "C" Rust functions (the ABI implemented by Rust doesn't even work for pure Rust programs); the x86_64 problem is an incompatibility with the C ABI (that only surfaces when linking Rust and C code).

  13. 19 remaining items

  14. SpriteOvO commented on Nov 18, 2024

    @SpriteOvO
    Member

    I can confirm that the code in #115609 (comment) (with #[repr(packed)] and __attribute__((packed))) still produces wrong output on RISC-V 64 with Rust v1.82.

    0x3fd239f7f0 0x3fd239f7f1 16 0.000000000000000000000000000000000000004396431
    
  15. added a commit that references this issue on Jun 16, 2025
  16. added a commit that references this issue on Jun 16, 2025
  17. added 3 commits that reference this issue on Feb 11, 2026
  18. added a commit that references this issue on Feb 12, 2026
  19. added a commit that references this issue on Feb 12, 2026
  20. folkertdev commented on Oct 29, 2026

    @folkertdev
    Contributor

    I tested the #[repr(packed, C)] version with qemu, and for all of riscv64, sparc64 and loongarch64 the output is correct:

    0x7ffe32f82a40 0x7ffe32f82a41 16 128 # x86_64
    0x2aaaab2aad60 0x2aaaab2aad61 16 128 # riscv64
    0x40000800a10 0x40000800a11 16 128 # sparc64
    0x555555d55d68 0x555555d55d69 16 128 # loongarch64
    

    So, I believe that this issue can be closed. If there are further issues, they should get a specific issue.

  21. RalfJung commented on Oct 30, 2026

    @RalfJung
    MemberAuthor

    Which PR fixed loongarch?

    The code in those ABIs is still looking at layout.fields which is often wrong and certainly quite fragile. But I guess that's just the current state of our ABI adjustments in general.

  22. beetrees commented on Oct 30, 2026

    @beetrees
    Contributor

    AFAICT loongarch64 was fixed in #145709.

  23. RalfJung commented on Oct 30, 2026

    @RalfJung
    MemberAuthor

    Thanks! Looks like it.

    Okay let's close it then. "The ABI code is a mess" is not actionable. (Well, it is, but we're already tracking that elsewhere.)

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ABIArea: Concerning the application binary interface (ABI)A-FFIArea: Foreign function interface (FFI)I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessO-SPARCTarget: SPARC processorsO-loongarchTarget: LoongArch (LA32R, LA32S, LA64)O-riscvTarget: RISC-V architectureP-mediumMedium priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions