Rust: Locals aligned to greater than page size can cause unsound behavior

Created on 19 Mar 2020  路  17Comments  路  Source: rust-lang/rust

Forked from https://github.com/rust-lang/rust/issues/70022

Minimal example

#[repr(align(0x10000))]
struct Aligned(u8);

fn main() {
    let x = Aligned(0);
    println!("{:#x}", &x as *const _ as usize);
}

Aligning the stack is done after the stack probe. Because stacks grow downwards and aligning the stack shifts it downwards, it can cause the end of the stack to extend past the guard page and cause invalid access exceptions or worse when those sections of the stack are touched.

Only confirmed that this occurs on pc-windows-msvc.

A-LLVM C-bug I-unsound 馃挜 ICEBreaker-LLVM O-linux O-windows-msvc P-high T-compiler

All 17 comments

This is most likely an LLVM bug.

Could someone who knows the technical details here report this bug upstream with LLVM?

@rustbot ping llvm

Hey LLVM ICE-breakers! This bug has been identified as a good
"LLVM ICE-breaking candidate". In case it's useful, here are some
[instructions] for tackling these sorts of bugs. Maybe take a look?
Thanks! <3

cc @camelid @comex @cuviper @DutchGhost @dyncat @hanna-kruppe @hdhoang @heyrutvik @JOE1994 @jryans @mmilenko @nagisa @nikic @Noah-Kennedy @SiavoshZarrasvand @spastorino @vertexclique

I think this is platform-specific but it still all happens to varying degrees on x86(-64). This safe code causes a stack overflow on Ubuntu:

#[repr(align(0x800000))]
struct Aligned(usize);

fn main() {
    let x = Aligned(2);
    println!("{}", x.0);
}

This safe code causes a segfault on Ubuntu:

#[repr(align(0x10000000))]
struct Aligned(usize);

fn main() {
    let x = Aligned(2);
    println!("{}", x.0);
}

This isn't specific to Windows, then, and can be used to cause stack overflows and/or segfaults in safe code.

I don't know how this would be done, but maybe doing such alignments should only be allowed if unsafe is used to keep it out of safe code? Embedded might depend on high alignments, so I don't think removing the ability to align with high numbers entirely would be a good solution.

Note that we do install a stack guard and associated handler to check for stack overflows. So just getting a stack overflow or segfault does not necessarily demonstrate a problem. Rust cannot statically make sure that your stack is always big enough, so being able to trigger a stack overflow through big locals or deep recursion is entirely expected behavior.

The bug here is about the locals not having the alignment that was requested via repr. As far as I can see, your tests do not demonstrate that this happens here.

The bug here is about the locals not having the alignment that was requested via repr.

The locals do absolutely have the required alignment. The issue is with the stack aligning prologue which can cause the function's frame to exist entirely past the guard page so unrelated memory can be stomped on.

I just tried this on godbolt: https://rust.godbolt.org/z/8xfn4v

#[repr(align(0x10000000))]
struct Aligned(usize);

pub fn foo() -> usize {
    Aligned(2).0
}

The output for x86_64-pc-windows-msvc:

example::foo:
        push    rbp
        mov     eax, 536870896    # 0x1ffffff0
        call    __chkstk
        sub     rsp, rax
        lea     rbp, [rsp + 128]
        and     rsp, -268435456    # 0xfffffffff0000000
        mov     qword ptr [rsp], 2
        mov     rax, qword ptr [rsp]
        lea     rsp, [rbp + 536870768]    # 0x1fffff70
        pop     rbp
        ret

In the worst case, the sub could move rsp to the very top of an aligned block, or anywhere more than a page size away from the aligned base. Then the large and will round this down, which might jump over the stack guard page, and this wasn't tested by __chkstk.

The output for x86_64-unknown-linux-gnu:

example::foo:
        push    rbp
        mov     rbp, rsp
        and     rsp, -268435456    # 0xfffffffff0000000
        mov     eax, 536870912    # 0x20000000
        call    __rust_probestack
        sub     rsp, rax
        mov     qword ptr [rsp], 2
        mov     rax, qword ptr [rsp]
        mov     rsp, rbp
        pop     rbp
        ret

Here the alignment is done first, but it's still bad. We might _start_ with rsp more than a page size unaligned, and then again the large and can jump over the guard page. Then __rust_probestack could be probing non-stack memory and think everything is fine.

Here's one more Linux test I ran locally, using LLVM 11's "probe-stack"="inline-asm":

example::foo:
        pushq   %rbp
        movq    %rsp, %rbp
        andq    $-268435456, %rsp    # 0xfffffffff0000000
        movq    %rsp, %r11
        subq    $536870912, %r11    # 0x20000000
.LBB0_1:
        subq    $4096, %rsp
        movq    $0, (%rsp)
        cmpq    %r11, %rsp
        jne     .LBB0_1
        movq    $2, (%rsp)
        movq    (%rsp), %rax
        movq    %rbp, %rsp
        popq    %rbp
        retq

Again, the initial alignment could jump the guard page, then the probe loop may be clobbering non-stack memory.
(cc @serge-sans-paille)

It seems that the ideal behavior here would be to add the alignment to the size passed to __chkstk/__rust_probestack and perform the stack alignment masking after the stack probing.

Yeah, I've been talking privately with Serge, and he's going to try implementing something like that.

I thought this is what already happens?

Aligning the stack is done after the stack probe.

And the fix would be to align first and then run the stack probe?

I thought this is what already happens?

Aligning the stack is done after the stack probe.

And the fix would be to align first and then run the stack probe?

The fix is to first probe the stack for the frame size plus the alignment, and then align the stack. Currently stack probes only check the frame size without adding the alignment to that size.

Note that the size of the alignment adjustment is a dynamic value -- the offset from whatever incoming stack pointer we get to an aligned pointer below that. Then the actual frame size is allocated after we have known alignment. If the stack guard page is anywhere in that total range, we need to fault, which is what the probing does.

I guess I am wondering why we can't first align and then probe for the already computed new stack size, that sounds like it avoids doing the aligning twice. But it probably doesn't work for other reasons I cannot see.

@RalfJung Because after aligning the current stack pointer can be past the guard page in a completely unrelated section of memory, that could be completely valid memory for the full size of the stack frame that the stack probe could succeed on, but it's not our stack and we'd be trampling that unrelated memory!

We don't need to do the aligning twice as we can do a simple conservative stack probe of frame size + alignment, which is always sound, but it can trigger stack overflows when there is still a bit of space left. A smarter combination of stack probing + aligning can also avoid computing the aligned address twice, but might involve more implementation effort. Regardless of what technique we use, this only matters for functions with locals that are aligned to a page size or greater.

Ah I see, I somehow assumed stack probing would still have access to the old top-of-stack and could thus probe all the way from there to the aligned stack with the new frame.

The examples I gave do have the old pointer in rbp. However, once we've moved rsp, a signal could be delivered and run its handler from that stack location, before we've probed that it's safe. For that matter, __chkstk and __rust_probestack need to have a valid stack to be called at all.

Was this page helpful?
0 / 5 - 0 ratings