RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / tests / static_pie.rs
6096 bytes ยท raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Stage 19: static PIE. The ELF loader now also takes ET_DYN files with
//! no PT_INTERP (what stock `musl-gcc -static-pie` makes), loads them at a
//! random base and reports it in the auxiliary vector; dynamically linked
//! programs (PT_INTERP) get ENOEXEC; classic ET_EXEC still works.
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(rustbelt::test_runner)]
#![reexport_test_harness_main = "test_main"]

extern crate alloc;

use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use bootloader::{BootInfo, entry_point};
use core::panic::PanicInfo;
use rustbelt::errno::Errno;
use rustbelt::fs::file::File;
use rustbelt::process::{self, ExitStatus, SpawnArgs};
use rustbelt::{elf, fs, memory, thread};

entry_point!(main);

fn main(boot_info: &'static BootInfo) -> ! {
    rustbelt::init();
    rustbelt::init_memory(boot_info);
    rustbelt::init_smp(); // piethreads' threads spread over 4 CPUs
    fs::init();
    rustbelt::init_storage();
    test_main();
    rustbelt::hlt_loop();
}

/// Run /mnt/disk0/bin/`name` with `args`; its exit status and stdout.
fn run(name: &str, args: &[&str]) -> (ExitStatus, String) {
    let (reader, writer) = fs::pipe::new();
    let mut spawn = SpawnArgs::kernel(name);
    spawn.args.extend(args.iter().map(|a| String::from(*a)));
    spawn.stdio[1] = Arc::new(writer);
    let p = process::spawn_path(&alloc::format!("/mnt/disk0/bin/{}", name), spawn).expect("spawn failed");
    let mut out = Vec::new();
    let mut buf = [0u8; 256];
    loop {
        match reader.read(&mut buf).unwrap() {
            0 => break,
            n => out.extend_from_slice(&buf[..n]),
        }
    }
    (process::wait(&p), String::from_utf8_lossy(&out).into_owned())
}

fn counts() -> (usize, usize, usize) {
    thread::sleep_ticks(3);
    (thread::thread_count(), process::count(), memory::frames_in_use())
}

/// "hello" from a static PIE: its relocations, AT_PHDR/AT_ENTRY/AT_BASE.
#[test_case]
fn static_pie_hello() {
    let (status, out) = run("piehello", &[]);
    assert!(out.starts_with("hello from a static PIE\n"), "output: {}", out);
    assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);
}

/// Every run lands at a different, page-aligned address in user space.
#[test_case]
fn static_pie_base_is_random() {
    let parse = |s: &str| u64::from_str_radix(s.trim().trim_start_matches("0x"), 16).unwrap();
    let (s1, a) = run("piehello", &["where"]);
    let (s2, b) = run("piehello", &["where"]);
    assert_eq!((s1, s2), (ExitStatus::Exited(0), ExitStatus::Exited(0)));
    let (a, b) = (parse(&a), parse(&b));
    assert_ne!(a, b, "same base twice");
    assert_eq!(a % 4096, b % 4096, "main moved within its page");
    assert!(a > memory::USER_START && a < rustbelt::mm::MMAP_TOP);
}

/// pthreads + thread-local storage (PT_TLS) + stdio + musl's ttyname on a
/// pty, as a static PIE. Nothing leaks.
#[test_case]
fn static_pie_threads_and_stdio() {
    let (status, out) = run("piethreads", &[]);
    assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);
    let before = counts();
    let (status, out) = run("piethreads", &[]);
    assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);
    assert!(out.contains("piethreads: 0 failed"));
    assert_eq!(counts(), before, "piethreads leaked something");
}

/// A dynamically linked program (PT_INTERP) is refused with ENOEXEC.
#[test_case]
fn dynamic_program_is_enoexec() {
    let image = fs::read_all("/mnt/disk0/bin/dynhello").unwrap();
    assert_eq!(elf::check(&image), Err(elf::ElfError::NeedsInterpreter));
    let err = process::spawn_path("/mnt/disk0/bin/dynhello", SpawnArgs::kernel("dynhello")).err();
    assert_eq!(err, Some(Errno::ENOEXEC));
}

/// The loader still takes the classic ET_EXEC files (Rust programs, the
/// no-libc C program, musl ET_EXECs), and checks PIEs at their base.
#[test_case]
fn exec_files_still_load() {
    for name in ["hello", "rawabi", "vdsotest", "exechello", "execthreads"] {
        let image = fs::read_all(&alloc::format!("/mnt/disk0/bin/{}", name)).unwrap();
        assert_eq!(u16::from_le_bytes([image[16], image[17]]), 2, "{} is not ET_EXEC", name);
        assert!(elf::check(&image).is_ok(), "{} doesn't load", name);
    }
    let pie = fs::read_all("/mnt/disk0/bin/piehello").unwrap();
    assert_eq!(u16::from_le_bytes([pie[16], pie[17]]), 3);
    let entry = elf::check(&pie).unwrap();
    assert!(entry > memory::USER_START + 0x40_0000, "PIE entry {:#x} not biased", entry);
    assert_eq!(run("rawabi", &["/tmp"]).0, ExitStatus::Exited(0));
}

/// Stage 20: stock `musl-gcc -static` (ET_EXEC at 0x400000, no special
/// flags) runs; its brk and mmap memory stay out of the image.
#[test_case]
fn stock_static_exec_hello() {
    let image = fs::read_all("/mnt/disk0/bin/exechello").unwrap();
    assert_eq!(u16::from_le_bytes([image[16], image[17]]), 2, "not ET_EXEC");
    assert!(elf::check(&image).unwrap() < 0x50_0000, "not linked at 0x400000");
    let before = counts();
    let (status, out) = run("exechello", &[]);
    assert!(out.starts_with("hello from a plain static executable\n"), "output: {}", out);
    assert!(out.contains("exechello: 0 failed"), "output: {}", out);
    assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);
    assert_eq!(counts(), before, "exechello leaked something");
}

/// Stage 20: the pthreads + stdio program again, built as a stock ET_EXEC.
#[test_case]
fn stock_static_exec_threads_and_stdio() {
    let image = fs::read_all("/mnt/disk0/bin/execthreads").unwrap();
    assert_eq!(u16::from_le_bytes([image[16], image[17]]), 2, "not ET_EXEC");
    let (status, out) = run("execthreads", &[]);
    assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);
    let before = counts();
    let (status, out) = run("execthreads", &[]);
    assert_eq!(status, ExitStatus::Exited(0), "output: {}", out);
    assert!(out.contains("piethreads: 0 failed"));
    assert_eq!(counts(), before, "execthreads leaked something");
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    rustbelt::test_panic_handler(info)
}