RustForge

rust-kernel

Rustbelt: a hobby x86_64 kernel written in Rust

rust-kernel / tests / vdso.rs
5461 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
//! Stage 19: the vDSO. Every process gets a little shared library from the
//! kernel (vdso/vdso.c) plus a read-only "vvar" page with the clock's
//! numbers, so musl's clock_gettime/gettimeofday/time/sched_getcpu don't
//! need a system call. The detailed checks run in user/vdsotest.c (a musl
//! program; exit code = failed checks); here we check the kernel side and
//! use `--trace` to prove no clock syscall happens.
#![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 bootloader::{BootInfo, entry_point};
use core::panic::PanicInfo;
use rustbelt::process::{self, ExitStatus, SpawnArgs};
use rustbelt::{fs, memory, syscall, thread, time, vdso};

entry_point!(main);

fn main(boot_info: &'static BootInfo) -> ! {
    rustbelt::init();
    rustbelt::init_memory(boot_info);
    rustbelt::init_smp(); // calibrates the TSC, which turns the fast path on
    fs::init();
    rustbelt::init_storage();
    test_main();
    rustbelt::hlt_loop();
}

fn run(args: &[&str], trace: bool) -> ExitStatus {
    let mut spawn = SpawnArgs::kernel("vdsotest");
    spawn.args.extend(args.iter().map(|a| String::from(*a)));
    spawn.trace = trace;
    let p = process::spawn_path("/mnt/disk0/bin/vdsotest", spawn).expect("spawn failed");
    process::wait(&p)
}

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

/// The embedded image is a one-page ELF shared object with the dynamic
/// symbol and SysV hash tables musl looks for.
#[test_case]
fn image_is_a_small_shared_object() {
    let image = vdso::IMAGE;
    assert_eq!(&image[..4], b"\x7fELF");
    assert_eq!(u16::from_le_bytes([image[16], image[17]]), 3, "not ET_DYN");
    assert_eq!(vdso::image_pages(), 1);
    let has = |needle: &[u8]| image.windows(needle.len()).any(|w| w == needle);
    for name in ["__vdso_clock_gettime", "__vdso_gettimeofday", "__vdso_time", "__vdso_getcpu", "LINUX_2.6"] {
        assert!(has(name.as_bytes()), "{} missing from the vDSO", name);
    }
}

/// The vvar page is published (even sequence number) and, after TSC
/// calibration on QEMU, allows the fast path.
#[test_case]
fn vvar_page_is_published() {
    assert_eq!(vdso::seq() % 2, 0);
    assert!(vdso::seq() >= 4, "published at boot and after calibration");
    assert!(vdso::tsc_trusted());
    assert_ne!(vdso::flags() & vdso::VVAR_TSC_OK, 0);
}

/// The kernel's clocks in nanoseconds: monotonic, and REALTIME - MONOTONIC
/// is the boot time (constant).
#[test_case]
fn kernel_clock_in_nanoseconds() {
    let mut prev = time::now_ns();
    for _ in 0..1000 {
        let now = time::now_ns();
        assert!(now >= prev);
        prev = now;
    }
    let (_, mult, boot) = time::clock_params();
    assert_ne!(mult, 0);
    let (r, m) = (time::realtime_ns(), time::now_ns());
    assert!(r >= boot + m - 1_000_000 && r <= boot + m, "realtime {} vs boot {} + now {}", r, boot, m);
    assert!(boot / 1_000_000_000 > 1_577_836_800, "boot time before 2020");
}

/// All of user/vdsotest.c: vDSO vs syscall agreement, monotonicity, other
/// clocks, gettimeofday/time/getcpu, auxv, the read-only vvar page, fork,
/// execve, ENOEXEC for a dynamic program. Nothing leaks.
#[test_case]
fn vdsotest_passes() {
    assert_eq!(run(&[], false), ExitStatus::Exited(0), "vdsotest failed (warm-up)");
    let before = counts();
    assert_eq!(run(&[], false), ExitStatus::Exited(0), "vdsotest failed");
    assert_eq!(counts(), before, "vdsotest leaked something");
}

fn clock_syscalls(trace: &str) -> usize {
    trace
        .lines()
        .filter(|l| ["] clock_gettime(", "] gettimeofday(", "] time(", "] getcpu("].iter().any(|c| l.contains(c)))
        .count()
}

/// 1000 rounds of clock_gettime(MONOTONIC and REALTIME), gettimeofday,
/// time and sched_getcpu under `--trace`: not one of them is a syscall.
#[test_case]
fn clock_reads_make_no_syscalls() {
    syscall::collect_trace(true);
    let status = run(&["calls"], true);
    let trace = syscall::collect_trace(false);
    assert_eq!(status, ExitStatus::Exited(0));
    assert!(trace.contains("exit_group(0)") || trace.contains("exit(0)"), "no trace collected:\n{}", trace);
    assert_eq!(clock_syscalls(&trace), 0, "clock syscalls in the trace:\n{}", trace);
}

/// Without a trustworthy TSC the vDSO makes the system calls instead, and
/// everything still passes.
#[test_case]
fn falls_back_to_syscalls() {
    vdso::force_syscall(true);
    assert_eq!(vdso::flags(), 0);
    syscall::collect_trace(true);
    let calls = run(&["calls"], true);
    let trace = syscall::collect_trace(false);
    let full = run(&[], false);
    vdso::force_syscall(false);
    assert_eq!(calls, ExitStatus::Exited(0));
    assert_eq!(full, ExitStatus::Exited(0), "vdsotest failed on the syscall path");
    assert!(clock_syscalls(&trace) >= 5000, "only {} clock syscalls", clock_syscalls(&trace));
    assert_ne!(vdso::flags() & vdso::VVAR_TSC_OK, 0);
}

/// `--trace` knows the new fallback calls by name.
#[test_case]
fn trace_names() {
    assert_eq!(syscall::trace_name(96), "gettimeofday");
    assert_eq!(syscall::trace_name(201), "time");
    assert_eq!(syscall::trace_name(309), "getcpu");
}

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