Support fuchsia fastboot set_active, reboot mode

Supports "fastboot set_active", "fastboot reboot-bootloader",
"fastboot reboot-recovery" using libabr when operating in standard
Fuchsia A/B/R mode.

Bug: 374776896

Change-Id: I2fab6147ed3508aa3e812b7a1d0a0c8e066104ab
diff --git a/gbl/efi/src/android_boot.rs b/gbl/efi/src/android_boot.rs
index 3eedc71..8c48ac6 100644
--- a/gbl/efi/src/android_boot.rs
+++ b/gbl/efi/src/android_boot.rs
@@ -14,7 +14,7 @@
 
 use crate::{efi_blocks::find_block_devices, fastboot::fastboot, ops::Ops};
 use efi::{exit_boot_services, EfiEntry};
-use libgbl::{android_boot::load_android_simple, gbl_print, gbl_println, GblOps, Result};
+use libgbl::{android_boot::load_android_simple, gbl_print, gbl_println, GblOps, Os, Result};
 
 // The following implements a demo for booting Android from disk. It can be run from
 // Cuttlefish by adding `--android_efi_loader=<path of this EFI binary>` to the command line.
@@ -31,7 +31,7 @@
 // end-to-end test for libraries developed so far.
 pub fn android_boot_demo(entry: EfiEntry) -> Result<()> {
     let blks = find_block_devices(&entry)?;
-    let mut ops = Ops::new(&entry, &blks[..]);
+    let mut ops = Ops::new(&entry, &blks[..], Some(Os::Android));
 
     match ops.should_stop_in_fastboot() {
         Ok(true) => {
diff --git a/gbl/efi/src/fuchsia_boot.rs b/gbl/efi/src/fuchsia_boot.rs
index 9ab05ef..aac8819 100644
--- a/gbl/efi/src/fuchsia_boot.rs
+++ b/gbl/efi/src/fuchsia_boot.rs
@@ -14,8 +14,9 @@
 
 use crate::utils::efi_to_zbi_mem_range_type;
 #[allow(unused_imports)]
-use crate::utils::get_efi_mem_attr;
-use crate::{efi_blocks::find_block_devices, fastboot::fastboot, ops::Ops};
+use crate::{
+    efi_blocks::find_block_devices, fastboot::fastboot, ops::Ops, utils::get_efi_mem_attr,
+};
 use core::fmt::Write;
 use efi::{efi_print, efi_println, EfiEntry, EfiMemoryAttributesTable, EfiMemoryMap};
 use efi_types::{
@@ -27,7 +28,7 @@
     fuchsia_boot::{zircon_check_enter_fastboot, zircon_load_verify_abr, zircon_part_name},
     partition::check_part_unique,
     IntegrationError::UnificationError,
-    Result,
+    Os, Result,
 };
 use safemath::SafeNum;
 use zbi::{zbi_format::zbi_mem_range_t, ZbiContainer, ZbiFlags, ZbiType};
@@ -65,7 +66,7 @@
     let mut load_buffer = vec![0u8; 128 * 1024 * 1024]; // 128MB
     let (_zbi_items, _kernel, slot) = {
         let blks = find_block_devices(&efi_entry)?;
-        let mut ops = Ops::new(&efi_entry, &blks[..]);
+        let mut ops = Ops::new(&efi_entry, &blks[..], Some(Os::Fuchsia));
         // Checks whether to enter fastboot mode.
         if zircon_check_enter_fastboot(&mut ops) {
             fastboot(&mut ops)?;
diff --git a/gbl/efi/src/ops.rs b/gbl/efi/src/ops.rs
index 419f56d..b34df2c 100644
--- a/gbl/efi/src/ops.rs
+++ b/gbl/efi/src/ops.rs
@@ -50,7 +50,7 @@
     ops::{AvbIoError, AvbIoResult, CertPermanentAttributes, ImageBuffer, SHA256_DIGEST_SIZE},
     partition::GblDisk,
     slots::{BootToken, Cursor},
-    GblOps, Result as GblResult,
+    GblOps, Os, Result as GblResult,
 };
 use safemath::SafeNum;
 use zbi::ZbiContainer;
@@ -84,12 +84,13 @@
     pub efi_entry: &'a EfiEntry,
     pub disks: &'b [EfiGblDisk<'a>],
     pub zbi_bootloader_files_buffer: Vec<u8>,
+    pub os: Option<Os>,
 }
 
 impl<'a, 'b> Ops<'a, 'b> {
     /// Creates a new instance of [Ops]
-    pub fn new(efi_entry: &'a EfiEntry, disks: &'b [EfiGblDisk<'a>]) -> Self {
-        Self { efi_entry, disks, zbi_bootloader_files_buffer: Default::default() }
+    pub fn new(efi_entry: &'a EfiEntry, disks: &'b [EfiGblDisk<'a>], os: Option<Os>) -> Self {
+        Self { efi_entry, disks, zbi_bootloader_files_buffer: Default::default(), os }
     }
 
     /// Gets the property of an FDT node from EFI FDT.
@@ -255,6 +256,10 @@
         self.disks
     }
 
+    fn expected_os(&mut self) -> Result<Option<Os>> {
+        Ok(self.os)
+    }
+
     fn zircon_add_device_zbi_items(
         &mut self,
         container: &mut ZbiContainer<&mut [u8]>,
@@ -450,7 +455,7 @@
         mock_efi.con_out.expect_write_str().with(eq("foo bar")).return_const(Ok(()));
         let installed = mock_efi.install();
 
-        let mut ops = Ops::new(installed.entry(), &[]);
+        let mut ops = Ops::new(installed.entry(), &[], None);
 
         assert!(write!(&mut ops, "{} {}", "foo", "bar").is_ok());
     }
diff --git a/gbl/libefi/src/ab_slots.rs b/gbl/libefi/src/ab_slots.rs
index 5a0e16b..773190c 100644
--- a/gbl/libefi/src/ab_slots.rs
+++ b/gbl/libefi/src/ab_slots.rs
@@ -176,7 +176,7 @@
         ops::{AvbIoResult, CertPermanentAttributes, SHA256_DIGEST_SIZE},
         partition::GblDisk,
         slots::{Bootability, Cursor, RecoveryTarget, UnbootableReason},
-        Gbl, GblOps, Result as GblResult,
+        Gbl, GblOps, Os, Result as GblResult,
     };
     use gbl_storage::{BlockIo, BlockIoNull, Disk, Gpt};
     use libgbl::{device_tree::DeviceTreeComponentsRegistry, ops::ImageBuffer};
@@ -267,6 +267,10 @@
             &[] as &[GblDisk<Disk<BlockIoNull, &mut [u8]>, Gpt<&mut [u8]>>]
         }
 
+        fn expected_os(&mut self) -> Result<Option<Os>> {
+            Ok(None)
+        }
+
         fn zircon_add_device_zbi_items(&mut self, _: &mut ZbiContainer<&mut [u8]>) -> Result<()> {
             unimplemented!();
         }
diff --git a/gbl/libfastboot/src/lib.rs b/gbl/libfastboot/src/lib.rs
index 8fac123..42cc2a5 100644
--- a/gbl/libfastboot/src/lib.rs
+++ b/gbl/libfastboot/src/lib.rs
@@ -375,6 +375,9 @@
     /// * `responder`: An instance of `InfoSender`.
     async fn r#continue(&mut self, responder: impl InfoSender) -> CommandResult<()>;
 
+    /// Backend for `fastboot set_active`.
+    async fn set_active(&mut self, slot: &str, responder: impl InfoSender) -> CommandResult<()>;
+
     /// Backend for `fastboot oem ...`.
     ///
     /// # Args
@@ -821,6 +824,23 @@
     }
 }
 
+// Handles `fastboot set_active`
+async fn set_active(
+    mut args: Split<'_, char>,
+    transport: &mut impl Transport,
+    fb_impl: &mut impl FastbootImplementation,
+) -> Result<()> {
+    let mut resp = Responder::new(transport);
+    let res = async {
+        let slot = next_arg(&mut args).ok_or("Missing slot")?;
+        fb_impl.set_active(slot, &mut resp).await
+    };
+    match res.await {
+        Ok(_) => reply_okay!(resp, ""),
+        Err(e) => reply_fail!(resp, "{}", e.to_str()),
+    }
+}
+
 /// Helper for handling "fastboot oem ...".
 async fn oem(
     cmd: &str,
@@ -873,6 +893,7 @@
         "reboot-bootloader" => reboot(RebootMode::Bootloader, transport, fb_impl).await,
         "reboot-fastboot" => reboot(RebootMode::Fastboot, transport, fb_impl).await,
         "reboot-recovery" => reboot(RebootMode::Recovery, transport, fb_impl).await,
+        "set_active" => set_active(args, transport, fb_impl).await,
         "continue" => {
             r#continue(transport, fb_impl).await?;
             return Ok(true);
@@ -1014,6 +1035,7 @@
         download_buffer: Vec<u8>,
         downloaded_size: usize,
         reboot_mode: Option<RebootMode>,
+        active_slot: Option<String>,
     }
 
     impl FastbootImplementation for FastbootTest {
@@ -1094,6 +1116,11 @@
             responder.send_info("Continuing to boot...").await
         }
 
+        async fn set_active(&mut self, slot: &str, _: impl InfoSender) -> CommandResult<()> {
+            self.active_slot = Some(slot.into());
+            Ok(())
+        }
+
         async fn oem<'b>(
             &mut self,
             cmd: &str,
@@ -1625,4 +1652,25 @@
         tcp_stream.add_length_prefixed_input(b"continue");
         block_on(run_tcp_session(&mut tcp_stream, &mut fastboot_impl)).unwrap();
     }
+
+    #[test]
+    fn test_set_active() {
+        let mut fastboot_impl: FastbootTest = Default::default();
+        fastboot_impl.download_buffer = vec![0u8; 1024];
+        let mut transport = TestTransport::new();
+        transport.add_input(b"set_active:a");
+        block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
+        assert_eq!(transport.out_queue, VecDeque::<Vec<u8>>::from([b"OKAY".into()]));
+        assert_eq!(fastboot_impl.active_slot, Some("a".into()));
+    }
+
+    #[test]
+    fn test_set_active_missing_slot() {
+        let mut fastboot_impl: FastbootTest = Default::default();
+        fastboot_impl.download_buffer = vec![0u8; 1024];
+        let mut transport = TestTransport::new();
+        transport.add_input(b"set_active");
+        block_on(process_next_command(&mut transport, &mut fastboot_impl)).unwrap();
+        assert_eq!(transport.out_queue, VecDeque::<Vec<u8>>::from([b"FAILMissing slot".into()]));
+    }
 }
