Skip to content

Tracking issue: 32bit x86 targets lose float NaN payload in return values #115567

Description

@RalfJung

On x86 (32bit) targets, returning a floating-point number from a "C" ABI function by-value can change the bits of their NaN payloads. (Specifically, the quiet bit gets set.) This is caused by using x87 registers to pass return values, as mandated by the ABI for this target; this requires a format conversion, and that conversion quiets NaNs. For targets that require SSE2, we have patched the "Rust" ABI to avoid this problem; x86-32 targets where SSE2 is optional have the same problem for "Rust" functions as well.

This is a known and long-standing problem, and very hard to fix. The purpose of this issue mostly is to document its existence and to give it a URL that can be referenced.

A proper fix would require patching LLVM to use different code to load float return values into an x87 register -- specifically, it has to be done in a way that NaN payloads are fully preserved. It would be lovely if we could find someone who both cares enough about x86-32 to make sure it correctly implements the Rust semantics and who has the expertise to write the LLVM patch this requires. :)

Related issues:

Prior issues:

Activity

  1. added
    C-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFC
    A-floating-pointArea: Floating point numbers and arithmetic
    A-ABIArea: Concerning the application binary interface (ABI)
    on Sep 5, 2023
  2. changed the title [-]Tracking issue: 32bit x86 targets lose NaN payload in return values[/-] [+]Tracking issue: 32bit x86 targets lose float NaN payload in return values[/+] on Oct 8, 2023
  3. Diggsey commented on Oct 14, 2023

    @Diggsey
    Contributor

    It looks like the 80-bit float load/store operations don't modify the bits, so in principle one could fix this by performing the conversion from 32-bit or 64-bit float to 80-bit float (and back) in software, although I expect that would be prohibitively expensive.

  4. RalfJung commented on Oct 14, 2023

    @RalfJung
    MemberAuthor

    Could we do a quick check whether the float is a NaN or not, and only do the software conversion for NaNs? That should be a lot faster I hope.

  5. Diggsey commented on Oct 15, 2023

    @Diggsey
    Contributor

    Came up with this code for software conversion:

    typedef union {
        long double ld;
        struct {
            unsigned long long ull;
            unsigned short us;
        };
    } ld_t;
    
    typedef union {
        double d;
        unsigned long long ull;
    } d_t;
    
    long double identity(double x) {
        d_t y;
        y.d = x;
        ld_t z;
        z.ull = (y.ull << 11) & 0x7FFFFFFFFFFFFFFFull | 0x8000000000000000ull;
        unsigned short e = (y.ull >> 52) & 0x7FF;
        z.us = ((e == 0x7FF) ? 0x7FFF : e + 0x3C00) | ((y.ull >> 48) & 0x8000);
        return z.ld;
    }
    identity:
            sub     esp, 28
            movq    xmm1, QWORD PTR [esp+32]
            movdqa  xmm0, xmm1
            psllq   xmm1, 11
            psrlq   xmm0, 32
            movd    eax, xmm0
            movdqa  xmm0, XMMWORD PTR .LC0
            mov     edx, eax
            shr     edx, 20
            por     xmm1, xmm0
            and     dx, 2047
            movq    QWORD PTR [esp], xmm1
            cmp     dx, 2047
            lea     ecx, [edx+15360]
            mov     edx, 32767
            cmovne  edx, ecx
            shr     eax, 16
            and     ax, -32768
            or      eax, edx
            mov     WORD PTR [esp+8], ax
            fld     TBYTE PTR [esp]
            add     esp, 28
            ret
    .LC0:
            .long   0
            .long   -2147483648
            .long   0
            .long   0

    Or, special casing NaNs:

    #include "math.h"
    
    typedef union {
        long double ld;
        struct {
            unsigned long long ull;
            unsigned short us;
        };
    } ld_t;
    
    typedef union {
        double d;
        unsigned long long ull;
    } d_t;
    
    long double identity(double x) {
        if (!isnan(x)) {
            return x;
        }
        d_t y;
        y.d = x;
        ld_t z;
        z.ull = (y.ull << 11) & 0x7FFFFFFFFFFFFFFFull | 0x8000000000000000ull;
        z.us = (y.ull >> 48) | 0x7FFF;
        return z.ld;
    }
    identity:
            sub     esp, 44
            fld     QWORD PTR [esp+48]
            fst     QWORD PTR [esp+8]
            fucomi  st, st(0)
            jp      .L2
            add     esp, 44
            ret
    .L2:
            fstp    st(0)
            mov     eax, DWORD PTR [esp+8]
            mov     edx, DWORD PTR [esp+12]
            movd    xmm0, eax
            movd    xmm1, edx
            mov     eax, edx
            punpckldq       xmm0, xmm1
            shr     eax, 16
            movdqa  xmm1, XMMWORD PTR .LC0
            psllq   xmm0, 11
            or      ax, 32767
            por     xmm0, xmm1
            mov     WORD PTR [esp+24], ax
            movq    QWORD PTR [esp+16], xmm0
            fld     TBYTE PTR [esp+16]
            add     esp, 44
            ret
    .LC0:
            .long   0
            .long   -2147483648
            .long   0
            .long   0```
  6. RalfJung commented on Oct 15, 2023

    @RalfJung
    MemberAuthor

    Nice. :)

    So, I wonder -- what would it take to actually use this when returning floats or doubles? Do we have to patch LLVM or can the codegen backend do this?

  7. RalfJung commented on Oct 16, 2023

    @RalfJung
    MemberAuthor

    Btw, can someone explain why this only affects return values? How are float arguments passed?

  8. Diggsey commented on Oct 16, 2023

    @Diggsey
    Contributor

    So, I wonder -- what would it take to actually use this when returning floats or doubles? Do we have to patch LLVM or can the codegen backend do this?

    No idea :)

    Btw, can someone explain why this only affects return values? How are float arguments passed?

    Arguments on x86 (with the C calling convention) are always passed on the stack regardless of type. So the problem still exists as soon as you fld the value, but it's not inherent to the calling convention.

  9. comex commented on Oct 19, 2023

    @comex
    Contributor

    By the same token, for non-extern "C" functions, floating point returns could be changed to go on the stack, even without SSE.

  10. added
    O-x86_32Target: x86 processors, 32 bit (like i686-*) (also known as IA-32, i386, i586, i686)
    and removed on Oct 25, 2023
  11. RalfJung commented on Dec 14, 2023

    @RalfJung
    MemberAuthor

    Here there is a claim that this issue only affects signaling NaN return values. Is that true? The example given here does seem to be a signaling NaN.

  12. beetrees commented on Jun 21, 2024

    @beetrees
    Contributor

    For reference: the only change made to f32/f64 NaN payloads by x87 load/store is to quieten signalling NaN payloads by setting the quiet bit. Additionally, only f32 and f64 are affected by this issue as neither f16 nor f128 are passed in x87 registers.

  13. added a commit that references this issue on Jul 3, 2024
  14. added 3 commits that reference this issue on Jul 12, 2024
  15. jerrywrice commented on Feb 27, 2025

    @jerrywrice

    Until a reasonable fix is chosen and incorporated, shouldn't there be a priority effort to patch these target's generated code so when this specific float value corruption occurs at run-time, either a panic is generated or a run-time error message logged? Or maybe this run-time safety check patch should be added via a feature or config option, so developers can choose to disable it if they aren't concerned and don't want the run-time overhead of the check. Later this all will be removed when the proper fix is incorporated. Otherwise, unless I'm misinterpreting, it sounds as if broken (unsound?) code is currently allowed 'in the wild' (even if in rare circumstances)? Or is this so rare that no-one who it impacts is concerned? If this is the case, then I hope that the relevant language documentation and relevant crate documentation (not just the target Tier documentation) prominently highlights this issue so it's not easily overlooked by anyone developing new code for these targets (granted that new code for these targets is probably itself a rare occurrence). Let me add that I'm very appreciative of the great work the community is doing - I'm very positive about Rust.

  16. added
    I-miscompileIssue: Correct Rust code lowers to incorrect machine code
    on Mar 4, 2026
  17. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Mar 4, 2026
  18. RalfJung commented on Mar 4, 2026

    @RalfJung
    MemberAuthor

    Otherwise, unless I'm misinterpreting, it sounds as if broken (unsound?) code is currently allowed 'in the wild' (even if in rare circumstances)? Or is this so rare that no-one who it impacts is concerned?

    Yes, this is a miscompilation bug. It looks like it just doesn't affect enough people to motivate anyone to fix it. x86-32 is a fairly niche target these days, and signaling NaNs are quite rare as well, so very, very few people care about the combination of those. Combine that with the fact that very few people have the expertise to fix a problem like this, and those people typically have higher-impact issues to worry about, and you get a perfect recipe for problems like this flying under the radar.

    If this is the case, then I hope that the relevant language documentation and relevant crate documentation (not just the target Tier documentation)

    This is a bug in how a target implements the language, so the target docs seem like the primary place to mention issues like this. I'm not sure where in the language docs it'd make sense to put this -- I guess we could have a section on this page summarizing target-specific floating point issues?

  19. apiraino commented on Mar 4, 2026

    @apiraino
    Contributor

    FWIW this is indeed mentioned in the target documentation but I wonder if it's visible enough or - as you point out - should be mentioned elsewhere as well.

  20. removed
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Jul 19, 2026
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-floating-pointArea: Floating point numbers and arithmeticC-tracking-issueCategory: An issue tracking the progress of sth. like the implementation of an RFCI-miscompileIssue: Correct Rust code lowers to incorrect machine codeO-x86_32Target: x86 processors, 32 bit (like i686-*) (also known as IA-32, i386, i586, i686)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions