| // Copyright 2024 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #![allow(clippy::indexing_slicing)] |
| |
| extern crate alloc; |
| |
| use crate::disk; |
| use alloc::borrow::ToOwned; |
| use alloc::vec::Vec; |
| use core::fmt::{self, Display, Formatter}; |
| use core::mem::size_of; |
| use log::{error, info, warn}; |
| use uefi::prelude::*; |
| use uefi::proto::device_path::DevicePath; |
| use uefi::table::runtime::{CapsuleFlags, Time, VariableAttributes, VariableKey, VariableVendor}; |
| use uefi::{guid, CStr16, CString16, Guid, Status}; |
| |
| const FWUPDATE_ATTEMPT_UPDATE: u32 = 0x0000_0001; |
| const FWUPDATE_ATTEMPTED: u32 = 0x0000_0002; |
| |
| const FWUPDATE_VENDOR: VariableVendor = |
| VariableVendor(guid!("0abba7dc-e516-4167-bbf5-4d9d1c739416")); |
| |
| const FWUPDATE_VERBOSE: &CStr16 = cstr16!("FWUPDATE_VERBOSE"); |
| const FWUPDATE_DEBUG_LOG: &CStr16 = cstr16!("FWUPDATE_DEBUG_LOG"); |
| |
| const MAX_UPDATE_CAPSULES: usize = 128; |
| |
| #[derive(Debug, Eq, PartialEq)] |
| pub enum FirmwareError { |
| GetVariableKeysFailed(Status), |
| GetVariableFailed(Status), |
| SetVariableFailed(Status), |
| UpdateInfoTooShort, |
| UpdateInfoMalformedDevicePath, |
| } |
| |
| impl Display for FirmwareError { |
| fn fmt(&self, f: &mut Formatter) -> fmt::Result { |
| match self { |
| Self::GetVariableKeysFailed(status) => { |
| write!(f, "failed to get variable keys: {status}") |
| } |
| Self::GetVariableFailed(status) => write!(f, "failed to read variable: {status}"), |
| Self::SetVariableFailed(status) => write!(f, "failed to write variable: {status}"), |
| Self::UpdateInfoTooShort => write!(f, "invalid update variable: not enough data"), |
| Self::UpdateInfoMalformedDevicePath => { |
| write!(f, "invalid update variable: malformed device path") |
| } |
| } |
| } |
| } |
| |
| /// This struct closely matches the format of the data written to UEFI |
| /// vars by the fwupd UEFI plugin [1], with an exception noted below. It |
| /// is used to create an update capsule. |
| /// |
| /// [`UpdateInfo::path`] is stored by reference rather than value, however |
| /// this is accounted for by both [`UpdateInfo::to_bytes`] and |
| /// [`TryFrom<&[u8]>`] for [`UpdateInfo`]. |
| /// |
| /// [1]: https://github.com/fwupd/fwupd/tree/main/plugins/uefi-capsule |
| #[derive(Debug, Eq, PartialEq)] |
| struct UpdateInfo<'a> { |
| // Version of UpdateInfo struct. |
| version: u32, |
| |
| // Info needed to apply an update. |
| efi_guid: Guid, |
| capsule_flags: CapsuleFlags, |
| hw_inst: u64, |
| |
| // Metadata used by fwupd to determine whether and when an update was attempted. |
| time_attempted: Time, |
| status: u32, |
| |
| // Path to firmware update blob. |
| path: &'a DevicePath, |
| } |
| |
| impl UpdateInfo<'_> { |
| /// Get the size in bytes of `self` when serialized to bytes. |
| fn serialized_len(&self) -> usize { |
| // 52 for the fixed fields (plus padding), plus the size of the |
| // device path. |
| // |
| // This should never overflow since we successfully read the |
| // data from a variable and the device path should not have |
| // changed since then. |
| #[allow(clippy::arithmetic_side_effects)] |
| { |
| 52 + self.path.as_bytes().len() |
| } |
| } |
| |
| fn to_bytes(&self) -> Vec<u8> { |
| let mut bytes: Vec<u8> = Vec::with_capacity(self.serialized_len()); |
| bytes.extend(self.version.to_le_bytes()); |
| bytes.extend(self.efi_guid.to_bytes()); |
| bytes.extend(self.capsule_flags.bits().to_le_bytes()); |
| bytes.extend(self.hw_inst.to_le_bytes()); |
| bytes.extend(self.time_attempted.year().to_le_bytes()); |
| |
| bytes.push(self.time_attempted.month()); |
| bytes.push(self.time_attempted.day()); |
| bytes.push(self.time_attempted.hour()); |
| bytes.push(self.time_attempted.minute()); |
| bytes.push(self.time_attempted.second()); |
| bytes.push(0); |
| bytes.extend(self.time_attempted.nanosecond().to_le_bytes()); |
| let time_zone = self.time_attempted.time_zone().unwrap_or(0x07ff); |
| bytes.extend(time_zone.to_le_bytes()); |
| bytes.push(self.time_attempted.daylight().bits()); |
| bytes.push(0); |
| |
| bytes.extend(self.status.to_le_bytes()); |
| bytes.extend(self.path.as_bytes()); |
| |
| bytes |
| } |
| |
| /// Set the `time_attempted` field to the current time. |
| /// |
| /// If the current time cannot be retrieved, log an error and leave |
| /// the `time_attempted` field unchanged. |
| fn update_time_attempted(&mut self, rt: &RuntimeServices) { |
| match rt.get_time() { |
| Ok(time) => self.time_attempted = time, |
| Err(err) => { |
| warn!("failed to get current time: {err}"); |
| } |
| } |
| } |
| } |
| |
| impl<'a> TryFrom<&[u8]> for UpdateInfo<'a> { |
| type Error = FirmwareError; |
| |
| fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { |
| if size_of::<UpdateInfo>() <= bytes.len() { |
| let version = u32::from_le_bytes(bytes[0..4].try_into().unwrap()); |
| let efi_guid = Guid::from_bytes(bytes[4..20].try_into().unwrap()); |
| let raw_flag_bits = u32::from_le_bytes(bytes[20..24].try_into().unwrap()); |
| let capsule_flags = CapsuleFlags::from_bits_retain(raw_flag_bits); |
| let hw_inst = u64::from_le_bytes(bytes[24..32].try_into().unwrap()); |
| let time = &bytes[32..48]; |
| // fwupd sometimes has invalid EFI_TIME structs in its vars. |
| // We update the time anyways, so just continue. |
| let time_attempted = Time::try_from(time).unwrap_or(Time::invalid()); |
| let status = u32::from_le_bytes(bytes[48..52].try_into().unwrap()); |
| let path = <&DevicePath>::try_from(&bytes[52..]) |
| .map_err(|_| FirmwareError::UpdateInfoMalformedDevicePath)?; |
| |
| let update = UpdateInfo { |
| version, |
| efi_guid, |
| capsule_flags, |
| hw_inst, |
| time_attempted, |
| status, |
| path, |
| }; |
| Ok(update) |
| } else { |
| Err(FirmwareError::UpdateInfoTooShort) |
| } |
| } |
| } |
| |
| /// A complete firmware update. |
| struct UpdateTable<'a> { |
| // Name of the update's associated UEFI variable. |
| name: CString16, |
| // The attributes of the update's associated UEFI variable. |
| attrs: VariableAttributes, |
| // The info needed to create an update capsule. |
| info: UpdateInfo<'a>, |
| } |
| |
| /// Get a list of all available updates by iterating through all UEFI |
| /// variables, searching for those with the [`FWUPDATE_VENDOR`] |
| /// GUID. Any such variables will be parsed into an [`UpdateInfo`], from |
| /// which an update can be applied. |
| /// |
| /// If no updates are found, an empty vector is returned. |
| /// |
| /// Any UEFI error causes early termination and the error to be returned. |
| fn get_update_table( |
| st: &SystemTable<Boot>, |
| variables: Vec<VariableKey>, |
| ) -> Result<Vec<UpdateTable>, FirmwareError> { |
| let mut updates: Vec<UpdateTable> = Vec::new(); |
| for var in variables { |
| // Must be a fwupd state variable. |
| if var.vendor != FWUPDATE_VENDOR { |
| continue; |
| } |
| |
| let name: CString16 = match var.name() { |
| Ok(n) => n.to_owned(), |
| Err(err) => { |
| error!("could not get variable name: {err}"); |
| continue; |
| } |
| }; |
| |
| // Skip fwupd-efi debugging settings. |
| if name == FWUPDATE_VERBOSE || name == FWUPDATE_DEBUG_LOG { |
| continue; |
| } |
| |
| if updates.len() > MAX_UPDATE_CAPSULES { |
| warn!("too many updates, ignoring {name}"); |
| } |
| |
| info!("found update {name}"); |
| |
| let (data, attrs) = st |
| .runtime_services() |
| .get_variable_boxed(&name, &FWUPDATE_VENDOR) |
| .map_err(|err| FirmwareError::GetVariableFailed(err.status()))?; |
| |
| let mut info = match UpdateInfo::try_from(&*data) { |
| Ok(i) => i, |
| Err(err) => { |
| // Delete the malformed variable. If this fails, log the |
| // error but otherwise ignore it. |
| if let Err(err) = st |
| .runtime_services() |
| .delete_variable(&name, &FWUPDATE_VENDOR) |
| { |
| warn!( |
| "failed to delete variable {name}-{vendor}: {err}", |
| vendor = FWUPDATE_VENDOR.0 |
| ); |
| } |
| |
| warn!("could not populate update info for {name}"); |
| return Err(err); |
| } |
| }; |
| |
| if (info.status & FWUPDATE_ATTEMPT_UPDATE) != 0 { |
| info.update_time_attempted(st.runtime_services()); |
| info.status = FWUPDATE_ATTEMPTED; |
| updates.push(UpdateTable { name, attrs, info }); |
| } |
| } |
| Ok(updates) |
| } |
| |
| /// Mark all updates as [`FWUPDATE_ATTEMPTED`] and note the time of the attempt. |
| fn set_update_statuses( |
| st: &SystemTable<Boot>, |
| updates: &Vec<UpdateTable>, |
| ) -> Result<(), FirmwareError> { |
| for update in updates { |
| st.runtime_services() |
| .set_variable( |
| &update.name, |
| &FWUPDATE_VENDOR, |
| update.attrs, |
| &update.info.to_bytes(), |
| ) |
| .map_err(|err| { |
| warn!( |
| "could not update variable status for {0}: {err}", |
| update.name |
| ); |
| FirmwareError::SetVariableFailed(err.status()) |
| })?; |
| } |
| Ok(()) |
| } |
| |
| pub fn update_firmware(st: &SystemTable<Boot>) -> Result<(), FirmwareError> { |
| let variables = st |
| .runtime_services() |
| .variable_keys() |
| .map_err(|err| FirmwareError::GetVariableKeysFailed(err.status()))?; |
| // Check if any updates are available by searching for and validating |
| // any update state variables. |
| let updates = get_update_table(st, variables)?; |
| |
| if updates.is_empty() { |
| info!("no firmware updates available"); |
| return Ok(()); |
| } |
| |
| let _ = disk::open_stateful_partition(st.boot_services()); |
| |
| // TODO(b/338423918): Create update capsules from each |
| // [`UpdateInfo`]. In particular, implement the translation from |
| // [`UpdateInfo::path`]` to its actual location on the stateful |
| // partition. |
| |
| set_update_statuses(st, &updates) |
| |
| // TODO(b/338423918): Apply the update capsules and reboot. |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use uefi::proto::device_path::build::{self, DevicePathBuilder}; |
| use uefi::proto::device_path::media::{PartitionFormat, PartitionSignature}; |
| |
| #[test] |
| fn test_update_info() { |
| // This test file is a direct copy of an efivarfs file created |
| // by `fwupd install`. |
| let data = include_bytes!( |
| "../test_data/\ |
| fwupd-61b65ccc-0116-4b62-80ed-ec5f089ae523-0-0abba7dc-e516-4167-bbf5-4d9d1c739416" |
| ); |
| // Efivarfs stores the UEFI variable attributes in the first |
| // four bytes. Drop those bytes so that only the variable's |
| // value remains. |
| let data = &data[4..]; |
| |
| // Create the expected device path. |
| let mut storage = Vec::new(); |
| let expected_path = DevicePathBuilder::with_vec(&mut storage) |
| .push(&build::media::HardDrive { |
| partition_number: 12, |
| partition_start: 0, |
| partition_size: 0, |
| partition_signature: PartitionSignature::Guid(guid!( |
| "99cc6f39-2fd1-4d85-b15a-543e7b023a1f" |
| )), |
| partition_format: PartitionFormat::GPT, |
| }) |
| .unwrap() |
| .push(&build::media::FilePath { |
| path_name: cstr16!( |
| r"\EFI\chromeos\fw\fwupd-61b65ccc-0116-4b62-80ed-ec5f089ae523.cap" |
| ), |
| }) |
| .unwrap() |
| .finalize() |
| .unwrap(); |
| |
| let expected_info = UpdateInfo { |
| version: 7, |
| efi_guid: guid!("61b65ccc-0116-4b62-80ed-ec5f089ae523"), |
| capsule_flags: CapsuleFlags::empty(), |
| hw_inst: 0, |
| time_attempted: Time::invalid(), |
| status: FWUPDATE_ATTEMPT_UPDATE, |
| path: expected_path, |
| }; |
| |
| // Parse the test data and compare with the expected value. |
| let info = UpdateInfo::try_from(data).unwrap(); |
| assert_eq!(info, expected_info); |
| |
| // Verify that converting it back to bytes gives the same value. |
| assert_eq!(info.to_bytes(), data); |
| |
| // Check the serialized length calculation. |
| assert_eq!(info.serialized_len(), data.len()); |
| } |
| } |