diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 307acca..beab609 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -28,7 +28,7 @@ jobs: run: cargo test --workspace qemu: - name: Build boot image + name: Build and boot smoke test runs-on: ubuntu-latest needs: check steps: @@ -40,3 +40,23 @@ jobs: run: sudo apt-get update && sudo apt-get install -y qemu-system-x86 - name: Build boot image run: cargo build --verbose + - name: Boot BIOS image and assert serial success marker + shell: bash + run: | + set +e + timeout --signal=TERM --kill-after=5s 20s cargo run -- bios > qemu.log 2>&1 + status=$? + set -e + cat qemu.log + + if ! grep -Fq "[BOOT_OK] Rustalis booted successfully." qemu.log; then + echo "Kernel did not emit the expected boot-success marker." + exit 1 + fi + + # A healthy kernel intentionally remains in its idle loop; timeout + # is therefore expected after the marker has been observed. + if [ "$status" -ne 0 ] && [ "$status" -ne 124 ]; then + echo "QEMU runner exited unexpectedly with status $status." + exit "$status" + fi diff --git a/kernel/src/arch/x86_64/interrupts.rs b/kernel/src/arch/x86_64/interrupts.rs index b24d1ac..04d5059 100644 --- a/kernel/src/arch/x86_64/interrupts.rs +++ b/kernel/src/arch/x86_64/interrupts.rs @@ -14,5 +14,7 @@ pub struct InterruptStackFrame { pub fn init() { // SAFETY: maskable interrupts stay disabled until an IDT and handlers // have been installed. This prevents delivery to an incomplete table. - unsafe { asm!("cli", options(nomem, nostack, preserves_flags)); } + unsafe { + asm!("cli", options(nomem, nostack, preserves_flags)); + } } diff --git a/kernel/src/fs.rs b/kernel/src/fs.rs index f8e9448..a552b1b 100644 --- a/kernel/src/fs.rs +++ b/kernel/src/fs.rs @@ -59,7 +59,10 @@ impl RamFs { if self.find(path).is_some() { return Err(FsError::AlreadyExists); } - let slot = self.entries.iter().position(|entry| !entry.used) + let slot = self + .entries + .iter() + .position(|entry| !entry.used) .ok_or(FsError::NoFreeSlots)?; let entry = &mut self.entries[slot]; entry.path[..path.len()].copy_from_slice(path.as_bytes()); @@ -100,8 +103,11 @@ impl RamFs { /// Return the path of the Nth occupied slot, for simple directory listings. pub fn path_at(&self, occupied_index: usize) -> Option<&str> { - self.entries.iter().filter(|entry| entry.used) - .nth(occupied_index).map(Entry::path) + self.entries + .iter() + .filter(|entry| entry.used) + .nth(occupied_index) + .map(Entry::path) } pub fn file_count(&self) -> usize { @@ -109,7 +115,9 @@ impl RamFs { } fn find(&self, path: &str) -> Option { - self.entries.iter().position(|entry| entry.used && entry.path() == path) + self.entries + .iter() + .position(|entry| entry.used && entry.path() == path) } } @@ -120,7 +128,10 @@ fn validate_path(path: &str) -> Result<&str, FsError> { if path.len() > MAX_PATH_BYTES { return Err(FsError::PathTooLong); } - if path.split('/').any(|part| part.is_empty() || part == "." || part == "..") { + if path + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + { return Err(FsError::InvalidPath); } Ok(path) diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 649d844..9827d45 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -1,7 +1,7 @@ #![no_std] #![no_main] -use bootloader_api::{entry_point, BootInfo}; +use bootloader_api::{BootInfo, entry_point}; use core::panic::PanicInfo; mod arch; @@ -34,10 +34,15 @@ fn kernel_main(boot_info: &'static mut BootInfo) -> ! { logger::info("RAM filesystem initialized; /welcome.txt verified"); logger::info_u64("RAM filesystem files", ramfs.file_count() as u64); } else { - logger::error("RAM filesystem smoke test failed"); + logger::error("RAM filesystem smoke test failed; boot success withheld"); + loop { + core::hint::spin_loop(); + } } - logger::info("Rustalis booted successfully."); + // This marker is a CI/debugging contract: emit it only after early init and + // the RAM filesystem smoke test have completed successfully. + logger::info("[BOOT_OK] Rustalis booted successfully."); loop { core::hint::spin_loop(); } diff --git a/src/main.rs b/src/main.rs index ebc462f..d01d7de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use ovmf_prebuilt::{Arch, FileType, Prebuilt, Source}; use std::env; -use std::process::{exit, Command}; +use std::process::{Command, exit}; fn main() { let uefi_path = env!("UEFI_PATH"); @@ -24,15 +24,23 @@ fn main() { cmd.args(["-serial", "mon:stdio", "-display", "none"]); if uefi { - let prebuilt = Prebuilt::fetch(Source::LATEST, "target/ovmf") - .expect("failed to obtain OVMF firmware"); + let prebuilt = + Prebuilt::fetch(Source::LATEST, "target/ovmf").expect("failed to obtain OVMF firmware"); let code = prebuilt.get_file(Arch::X64, FileType::Code); let vars = prebuilt.get_file(Arch::X64, FileType::Vars); - cmd.arg("-drive").arg(format!("format=raw,file={uefi_path}")); - cmd.arg("-drive").arg(format!("if=pflash,format=raw,unit=0,file={},readonly=on", code.display())); - cmd.arg("-drive").arg(format!("if=pflash,format=raw,unit=1,file={},snapshot=on", vars.display())); + cmd.arg("-drive") + .arg(format!("format=raw,file={uefi_path}")); + cmd.arg("-drive").arg(format!( + "if=pflash,format=raw,unit=0,file={},readonly=on", + code.display() + )); + cmd.arg("-drive").arg(format!( + "if=pflash,format=raw,unit=1,file={},snapshot=on", + vars.display() + )); } else { - cmd.arg("-drive").arg(format!("format=raw,file={bios_path}")); + cmd.arg("-drive") + .arg(format!("format=raw,file={bios_path}")); } let status = cmd.status().expect("failed to start qemu-system-x86_64");