Skip to content

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

Open
@glandium

Description

@glandium
Contributor

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

added
C-bugCategory: This is a bug.
regression-untriagedUntriaged performance or correctness regression.
on Jun 10, 2023
added
I-prioritizeIssue: Indicates that prioritization has been requested for this issue.
on Jun 10, 2023
glandium

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).

saethlin

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.

saethlin

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.

glandium

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.

saethlin

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?

ChrisDenton

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.

saethlin

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.

ChrisDenton

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.

glandium

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.

ChrisDenton

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.

saethlin

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.

110 remaining items

Loading
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

        Participants

        @briansmith@Nemo157@RalfJung@riking@wesleywiser

        Issue actions

          MSVC on x86-32 Windows fails to align variables to their required alignment · Issue #112480 · rust-lang/rust