| //@ run-pass | |
| //@ only-x86_64-unknown-linux-gnu | |
| //@ revisions: ssp no-ssp | |
| //@ [ssp] compile-flags: -Z stack-protector=all | |
| //@ compile-flags: -C opt-level=2 | |
| //@ compile-flags: -g | |
| //@ ignore-backends: gcc | |
| #![allow(function_casts_as_integer)] | |
| use std::env; | |
| use std::ffi::c_void; | |
| use std::process::{Command, ExitStatus}; | |
| fn main() { | |
| if env::args().len() == 1 { | |
| // The test is initially run without arguments. Start the process again, | |
| // this time *with* an argument; in this configuration, the test program | |
| // will deliberately smash the stack. | |
| let cur_argv0 = env::current_exe().unwrap(); | |
| let mut child = Command::new(&cur_argv0); | |
| child.arg("stacksmash"); | |
| if cfg!(ssp) { | |
| assert_stack_smash_prevented(&mut child); | |
| } else { | |
| assert_stack_smashed(&mut child); | |
| } | |
| } else { | |
| vulnerable_function(); | |
| // If we return here the test is broken: it should either have called | |
| // malicious_code() which terminates the process, or be caught by the | |
| // stack check which also terminates the process. | |
| panic!("TEST BUG: stack smash unsuccessful"); | |
| } | |
| } | |
| // Avoid inlining to make sure the return address is pushed to stack. | |
| #[inline(never)] | |
| fn vulnerable_function() { | |
| let mut x = 5usize; | |
| let stackaddr = &mut x as *mut usize; | |
| let bad_code_ptr = malicious_code as usize; | |
| // Overwrite the on-stack return address with the address of `malicious_code()`, | |
| // thereby jumping to that function when returning from `vulnerable_function()`. | |
| unsafe { | |
| fill(stackaddr, bad_code_ptr, 20); | |
| } | |
| // Capture the address, so the write is not optimized away. | |
| std::hint::black_box(stackaddr); | |
| } | |
| // Use an uninlined function with its own stack frame to make sure that we don't | |
| // clobber e.g. the counter or address local variable. | |
| #[inline(never)] | |
| unsafe fn fill(addr: *mut usize, val: usize, count: usize) { | |
| let mut addr = addr; | |
| for _ in 0..count { | |
| *addr = val; | |
| addr = addr.add(1); | |
| } | |
| } | |
| // We jump to malicious_code() having wreaked havoc with the previous stack | |
| // frame and not setting up a new one. This function is therefore constrained, | |
| // e.g. both println!() and std::process::exit() segfaults if called. We | |
| // therefore keep the amount of work to a minimum by calling POSIX functions | |
| // directly. | |
| // The function is un-inlined just to make it possible to set a breakpoint here. | |
| #[inline(never)] | |
| fn malicious_code() { | |
| let msg = [112u8, 119u8, 110u8, 101u8, 100u8, 33u8, 0u8]; // "pwned!\0" ascii | |
| unsafe { | |
| write(1, &msg as *const u8 as *const c_void, msg.len()); | |
| _exit(0); | |
| } | |
| } | |
| extern "C" { | |
| fn write(fd: i32, buf: *const c_void, count: usize) -> isize; | |
| fn _exit(status: i32) -> !; | |
| } | |
| fn assert_stack_smash_prevented(cmd: &mut Command) { | |
| let (status, stdout, stderr) = run(cmd); | |
| assert!(!status.success()); | |
| assert!(stdout.is_empty()); | |
| assert!(stderr.contains("stack smashing detected")); | |
| } | |
| fn assert_stack_smashed(cmd: &mut Command) { | |
| let (status, stdout, stderr) = run(cmd); | |
| assert!(status.success()); | |
| assert!(stdout.contains("pwned!")); | |
| assert!(stderr.is_empty()); | |
| } | |
| fn run(cmd: &mut Command) -> (ExitStatus, String, String) { | |
| let output = cmd.output().unwrap(); | |
| let stdout = String::from_utf8_lossy(&output.stdout); | |
| let stderr = String::from_utf8_lossy(&output.stderr); | |
| println!("status: {}", output.status); | |
| println!("stdout: {}", stdout); | |
| println!("stderr: {}", stderr); | |
| (output.status, stdout.to_string(), stderr.to_string()) | |
| } |