RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / tests / user_mode.rs
7236 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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! User mode: real ring-3 programs (user/*.rs, loaded from the FAT32 disk)
//! run as processes, make syscalls, get killed when they misbehave, get
//! preempted when they hog the CPU, spawn children and wait for them.
//! Plus the ELF loader, user-pointer checks and blocking primitives.
#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(rust_kernel::test_runner)]
#![reexport_test_harness_main = "test_main"]

extern crate alloc;

use alloc::{string::String, sync::Arc, vec, vec::Vec};
use bootloader::{BootInfo, entry_point};
use core::panic::PanicInfo;
use core::sync::atomic::{AtomicBool, Ordering};
use rust_kernel::errno::Errno;
use rust_kernel::fs::file::File;
use rust_kernel::interrupts::TICKS;
use rust_kernel::memory::{self, USER_START};
use rust_kernel::process::{self, ExitStatus, SpawnArgs};
use rust_kernel::thread::{self, WaitQueue};
use rust_kernel::{elf, fs, syscall};

entry_point!(main);

fn main(boot_info: &'static BootInfo) -> ! {
    rust_kernel::init();
    rust_kernel::init_memory(boot_info);
    fs::init();
    rust_kernel::init_storage();
    test_main();
    rust_kernel::hlt_loop();
}

const BIN: &str = "/mnt/disk0/bin";

/// Start /mnt/disk0/bin/<name> with these arguments (after the name).
fn run(name: &str, extra_args: &[&str]) -> Arc<process::Process> {
    let mut spawn = SpawnArgs::kernel(name);
    spawn.args.extend(extra_args.iter().map(|a| String::from(*a)));
    process::spawn_path(&alloc::format!("{}/{}", BIN, name), spawn).expect("spawn failed")
}

/// hello does ~10 syscalls (open/read/write/close on the ramfs, error cases)
/// and exits with the number of failed checks.
#[test_case]
fn hello_program_passes_its_syscall_checks() {
    let p = run("hello", &[]);
    assert_eq!(process::wait(&p), ExitStatus::Exited(0));
    // It created /tmp/note.txt through the syscalls; the kernel sees it too.
    assert_eq!(fs::lookup("/tmp/note.txt").expect("hello's file is missing").size(), 19);
}

/// demo checks cwd, FAT reads, stat, lseek, getdents64, mkdir/rmdir/unlink,
/// pipes, spawn + wait (incl. a killed child) -- exit code = failures.
#[test_case]
fn demo_program_passes_its_checks() {
    let p = run("demo", &[]);
    assert_eq!(process::wait(&p), ExitStatus::Exited(0));
}

/// evil reads kernel memory -> page fault in ring 3 -> killed. We're still here.
#[test_case]
fn kernel_memory_access_kills_only_the_process() {
    let p = run("evil", &[]);
    let status = process::wait(&p);
    assert_eq!(status, ExitStatus::Killed("page fault"));
    assert_eq!(status.code(), 139);
}

/// spin never makes a syscall while it loops. If preemption didn't work,
/// this kernel thread couldn't run again until spin finished.
#[test_case]
fn spinning_process_is_preempted() {
    let p = run("spin", &[]);
    let start = TICKS.load(Ordering::Relaxed);
    let mut samples_while_spinning = 0;
    while p.status().is_none() {
        // Busy-wait (no hlt): we only get here again if the timer took the
        // CPU away from spin and gave it back to us.
        if TICKS.load(Ordering::Relaxed) > start + 3 {
            samples_while_spinning += 1;
        }
        assert!(TICKS.load(Ordering::Relaxed) < start + 18 * 30, "spin didn't finish");
        core::hint::spin_loop();
    }
    assert_eq!(process::wait(&p), ExitStatus::Exited(0));
    assert!(samples_while_spinning > 0, "never ran while spin was running");
}

/// Arguments arrive in the child; its exit code comes back through wait.
/// (Tracing is on: its syscalls get printed to the serial log.)
#[test_case]
fn arguments_and_exit_codes() {
    let mut spawn = SpawnArgs::kernel("child");
    spawn.args.push(String::from("7"));
    spawn.trace = true;
    let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();
    assert_eq!(process::wait(&p), ExitStatus::Exited(7));
    // Reaped: the pid is gone from the process table.
    assert!(process::get(p.pid).is_none());
}

/// A child's stdout can be a pipe: we read what it printed.
#[test_case]
fn child_stdout_into_a_pipe() {
    let (reader, writer) = fs::pipe::new();
    let mut spawn = SpawnArgs::kernel("child");
    spawn.args.push(String::from("pipe"));
    spawn.stdio[1] = Arc::new(writer);
    let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap();
    let mut out = Vec::new();
    let mut buf = [0u8; 32];
    loop {
        // Blocks until the child writes or exits (its exit closes the pipe).
        match reader.read(&mut buf).unwrap() {
            0 => break,
            n => out.extend_from_slice(&buf[..n]),
        }
    }
    assert!(out.starts_with(b"hello through a pipe from pid"));
    assert_eq!(process::wait(&p), ExitStatus::Exited(0));
}

#[test_case]
fn spawn_errors() {
    let spawn = || SpawnArgs::kernel("x");
    assert_eq!(process::spawn_path("/mnt/disk0/bin/nope", spawn()).err(), Some(Errno::ENOENT));
    assert_eq!(process::spawn_path("/mnt/disk0/readme.txt", spawn()).err(), Some(Errno::ENOEXEC));
    assert_eq!(process::spawn_path("/mnt/disk0/bin", spawn()).err(), Some(Errno::EISDIR));
    assert_eq!(process::wait_child(12345, 1).err(), Some(Errno::ECHILD));
}

#[test_case]
fn elf_loader_rejects_garbage() {
    let hello = fs::read_all("/mnt/disk0/bin/hello").unwrap();
    assert_eq!(elf::check(&[b'x'; 128]), Err(elf::ElfError::NotElf));
    assert_eq!(elf::check(b"\x7fELF"), Err(elf::ElfError::TooShort));
    let entry = elf::check(&hello).expect("hello should parse");
    assert!(entry >= USER_START);
    // A truncated program: the header is fine, the program headers are cut off.
    assert!(elf::check(&hello[..70]).is_err());
}

#[test_case]
fn user_pointers_are_validated() {
    // Kernel addresses, unmapped user addresses and overflowing ranges all fail.
    assert_eq!(syscall::user_slice(0x20_0000, 16).err(), Some(Errno::EFAULT));
    assert_eq!(syscall::user_slice(USER_START, 16).err(), Some(Errno::EFAULT));
    assert_eq!(syscall::user_slice(u64::MAX - 4, 16).err(), Some(Errno::EFAULT));
    assert!(!memory::user_range_ok(USER_START - 8, 16, false));
    // Zero bytes: any pointer is fine.
    assert_eq!(syscall::user_slice(1, 0).map(|s| s.len()), Ok(0));
}

/// sleep_ticks really blocks: the thread is off the run queue meanwhile.
#[test_case]
fn sleep_blocks_for_the_requested_time() {
    let start = TICKS.load(Ordering::Relaxed);
    thread::sleep_ticks(4);
    let slept = TICKS.load(Ordering::Relaxed) - start;
    assert!((4..8).contains(&slept), "slept {} ticks", slept);
}

static FLAG: AtomicBool = AtomicBool::new(false);
static QUEUE: WaitQueue = WaitQueue::new();

/// A thread blocked on a WaitQueue wakes when another thread signals it.
#[test_case]
fn wait_queue_wakes_the_waiter() {
    thread::spawn("waker", || {
        thread::sleep_ticks(2);
        FLAG.store(true, Ordering::SeqCst);
        QUEUE.wake_all();
    });
    let start = TICKS.load(Ordering::Relaxed);
    let value = QUEUE.wait_until(|| FLAG.load(Ordering::SeqCst).then_some(42));
    assert_eq!(value, 42);
    assert!(TICKS.load(Ordering::Relaxed) - start >= 2);
    let _ = vec![0u8; 1]; // the heap still works after all the switching
}

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