rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.61k stars 12.62k forks source link

Address boundary error caused by `format_args` when hijacking execution via `_start` #130841

Closed 5-pebbles closed 2 weeks ago

5-pebbles commented 2 weeks ago

When running the following code on Linux, I would expect it to create a fmt::Arguments and then exit:

#![no_main]
#![no_std]

#[no_mangle]
extern "C" fn _start() {
    let _ = core::format_args!("{}", (4 + 84) * 2);

    unsafe {
        core::arch::asm!(
            "syscall",
            in("rax") 60, // exit call
            in("rdi") 0, // exit code
            options(noreturn)
        );
    }
}

#[cfg(not(test))]
#[panic_handler]
fn panic_handler(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

Instead, the process is terminated by signal SIGSEGV (Address boundary error). As far as I can tell the same thing occurs when using the standard library.

The error seems to be caused by a misaligned movaps instruction:

00000000000010c0 <_start>:
    10c0:   48 81 ec 88 00 00 00    sub    $0x88,%rsp
    10c7:   b8 04 00 00 00          mov    $0x4,%eax
    10cc:   83 c0 54                add    $0x54,%eax
    10cf:   89 44 24 0c             mov    %eax,0xc(%rsp)
    10d3:   0f 90 c0                seto   %al
    10d6:   a8 01                   test   $0x1,%al
    10d8:   75 15                   jne    10ef <_start+0x2f>
    10da:   8b 44 24 0c             mov    0xc(%rsp),%eax
    10de:   b9 02 00 00 00          mov    $0x2,%ecx
    10e3:   0f af c1                imul   %ecx,%eax
    10e6:   0f 90 c0                seto   %al
    10e9:   a8 01                   test   $0x1,%al
    10eb:   75 75                   jne    1162 <_start+0xa2>
    10ed:   eb 10                   jmp    10ff <_start+0x3f>
    10ef:   48 8d 3d 42 2d 00 00    lea    0x2d42(%rip),%rdi        # 3e38 <__GNU_EH_FRAME_HDR+0x1b40>
    10f6:   48 8d 05 43 ff ff ff    lea    -0xbd(%rip),%rax        # 1040 <_ZN4core9panicking11panic_const24panic_const_add_overflow17hc27bc8df89dbb50dE>
    10fd:   ff d0                   call   *%rax
    10ff:   48 8d 0d 26 0f 00 00    lea    0xf26(%rip),%rcx        # 202c <_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h4d2c4c91afac718cE+0x4bc>
    1106:   48 89 4c 24 78          mov    %rcx,0x78(%rsp)
    110b:   48 8d 05 5e 0a 00 00    lea    0xa5e(%rip),%rax        # 1b70 <_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h4d2c4c91afac718cE>
    1112:   48 89 84 24 80 00 00    mov    %rax,0x80(%rsp)
    1119:   00 
    111a:   48 89 4c 24 68          mov    %rcx,0x68(%rsp)
    111f:   48 89 44 24 70          mov    %rax,0x70(%rsp)
    1124:   0f 10 44 24 68          movups 0x68(%rsp),%xmm0
    1129:   0f 29 44 24 50          movaps %xmm0,0x50(%rsp)
    112e:   0f 28 44 24 50          movaps 0x50(%rsp),%xmm0
    1133:   0f 29 44 24 40          movaps %xmm0,0x40(%rsp)
    1138:   48 8d 35 c1 0e 00 00    lea    0xec1(%rip),%rsi        # 2000 <_ZN4core3fmt3num3imp52_$LT$impl$u20$core..fmt..Display$u20$for$u20$i32$GT$3fmt17h4d2c4c91afac718cE+0x490>
    113f:   48 8d 7c 24 10          lea    0x10(%rsp),%rdi
    1144:   48 8d 4c 24 40          lea    0x40(%rsp),%rcx
    1149:   41 b8 01 00 00 00       mov    $0x1,%r8d
    114f:   4c 89 c2                mov    %r8,%rdx
    1152:   e8 39 00 00 00          call   1190 <_ZN4core3fmt9Arguments6new_v117h3f576efd651392b0E>
    1157:   b8 3c 00 00 00          mov    $0x3c,%eax
    115c:   31 ff                   xor    %edi,%edi
    115e:   0f 05                   syscall
    1160:   0f 0b                   ud2
    1162:   48 8d 3d cf 2c 00 00    lea    0x2ccf(%rip),%rdi        # 3e38 <__GNU_EH_FRAME_HDR+0x1b40>
    1169:   48 8d 05 10 ff ff ff    lea    -0xf0(%rip),%rax        # 1080 <_ZN4core9panicking11panic_const24panic_const_mul_overflow17h40147af1723d351fE>
    1170:   ff d0                   call   *%rax
    1172:   66 2e 0f 1f 84 00 00    cs nopw 0x0(%rax,%rax,1)
    1179:   00 00 00 
    117c:   0f 1f 40 00             nopl   0x0(%rax)

On this line: 1129: 0f 29 44 24 5 movaps %xmm0,0x50(%rsp) the stack pointer is not 16 byte aligned as is required by movaps.

This example does not produce the error at optimization levels other than 0, however I expect the issue is still present. The fmt::Arguments is just constructed at compile time.

The issue can be negated by adding an arbitrary arithmetic operation after the format. This causes the compiler to replace the movaps with mov:

#![no_main]
#![no_std]

#[no_mangle]
extern "C" fn _start() {
    let _ = core::format_args!("{}", (4 + 84) * 2);

    let _ = 2 + 5; // this line

    unsafe {
        core::arch::asm!(
            "syscall",
            in("rax") 60, // exit call
            in("rdi") 0, // exit code
            options(noreturn)
        );
    }
}

#[cfg(not(test))]
#[panic_handler]
fn panic_handler(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

Is this is expected behavior, if so how can I prevent it?

Command

rustc src/main.rs -C link-arg=-nostartfiles -C panic=abort && ./main

rustc --version --verbose

rustc 1.81.0 (eeb90cda1 2024-09-04)
binary: rustc
commit-hash: eeb90cda1969383f56a2637cbd3037bdf598841c
commit-date: 2024-09-04
host: x86_64-unknown-linux-gnu
release: 1.81.0
LLVM version: 18.1.7

[!Note] I have been unable to test on nightly due to issues linking with rust_eh_personality, I will update this if/when I figure those out.

Noratrieb commented 2 weeks ago

By using extern "C" fn, you tell the compiler to use the system C ABI. This will cause it to insert a prologue that makes assumptions that are upheld by the C ABI. But _start is not called with the C ABI, it's different. You need to mark the function it as #[naked] or use global_asm! to define _start to get precise control and set up something the C ABI is happy with.

For an example, see origin: https://github.com/sunfishcode/origin/blob/287fd6923ad86ea787422ffb49efcdf1df0c6093/src/arch/x86_64.rs#L29

hanna-kruppe commented 2 weeks ago

The x86_64 Linux target (specifically the psABI: https://gitlab.com/x86-psABIs/x86-64-ABI) requires that the stack is 16-byte aligned before a call instruction is executed. Since that pushes the return address on the stack, the ABI requires rsp % 16 == 8 on function entry. So the generated code seems correct: it adjusts rsp by 0x88 so it’s 16 byte aligned afterwards. It crashes because rsp wasn’t aligned as required, shuffling code around so that it doesn’t generate movaps instructions doesn’t fix the problem.

The psABI also says that rsp is 16-byte aligned on entry to _start, so I guess you can’t define that function like any other. There may also be other oddities that you need to accommodate if you define your own _start. If you absolutely must do so, you could implement it in assembly, in a Rust file as naked function (100% assembly) or you could wait for something like https://github.com/rust-lang/rfcs/pull/3594

5-pebbles commented 2 weeks ago

Alright, that makes sense. Thank you both very much!