blob: 332204893d448ac89380efdebba945e4f9c928dc [file] [log] [blame]
// 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());
}
}