diff --git a/gbl/libgbl/src/fastboot/mod.rs b/gbl/libgbl/src/fastboot/mod.rs
index e9f2cca..ea95e14 100644
--- a/gbl/libgbl/src/fastboot/mod.rs
+++ b/gbl/libgbl/src/fastboot/mod.rs
@@ -15,10 +15,12 @@
 //! Fastboot backend for libgbl.
 
 use crate::{
+    fuchsia_boot::GblAbrOps,
     gbl_print, gbl_println,
     partition::{check_part_unique, GblDisk, PartitionIo},
     GblOps,
 };
+pub use abr::{mark_slot_active, set_one_shot_bootloader, set_one_shot_recovery, SlotIndex};
 use core::{
     array::from_fn,
     cmp::min,
@@ -267,13 +269,21 @@
         resp.send_info("Syncing storage...").await?;
         self.sync_all_blocks().await?;
         match mode {
-            RebootMode::Normal => {
-                resp.send_info("Rebooting...").await?;
-                resp.send_okay("").await?;
-                self.gbl_ops.reboot();
+            RebootMode::Normal => resp.send_info("Rebooting...").await?,
+            RebootMode::Bootloader => {
+                let f = self.gbl_ops.reboot_bootloader()?;
+                resp.send_info("Rebooting to bootloader...").await?;
+                f()
+            }
+            RebootMode::Recovery => {
+                let f = self.gbl_ops.reboot_recovery()?;
+                resp.send_info("Rebooting to recovery...").await?;
+                f()
             }
             _ => return Err("Unsupported".into()),
         }
+        resp.send_okay("").await?;
+        self.gbl_ops.reboot();
         Ok(())
     }
 
@@ -439,6 +449,22 @@
         Ok(self.sync_all_blocks().await?)
     }
 
+    async fn set_active(&mut self, slot: &str, _: impl InfoSender) -> CommandResult<()> {
+        self.sync_all_blocks().await?;
+        match self.gbl_ops.expected_os_is_fuchsia()? {
+            // TODO(b/374776896): Prioritizes platform specific `set_active_slot`  if available.
+            true => Ok(mark_slot_active(
+                &mut GblAbrOps(self.gbl_ops),
+                match slot {
+                    "a" => SlotIndex::A,
+                    "b" => SlotIndex::B,
+                    _ => return Err("Invalid slot index for Fuchsia A/B/R".into()),
+                },
+            )?),
+            _ => Err("Not supported".into()),
+        }
+    }
+
     async fn oem<'s>(
         &mut self,
         cmd: &str,
@@ -731,7 +757,13 @@
 #[cfg(test)]
 mod test {
     use super::*;
-    use crate::ops::test::{FakeGblOps, FakeGblOpsStorage};
+    use crate::{
+        ops::test::{FakeGblOps, FakeGblOpsStorage},
+        Os,
+    };
+    use abr::{
+        get_and_clear_one_shot_bootloader, get_boot_slot, mark_slot_unbootable, ABR_DATA_SIZE,
+    };
     use core::{
         mem::size_of,
         pin::{pin, Pin},
@@ -2015,4 +2047,153 @@
             listener.dump_usb_out_queue()
         );
     }
+
+    /// Helper for testing fastboot set_active in fuchsia A/B/R mode.
+    fn test_run_gbl_fastboot_set_active_fuchsia_abr(cmd: &str, slot: SlotIndex) {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let buffers = vec![vec![0u8; 128 * 1024]; 2];
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Fuchsia);
+        let listener: SharedTestListener = Default::default();
+        let (usb, tcp) = (&listener, &listener);
+
+        mark_slot_unbootable(&mut GblAbrOps(&mut gbl_ops), SlotIndex::A).unwrap();
+        mark_slot_unbootable(&mut GblAbrOps(&mut gbl_ops), SlotIndex::B).unwrap();
+
+        // Flash some data to `durable_boot` after A/B/R metadata. This is for testing that sync
+        // storage is done first.
+        let data = vec![0x55u8; 4 * 1024 - ABR_DATA_SIZE];
+        listener.add_usb_input(b"oem gbl-enable-async-block-io");
+        listener.add_usb_input(format!("download:{:#x}", 4 * 1024 - ABR_DATA_SIZE).as_bytes());
+        listener.add_usb_input(&data);
+        listener.add_usb_input(format!("flash:durable_boot//{:#x}", ABR_DATA_SIZE).as_bytes());
+        // Issues set_active commands
+        listener.add_usb_input(cmd.as_bytes());
+        listener.add_usb_input(b"continue");
+        block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp)));
+
+        assert_eq!(
+            listener.usb_out_queue(),
+            make_expected_usb_out(&[
+                b"OKAY",
+                b"DATA00000fe0",
+                b"OKAY",
+                b"INFOAn IO task is launched. To sync manually, run \"oem gbl-sync-blocks\".",
+                b"OKAY",
+                b"OKAY",
+                b"INFOSyncing storage...",
+                b"OKAY",
+            ]),
+            "\nActual USB output:\n{}",
+            listener.dump_usb_out_queue()
+        );
+
+        assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (slot, false));
+        // Verifies storage sync
+        assert_eq!(
+            storage[0].partition_io(None).unwrap().dev().io().storage[ABR_DATA_SIZE..],
+            data
+        );
+    }
+
+    #[test]
+    fn test_run_gbl_fastboot_set_active_fuchsia_abr_a() {
+        test_run_gbl_fastboot_set_active_fuchsia_abr("set_active:a", SlotIndex::A);
+    }
+
+    #[test]
+    fn test_run_gbl_fastboot_set_active_fuchsia_abr_b() {
+        test_run_gbl_fastboot_set_active_fuchsia_abr("set_active:b", SlotIndex::B);
+    }
+
+    #[test]
+    fn test_run_gbl_fastboot_set_active_fuchsia_abr_invalid_slot() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let buffers = vec![vec![0u8; 128 * 1024]; 2];
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Fuchsia);
+        let listener: SharedTestListener = Default::default();
+        let (usb, tcp) = (&listener, &listener);
+
+        listener.add_usb_input(b"set_active:r");
+        listener.add_usb_input(b"continue");
+        block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp)));
+
+        assert_eq!(
+            listener.usb_out_queue(),
+            make_expected_usb_out(&[
+                b"FAILInvalid slot index for Fuchsia A/B/R",
+                b"INFOSyncing storage...",
+                b"OKAY",
+            ]),
+            "\nActual USB output:\n{}",
+            listener.dump_usb_out_queue()
+        );
+    }
+
+    #[test]
+    fn test_run_gbl_fastboot_fuchsia_reboot_bootloader_abr() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let buffers = vec![vec![0u8; 128 * 1024]; 2];
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Fuchsia);
+        let listener: SharedTestListener = Default::default();
+        let (usb, tcp) = (&listener, &listener);
+
+        listener.add_usb_input(b"reboot-bootloader");
+        listener.add_usb_input(b"continue");
+        block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp)));
+
+        assert_eq!(
+            listener.usb_out_queue(),
+            make_expected_usb_out(&[
+                b"INFOSyncing storage...",
+                b"INFORebooting to bootloader...",
+                b"OKAY",
+                b"FAILUnknown",
+                b"INFOSyncing storage...",
+                b"OKAY",
+            ]),
+            "\nActual USB output:\n{}",
+            listener.dump_usb_out_queue()
+        );
+
+        assert_eq!(get_and_clear_one_shot_bootloader(&mut GblAbrOps(&mut gbl_ops)), Ok(true));
+    }
+
+    #[test]
+    fn test_run_gbl_fastboot_fuchsia_reboot_recovery_abr() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let buffers = vec![vec![0u8; 128 * 1024]; 2];
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Fuchsia);
+        let listener: SharedTestListener = Default::default();
+        let (usb, tcp) = (&listener, &listener);
+
+        listener.add_usb_input(b"reboot-recovery");
+        listener.add_usb_input(b"continue");
+        block_on(run_gbl_fastboot_stack::<2>(&mut gbl_ops, buffers, Some(usb), Some(tcp)));
+
+        assert_eq!(
+            listener.usb_out_queue(),
+            make_expected_usb_out(&[
+                b"INFOSyncing storage...",
+                b"INFORebooting to recovery...",
+                b"OKAY",
+                b"FAILUnknown",
+                b"INFOSyncing storage...",
+                b"OKAY",
+            ]),
+            "\nActual USB output:\n{}",
+            listener.dump_usb_out_queue()
+        );
+
+        // One shot recovery is set.
+        assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::R, false));
+        assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::A, false));
+    }
 }
diff --git a/gbl/libgbl/src/fuchsia_boot/mod.rs b/gbl/libgbl/src/fuchsia_boot/mod.rs
index 371d91d..ed21735 100644
--- a/gbl/libgbl/src/fuchsia_boot/mod.rs
+++ b/gbl/libgbl/src/fuchsia_boot/mod.rs
@@ -35,7 +35,10 @@
 const ABR_PARTITION_ALIASES: &[&str] = &[DURABLE_BOOT_PARTITION, MISC_PARTITION];
 
 /// Helper function to find partition given a list of possible aliases.
-fn find_part_aliases<'a, 'b>(ops: &mut impl GblOps<'a>, aliases: &'b [&str]) -> Result<&'b str> {
+fn find_part_aliases<'a, 'b>(
+    ops: &mut (impl GblOps<'a> + ?Sized),
+    aliases: &'b [&str],
+) -> Result<&'b str> {
     Ok(*aliases
         .iter()
         .find(|v| matches!(ops.partition_size(v), Ok(Some(_))))
@@ -43,9 +46,9 @@
 }
 
 /// `GblAbrOps` wraps an object implementing `GblOps` and implements the `abr::Ops` trait.
-struct GblAbrOps<'a, T>(&'a mut T);
+pub(crate) struct GblAbrOps<'a, T: ?Sized>(pub &'a mut T);
 
-impl<'b, T: GblOps<'b>> AbrOps for GblAbrOps<'_, T> {
+impl<'b, T: GblOps<'b> + ?Sized> AbrOps for GblAbrOps<'_, T> {
     fn read_abr_metadata(&mut self, out: &mut [u8]) -> Result<()> {
         let part = find_part_aliases(self.0, &ABR_PARTITION_ALIASES)?;
         self.0.read_from_partition_sync(part, 0, out)
diff --git a/gbl/libgbl/src/lib.rs b/gbl/libgbl/src/lib.rs
index 16a8a14..c580d90 100644
--- a/gbl/libgbl/src/lib.rs
+++ b/gbl/libgbl/src/lib.rs
@@ -63,7 +63,7 @@
 pub use boot_reason::KnownBootReason;
 pub use error::{IntegrationError, Result};
 use liberror::Error;
-pub use ops::GblOps;
+pub use ops::{GblOps, Os};
 
 use overlap::is_overlap;
 
diff --git a/gbl/libgbl/src/ops.rs b/gbl/libgbl/src/ops.rs
index 64e62ad..f9c3a66 100644
--- a/gbl/libgbl/src/ops.rs
+++ b/gbl/libgbl/src/ops.rs
@@ -17,8 +17,10 @@
 pub use crate::image_buffer::ImageBuffer;
 use crate::{
     error::Result as GblResult,
+    fuchsia_boot::GblAbrOps,
     partition::{check_part_unique, read_unique_partition, write_unique_partition, GblDisk},
 };
+pub use abr::{set_one_shot_bootloader, set_one_shot_recovery, SlotIndex};
 use core::{ffi::CStr, fmt::Write, num::NonZeroUsize, ops::DerefMut, result::Result};
 use gbl_async::block_on;
 use libutils::aligned_subslice;
@@ -34,6 +36,15 @@
 use super::device_tree;
 use super::slots;
 
+/// Target Type of OS to boot.
+#[derive(PartialEq, Debug, Copy, Clone)]
+pub enum Os {
+    /// Android
+    Android,
+    /// Fuchsia
+    Fuchsia,
+}
+
 // https://stackoverflow.com/questions/41081240/idiomatic-callbacks-in-rust
 // should we use traits for this? or optional/box FnMut?
 //
@@ -67,6 +78,32 @@
     /// way.
     fn reboot(&mut self);
 
+    /// Reboots into recovery mode
+    ///
+    /// On success, returns a closure that performs the reboot.
+    fn reboot_recovery(&mut self) -> Result<impl FnOnce() + '_, Error> {
+        if self.expected_os_is_fuchsia()? {
+            // TODO(b/363075013): Checks and prioritizes platform specific
+            // `set_boot_reason()`.
+            set_one_shot_recovery(&mut GblAbrOps(self), true)?;
+            return Ok(|| self.reboot());
+        }
+        Err(Error::Unsupported)
+    }
+
+    /// Reboots into bootloader fastboot mode
+    ///
+    /// On success, returns a closure that performs the reboot.
+    fn reboot_bootloader(&mut self) -> Result<impl FnOnce() + '_, Error> {
+        if self.expected_os_is_fuchsia()? {
+            // TODO(b/363075013): Checks and prioritizes platform specific
+            // `set_boot_reason()`.
+            set_one_shot_bootloader(&mut GblAbrOps(self), true)?;
+            return Ok(|| self.reboot());
+        }
+        Err(Error::Unsupported)
+    }
+
     /// Returns the list of disk devices on this platform.
     ///
     /// Notes that the return slice doesn't capture the life time of `&self`, meaning that the slice
@@ -131,6 +168,15 @@
         }
     }
 
+    /// Returns which OS to load, or `None` to try to auto-detect based on disk layout & contents.
+    fn expected_os(&mut self) -> Result<Option<Os>, Error>;
+
+    /// Returns if the expected_os is fuchsia
+    fn expected_os_is_fuchsia(&mut self) -> Result<bool, Error> {
+        // TODO(b/374776896): Implement auto detection.
+        Ok(self.expected_os()?.map(|v| v == Os::Fuchsia).unwrap_or(false))
+    }
+
     /// Adds device specific ZBI items to the given `container`
     fn zircon_add_device_zbi_items(
         &mut self,
@@ -289,6 +335,7 @@
 pub(crate) mod test {
     use super::*;
     use crate::partition::GblDisk;
+    use abr::{get_and_clear_one_shot_bootloader, get_boot_slot};
     use avb::{CertOps, Ops};
     use avb_test::TestOps as AvbTestOps;
     use core::ops::{Deref, DerefMut};
@@ -364,6 +411,12 @@
 
         /// For returned by `fn get_zbi_bootloader_files_buffer()`
         pub zbi_bootloader_files_buffer: Vec<u8>,
+
+        /// For checking that `Self::reboot` is called.
+        pub rebooted: bool,
+
+        /// For return by `Self::expected_os()`
+        pub os: Option<Os>,
     }
 
     /// Print `console_out` output, which can be useful for debugging.
@@ -421,7 +474,9 @@
             self.stop_in_fastboot.unwrap_or(Ok(false))
         }
 
-        fn reboot(&mut self) {}
+        fn reboot(&mut self) {
+            self.rebooted = true;
+        }
 
         fn disks(
             &self,
@@ -432,6 +487,10 @@
             self.partitions
         }
 
+        fn expected_os(&mut self) -> Result<Option<Os>, Error> {
+            Ok(self.os)
+        }
+
         fn zircon_add_device_zbi_items(
             &mut self,
             container: &mut ZbiContainer<&mut [u8]>,
@@ -523,4 +582,49 @@
             unimplemented!();
         }
     }
+
+    #[test]
+    fn test_fuchsia_reboot_bootloader() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Fuchsia);
+        (gbl_ops.reboot_bootloader().unwrap())();
+        assert!(gbl_ops.rebooted);
+        assert_eq!(get_and_clear_one_shot_bootloader(&mut GblAbrOps(&mut gbl_ops)), Ok(true));
+    }
+
+    #[test]
+    fn test_non_fuchsia_reboot_bootloader() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Android);
+        assert!(gbl_ops.reboot_bootloader().is_err_and(|e| e == Error::Unsupported));
+        assert_eq!(get_and_clear_one_shot_bootloader(&mut GblAbrOps(&mut gbl_ops)), Ok(false));
+    }
+
+    #[test]
+    fn test_fuchsia_reboot_recovery() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Fuchsia);
+        (gbl_ops.reboot_recovery().unwrap())();
+        assert!(gbl_ops.rebooted);
+        // One shot recovery is set.
+        assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::R, false));
+        assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::A, false));
+    }
+
+    #[test]
+    fn test_non_fuchsia_reboot_recovery() {
+        let mut storage = FakeGblOpsStorage::default();
+        storage.add_raw_device(c"durable_boot", [0x00u8; 4 * 1024]);
+        let mut gbl_ops = FakeGblOps::new(&storage);
+        gbl_ops.os = Some(Os::Android);
+        assert!(gbl_ops.reboot_recovery().is_err_and(|e| e == Error::Unsupported));
+        // One shot recovery is not set.
+        assert_eq!(get_boot_slot(&mut GblAbrOps(&mut gbl_ops), true), (SlotIndex::A, false));
+    }
 }