//! Stage 21: boot contract v2. scripts/qemu.sh gives this test kernel one //! more disk, target/rootfs-gpt.img (build.rs): a GPT disk with an ext2 //! partition of type "Linux root (x86-64)" and an exFAT "Microsoft basic //! data" partition. Its /sbin/init, /bin/init and /bin/sh are all //! user/inittest.c, which exits 7 (from /etc/init-status) when every check //! passes as /sbin/init, 21 as /bin/init and 22 as /bin/sh. //! //! (The other half, "no root partition = the old layout", is checked by //! tests/fat.rs and by scripts/run-headless.sh; main.rs's use of all this, //! with QEMU's exit code, by scripts/boot-v2-check.sh.) #![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::vec::Vec; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rustbelt::boot; use rustbelt::errno::Errno; use rustbelt::fs::{self, InodeKind}; use rustbelt::process::{self, ExitStatus}; use rustbelt::{memory, thread}; entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { rustbelt::init(); rustbelt::init_memory(boot_info); rustbelt::init_smp(); fs::init(); rustbelt::init_storage(); rustbelt::timer::start(); // (its kernel stack isn't a leak) test_main(); rustbelt::hlt_loop(); } fn mount_type(path: &str) -> Option<&'static str> { fs::mounts().into_iter().find(|(p, _)| p == path).map(|(_, t)| t) } /// The first process this kernel starts: /sbin/init as pid 1. inittest /// checks pid 1, argv, PATH, the mounts, /mnt/share and msync itself, and /// exits 7 only if all of that held. #[test_case] fn init_runs_as_pid_1_and_its_status_comes_back() { let mut log = Vec::new(); let before = memory::frames_in_use(); let (path, status) = boot::run_init(false, &mut log).expect("no init started"); assert_eq!(path, "/sbin/init"); assert_eq!(status, ExitStatus::Exited(7), "inittest's checks failed (see its [inittest] lines)"); assert!(log.is_empty(), "{:?}", log); // What init wrote to the share is really there. assert_eq!(fs::read_all("/mnt/share/from-init.txt").unwrap(), b"written by init\n"); thread::sleep_ticks(3); assert_eq!(process::count(), 0); assert_eq!(memory::frames_in_use(), before, "running init leaked frames"); } #[test_case] fn the_root_partition_is_slash_and_the_rest_is_as_before() { assert!(boot::v2()); assert_eq!(mount_type("/"), Some("ext2")); assert!(fs::read_all("/etc/rootfs-v2").unwrap().starts_with(b"This is the ext2 root")); // The kernel's own mounts are still there... assert_eq!(mount_type("/tmp"), Some("ramfs")); assert!(mount_type("/dev").is_some() && mount_type("/proc").is_some()); // ...the FAT32 data disk is still /mnt/disk0 (B:), and the other disks // keep their numbers: the root and the share don't take one. assert_eq!(mount_type("/mnt/disk0"), Some("fat32")); assert_eq!(mount_type("/mnt/disk1"), Some("ext2")); assert_eq!(mount_type("/mnt/disk2"), Some("exfat")); assert!(mount_type("/mnt/disk9").is_some()); assert!(mount_type("/mnt/disk10").is_none()); assert!(fs::lookup("/mnt/disk0/bin").is_ok()); // The exFAT partition is the share, mounted read-write. assert_eq!(mount_type("/mnt/share"), Some("exfat")); assert_eq!(fs::read_all("/mnt/share/hello.txt").unwrap(), b"Hello from the exFAT share!\n"); let f = fs::create("/mnt/share/kernel.txt", InodeKind::File).unwrap(); f.write_at(0, b"rw").unwrap(); assert_eq!(fs::read_all("/mnt/share/kernel.txt").unwrap(), b"rw"); fs::remove("/mnt/share/kernel.txt", false).unwrap(); } #[test_case] fn init_gets_the_documented_argv_and_env() { let s = |v: &[&str]| v.iter().map(|x| String::from(*x)).collect::>(); let a = boot::init_args("/sbin/init", false); assert_eq!(a.args, s(&["init"])); assert_eq!(a.env, s(&["PATH=/sbin:/bin:/usr/bin:/mnt/disk0/bin", "HOME=/", "TERM=linux"])); let a = boot::init_args("/bin/init", true); assert_eq!(a.args, s(&["init", "selftest"])); assert!(a.env.iter().any(|e| e == "RUSTBELT_HEADLESS=1")); let a = boot::init_args("/bin/sh", true); assert_eq!(a.args, s(&["sh", "/scripts/selftest.sh"])); } /// Take away /sbin/init, then /bin/init, then /bin/sh (the disk is /// snapshot=on, so the file stays): the next one in line starts each time. #[test_case] fn the_fallback_chain() { let mut log = Vec::new(); fs::remove("/sbin/init", false).unwrap(); assert_eq!(boot::run_init(false, &mut log), Ok(("/bin/init", ExitStatus::Exited(21)))); fs::remove("/bin/init", false).unwrap(); assert_eq!(boot::run_init(false, &mut log), Ok(("/bin/sh", ExitStatus::Exited(22)))); fs::remove("/bin/sh", false).unwrap(); assert_eq!(boot::run_init(false, &mut log), Err(Errno::ENOENT)); // Something that isn't a program is skipped (and logged), like Linux. fs::create("/sbin/init", InodeKind::File).unwrap().write_at(0, b"not an ELF file").unwrap(); assert_eq!(boot::run_init(false, &mut log), Err(Errno::ENOENT)); assert_eq!(log.len(), 1, "{:?}", log); assert!(log[0].starts_with("/sbin/init: can't start it"), "{:?}", log); } /// pid 1's status -> the value for isa-debug-exit (QEMU exits with /// 2 * value + 1): exit(n) -> 33 + 2n. #[test_case] fn exit_status_to_qemu() { assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(0)), 0x10); // 33, as before assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(1)), 0x11); // 35, as before assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(7)), 0x17); // 47 assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(111)), 0x7f); // 255 assert_eq!(boot::qemu_exit_value(ExitStatus::Exited(112)), 0x11); assert_eq!(boot::qemu_exit_value(ExitStatus::Killed(9, "SIGKILL")), 0x11); } #[panic_handler] fn panic(info: &PanicInfo) -> ! { rustbelt::test_panic_handler(info) }