//! Stage 14, the process model: fork with copy-on-write, execve with a //! Linux-style initial stack, mmap and demand paging, signals, wait4, job //! control. Most checks run inside the user test programs (forktest, //! cowtest, mmaptest, sigtest: exit code = failed checks); this file starts //! them and adds what only the kernel can see: memory leaks, the process //! table limit, Ctrl+C from the keyboard. #![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, sync::Arc}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rustbelt::process::{self, ExitStatus, SpawnArgs}; use rustbelt::{fs, memory, signal, thread, tty}; entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { rustbelt::init(); rustbelt::init_memory(boot_info); rustbelt::init_smp(); // all 4 CPUs run threads fs::init(); rustbelt::init_storage(); test_main(); rustbelt::hlt_loop(); } const BIN: &str = "/mnt/disk0/bin"; fn spawn(name: &str, args: &[&str], env: &[&str]) -> Arc { let mut spawn = SpawnArgs::kernel(name); spawn.args.extend(args.iter().map(|a| String::from(*a))); if !env.is_empty() { spawn.env = env.iter().map(|e| String::from(*e)).collect(); } process::spawn_path(&alloc::format!("{}/{}", BIN, name), spawn).expect("spawn failed") } fn run(name: &str, args: &[&str]) -> ExitStatus { let p = spawn(name, args, &[]); process::wait(&p) // (also removes it from the process table) } /// Writes in a forked child don't show up in the parent (and back), /// MAP_SHARED pages are shared. #[test_case] fn copy_on_write_isolation() { assert_eq!(run("cowtest", &[]), ExitStatus::Exited(0)); } /// fork, wait4, execve, vfork, dup2 through pipes, a SIGSEGV'd child. #[test_case] fn fork_exec_wait() { assert_eq!(run("forktest", &[]), ExitStatus::Exited(0)); } /// A program started by the kernel gets argv, envp and auxv exactly like /// on Linux (AT_PHDR, AT_PAGESZ, AT_ENTRY, AT_RANDOM, AT_EXECFN...). #[test_case] fn exec_argv_envp_auxv() { let p = spawn("forktest", &["args", "two words", ""], &["FOO=bar", "EMPTY="]); assert_eq!(process::wait(&p), ExitStatus::Exited(0)); } /// mmap/munmap/mprotect/brk, demand paging, and faults delivered to a /// SIGSEGV handler that fixes them. #[test_case] fn mmap_and_fault_handlers() { assert_eq!(run("mmaptest", &[]), ExitStatus::Exited(0)); } /// Without a handler, a bad write is SIGSEGV's default: terminate, 139. #[test_case] fn unhandled_fault_is_sigsegv() { let status = run("mmaptest", &["crash"]); assert!(matches!(status, ExitStatus::Killed(signal::SIGSEGV, _)), "{:?}", status); assert_eq!(status.code(), 139); assert_eq!(status.wait_status(), 11); } /// Handlers + sigreturn, masks, default actions, SIGCHLD, stop/continue, /// SIGPIPE, EINTR and SA_RESTART, kill on process groups. #[test_case] fn signals() { assert_eq!(run("sigtest", &[]), ExitStatus::Exited(0)); } /// wait4's status word, the way the W* macros decode it. #[test_case] fn wait_status_encoding() { assert_eq!(ExitStatus::Exited(3).wait_status(), 0x300); assert_eq!(ExitStatus::Exited(256 + 7).wait_status(), 0x700); // only 8 bits survive assert_eq!(ExitStatus::Killed(signal::SIGKILL, "x").wait_status(), 9); assert_eq!(ExitStatus::Killed(signal::SIGINT, "x").code(), 130); } /// A fork bomb hits the process limit (EAGAIN) instead of eating all /// memory, and after killing it everything is given back. #[test_case] fn fork_bomb_is_bounded_and_leaks_nothing() { assert_eq!(run("forktest", &["bomb"]), ExitStatus::Exited(0)); // warm up (caches, page tables) let (frames, processes) = (memory::frames_in_use(), process::count()); assert_eq!(run("forktest", &["bomb"]), ExitStatus::Exited(0)); thread::sleep_ticks(2); // let exited threads' stacks be freed assert_eq!(process::count(), processes); assert_eq!(memory::frames_in_use(), frames, "frames leaked by the fork bomb"); } /// Many rounds of fork + exec + exit + wait: the frame count comes back /// exactly (shared-frame refcounts drop to zero, page tables are freed). #[test_case] fn no_frame_leaks_across_fork_exec_exit() { run("forktest", &[]); run("cowtest", &[]); let frames = memory::frames_in_use(); let shared = memory::shared_frame_count(); for _ in 0..3 { assert_eq!(run("forktest", &[]), ExitStatus::Exited(0)); assert_eq!(run("cowtest", &[]), ExitStatus::Exited(0)); } thread::sleep_ticks(2); assert_eq!(memory::shared_frame_count(), shared, "copy-on-write refcounts left behind"); assert_eq!(memory::frames_in_use(), frames, "frames leaked across fork/exec/exit"); } /// Ctrl+C on the console: the tty sends SIGINT to the foreground process /// group, and `spin` (which never makes a syscall) dies of it. #[test_case] fn ctrl_c_interrupts_the_foreground_job() { let p = spawn("spin", &["1000"], &[]); // ~minutes of spinning thread::sleep_ticks(4); assert!(p.status().is_none(), "spin ended by itself"); tty::set_foreground(p.pgid()); tty::input_char('\u{3}'); // what the keyboard driver delivers for Ctrl+C let status = process::wait(&p); tty::set_foreground(0); assert!(matches!(status, ExitStatus::Killed(signal::SIGINT, _)), "{:?}", status); assert_eq!(status.code(), 130); } /// Ctrl+C with nobody in the foreground hits nobody. #[test_case] fn ctrl_c_without_a_foreground_job_does_nothing() { let p = spawn("spin", &["2"], &[]); tty::set_foreground(0); tty::input_char('\u{3}'); assert_eq!(process::wait(&p), ExitStatus::Exited(0)); } #[panic_handler] fn panic(info: &PanicInfo) -> ! { rustbelt::test_panic_handler(info) }