diff --git a/Cargo.lock b/Cargo.lock index ff58ebe..e365813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,16 @@ name = "picker" version = "0.1.0" dependencies = [ + "bitfield 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", "rlibc 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "uefi 0.1.2 (git+https://github.com/csssuf/rust-uefi)", ] +[[package]] +name = "bitfield" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bitflags" version = "0.9.1" @@ -25,6 +31,7 @@ dependencies = [ ] [metadata] +"checksum bitfield 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "eb988c10184429ae27919881dd9ce0298b87557d12c508e664353eafb336580e" "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" "checksum rlibc 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc874b127765f014d792f16763a81245ab80500e2ad921ed4ee9e82481ee08fe" "checksum uefi 0.1.2 (git+https://github.com/csssuf/rust-uefi)" = "" diff --git a/Cargo.toml b/Cargo.toml index e670e12..1f5658d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,9 @@ authors = [ "james.forcier@coreos.com" ] [lib] name = "picker" crate-type = ["dylib"] +path = "src/picker.rs" [dependencies] uefi = { git = "https://github.com/csssuf/rust-uefi" } rlibc = "1.0" +bitfield = "0.12.0" diff --git a/src/boot/mod.rs b/src/boot/mod.rs index 259ec0f..d5bb331 100644 --- a/src/boot/mod.rs +++ b/src/boot/mod.rs @@ -18,10 +18,12 @@ use core::ptr; use uefi::*; -// When gptprio is implemented, boot_data may change to another type. For now it's just a path. +#[derive(Clone, Copy)] pub struct BootOption { pub display: &'static str, pub boot_data: &'static str, + pub default: bool, + pub guid: Guid, } fn str_to_device_path(image: &str) -> Result<&protocol::DevicePathProtocol, Status> { diff --git a/src/menu/mod.rs b/src/menu/mod.rs index fad8bad..c140b4b 100644 --- a/src/menu/mod.rs +++ b/src/menu/mod.rs @@ -35,10 +35,16 @@ where loop { write("Option 1: "); write(option_a.display); + if option_a.default { + write(" (default)"); + } write("\r\n"); write("Option 2: "); write(option_b.display); + if option_b.default { + write(" (default)"); + } write("\r\n"); write("Enter boot choice: "); @@ -50,7 +56,13 @@ where }, Ok(None) => { write("Taking default.\r\n"); - return Ok(None); + if option_a.default { + return Ok(Some(option_a)); + } else if option_b.default { + return Ok(Some(option_b)); + } else { + return Ok(None); + } } Err(e) => { write("Error reading: "); diff --git a/src/picker.rs b/src/picker.rs index b2a4a96..5cdc266 100644 --- a/src/picker.rs +++ b/src/picker.rs @@ -15,10 +15,13 @@ #![no_std] #![feature(lang_items)] +#[macro_use] +extern crate bitfield; extern crate rlibc; extern crate uefi; -use uefi::*; +use uefi::{Guid, Handle, SimpleTextOutput, Status}; +use uefi::util::parent_device_path; pub mod boot; pub mod menu; @@ -27,28 +30,90 @@ pub mod uefi_entry; use boot::BootOption; -const BOOTPATH_1: &'static str = "\\efi\\boot\\shim_a.efi"; -const BOOTPATH_2: &'static str = "\\efi\\boot\\shim_b.efi"; +const BOOTPATH_USR_A: &'static str = "\\EFI\\coreos\\shim_a.efi"; +const BOOTPATH_USR_B: &'static str = "\\EFI\\coreos\\shim_b.efi"; + +const PART_UUID_USR_A: Guid = Guid( + 0x7130_C94A, + 0x213A, + 0x4E5A, + [0x8E, 0x26, 0x6C, 0xCE, 0x96, 0x62, 0xF1, 0x32], +); +const PART_UUID_USR_B: Guid = Guid( + 0xE03D_D35C, + 0x7C2D, + 0x4A47, + [0xB3, 0xFE, 0x27, 0xF1, 0x57, 0x80, 0xA5, 0x7C], +); pub fn efi_main(image_handle: Handle) -> Status { let sys_table = uefi::get_system_table(); + let bs = sys_table.boot_services(); let cons = sys_table.console(); cons.write("picker v0.0.1\r\n"); - let option_a = BootOption { - display: "application a", - boot_data: BOOTPATH_1, + let mut option_a = BootOption { + display: "USR-A", + boot_data: BOOTPATH_USR_A, + default: false, + guid: PART_UUID_USR_A, }; - let option_b = BootOption { - display: "application b", - boot_data: BOOTPATH_2, + let mut option_b = BootOption { + display: "USR-B", + boot_data: BOOTPATH_USR_B, + default: false, + guid: PART_UUID_USR_B, }; - match menu::boot_menu(&option_a, &option_b).and_then(|option| { - let result = option.unwrap_or(&option_a); - boot::boot(result, image_handle) - }) { + let this = uefi::protocol::get_current_image(); + let partition = bs.handle_protocol::(this.device_handle) + .and_then(parent_device_path) + .and_then(|parent_path| util::GptDisk::read_from(parent_path)) + .map(|disk| util::gptprio::next(disk.partitions)); + + match partition { + Ok(Some(gptprio_partition)) => { + if option_a.guid == gptprio_partition.unique_partition_guid { + option_a.default = true; + } else if option_b.guid == gptprio_partition.unique_partition_guid { + option_b.default = true; + } else { + cons.write( + "Unknown gptprio partition chosen as next. Defaulting to USR-A.\r\n", + ); + option_a.default = true; + } + } + Ok(None) => { + cons.write( + "No acceptable gptprio partitions found; defaulting to USR-A.\r\n", + ); + option_a.default = true; + } + Err(e) => { + cons.write("error reading from disk: "); + cons.write(e.str()); + return e; + } + } + + let boot_result = menu::boot_menu(&option_a, &option_b) + .and_then(|option| { + match option { + Some(boot_choice) => Ok(boot_choice), + None => { + cons.write( + "No option selected and no default was set. Can't proceed.\r\n", + ); + // FIXME(csssuf) is this the best error to use here? + Err(Status::NoMedia) + } + } + }) + .and_then(|boot_option| boot::boot(boot_option, image_handle)); + + match boot_result { Ok(_) => Status::Success, Err(e) => e, } diff --git a/src/util/gpt.rs b/src/util/gpt.rs new file mode 100644 index 0000000..647e49d --- /dev/null +++ b/src/util/gpt.rs @@ -0,0 +1,220 @@ +// Copyright 2017 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate uefi; + +use core::{mem, slice}; + +use uefi::{Guid, Handle, Status, GET_PROTOCOL}; +use uefi::protocol::{get_current_image_handle, BlockIOProtocol, DevicePathProtocol}; + +// "EFI PART", u64 constant given in UEFI spec +const HEADER_SIGNATURE: u64 = 0x5452_4150_2049_4645; + +#[repr(C, packed)] +pub struct GptHeader { + signature: u64, + revision: u32, + header_size: u32, + header_crc32: u32, + _reserved: u32, + my_lba: u64, + alternate_lba: u64, + first_usable_lba: u64, + last_usable_lba: u64, + disk_guid: Guid, + partition_entry_lba: u64, + num_partition_entries: u32, + sizeof_partition_entry: u32, + partition_entry_array_crc32: u32, +} + +impl GptHeader { + fn validate(&mut self, my_lba: u64) -> Result<(), Status> { + let bs = uefi::get_system_table().boot_services(); + + if self.signature != HEADER_SIGNATURE { + return Err(Status::InvalidParameter); + } + + if self.my_lba != my_lba { + // FIXME(csssuf): is there a better error to use here? spec doesn't say + return Err(Status::VolumeCorrupted); + } + + let my_crc32 = self.header_crc32; + + self.header_crc32 = 0; + let crc32 = bs.calculate_crc32_sized(self, self.header_size as usize)?; + self.header_crc32 = my_crc32; + + if crc32 != self.header_crc32 { + return Err(Status::CrcError); + } + + Ok(()) + } +} + +#[derive(Copy)] +#[repr(C, packed)] +pub struct GptPartitionEntry { + pub(crate) partition_type_guid: Guid, + pub(crate) unique_partition_guid: Guid, + starting_lba: u64, + ending_lba: u64, + pub(crate) attributes: u64, + pub(crate) partition_name: [u16; 36], +} + +impl Clone for GptPartitionEntry { + fn clone(&self) -> GptPartitionEntry { + GptPartitionEntry { + partition_type_guid: self.partition_type_guid, + unique_partition_guid: self.unique_partition_guid, + starting_lba: self.starting_lba, + ending_lba: self.ending_lba, + attributes: self.attributes, + partition_name: self.partition_name, + } + } +} + +type GptPartitionTable<'a> = &'a [&'a mut GptPartitionEntry]; + +pub struct GptDisk<'a> { + block_device: &'static BlockIOProtocol, + primary_header: &'a mut GptHeader, + alternate_header: &'a mut GptHeader, + raw_partitions: &'a mut [u8], + pub partitions: GptPartitionTable<'a>, +} + +impl<'a> Drop for GptDisk<'a> { + fn drop(&mut self) { + let bs = uefi::get_system_table().boot_services(); + + self.block_device.free_read(self.raw_partitions); + bs.free_pages( + self.primary_header, + self.block_device.required_pages_block(1), + ); + bs.free_pages( + self.alternate_header, + self.block_device.required_pages_block(1), + ); + } +} + +impl<'a> GptDisk<'a> { + /// Read the GPT header from a given device, and perform all necessary validation on it. + pub fn read_from(device: &DevicePathProtocol) -> Result { + let bs = uefi::get_system_table().boot_services(); + + let (handle, _usable_device) = bs.locate_device_path::(device)?; + let protocol = bs.open_protocol::( + handle, + get_current_image_handle(), + Handle::default(), + GET_PROTOCOL, + )?; + + let primary_block = protocol.read_blocks(1, 1)?; + let primary_header = unsafe { &mut *(primary_block.as_mut_ptr() as *mut GptHeader) }; + + let alternate_block = protocol.read_blocks(primary_header.alternate_lba, 1)?; + let alternate_header = unsafe { &mut *(alternate_block.as_mut_ptr() as *mut GptHeader) }; + + let mut out = GptDisk { + block_device: protocol, + primary_header: primary_header, + alternate_header: alternate_header, + raw_partitions: &mut [], + partitions: &[], + }; + + unsafe { out.validate_headers()? }; + + out.read_partitions()?; + + unsafe { out.validate_partitions()? }; + + Ok(out) + } + + /// Validate an instance of a `GptHeader`. + /// The UEFI spec gives the steps required to validate a GPT header as follows: + /// >* Check the Signature + /// >* Check the Header CRC + /// >* Check that the MyLBA entry points to the LBA that contains the GUID Partition Table + /// >* Check the CRC of the GUID Partition Entry Array + /// + /// >If the GPT is the primary table, stored at LBA 1: + /// + /// >* Check the AlternateLBA to see if it is a valid GPT + unsafe fn validate_headers(&mut self) -> Result<(), Status> { + // The primary header is always read from LBA 1, so we call `validate` with that value. + self.primary_header.validate(1)?; + self.alternate_header + .validate(self.primary_header.alternate_lba)?; + + Ok(()) + } + + unsafe fn validate_partitions(&mut self) -> Result<(), Status> { + let partition_crc32 = uefi::get_system_table() + .boot_services() + .calculate_crc32_sized(self.raw_partitions.as_ptr(), self.raw_partitions.len())?; + + if partition_crc32 != self.primary_header.partition_entry_array_crc32 { + return Err(Status::CrcError); + } + + Ok(()) + } + + /// Read the partition entry array from this disk and return it. + fn read_partitions(&mut self) -> Result<(), Status> { + let bs = uefi::get_system_table().boot_services(); + + let num_partitions = self.primary_header.num_partition_entries as usize; + self.read_raw_partitions()?; + + let entries_ptr = bs.allocate_pool::<&mut GptPartitionEntry>( + num_partitions * mem::size_of::<&mut GptPartitionEntry>(), + )?; + + let entries = unsafe { slice::from_raw_parts_mut(entries_ptr, num_partitions) }; + for part_number in 0..(self.primary_header.num_partition_entries) { + let offset = (part_number * self.primary_header.sizeof_partition_entry) as isize; + + unsafe { + let entry_ptr = self.raw_partitions.as_ptr().offset(offset); + let entry = &mut *(entry_ptr as *mut GptPartitionEntry); + (*entries)[part_number as usize] = entry; + } + } + + self.partitions = entries; + Ok(()) + } + + fn read_raw_partitions(&mut self) -> Result<(), Status> { + let read_size = (self.primary_header.num_partition_entries * + self.primary_header.sizeof_partition_entry) as usize; + self.raw_partitions = self.block_device + .read_bytes(self.primary_header.partition_entry_lba, read_size)?; + Ok(()) + } +} diff --git a/src/util/gptprio.rs b/src/util/gptprio.rs new file mode 100644 index 0000000..d5a84e2 --- /dev/null +++ b/src/util/gptprio.rs @@ -0,0 +1,53 @@ +// Copyright 2017 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +use util::gpt::GptPartitionEntry; + +use uefi::Guid; + +static COREOS_ROOTFS_TYPE: Guid = Guid( + 0x5DFB_F5F4, + 0x2848, + 0x4BAC, + [0xAA, 0x5E, 0x0D, 0x9A, 0x20, 0xB7, 0x45, 0xA6], +); + +bitfield! { + struct GptprioAttributes(u64); + _required, _: 0; + _no_block_io, _: 1; + _legacy_bios_bootable, _: 2; + _unused1, _: 47, 3; + gptprio_priority, _: 51, 48; + gptprio_tries_left, _: 55, 52; + gptprio_successful, _: 56; + _unused2, _: 63, 57; +} + +pub fn next(partitions: &[&mut GptPartitionEntry]) -> Option { + let partition_iter = partitions.into_iter(); + + partition_iter + .filter(|partition| { + partition.partition_type_guid == COREOS_ROOTFS_TYPE + }) + .filter(|partition| { + let attributes = GptprioAttributes(partition.attributes); + attributes.gptprio_successful() || attributes.gptprio_tries_left() > 0 + }) + .max_by_key(|partition| { + GptprioAttributes(partition.attributes).gptprio_priority() + }) + .map(|part| **part) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index b890520..e911703 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -14,8 +14,11 @@ extern crate uefi; +mod gpt; +pub mod gptprio; mod input; +pub use self::gpt::*; pub use self::input::*; use core::fmt::Arguments;