Skip to content

MSVC on x86-32 Windows fails to align variables to their required alignment #112480

Open

Description

@glandium

View all comments

This is a regression from #98112. I suppose it's not possible to disable this specific check only while preserving debug assertions...

The core problem is that the x86 ABI on Windows doesn't guarantee the stack alignment above 4. See for example https://developercommunity.visualstudio.com/t/vs2017-64-bit-int-alignment-problem/294259

And while some types have an alignment reported of 8 (e.g. UINT64), in practice, the C compiler will happily not align them on the stack.

So for example, this C code, compiled by MSVC for 32-bits:

#include <cinttypes>
extern void hoge(uint64_t*);
void foo() {
    uint64_t a;
    hoge(&a);
}

will produce this assembly:

_a$ = -8                                                ; size = 8
void foo(void) PROC                                        ; foo
        push    ebp
        mov     ebp, esp
        sub     esp, 8
        lea     eax, DWORD PTR _a$[ebp]
        push    eax
        call    void hoge(unsigned __int64 *)                        ; hoge
        add     esp, 4
        mov     esp, ebp
        pop     ebp
        ret     0
void foo(void) ENDP

(on godbolt)

If the stack pointer is not 8-bytes aligned when entering the function, the pointer passed to hoge is not going to be 8-bytes aligned.

As mentioned in the linked community post above, adding alignas(8) to the type definition makes the compiler align the stack:

#include <cinttypes>
extern void hoge(uint64_t*);
void foo() {
    alignas(8) uint64_t a;
    hoge(&a);
}

becomes

_a$ = -8                                                ; size = 8
void foo(void) PROC                                        ; foo
        push    ebx
        mov     ebx, esp
        sub     esp, 8
        and     esp, -8                             ; fffffff8H
        add     esp, 4
        push    ebp
        mov     ebp, DWORD PTR [ebx+4]
        mov     DWORD PTR [esp+4], ebp
        mov     ebp, esp
        sub     esp, 8
        lea     eax, DWORD PTR _a$[ebp]
        push    eax
        call    void hoge(unsigned __int64 *)                        ; hoge
        add     esp, 4
        mov     esp, ebp
        pop     ebp
        mov     esp, ebx
        pop     ebx
        ret     0
void foo(void) ENDP

(on godbolt)

Now, what this means is that if that hoge function is a rust FFI function, and it uses that pointer, the "misaligned pointer dereference" check is hit and panic ensues.

Real life case, for the curious:
https://github.com/servo/dwrote-rs/blob/master/src/font_file_loader_impl.rs#L116-L123

That function is called from dwrite.dll (which comes with Windows).

t-opsem FCP comment

Summary of the MSVC alignment rules

