Provide a slots::Manager over GBL AB slots protocol
Write an implementation for slots::Manager that uses the custom AB
slots EFI protocol on Android.
It is a vendor implementation detail whether or where this information
is backed on disk.
The wrapping allows higher level GBL code to abstract over whether the
platform is booting Android or Fuchsia and, if Android, vendor
implementation specifics.
Bug: b/345531636
Change-Id: Ie31a7e61b6734d7e33d08782b97d410a6f82b0bd
diff --git a/gbl/libefi/BUILD b/gbl/libefi/BUILD
index f1c1e0b..ae4a17b 100644
--- a/gbl/libefi/BUILD
+++ b/gbl/libefi/BUILD
@@ -102,29 +102,24 @@
rust_library(
name = "libefi",
- srcs = [
- "src/allocation.rs",
- "src/defs.rs", # Generated by :efi_defs_genrule
- "src/lib.rs",
- "src/protocol.rs",
- "src/protocol/ab_slot.rs",
- "src/protocol/android_boot.rs",
- "src/protocol/block_io.rs",
- "src/protocol/device_path.rs",
- "src/protocol/loaded_image.rs",
- "src/protocol/riscv.rs",
- "src/protocol/simple_network.rs",
- "src/protocol/simple_text_input.rs",
- "src/protocol/simple_text_output.rs",
- ],
+ # Explicitly list src/defs.rs because it is generated by efi_defs_genrule
+ srcs = glob(["**/*.rs"]) + ["src/defs.rs"],
crate_name = "efi",
data = [":efi_defs_genrule"],
rustc_flags = ANDROID_RUST_LINTS,
- deps = ["@zerocopy"],
+ deps = [
+ "@gbl//libgbl",
+ "@gbl//libstorage",
+ "@zerocopy",
+ ],
)
rust_test(
name = "libefi_test",
crate = ":libefi",
rustc_flags = ANDROID_RUST_LINTS,
+ deps = [
+ "@gbl//libavb:sysdeps",
+ "@gbl//libstorage:libstorage_testlib",
+ ],
)
diff --git a/gbl/libefi/defs/protocols/ab_slot_protocol.h b/gbl/libefi/defs/protocols/ab_slot_protocol.h
index 54baee6..ffcfcde 100644
--- a/gbl/libefi/defs/protocols/ab_slot_protocol.h
+++ b/gbl/libefi/defs/protocols/ab_slot_protocol.h
@@ -57,7 +57,6 @@
} EfiGblSlotInfo;
typedef struct EfiGblSlotMetadataBlock {
- uint32_t boot_reason;
// Value of 1 if persistent metadata tracks slot unbootable reasons.
uint8_t unbootable_metadata;
uint8_t max_retries;
diff --git a/gbl/libefi/src/ab_slots.rs b/gbl/libefi/src/ab_slots.rs
new file mode 100644
index 0000000..97b433b
--- /dev/null
+++ b/gbl/libefi/src/ab_slots.rs
@@ -0,0 +1,616 @@
+// Copyright 2024, The Android Open Source Project
+//
+// 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 gbl_storage;
+extern crate libgbl as gbl;
+
+use gbl::slots::{
+ BootTarget, BootToken, Error, Manager, OneShot, RecoveryTarget, Slot, SlotIterator, Suffix,
+ Tries, UnbootableReason,
+};
+
+use crate::defs::{
+ EFI_BOOT_REASON_BOOTLOADER, EFI_BOOT_REASON_EMPTY_EFI_BOOT_REASON, EFI_BOOT_REASON_RECOVERY,
+};
+use crate::protocol::{ab_slot, Protocol};
+use crate::ErrorTypes;
+
+const SUBREASON_BUF_LEN: usize = 64;
+
+/// Implementation for A/B slot manager based on custom EFI protocol.
+pub struct ABManager<'a> {
+ protocol: Protocol<'a, ab_slot::GblSlotProtocol>,
+ boot_token: Option<BootToken>,
+ last_set_active_idx: Option<u8>,
+}
+
+impl<'a> ABManager<'a> {
+ #[cfg(test)]
+ fn new_without_token(protocol: Protocol<'a, ab_slot::GblSlotProtocol>) -> Self {
+ Self { protocol, boot_token: None, last_set_active_idx: None }
+ }
+}
+
+impl gbl::slots::private::SlotGet for ABManager<'_> {
+ fn get_slot_by_number(&self, number: usize) -> Result<Slot, Error> {
+ let idx = u8::try_from(number).or(Err(Error::BadSlotIndex(number)))?;
+ let info = self.protocol.get_slot_info(idx).map_err(|e| match e.err() {
+ ErrorTypes::Unknown => Error::Other,
+ _ => Error::BadSlotIndex(number),
+ })?;
+ info.try_into()
+ }
+}
+
+impl Manager for ABManager<'_> {
+ fn get_boot_target(&self) -> BootTarget {
+ let slot = self.get_slot_last_set_active();
+ let mut subreason = [0u8; SUBREASON_BUF_LEN];
+ let (reason, _) = self.protocol.get_boot_reason(subreason.as_mut_slice()).unwrap();
+ // Don't currently care about the subreason
+ // CStr::from_bytes_until_nul(&subreason[..strlen]).unwrap()
+ if reason == EFI_BOOT_REASON_RECOVERY {
+ BootTarget::Recovery(RecoveryTarget::Slotted(slot))
+ } else {
+ BootTarget::NormalBoot(slot)
+ }
+ }
+
+ fn slots_iter(&self) -> SlotIterator {
+ SlotIterator::new(self)
+ }
+
+ fn get_slot_last_set_active(&self) -> Slot {
+ use gbl::slots::private::SlotGet;
+
+ if let Some(idx) = self.last_set_active_idx {
+ self.get_slot_by_number(idx.into()).expect("cannot get slot last set active")
+ } else {
+ self.protocol
+ .get_current_slot()
+ .map(|efi_slot| efi_slot.try_into().expect("invalid slot info from protocol"))
+ .expect("cannot get current slot")
+ }
+ }
+
+ fn mark_boot_attempt(&mut self) -> Result<BootToken, Error> {
+ self.protocol.mark_boot_attempt().or(Err(Error::OperationProhibited))?;
+ self.boot_token.take().ok_or(Error::OperationProhibited)
+ }
+
+ fn set_active_slot(&mut self, slot_suffix: Suffix) -> Result<(), Error> {
+ let idx: u8 = self
+ .slots_iter()
+ .position(|s| s.suffix == slot_suffix)
+ .ok_or(Error::NoSuchSlot(slot_suffix))?
+ .try_into()
+ // This 'or' is technically unreachable because the protocol
+ // can't give us an index larger than a u8.
+ .or(Err(Error::Other))?;
+ self.protocol.set_active_slot(idx).or(Err(Error::Other)).and_then(|_| {
+ self.last_set_active_idx = Some(idx);
+ Ok(())
+ })
+ }
+
+ fn set_slot_unbootable(
+ &mut self,
+ slot_suffix: Suffix,
+ reason: UnbootableReason,
+ ) -> Result<(), Error> {
+ let idx: u8 = self
+ .slots_iter()
+ .position(|s| s.suffix == slot_suffix)
+ .ok_or(Error::NoSuchSlot(slot_suffix))?
+ .try_into()
+ // This 'or' is technically unreachable because the protocol
+ // can't give us an index larger than a u8.
+ .or(Err(Error::Other))?;
+ self.protocol.set_slot_unbootable(idx, u8::from(reason).into()).or(Err(Error::Other))
+ }
+
+ fn get_max_retries(&self) -> Tries {
+ let block = self.protocol.load_boot_data().unwrap();
+ block.max_retries.into()
+ }
+
+ fn get_oneshot_status(&self) -> Option<OneShot> {
+ let mut subreason = [0u8; SUBREASON_BUF_LEN];
+ let (reason, _) = self.protocol.get_boot_reason(subreason.as_mut_slice()).ok()?;
+ // Currently we only care if the primary boot reason is BOOTLOADER.
+ // CStr::from_bytes_until_nul(&subreason[..strlen]).ok()?
+ match reason {
+ EFI_BOOT_REASON_BOOTLOADER => Some(OneShot::Bootloader),
+ _ => None,
+ }
+ }
+
+ fn set_oneshot_status(&mut self, os: OneShot) -> Result<(), Error> {
+ // Android doesn't have a concept of OneShot to recovery,
+ // and the subreason shouldn't matter.
+ match os {
+ OneShot::Bootloader => {
+ self.protocol.set_boot_reason(EFI_BOOT_REASON_BOOTLOADER, &[]).or(Err(Error::Other))
+ }
+ _ => Err(Error::OperationProhibited),
+ }
+ }
+
+ fn clear_oneshot_status(&mut self) {
+ let mut subreason = [0u8; SUBREASON_BUF_LEN];
+ // Only clear if the boot reason is the one we care about.
+ // CStr::from_bytes_until_nul(&subreason[..strlen]).or(Err(Error::Other))?
+ if let Ok((EFI_BOOT_REASON_BOOTLOADER, _)) =
+ self.protocol.get_boot_reason(subreason.as_mut_slice())
+ {
+ let _ = self.protocol.set_boot_reason(EFI_BOOT_REASON_EMPTY_EFI_BOOT_REASON, &[]);
+ }
+ }
+
+ fn write_back(&mut self, _: &mut dyn gbl_storage::AsBlockDevice) {
+ // Note: `expect` instead of swallowing the error.
+ // It is important that changes are not silently dropped.
+ self.protocol.flush().expect("could not write back modifications to slot metadata");
+ }
+}
+
+#[cfg(test)]
+mod test {
+ extern crate avb_sysdeps;
+
+ use super::*;
+ use crate::defs::{
+ EfiGblSlotInfo, EfiGblSlotMetadataBlock, EfiGblSlotProtocol, EfiStatus,
+ EFI_BOOT_REASON_EMPTY_EFI_BOOT_REASON, EFI_BOOT_REASON_RECOVERY, EFI_BOOT_REASON_WATCHDOG,
+ EFI_STATUS_INVALID_PARAMETER, EFI_STATUS_SUCCESS,
+ };
+ use crate::protocol::{Protocol, ProtocolInfo};
+ use crate::test::*;
+ use crate::{DeviceHandle, EfiEntry};
+ use core::ptr::null_mut;
+ use gbl::slots::{Bootability, Cursor, RecoveryTarget, UnbootableReason};
+ use gbl::{Gbl, GblOps, Result as GblResult};
+ use gbl_storage_testlib::TestBlockDevice;
+ // TODO(b/350526796): use ptr.is_aligned() when Rust 1.79 is in Android
+ use std::mem::align_of;
+ use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
+
+ // The thread-local atomics are an ugly, ugly hack to pass state between
+ // the protocol method functions and the rest of the test body.
+ // Because the variables are thread-local, it is safe to run tests concurrently
+ // so long as they establish correct initial values.
+ // Also, because no atomic is being read or written to by more than one thread,
+ // Ordering::Relaxed is perfectly fine.
+ thread_local! {
+ static ATOMIC: AtomicBool = AtomicBool::new(false);
+ }
+
+ thread_local! {
+ static BOOT_REASON: AtomicU32 = AtomicU32::new(EFI_BOOT_REASON_EMPTY_EFI_BOOT_REASON);
+ }
+
+ // This provides reasonable defaults for all tests that need to get slot info.
+ //
+ // SAFETY: checks that `info` is properly aligned and not null.
+ // Caller must make sure `info` points to a valid EfiGblSlotInfo struct.
+ unsafe extern "C" fn get_info(
+ _: *mut EfiGblSlotProtocol,
+ idx: u8,
+ info: *mut EfiGblSlotInfo,
+ ) -> EfiStatus {
+ // TODO(b/350526796): use ptr.is_aligned() when Rust 1.79 is in Android
+ if !info.is_null() && (info as usize) % align_of::<EfiGblSlotInfo>() == 0 && idx < 3 {
+ let slot_info = EfiGblSlotInfo {
+ suffix: ('a' as u8 + idx).into(),
+ unbootable_reason: 0,
+ priority: idx + 1,
+ tries: idx,
+ successful: 2 & idx,
+ merge_status: 0,
+ };
+ unsafe { *info = slot_info };
+ EFI_STATUS_SUCCESS
+ } else {
+ EFI_STATUS_INVALID_PARAMETER
+ }
+ }
+
+ extern "C" fn flush(_: *mut EfiGblSlotProtocol) -> EfiStatus {
+ ATOMIC.with(|a| a.store(true, Ordering::Relaxed));
+ EFI_STATUS_SUCCESS
+ }
+
+ struct TestGblOps<'a> {
+ manager: ABManager<'a>,
+ }
+
+ impl<'a> TestGblOps<'a> {
+ fn new(protocol: Protocol<'a, ab_slot::GblSlotProtocol>) -> Self {
+ Self { manager: ABManager::new_without_token(protocol) }
+ }
+ }
+
+ impl<'b> GblOps for TestGblOps<'b> {
+ fn load_slot_interface<'a, B: gbl_storage::AsBlockDevice>(
+ &'a mut self,
+ block_dev: &'a mut B,
+ boot_token: BootToken,
+ ) -> GblResult<Cursor<'a, B>> {
+ self.manager.boot_token = Some(boot_token);
+ Ok(Cursor { ctx: &mut self.manager, block_dev })
+ }
+ }
+
+ fn generate_protocol<'a, P: ProtocolInfo>(
+ efi_entry: &'a EfiEntry,
+ proto: &'a mut P::InterfaceType,
+ ) -> Protocol<'a, P> {
+ // SAFETY: proto is a valid pointer and lasts at least as long as efi_entry.
+ unsafe { Protocol::<'a, P>::new(DeviceHandle::new(null_mut()), proto, efi_entry) }
+ }
+
+ #[test]
+ fn test_manager_flush_on_close() {
+ ATOMIC.with(|a| a.store(false, Ordering::Relaxed));
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol { flush: Some(flush), ..Default::default() };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+
+ {
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let _ = gbl.load_slot_interface(&mut block_device).unwrap();
+ }
+ });
+ assert!(ATOMIC.with(|a| a.load(Ordering::Relaxed)));
+ }
+
+ #[test]
+ fn test_iterator() {
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol {
+ get_slot_info: Some(get_info),
+ flush: Some(flush),
+ ..Default::default()
+ };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let cursor = gbl.load_slot_interface(&mut block_device).unwrap();
+
+ let slots: Vec<Slot> = cursor.ctx.slots_iter().collect();
+ assert_eq!(
+ slots,
+ vec![
+ Slot {
+ suffix: 'a'.into(),
+ priority: 1usize.into(),
+ bootability: Bootability::Unbootable(UnbootableReason::Unknown),
+ },
+ Slot {
+ suffix: 'b'.into(),
+ priority: 2usize.into(),
+ bootability: Bootability::Retriable(1usize.into()),
+ },
+ Slot {
+ suffix: 'c'.into(),
+ priority: 3usize.into(),
+ bootability: Bootability::Successful,
+ }
+ ]
+ )
+ });
+ }
+
+ #[test]
+ fn test_active_slot() {
+ // SAFETY: verfies that `info` properly aligned and not null.
+ // It is the callers responsibility to make sure
+ // that `info` points to a valid EfiGblSlotInfo.
+ unsafe extern "C" fn get_current_slot(
+ _: *mut EfiGblSlotProtocol,
+ info: *mut EfiGblSlotInfo,
+ ) -> EfiStatus {
+ // TODO(b/350526796): use ptr.is_aligned() when Rust 1.79 is in Android
+ if info.is_null() || (info as usize) % align_of::<EfiGblSlotInfo>() != 0 {
+ return EFI_STATUS_INVALID_PARAMETER;
+ }
+ let slot_info = EfiGblSlotInfo {
+ suffix: 'a' as u32,
+ unbootable_reason: 0,
+ priority: 7,
+ tries: 15,
+ successful: 1,
+ merge_status: 0,
+ };
+
+ unsafe { *info = slot_info };
+ EFI_STATUS_SUCCESS
+ }
+
+ // SAFETY: verifies that `reason` and `subreason_size` are aligned and not null.
+ // It is the caller's responsibility to make sure that `reason`
+ // and `subreason_size` point to valid output parameters.
+ unsafe extern "C" fn get_boot_reason(
+ _: *mut EfiGblSlotProtocol,
+ reason: *mut u32,
+ subreason_size: *mut usize,
+ _subreason: *mut u8,
+ ) -> EfiStatus {
+ if reason.is_null()
+ || subreason_size.is_null()
+ // TODO(b/350526796): use ptr.is_aligned() when Rust 1.79 is in Android
+ || (reason as usize) % align_of::<u32>() != 0
+ || (subreason_size as usize) % align_of::<usize>() != 0
+ {
+ return EFI_STATUS_INVALID_PARAMETER;
+ }
+
+ unsafe {
+ *reason = BOOT_REASON.with(|r| r.load(Ordering::Relaxed));
+ *subreason_size = 0;
+ }
+ EFI_STATUS_SUCCESS
+ }
+
+ BOOT_REASON.with(|r| r.store(EFI_BOOT_REASON_EMPTY_EFI_BOOT_REASON, Ordering::Relaxed));
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol {
+ get_current_slot: Some(get_current_slot),
+ get_boot_reason: Some(get_boot_reason),
+ flush: Some(flush),
+ ..Default::default()
+ };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let cursor = gbl.load_slot_interface(&mut block_device).unwrap();
+
+ let slot = Slot {
+ suffix: 'a'.into(),
+ priority: 7usize.into(),
+ bootability: Bootability::Successful,
+ };
+ assert_eq!(cursor.ctx.get_boot_target(), BootTarget::NormalBoot(slot));
+ assert_eq!(cursor.ctx.get_slot_last_set_active(), slot);
+
+ BOOT_REASON.with(|r| r.store(EFI_BOOT_REASON_RECOVERY, Ordering::Relaxed));
+
+ assert_eq!(
+ cursor.ctx.get_boot_target(),
+ BootTarget::Recovery(RecoveryTarget::Slotted(slot))
+ );
+ });
+ }
+
+ #[test]
+ fn test_mark_boot_attempt() {
+ extern "C" fn mark_boot_attempt(_: *mut EfiGblSlotProtocol) -> EfiStatus {
+ ATOMIC.with(|a| a.store(true, Ordering::Relaxed));
+ EFI_STATUS_SUCCESS
+ }
+
+ ATOMIC.with(|a| a.store(false, Ordering::Relaxed));
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol {
+ mark_boot_attempt: Some(mark_boot_attempt),
+ flush: Some(flush),
+ ..Default::default()
+ };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let cursor = gbl.load_slot_interface(&mut block_device).unwrap();
+ assert!(cursor.ctx.mark_boot_attempt().is_ok());
+ assert!(ATOMIC.with(|a| a.load(Ordering::Relaxed)));
+
+ assert_eq!(cursor.ctx.mark_boot_attempt(), Err(gbl::slots::Error::OperationProhibited));
+ });
+ }
+
+ #[test]
+ fn test_get_max_retries() {
+ // SAFETY: verifies that `meta` is properly aligned and not null.
+ // It is the caller's responsibility to make sure that `meta` points to
+ // a valid EfiGblSlotMetadataBlock.
+ unsafe extern "C" fn load_boot_data(
+ _: *mut EfiGblSlotProtocol,
+ meta: *mut EfiGblSlotMetadataBlock,
+ ) -> EfiStatus {
+ // TODO(b/350526796): use ptr.is_aligned() when Rust 1.79 is in Android
+ if meta.is_null() || (meta as usize) % align_of::<EfiGblSlotMetadataBlock>() != 0 {
+ return EFI_STATUS_INVALID_PARAMETER;
+ }
+
+ let meta_block = EfiGblSlotMetadataBlock {
+ unbootable_metadata: 1,
+ max_retries: 66,
+ slot_count: 32, // why not?
+ };
+
+ unsafe { *meta = meta_block };
+ EFI_STATUS_SUCCESS
+ }
+
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol {
+ load_boot_data: Some(load_boot_data),
+ flush: Some(flush),
+ ..Default::default()
+ };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let cursor = gbl.load_slot_interface(&mut block_device).unwrap();
+ assert_eq!(cursor.ctx.get_max_retries(), 66usize.into());
+ });
+ }
+
+ #[test]
+ fn test_set_active_slot() {
+ extern "C" fn set_active_slot(_: *mut EfiGblSlotProtocol, idx: u8) -> EfiStatus {
+ // This is deliberate: we want to make sure that other logic catches
+ // 'no such slot' first but we also want to verify that errors propagate.
+ if idx != 2 {
+ EFI_STATUS_SUCCESS
+ } else {
+ EFI_STATUS_INVALID_PARAMETER
+ }
+ }
+
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol {
+ get_slot_info: Some(get_info),
+ set_active_slot: Some(set_active_slot),
+ flush: Some(flush),
+ ..Default::default()
+ };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let cursor = gbl.load_slot_interface(&mut block_device).unwrap();
+
+ assert_eq!(cursor.ctx.set_active_slot('b'.into()), Ok(()));
+ assert_eq!(cursor.ctx.set_active_slot('c'.into()), Err(Error::Other));
+
+ let bad_suffix = '$'.into();
+ assert_eq!(cursor.ctx.set_active_slot(bad_suffix), Err(Error::NoSuchSlot(bad_suffix)));
+ });
+ }
+
+ #[test]
+ fn test_set_slot_unbootable() {
+ extern "C" fn set_slot_unbootable(
+ _: *mut EfiGblSlotProtocol,
+ idx: u8,
+ _: u32,
+ ) -> EfiStatus {
+ // Same thing here as with set_active_slot.
+ // We want to make sure that iteration over the slots
+ // catches invalid suffixes, but we also want to make sure
+ // that errors from the protocol percolate up.
+ if idx == 0 {
+ EFI_STATUS_SUCCESS
+ } else {
+ EFI_STATUS_INVALID_PARAMETER
+ }
+ }
+
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol {
+ get_slot_info: Some(get_info),
+ set_slot_unbootable: Some(set_slot_unbootable),
+ flush: Some(flush),
+ ..Default::default()
+ };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let cursor = gbl.load_slot_interface(&mut block_device).unwrap();
+
+ assert_eq!(
+ cursor.ctx.set_slot_unbootable('a'.into(), UnbootableReason::SystemUpdate),
+ Ok(())
+ );
+
+ assert_eq!(
+ cursor.ctx.set_slot_unbootable('b'.into(), UnbootableReason::UserRequested),
+ Err(Error::Other)
+ );
+
+ let bad_suffix = '$'.into();
+ assert_eq!(
+ cursor.ctx.set_slot_unbootable(bad_suffix, UnbootableReason::NoMoreTries),
+ Err(Error::NoSuchSlot(bad_suffix))
+ );
+ });
+ }
+
+ #[test]
+ fn test_oneshot() {
+ // SAFETY: checks that `reason` is not null and is properly aligned.
+ // Caller must make sure reason points to a valid u32.
+ unsafe extern "C" fn get_boot_reason(
+ _: *mut EfiGblSlotProtocol,
+ reason: *mut u32,
+ _: *mut usize,
+ _: *mut u8,
+ ) -> EfiStatus {
+ // TODO(b/350526796): use ptr.is_aligned() when Rust 1.79 is in Android
+ if reason.is_null() || (reason as usize) % align_of::<u32>() != 0 {
+ return EFI_STATUS_INVALID_PARAMETER;
+ }
+
+ unsafe { *reason = BOOT_REASON.with(|r| r.load(Ordering::Relaxed)) };
+
+ EFI_STATUS_SUCCESS
+ }
+
+ extern "C" fn set_boot_reason(
+ _: *mut EfiGblSlotProtocol,
+ reason: u32,
+ _: usize,
+ _: *const u8,
+ ) -> EfiStatus {
+ BOOT_REASON.with(|r| r.store(reason, Ordering::Relaxed));
+ EFI_STATUS_SUCCESS
+ }
+
+ BOOT_REASON.with(|r| r.store(EFI_BOOT_REASON_EMPTY_EFI_BOOT_REASON, Ordering::Relaxed));
+ run_test(|image_handle, systab_ptr| {
+ let mut ab = EfiGblSlotProtocol {
+ get_boot_reason: Some(get_boot_reason),
+ set_boot_reason: Some(set_boot_reason),
+ flush: Some(flush),
+ ..Default::default()
+ };
+ let efi_entry = EfiEntry { image_handle, systab_ptr };
+ let protocol = generate_protocol::<ab_slot::GblSlotProtocol>(&efi_entry, &mut ab);
+ let mut block_device: TestBlockDevice = Default::default();
+ let mut test_ops = TestGblOps::new(protocol);
+ let mut gbl = Gbl::<TestGblOps>::new(&mut test_ops);
+ let cursor = gbl.load_slot_interface(&mut block_device).unwrap();
+
+ assert_eq!(cursor.ctx.get_oneshot_status(), None);
+ assert_eq!(
+ cursor.ctx.set_oneshot_status(OneShot::Continue(RecoveryTarget::Dedicated)),
+ Err(gbl::slots::Error::OperationProhibited)
+ );
+ assert_eq!(cursor.ctx.set_oneshot_status(OneShot::Bootloader), Ok(()));
+ assert_eq!(cursor.ctx.get_oneshot_status(), Some(OneShot::Bootloader));
+
+ cursor.ctx.clear_oneshot_status();
+ assert_eq!(cursor.ctx.get_oneshot_status(), None);
+
+ BOOT_REASON.with(|r| r.store(EFI_BOOT_REASON_WATCHDOG, Ordering::Relaxed));
+ assert_eq!(cursor.ctx.get_oneshot_status(), None);
+ cursor.ctx.clear_oneshot_status();
+ assert_eq!(BOOT_REASON.with(|r| r.load(Ordering::Relaxed)), EFI_BOOT_REASON_WATCHDOG);
+ });
+ }
+}
diff --git a/gbl/libefi/src/lib.rs b/gbl/libefi/src/lib.rs
index 31a16d8..bb33b72 100644
--- a/gbl/libefi/src/lib.rs
+++ b/gbl/libefi/src/lib.rs
@@ -74,6 +74,9 @@
use protocol::simple_text_output::SimpleTextOutputProtocol;
use protocol::{Protocol, ProtocolInfo};
+/// The Android EFI protocol implementation of an A/B slot manager.
+pub mod ab_slots;
+
mod error {
use super::defs::EFI_STATUS_SUCCESS;
use super::EfiStatus;
@@ -678,6 +681,13 @@
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct DeviceHandle(EfiHandle);
+impl DeviceHandle {
+ /// Public constructor
+ pub fn new(handle: EfiHandle) -> Self {
+ Self(handle)
+ }
+}
+
/// `LocatedHandles` holds the array of handles return by
/// `BootServices::locate_handle_buffer_by_protocol()`.
pub struct LocatedHandles<'a> {
@@ -1050,7 +1060,7 @@
}
/// Get the pointer to an object as an EfiHandle type.
- fn as_efi_handle<T>(val: &mut T) -> EfiHandle {
+ pub fn as_efi_handle<T>(val: &mut T) -> EfiHandle {
val as *mut T as *mut _
}
diff --git a/gbl/libefi/src/protocol/ab_slot.rs b/gbl/libefi/src/protocol/ab_slot.rs
index 49637da..4e03029 100644
--- a/gbl/libefi/src/protocol/ab_slot.rs
+++ b/gbl/libefi/src/protocol/ab_slot.rs
@@ -13,14 +13,19 @@
// limitations under the License.
//! Rust wrapper for `EFI_GBL_SLOT_PROTOCOL`.
+extern crate libgbl;
use crate::defs::{
EfiBootReason, EfiGblSlotInfo, EfiGblSlotMetadataBlock, EfiGblSlotProtocol, EfiGuid,
EfiUnbootableReason, EFI_STATUS_INVALID_PARAMETER, EFI_STATUS_NOT_FOUND,
+ EFI_UNBOOTABLE_REASON_NO_MORE_TRIES, EFI_UNBOOTABLE_REASON_SYSTEM_UPDATE,
+ EFI_UNBOOTABLE_REASON_USER_REQUESTED, EFI_UNBOOTABLE_REASON_VERIFICATION_FAILURE,
};
use crate::protocol::{Protocol, ProtocolInfo};
use crate::{efi_call, error::EfiError, map_efi_err, EfiResult};
+use libgbl::slots::{Bootability, Slot, UnbootableReason};
+
/// Wraps `EFI_GBL_SLOT_PROTOCOL`.
pub struct GblSlotProtocol;
@@ -31,6 +36,31 @@
EfiGuid::new(0xDEADBEEF, 0xCAFE, 0xD00D, [0xCA, 0xBB, 0xA6, 0xE5, 0xCA, 0xBB, 0xA6, 0xE5]);
}
+fn from_efi_unbootable_reason(reason: EfiUnbootableReason) -> UnbootableReason {
+ match reason {
+ EFI_UNBOOTABLE_REASON_NO_MORE_TRIES => UnbootableReason::NoMoreTries,
+ EFI_UNBOOTABLE_REASON_SYSTEM_UPDATE => UnbootableReason::SystemUpdate,
+ EFI_UNBOOTABLE_REASON_USER_REQUESTED => UnbootableReason::UserRequested,
+ EFI_UNBOOTABLE_REASON_VERIFICATION_FAILURE => UnbootableReason::VerificationFailure,
+ _ => UnbootableReason::Unknown,
+ }
+}
+
+impl TryFrom<EfiGblSlotInfo> for libgbl::slots::Slot {
+ type Error = libgbl::slots::Error;
+ fn try_from(info: EfiGblSlotInfo) -> Result<Self, Self::Error> {
+ Ok(Slot {
+ suffix: info.suffix.try_into()?,
+ priority: info.priority.into(),
+ bootability: match (info.successful, info.tries) {
+ (s, _) if s != 0 => Bootability::Successful,
+ (0, t) if t > 0 => Bootability::Retriable(info.tries.into()),
+ _ => Bootability::Unbootable(from_efi_unbootable_reason(info.unbootable_reason)),
+ },
+ })
+ }
+}
+
impl<'a> Protocol<'a, GblSlotProtocol> {
/// Wrapper of `EFI_GBL_SLOT_PROTOCOL.load_boot_data()`
pub fn load_boot_data(&self) -> EfiResult<EfiGblSlotMetadataBlock> {
diff --git a/gbl/libgbl/src/lib.rs b/gbl/libgbl/src/lib.rs
index 035a544..5ccc794 100644
--- a/gbl/libgbl/src/lib.rs
+++ b/gbl/libgbl/src/lib.rs
@@ -37,7 +37,6 @@
use avb::{HashtreeErrorMode, SlotVerifyData, SlotVerifyError, SlotVerifyFlags};
use core::ffi::CStr;
use gbl_storage::AsMultiBlockDevices;
-use spin::Mutex;
pub mod boot_mode;
pub mod boot_reason;
@@ -50,7 +49,7 @@
/// querying and modifying slotted boot behavior.
pub mod slots;
-use slots::{BootTarget, BootToken, Cursor, Manager, OneShot, SuffixBytes, UnbootableReason};
+use slots::{BootTarget, BootToken, Cursor, OneShot, SuffixBytes, UnbootableReason};
pub use avb::Descriptor;
pub use boot_mode::BootMode;
@@ -164,14 +163,13 @@
(boot_image, init_boot_image, vendor_boot_image, partitions_ram_map)
}
-static BOOT_TOKEN: Mutex<Option<BootToken>> = Mutex::new(Some(BootToken(())));
-
/// GBL object that provides implementation of helpers for boot process.
pub struct Gbl<'a, G>
where
G: GblOps,
{
ops: &'a mut G,
+ boot_token: Option<BootToken>,
}
impl<'a, G> Gbl<'a, G>
@@ -183,7 +181,7 @@
/// # Arguments
/// * `ops` - the [GblOps] callbacks to use
pub fn new(ops: &'a mut G) -> Self {
- Self { ops }
+ Self { ops, boot_token: Some(BootToken(())) }
}
/// Verify + Load Image Into memory
@@ -230,14 +228,14 @@
///
/// * `Ok(Cursor)` - Cursor object that manages a Manager
/// * `Err(Error)` - on failure
- pub fn load_slot_interface<'b, B: gbl_storage::AsBlockDevice, M: Manager>(
- &mut self,
- block_device: &'b mut B,
- ) -> Result<Cursor<'b, B, M>> {
- let boot_token = BOOT_TOKEN.lock().take().ok_or(Error::OperationProhibited)?;
+ pub fn load_slot_interface<B: gbl_storage::AsBlockDevice>(
+ &'a mut self,
+ block_device: &'a mut B,
+ ) -> Result<Cursor<'a, B>> {
+ let boot_token = self.boot_token.take().ok_or(Error::OperationProhibited)?;
self.ops
- .load_slot_interface::<B, M>(block_device, boot_token)
- .map_err(|_| Error::OperationProhibited.into())
+ .load_slot_interface::<B>(block_device, boot_token)
+ .map_err(move |_| Error::OperationProhibited.into())
}
/// Info Load
@@ -388,7 +386,7 @@
partitions_to_verify: &[&CStr],
partitions_ram_map: &'d mut [PartitionRamMap<'b, 'c>],
slot_verify_flags: SlotVerifyFlags,
- slot_cursor: Cursor<B, impl Manager>,
+ slot_cursor: Cursor<B>,
kernel_load_buffer: &mut [u8],
ramdisk_load_buffer: &mut [u8],
fdt: &mut [u8],
@@ -439,7 +437,7 @@
partitions_to_verify: &[&CStr],
partitions_ram_map: &'d mut [PartitionRamMap<'b, 'c>],
slot_verify_flags: SlotVerifyFlags,
- mut slot_cursor: Cursor<B, impl Manager>,
+ mut slot_cursor: Cursor<B>,
) -> Result<(KernelImage<'e>, BootToken)> {
let mut oneshot_status = slot_cursor.ctx.get_oneshot_status();
slot_cursor.ctx.clear_oneshot_status();
diff --git a/gbl/libgbl/src/ops.rs b/gbl/libgbl/src/ops.rs
index 5fcde0a..bbf88c6 100644
--- a/gbl/libgbl/src/ops.rs
+++ b/gbl/libgbl/src/ops.rs
@@ -83,19 +83,27 @@
fn visit_block_devices(
&mut self,
f: &mut dyn FnMut(&mut dyn BlockIoSync, u64, u64),
- ) -> Result<(), GblOpsError>;
+ ) -> Result<(), GblOpsError> {
+ Err(GblOpsError(Some("not defined yet")))
+ }
/// Prints a ASCII character to the platform console.
- fn console_put_char(&mut self, ch: u8) -> Result<(), GblOpsError>;
+ fn console_put_char(&mut self, ch: u8) -> Result<(), GblOpsError> {
+ Err(GblOpsError(Some("not defined yet")))
+ }
/// This method can be used to implement platform specific mechanism for deciding whether boot
/// should abort and enter Fastboot mode.
- fn should_stop_in_fastboot(&mut self) -> Result<bool, GblOpsError>;
+ fn should_stop_in_fastboot(&mut self) -> Result<bool, GblOpsError> {
+ Err(GblOpsError(Some("not defined yet")))
+ }
/// Platform specific kernel boot implementation.
///
/// Implementation is not expected to return on success.
- fn boot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError>;
+ fn boot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError> {
+ Err(GblOpsError(Some("not defined yet")))
+ }
// TODO(b/334962570): figure out how to plumb ops-provided hash implementations into
// libavb. The tricky part is that libavb hashing APIs are global with no way to directly
@@ -109,7 +117,7 @@
// Nevertype could be used here when it is stable https://github.com/serde-rs/serde/issues/812
fn do_fastboot<B: gbl_storage::AsBlockDevice>(
&self,
- cursor: &mut slots::Cursor<B, impl slots::Manager>,
+ cursor: &mut slots::Cursor<B>,
) -> GblResult<()> {
Err(Error::NotImplemented.into())
}
@@ -122,11 +130,11 @@
}
/// Load and initialize a slot manager and return a cursor over the manager on success.
- fn load_slot_interface<'b, B: gbl_storage::AsBlockDevice, M: slots::Manager>(
- &mut self,
- block_device: &'b mut B,
+ fn load_slot_interface<'a, B: gbl_storage::AsBlockDevice>(
+ &'a mut self,
+ block_device: &'a mut B,
boot_token: slots::BootToken,
- ) -> GblResult<slots::Cursor<'b, B, M>> {
+ ) -> GblResult<slots::Cursor<'a, B>> {
Err(Error::OperationProhibited.into())
}
diff --git a/gbl/libgbl/src/slots.rs b/gbl/libgbl/src/slots.rs
index ddfa396..26f9991 100644
--- a/gbl/libgbl/src/slots.rs
+++ b/gbl/libgbl/src/slots.rs
@@ -89,6 +89,14 @@
}
}
+impl TryFrom<u32> for Suffix {
+ type Error = Error;
+
+ fn try_from(value: u32) -> Result<Self, Self::Error> {
+ char::from_u32(value).ok_or(Error::Other).map(Self)
+ }
+}
+
// Includes a null terminator
const SUFFIX_CSTR_MAX_BYTES: usize = size_of::<Suffix>() + 1;
@@ -273,6 +281,8 @@
NoSuchSlot(Suffix),
/// The backend policy has denied permission for the given operation.
OperationProhibited,
+ /// Similar to NoSuchSlot but used when index is used for lookup internally.
+ BadSlotIndex(usize),
/// Unspecified error.
Other,
}
@@ -329,11 +339,15 @@
/// Returns the current active slot,
/// or Recovery if the system will try to boot to recovery.
+ ///
+ /// TODO(b/350572566): change to return Result after conducting integration tests
fn get_boot_target(&self) -> BootTarget;
/// Returns the slot last set active.
/// Note that this is different from get_boot_target in that
/// the slot last set active cannot be Recovery.
+ ///
+ /// TODO(b/350572566): change to return Result after conducting integration tests
fn get_slot_last_set_active(&self) -> Slot {
// We can safely assume that we have at least one slot.
self.slots_iter().max_by_key(|slot| (slot.priority, slot.suffix.rank())).unwrap()
@@ -370,6 +384,8 @@
) -> Result<(), Error>;
/// Default for initial tries
+ ///
+ /// TODO(b/350572566): change to return Result after conducting integration tests
fn get_max_retries(&self) -> Tries {
7u8.into()
}
@@ -406,19 +422,19 @@
/// This is useful for partition based slot setups,
/// where we do not write back every interaction in order to coalesce writes
/// and preserve disk lifetime.
- fn write_back<B: gbl_storage::AsBlockDevice>(&mut self, block_dev: &mut B) {}
+ fn write_back(&mut self, block_dev: &mut dyn gbl_storage::AsBlockDevice) {}
}
/// RAII helper object for coalescing changes.
-pub struct Cursor<'a, B: gbl_storage::AsBlockDevice, M: Manager> {
+pub struct Cursor<'a, B: gbl_storage::AsBlockDevice> {
/// The backing manager for slot metadata.
- pub ctx: M,
+ pub ctx: &'a mut dyn Manager,
/// The backing disk. Used for partition-backed metadata implementations
/// and for fastboot.
pub block_dev: &'a mut B,
}
-impl<'a, B: gbl_storage::AsBlockDevice, M: Manager> Drop for Cursor<'a, B, M> {
+impl<'a, B: gbl_storage::AsBlockDevice> Drop for Cursor<'a, B> {
fn drop(&mut self) {
self.ctx.write_back(&mut self.block_dev);
}
diff --git a/gbl/libgbl/src/slots/android.rs b/gbl/libgbl/src/slots/android.rs
index 6e3aab5..9b9aaf8 100644
--- a/gbl/libgbl/src/slots/android.rs
+++ b/gbl/libgbl/src/slots/android.rs
@@ -377,7 +377,7 @@
fn clear_oneshot_status(&mut self) {}
- fn write_back<B: gbl_storage::AsBlockDevice>(&mut self, block_dev: &mut B) {
+ fn write_back(&mut self, block_dev: &mut dyn gbl_storage::AsBlockDevice) {
self.sync_to_disk(block_dev)
}
}
diff --git a/gbl/libgbl/src/slots/fuchsia.rs b/gbl/libgbl/src/slots/fuchsia.rs
index c86e168..2ba6bc9 100644
--- a/gbl/libgbl/src/slots/fuchsia.rs
+++ b/gbl/libgbl/src/slots/fuchsia.rs
@@ -283,7 +283,7 @@
}
}
- fn write_back<B: gbl_storage::AsBlockDevice>(&mut self, block_dev: &mut B) {
+ fn write_back(&mut self, block_dev: &mut dyn gbl_storage::AsBlockDevice) {
self.sync_to_disk(block_dev);
}
}
@@ -645,7 +645,6 @@
include_bytes!("../../testdata/writeback_test_disk.bin").as_slice().into();
assert!(block_dev.sync_gpt().is_ok());
let mut read_buffer: [u8; size_of::<AbrData>()] = Default::default();
- let mut abr_data;
let mut sb: SlotBlock<AbrData> = Default::default();
sb.partition = PARTITION;
@@ -653,16 +652,12 @@
// New block to trigger drop on the cursor.
{
- let mut cursor = Cursor { ctx: sb, block_dev: &mut block_dev };
+ let cursor = Cursor { ctx: &mut sb, block_dev: &mut block_dev };
assert!(cursor.ctx.set_active_slot('b'.into()).is_ok());
- abr_data = cursor.ctx.get_data().clone();
}
- // Need to manually recalculate crc because the cursor updates that
- // right before writing to disk.
- abr_data.prepare_for_sync();
let res = block_dev.read_gpt_partition(PARTITION, OFFSET, &mut read_buffer);
assert!(res.is_ok());
- assert_eq!(read_buffer, abr_data.as_bytes());
+ assert_eq!(read_buffer, sb.get_data().as_bytes());
}
}
diff --git a/gbl/libgbl/src/slots/partition.rs b/gbl/libgbl/src/slots/partition.rs
index 675b0c9..9f4f87c 100644
--- a/gbl/libgbl/src/slots/partition.rs
+++ b/gbl/libgbl/src/slots/partition.rs
@@ -130,7 +130,7 @@
///
/// Does NOT write back to disk if no changes have been made and the cache is clean.
/// Panics if the write attempt fails.
- pub fn sync_to_disk<BlockDev: gbl_storage::AsBlockDevice>(&mut self, block_dev: &mut BlockDev) {
+ pub fn sync_to_disk(&mut self, block_dev: &mut dyn gbl_storage::AsBlockDevice) {
if self.cache_status == CacheStatus::Clean {
return;
}