//@ 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 use std::env; 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, msg.len()); _exit(0); } } extern "C" { fn write(fd: i32, buf: *const u8, 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()) }