From 85016e91a84dd8fb8f47adf85b4acfd99b4e64a8 Mon Sep 17 00:00:00 2001 From: Nicholas Bishop Date: Sun, 7 May 2023 22:28:18 -0400 Subject: [PATCH] Make UEFI shell protocols testable Add a new `shell_launcher` app that gets booted first when launching QEMU. This app launches the shell app (which is built alongside the OVMF files). The shell app's command line is set so that it will launch the test-runner app. Doing it this way allows us to avoid the shell app's built-in five second startup delay. By launching the test-runner inside the shell, in future commits we'll be able to test UEFI shell protocols. --- uefi-test-runner/src/bin/shell_launcher.rs | 86 ++++++++++++++++++++++ xtask/src/qemu.rs | 66 +++++++++++++---- 2 files changed, 137 insertions(+), 15 deletions(-) create mode 100644 uefi-test-runner/src/bin/shell_launcher.rs diff --git a/uefi-test-runner/src/bin/shell_launcher.rs b/uefi-test-runner/src/bin/shell_launcher.rs new file mode 100644 index 000000000..414f12535 --- /dev/null +++ b/uefi-test-runner/src/bin/shell_launcher.rs @@ -0,0 +1,86 @@ +//! This application launches the UEFI shell app and runs the main +//! uefi-test-running app inside that shell. This allows testing of protocols +//! that require the shell. +//! +//! Launching the shell this way (rather than directly making it the boot +//! executable) makes it possible to avoid the shell's built-in five second +//! startup delay. + +#![no_std] +#![no_main] + +extern crate alloc; + +use alloc::vec::Vec; +use log::info; +use uefi::prelude::*; +use uefi::proto::device_path::build::{self, DevicePathBuilder}; +use uefi::proto::device_path::{DevicePath, DeviceSubType, DeviceType, LoadedImageDevicePath}; +use uefi::proto::loaded_image::LoadedImage; +use uefi::table::boot::LoadImageSource; +use uefi::Status; + +/// Get the device path of the shell app. This is the same as the +/// currently-loaded image's device path, but with the file path part changed. +fn get_shell_app_device_path<'a>( + boot_services: &BootServices, + storage: &'a mut Vec, +) -> &'a DevicePath { + let loaded_image_device_path = boot_services + .open_protocol_exclusive::(boot_services.image_handle()) + .expect("failed to open LoadedImageDevicePath protocol"); + + let mut builder = DevicePathBuilder::with_vec(storage); + for node in loaded_image_device_path.node_iter() { + if node.full_type() == (DeviceType::MEDIA, DeviceSubType::MEDIA_FILE_PATH) { + break; + } + builder = builder.push(&node).unwrap(); + } + builder = builder + .push(&build::media::FilePath { + path_name: cstr16!(r"efi\boot\shell.efi"), + }) + .unwrap(); + builder.finalize().unwrap() +} + +#[entry] +fn efi_main(image: Handle, mut st: SystemTable) -> Status { + uefi_services::init(&mut st).unwrap(); + let boot_services = st.boot_services(); + + let mut storage = Vec::new(); + let shell_image_path = get_shell_app_device_path(boot_services, &mut storage); + + // Load the shell app. + let shell_image_handle = boot_services + .load_image( + image, + LoadImageSource::FromFilePath { + file_path: shell_image_path, + from_boot_manager: false, + }, + ) + .expect("failed to load shell app"); + + // Set the command line passed to the shell app so that it will run the + // test-runner app. This automatically turns off the five-second delay. + let mut shell_loaded_image = boot_services + .open_protocol_exclusive::(shell_image_handle) + .expect("failed to open LoadedImage protocol"); + let load_options = cstr16!(r"shell.efi test_runner.efi"); + unsafe { + shell_loaded_image.set_load_options( + load_options.as_ptr().cast(), + load_options.num_bytes() as u32, + ); + } + + info!("launching the shell app"); + boot_services + .start_image(shell_image_handle) + .expect("failed to launch the shell app"); + + Status::SUCCESS +} diff --git a/xtask/src/qemu.rs b/xtask/src/qemu.rs index a63f1116a..6ad04f97a 100644 --- a/xtask/src/qemu.rs +++ b/xtask/src/qemu.rs @@ -35,6 +35,9 @@ const ENV_VAR_OVMF_CODE: &str = "OVMF_CODE"; /// Environment variable for overriding the path of the OVMF vars file. const ENV_VAR_OVMF_VARS: &str = "OVMF_VARS"; +/// Environment variable for overriding the path of the OVMF shell file. +const ENV_VAR_OVMF_SHELL: &str = "OVMF_SHELL"; + /// Download `url` and return the raw data. fn download_url(url: &str) -> Result> { let agent: Agent = ureq::AgentBuilder::new() @@ -145,6 +148,7 @@ fn update_prebuilt() -> Result { enum OvmfFileType { Code, Vars, + Shell, } impl OvmfFileType { @@ -152,6 +156,14 @@ impl OvmfFileType { match self { Self::Code => "code", Self::Vars => "vars", + Self::Shell => "shell", + } + } + + fn extension(&self) -> &'static str { + match self { + Self::Code | Self::Vars => "fd", + Self::Shell => "efi", } } @@ -171,6 +183,10 @@ impl OvmfFileType { opt_path = &opt.ovmf_vars; var_name = ENV_VAR_OVMF_VARS; } + Self::Shell => { + opt_path = &None; + var_name = ENV_VAR_OVMF_SHELL; + } } if let Some(path) = opt_path { Some(path.clone()) @@ -183,6 +199,7 @@ impl OvmfFileType { struct OvmfPaths { code: PathBuf, vars: PathBuf, + shell: PathBuf, } impl OvmfPaths { @@ -209,7 +226,11 @@ impl OvmfPaths { } else { let prebuilt_dir = update_prebuilt()?; - Ok(prebuilt_dir.join(format!("{arch}/{}.fd", file_type.as_str()))) + Ok(prebuilt_dir.join(format!( + "{arch}/{}.{}", + file_type.as_str(), + file_type.extension() + ))) } } @@ -218,8 +239,9 @@ impl OvmfPaths { fn find(opt: &QemuOpt, arch: UefiArch) -> Result { let code = Self::find_ovmf_file(OvmfFileType::Code, opt, arch)?; let vars = Self::find_ovmf_file(OvmfFileType::Vars, opt, arch)?; + let shell = Self::find_ovmf_file(OvmfFileType::Shell, opt, arch)?; - Ok(Self { code, vars }) + Ok(Self { code, vars, shell }) } } @@ -362,7 +384,7 @@ fn process_qemu_io(mut monitor_io: Io, mut serial_io: Io, tmp_dir: &Path) -> Res } /// Create an EFI boot directory to pass into QEMU. -fn build_esp_dir(opt: &QemuOpt) -> Result { +fn build_esp_dir(opt: &QemuOpt, ovmf_paths: &OvmfPaths) -> Result { let build_mode = if opt.build_mode.release { "release" } else { @@ -372,21 +394,36 @@ fn build_esp_dir(opt: &QemuOpt) -> Result { .join(opt.target.as_triple()) .join(build_mode); let esp_dir = build_dir.join("esp"); + + // Create boot dir. let boot_dir = esp_dir.join("EFI").join("Boot"); - let built_file = if let Some(example) = &opt.example { - build_dir.join("examples").join(format!("{example}.efi")) - } else { - build_dir.join("uefi-test-runner.efi") - }; - let output_file = match *opt.target { + if !boot_dir.exists() { + fs_err::create_dir_all(&boot_dir)?; + } + + let boot_file_name = match *opt.target { UefiArch::AArch64 => "BootAA64.efi", UefiArch::IA32 => "BootIA32.efi", UefiArch::X86_64 => "BootX64.efi", }; - if !boot_dir.exists() { - fs_err::create_dir_all(&boot_dir)?; - } - fs_err::copy(built_file, boot_dir.join(output_file))?; + + if let Some(example) = &opt.example { + // Launch examples directly. + let src_path = build_dir.join("examples").join(format!("{example}.efi")); + fs_err::copy(src_path, boot_dir.join(boot_file_name))?; + } else { + // For the test-runner, launch the `shell_launcher` binary first. That + // will then launch the UEFI shell, and run the `uefi-test-runner` + // inside the shell. This allows the test-runner to test protocols that + // use the shell. + let shell_launcher = build_dir.join("shell_launcher.efi"); + fs_err::copy(shell_launcher, boot_dir.join(boot_file_name))?; + + fs_err::copy(&ovmf_paths.shell, boot_dir.join("shell.efi"))?; + + let test_runner = build_dir.join("uefi-test-runner.efi"); + fs_err::copy(test_runner, boot_dir.join("test_runner.efi"))?; + }; Ok(esp_dir) } @@ -413,8 +450,6 @@ impl Drop for ChildWrapper { } pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> { - let esp_dir = build_esp_dir(opt)?; - let qemu_exe = match arch { UefiArch::AArch64 => "qemu-system-aarch64", UefiArch::IA32 | UefiArch::X86_64 => "qemu-system-x86_64", @@ -516,6 +551,7 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> { // Mount a local directory as a FAT partition. cmd.arg("-drive"); let mut drive_arg = OsString::from("format=raw,file=fat:rw:"); + let esp_dir = build_esp_dir(opt, &ovmf_paths)?; drive_arg.push(esp_dir); cmd.arg(drive_arg);