about summary refs log tree commit diff
path: root/tests/ui/abi/stack-protector.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/ui/abi/stack-protector.rs')
-rw-r--r--tests/ui/abi/stack-protector.rs99
1 files changed, 99 insertions, 0 deletions
diff --git a/tests/ui/abi/stack-protector.rs b/tests/ui/abi/stack-protector.rs
new file mode 100644
index 00000000000..24bd2e21943
--- /dev/null
+++ b/tests/ui/abi/stack-protector.rs
@@ -0,0 +1,99 @@
+// 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); }
+}
+
+// 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())
+}