Activity

  1. added
    C-bugCategory: This is a bug.
    regression-untriagedUntriaged performance or correctness regression.
    on Jun 10, 2023
  2. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Jun 10, 2023
  3. glandium commented on Jun 10, 2023

    @glandium
    ContributorAuthor

    And while some types have an alignment reported of 8 (e.g. UINT64)

    Let me substantiate this:

    #include <cinttypes>
    int foo() {
        return alignof(uint64_t);
    }
    

    compiles to:

    int foo(void) PROC                                        ; foo
            push    ebp
            mov     ebp, esp
            mov     eax, 8
            pop     ebp
            ret     0
    int foo(void) ENDP
    

    (on godbolt)

    If the stack pointer is not 8-bytes aligned when entering the function, the pointer passed to hoge is not going to be 8-bytes aligned.

    Actually, if the stack pointer is 8-bytes aligned when entering the function, the pointer passed to hoge is going to be unaligned (because there's a push first).

  4. saethlin commented on Jun 10, 2023

    @saethlin
    Member

    I suppose it's not possible to disable this specific check only while preserving debug assertions...

    If you can pass unstable flags, currently, you can say -Zmir-enable-passes=-CheckAlignment.

  5. saethlin commented on Jun 10, 2023

    @saethlin
    Member

    I think your report here is a duplicate of https://developercommunity.visualstudio.com/t/visual-c-alignof-is-not-conformant-on-x86/1258506

    That is, this appears to be a known MSVC bug.

  6. glandium commented on Jun 10, 2023

    @glandium
    ContributorAuthor

    The MSVC bug would be the value returned by alignof. That doesn't change anything for the alignment check in rust.

  7. saethlin commented on Jun 10, 2023

    @saethlin
    Member

    std::mem::align_of::<u64>() returns 8 on i686-pc-windows-msvc. Are you saying that is a bug?

  8. ChrisDenton commented on Jun 10, 2023

    @ChrisDenton
    Member

    It's complicated. See https://learn.microsoft.com/en-us/cpp/c-language/alignment-c?view=msvc-170

    The compiler generally aligns data on natural boundaries that are based on the target processor and the size of the data. Data is aligned on up to 4-byte boundaries on 32-bit processors, and 8-byte boundaries on 64-bit processors.

    So a type with a preferred alignment of 8 bytes can be 4-byte aligned on 32-bit targets.

  9. saethlin commented on Jun 10, 2023

    @saethlin
    Member

    Ah, but std::mem::align_of does not return the preferred alignment.

    Returns the ABI-required minimum alignment of a type in bytes.

    Every reference to a value of the type T must be a multiple of this number.

    This is the alignment used for struct fields. It may be smaller than the preferred alignment.

  10. ChrisDenton commented on Jun 10, 2023

    @ChrisDenton
    Member

    But see also https://learn.microsoft.com/en-us/cpp/build/reference/zp-struct-member-alignment?view=msvc-170

    The compiler stores members after the first one on a boundary that's the smaller of either the size of the member type, or an N-byte boundary.

    Where N is 8 for x86.

  11. glandium commented on Jun 10, 2023

    @glandium
    ContributorAuthor

    It's true that from the perspective that in struct { int a; uint64_t b; }, b is 8-bytes aligned, alignof returning 8 is actually correct. But it's more indicative of "you'll be safe if you align to 8 bytes" than "you must align to 8 bytes" or "it will always be aligned to 8 bytes". Because the struct itself, on the stack, will still be 4-bytes aligned. The x86 windows ABI is weird that way... but that essentially breaks the assumption from the alignment check.

  12. ChrisDenton commented on Jun 10, 2023

    @ChrisDenton
    Member

    Right. But it matters in terms of how this is addressed. It would be wrong to change align_of as that's documented to be for struct fields.

    For the short term, skipping this check on i686-windows might be the only decent options (which is unfortunate). But longer term it would be better to have a proper fix. I'm not really what that would look like tbh. As you say, it's breaking a core assumption.

  13. saethlin commented on Jun 10, 2023

    @saethlin
    Member

    The x86 windows ABI is weird that way... but that essentially breaks the assumption from the alignment check.

    There is no assumption made by the alignment check. We unambiguously tell LLVM that a dereference of a *const u64 on i686-pc-windows-msvc is 8-byte-aligned: https://godbolt.org/z/MrdTPqWbY

    pub unsafe extern "C" fn read_u64(ptr: *const u64) -> u64 {
        *ptr
    }
    define i64 @_ZN7example8read_u6417hbc6f74c29a983155E(ptr %ptr) unnamed_addr #0 !dbg !5 {
      %0 = load i64, ptr %ptr, align 8, !dbg !10, !noundef !9
      ret i64 %0, !dbg !11
    }

    If you are saying that this alignment assumption is wrong, then we have a soundness issue. The bug is not in the alignment check, we either have invalid codegen from MSVC because it does not sufficiently align its uint64_ts, or we have invalid codegen from rustc because it assumes u64 is 8-byte-aligned on this target when it is not.

  14. 85 remaining items

  15. added a commit that references this issue on Apr 24, 2025
  16. added 2 commits that reference this issue on Apr 24, 2025
  17. added a commit that references this issue on Apr 24, 2025
  18. Jules-Bertholet commented on Jun 11, 2025

    @Jules-Bertholet
    Contributor

    https://learn.microsoft.com/en-us/cpp/c-language/alignment-c says (emphasis mine):

    Use the C11 keyword _Alignof to get the preferred alignment of a type or variable, and _Alignas to specify a custom alignment for a variable or user-defined type.

    The convenience macros alignof and alignas, defined in <stdalign.h>, map directly to _Alignof and _Alignas, respectively. These macros match the keywords used in C++. So using the macros instead of the C keywords may be helpful for code portability if you share any code between the two languages.

    Let’s compare to the C standard (emphasis again mine):

    The alignof operator yields the alignment requirement of its operand type. The operand is not evaluated and the result is an integer constant expression. When applied to an array type, the result is the alignment requirement of the element type.

    I think the correct interpretation here is that x86-32 MSVC’s alignof is not standard-compliant. MSVC only requires uint64_t to be aligned to 4 bytes (or maybe even less), but prefers it to be aligned to 8. alignof/alignas on that compiler use this preferred alignment, in contravention of the standard, but that does not reflect the minimum alignment required to avoid UB.

  19. RalfJung commented on Jun 12, 2025

    @RalfJung
    Member

    It is somewhat ironic that the same section of the docs proclaims some practices that help portability, while also describing a practice that can easily lead to portable code having UB (since portable C code could easily rely on alignof returning the required alignment of a type for correctness).

  20. briansmith commented on Jun 20, 2025

    @briansmith
    Contributor

    I think the correct interpretation here is that x86-32 MSVC’s alignof is not standard-compliant. MSVC only requires uint64_t to be aligned to 4 bytes (or maybe even less), but prefers it to be aligned to 8. alignof/alignas on that compiler use this preferred alignment, in contravention of the standard, but that does not reflect the minimum alignment required to avoid UB.

    Note that this is the same for GCC until GCC 8: https://gcc.gnu.org/gcc-8/porting_to.html#alignof

  21. mirh commented on Jul 5, 2025

    @mirh

    FWIW gcc has still its own bugs

  22. Jules-Bertholet commented on May 5, 2026

    @Jules-Bertholet
    Contributor

    A "fun" interaction between this issue and other MSVC weirdness: #81996 (comment)

  23. Jules-Bertholet commented on May 5, 2026

    @Jules-Bertholet
    Contributor

    In fact, thanks to this weirdness, we can more or less polyfill a standard-compliant alignof macro in MSVC: https://godbolt.org/z/4M81KGhdM

  24. glandium commented on May 7, 2026

    @glandium
    ContributorAuthor

    In fact, thanks to this weirdness, we can more or less polyfill a standard-compliant alignof macro in MSVC: https://godbolt.org/z/4M81KGhdM

    Does clang-cl output the same thing?

  25. Jules-Bertholet commented on May 8, 2026

    @Jules-Bertholet
    Contributor

    Does clang-cl output the same thing?

    Yes.

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)C-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessO-windows-msvcToolchain: MSVC, Operating system: WindowsT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.disposition-closeThis PR / issue is in PFCP or FCP with a disposition to close it.finished-final-comment-periodThe final comment period is finished for this PR / Issue.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions