Add API for loading, verifying, booting zircon

Bug: 334962583

Change-Id: Ia78695240efd729ea1bbb896a1f422e6c68933ac
diff --git a/gbl/efi/src/fuchsia_boot.rs b/gbl/efi/src/fuchsia_boot.rs
index 5ba17d4..336a790 100644
--- a/gbl/efi/src/fuchsia_boot.rs
+++ b/gbl/efi/src/fuchsia_boot.rs
@@ -195,7 +195,7 @@
     let mut load_buffer = vec![0u8; 128 * 1024 * 1024]; // 128MB
     let zbi_kernel = load_fuchsia_simple(&efi_entry, &mut load_buffer[..])?;
     #[allow(unused_variables)]
-    let (original, relocated, kernel_entry) = relocate_to_tail(&mut zbi_kernel[..])?;
+    let (original, relocated, _) = relocate_to_tail(&mut zbi_kernel[..])?;
 
     #[cfg(target_arch = "aarch64")]
     {
@@ -206,7 +206,7 @@
         let (_, remains) = zbi_get_unused_buffer(relocated)?;
         let _ = efi::exit_boot_services(efi_entry, remains).unwrap();
         // SAFETY: For demo, we assume images are provided valid.
-        unsafe { boot::aarch64::jump_zircon_el2_or_lower(relocated, kernel_entry, original) };
+        unsafe { boot::aarch64::jump_zircon_el2_or_lower(relocated, original) };
     }
 
     #[cfg(any(target_arch = "x86_64", target_arch = "x86"))]
diff --git a/gbl/efi/src/ops.rs b/gbl/efi/src/ops.rs
index 09c0315..0aa1050 100644
--- a/gbl/efi/src/ops.rs
+++ b/gbl/efi/src/ops.rs
@@ -18,7 +18,11 @@
 
 use core::fmt::Write;
 use efi::{efi_print, efi_println, EfiEntry};
-use libgbl::{GblOps, GblOpsError};
+use libgbl::{
+    slots::{BootToken, Cursor},
+    BootImages, GblOps, GblOpsError, Result as GblResult,
+};
+use zbi::ZbiContainer;
 
 pub struct Ops<'a> {
     pub efi_entry: &'a EfiEntry,
@@ -33,6 +37,10 @@
 }
 
 impl GblOps for Ops<'_> {
+    fn console_out(&mut self) -> Option<&mut dyn Write> {
+        unimplemented!();
+    }
+
     fn should_stop_in_fastboot(&mut self) -> Result<bool, GblOpsError> {
         // TODO(b/349829690): also query GblSlotProtocol.get_boot_reason() for board-specific
         // fastboot triggers.
@@ -43,4 +51,49 @@
         }
         Ok(found?)
     }
+
+    fn preboot(&mut self, _: BootImages) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    async fn read_from_partition(
+        &mut self,
+        _: &str,
+        _: u64,
+        _: &mut [u8],
+    ) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    async fn write_to_partition(
+        &mut self,
+        _: &str,
+        _: u64,
+        _: &mut [u8],
+    ) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    fn partition_size(&mut self, _: &str) -> Result<Option<u64>, GblOpsError> {
+        unimplemented!();
+    }
+
+    fn zircon_add_device_zbi_items(
+        &mut self,
+        _: &mut ZbiContainer<&mut [u8]>,
+    ) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    fn do_fastboot<B: gbl_storage::AsBlockDevice>(&self, _: &mut Cursor<B>) -> GblResult<()> {
+        unimplemented!();
+    }
+
+    fn load_slot_interface<'a, B: gbl_storage::AsBlockDevice>(
+        &'a mut self,
+        _: &'a mut B,
+        _: BootToken,
+    ) -> GblResult<Cursor<'a, B>> {
+        unimplemented!();
+    }
 }
diff --git a/gbl/libboot/BUILD b/gbl/libboot/BUILD
index 83c20e8..2d207fa 100644
--- a/gbl/libboot/BUILD
+++ b/gbl/libboot/BUILD
@@ -75,6 +75,7 @@
     rustc_flags = ANDROID_RUST_LINTS,
     deps = [
         ":x86_bootparam_defs",
+        "@gbl//third_party/libzbi",
         "@zerocopy",
     ] + select({
         "@gbl//toolchain:gbl_rust_uefi_aarch64": [
diff --git a/gbl/libboot/src/aarch64.rs b/gbl/libboot/src/aarch64.rs
index 32384c1..6655c91 100644
--- a/gbl/libboot/src/aarch64.rs
+++ b/gbl/libboot/src/aarch64.rs
@@ -17,6 +17,7 @@
 //!   https://www.kernel.org/doc/html/v5.11/arm64/booting.html
 
 use core::arch::asm;
+use zbi::ZbiContainer;
 
 /// ARM exception levels.
 #[allow(missing_docs)]
@@ -119,13 +120,14 @@
 ///
 /// # Safety
 ///
-/// Caller must ensure that `zbi_kernel` contains a valid zircon kernel ZBI item and `entry_off` is
-/// the correct kernel entry offset.
-pub unsafe fn jump_zircon_el2_or_lower(zbi_kernel: &[u8], entry_off: usize, zbi_item: &[u8]) -> ! {
+/// Caller must ensure that `zbi_kernel` contains a valid zircon kernel ZBI item.
+pub unsafe fn jump_zircon_el2_or_lower(kernel: &[u8], zbi_item: &[u8]) -> ! {
     assert_ne!(current_el(), ExceptionLevel::EL3);
-    flush_dcache_buffer(zbi_kernel);
+    let (entry, _) =
+        ZbiContainer::parse(zbi_item).unwrap().get_kernel_entry_and_reserved_memory_size().unwrap();
+    flush_dcache_buffer(kernel);
     flush_dcache_buffer(zbi_item);
-    let addr = (zbi_kernel.as_ptr() as usize).checked_add(entry_off).unwrap();
+    let addr = (kernel.as_ptr() as usize).checked_add(usize::try_from(entry).unwrap()).unwrap();
     // SAFETY:
     // * `zbi_kernel` and `zbi_item` have been flushed.
     // * By requirement of this function, the computed `addr` is a valid kernel entry point.
diff --git a/gbl/libefi/BUILD b/gbl/libefi/BUILD
index 2f4f7fb..be18e31 100644
--- a/gbl/libefi/BUILD
+++ b/gbl/libefi/BUILD
@@ -111,5 +111,6 @@
     deps = [
         "@gbl//libavb:sysdeps",
         "@gbl//libstorage:libstorage_testlib",
+        "@gbl//third_party/libzbi",
     ],
 )
diff --git a/gbl/libefi/src/ab_slots.rs b/gbl/libefi/src/ab_slots.rs
index ecc9fc4..0fd0282 100644
--- a/gbl/libefi/src/ab_slots.rs
+++ b/gbl/libefi/src/ab_slots.rs
@@ -180,12 +180,16 @@
     use core::ptr::null_mut;
     use gbl::{
         slots::{Bootability, Cursor, RecoveryTarget, UnbootableReason},
-        Gbl, GblOps, GblOpsError, Result as GblResult,
+        BootImages, Gbl, GblOps, GblOpsError, 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};
+    use std::{
+        fmt::Write,
+        mem::align_of,
+        sync::atomic::{AtomicBool, AtomicU32, Ordering},
+    };
+    use zbi::ZbiContainer;
 
     // The thread-local atomics are an ugly, ugly hack to pass state between
     // the protocol method functions and the rest of the test body.
@@ -243,8 +247,49 @@
     }
 
     impl<'b> GblOps for TestGblOps<'b> {
+        fn console_out(&mut self) -> Option<&mut dyn Write> {
+            unimplemented!();
+        }
+
         fn should_stop_in_fastboot(&mut self) -> Result<bool, GblOpsError> {
-            Ok(false)
+            unimplemented!();
+        }
+
+        fn preboot(&mut self, _: BootImages) -> Result<(), GblOpsError> {
+            unimplemented!();
+        }
+
+        async fn read_from_partition(
+            &mut self,
+            _: &str,
+            _: u64,
+            _: &mut [u8],
+        ) -> Result<(), GblOpsError> {
+            unimplemented!();
+        }
+
+        async fn write_to_partition(
+            &mut self,
+            _: &str,
+            _: u64,
+            _: &mut [u8],
+        ) -> Result<(), GblOpsError> {
+            unimplemented!();
+        }
+
+        fn partition_size(&mut self, _: &str) -> Result<Option<u64>, GblOpsError> {
+            unimplemented!();
+        }
+
+        fn zircon_add_device_zbi_items(
+            &mut self,
+            _: &mut ZbiContainer<&mut [u8]>,
+        ) -> Result<(), GblOpsError> {
+            unimplemented!();
+        }
+
+        fn do_fastboot<B: gbl_storage::AsBlockDevice>(&self, _: &mut Cursor<B>) -> GblResult<()> {
+            unimplemented!();
         }
 
         fn load_slot_interface<'a, B: gbl_storage::AsBlockDevice>(
diff --git a/gbl/libgbl/BUILD b/gbl/libgbl/BUILD
index 036422f..0b64c95 100644
--- a/gbl/libgbl/BUILD
+++ b/gbl/libgbl/BUILD
@@ -30,7 +30,9 @@
         "@bitflags",
         "@crc32fast",
         "@cstr",
+        "@gbl//libabr",
         "@gbl//libasync",
+        "@gbl//libboot",
         "@gbl//libfastboot",
         "@gbl//libsafemath",
         "@gbl//libstorage",
@@ -59,9 +61,12 @@
         "@gbl//libgbl/testdata:sparse_test_raw.bin",
         "@gbl//libgbl/testdata:testkey_rsa4096_pub.bin",
         "@gbl//libgbl/testdata:writeback_test_disk.bin",
-        "@gbl//libgbl/testdata:zircon_a.bin",
         "@gbl//libgbl/testdata:zircon_a.vbmeta",
         "@gbl//libgbl/testdata:zircon_a.vbmeta.cert",
+        "@gbl//libgbl/testdata:zircon_a.zbi",
+        "@gbl//libgbl/testdata:zircon_b.zbi",
+        "@gbl//libgbl/testdata:zircon_r.zbi",
+        "@gbl//libgbl/testdata:zircon_slotless.zbi",
     ],
     rustc_flags = ANDROID_RUST_LINTS,
     deps = [
@@ -76,22 +81,3 @@
         "@uuid",
     ],
 )
-
-rust_test(
-    name = "integration_test",
-    srcs = ["tests/integration_tests.rs"],
-    compile_data = [
-        "@gbl//libgbl/testdata:zircon_a.bin",
-        "@gbl//libgbl/testdata:zircon_b.bin",
-        "@gbl//libgbl/testdata:zircon_r.bin",
-        "@gbl//libgbl/testdata:zircon_gpt.bin",
-    ],
-    rustc_flags = ANDROID_RUST_LINTS,
-    deps = [
-        ":libgbl",
-        "@avb//:avb_crypto_ops_sha_impl_staticlib",
-        "@gbl//libavb:sysdeps",
-        "@gbl//libstorage",
-        "@gbl//libstorage:libstorage_testlib",
-    ],
-)
diff --git a/gbl/libgbl/src/error.rs b/gbl/libgbl/src/error.rs
index fbc8ac2..396adb4 100644
--- a/gbl/libgbl/src/error.rs
+++ b/gbl/libgbl/src/error.rs
@@ -45,6 +45,12 @@
     AvbOpsBusy,
     /// Buffers overlap and can cause undefined behavior and data corruption.
     BufferOverlap,
+    /// Input parameter is invalid.
+    InvalidInput,
+    /// Buffer is too small.
+    BufferTooSmall,
+    /// Buffer does not meet alignment requirement.
+    InvalidAlignment,
 }
 
 impl Display for Error {
@@ -59,6 +65,9 @@
             Error::Internal => write!(f, "Internal error"),
             Error::AvbOpsBusy => write!(f, "AvbOps were already borrowed"),
             Error::BufferOverlap => write!(f, "Buffers overlap"),
+            Error::InvalidInput => write!(f, "Invalid Input"),
+            Error::BufferTooSmall => write!(f, "Buffer is too small"),
+            Error::InvalidAlignment => write!(f, "Buffer does not meet alignment requirement"),
         }
     }
 }
@@ -150,8 +159,10 @@
         GblSlotsError(crate::slots::Error),
         FromBytesUntilNulError(FromBytesUntilNulError),
         FromBytesWithNulError(FromBytesWithNulError),
-        StorageError(StorageError),
         SafeMathError(safemath::Error),
+        StorageError(StorageError),
+        TryFromIntError(core::num::TryFromIntError),
+        ZbiError(zbi::ZbiError),
     }
 }
 
diff --git a/gbl/libgbl/src/fuchsia_boot/mod.rs b/gbl/libgbl/src/fuchsia_boot/mod.rs
new file mode 100644
index 0000000..4983502
--- /dev/null
+++ b/gbl/libgbl/src/fuchsia_boot/mod.rs
@@ -0,0 +1,396 @@
+// 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.
+
+//! This file provides APIs for loading, verifying and booting Fuchsia/Zircon.
+
+use crate::{Error as GblError, GblOps, Result as GblResult};
+pub use abr::SlotIndex;
+use safemath::SafeNum;
+use zbi::{ZbiContainer, ZbiFlags, ZbiHeader, ZbiType};
+use zerocopy::AsBytes;
+
+/// Kernel load address alignment. Value taken from
+/// https://fuchsia.googlesource.com/fuchsia/+/4f204d8a0243e84a86af4c527a8edcc1ace1615f/zircon/kernel/target/arm64/boot-shim/BUILD.gn#38
+pub const ZIRCON_KERNEL_ALIGN: usize = 64 * 1024;
+
+/// A helper for getting a subslice with an aligned address.
+fn aligned_subslice(buffer: &mut [u8], alignment: usize) -> GblResult<&mut [u8]> {
+    let addr = SafeNum::from(buffer.as_ptr() as usize);
+    let aligned_offset = addr.round_up(alignment) - addr;
+    Ok(buffer.get_mut(aligned_offset.try_into()?..).ok_or(GblError::BufferTooSmall)?)
+}
+
+/// A helper for splitting the trailing unused portion of a ZBI container buffer.
+///
+/// Returns a tuple of used subslice and unused subslice
+fn zbi_split_unused_buffer(zbi: &mut [u8]) -> GblResult<(&mut [u8], &mut [u8])> {
+    Ok(zbi.split_at_mut(ZbiContainer::parse(&zbi[..])?.container_size()))
+}
+
+/// Relocates a ZBI kernel to a different buffer.
+///
+/// * `dest` must be aligned to `ZIRCON_KERNEL_ALIGN`.
+/// * `dest` will be a ZBI container containing only the kernel item.
+pub fn relocate_kernel(kernel: &[u8], dest: &mut [u8]) -> GblResult<()> {
+    if (dest.as_ptr() as usize % ZIRCON_KERNEL_ALIGN) != 0 {
+        return Err(GblError::InvalidAlignment.into());
+    }
+
+    let kernel = ZbiContainer::parse(&kernel[..])?;
+    let kernel_item = kernel.is_bootable()?;
+    let hdr = kernel_item.header;
+    // Creates a new ZBI kernel item at the destination.
+    let mut relocated = ZbiContainer::new(&mut dest[..])?;
+    let zbi_type = ZbiType::try_from(hdr.type_)?;
+    relocated.create_entry_with_payload(
+        zbi_type,
+        hdr.extra,
+        hdr.get_flags() & !ZbiFlags::CRC32,
+        kernel_item.payload.as_bytes(),
+    )?;
+    let (_, reserved_memory_size) = relocated.get_kernel_entry_and_reserved_memory_size()?;
+    match reserved_memory_size > u64::try_from(zbi_split_unused_buffer(dest)?.1.len())? {
+        true => Err(GblError::BufferTooSmall.into()),
+        _ => Ok(()),
+    }
+}
+
+/// Relocate a ZBI kernel to the trailing unused buffer.
+///
+/// Returns the original kernel subslice and relocated kernel subslice.
+pub fn relocate_to_tail(kernel: &mut [u8]) -> GblResult<(&mut [u8], &mut [u8])> {
+    let reloc_size = ZbiContainer::parse(&kernel[..])?.get_buffer_size_for_kernel_relocation()?;
+    let (original, relocated) = zbi_split_unused_buffer(kernel)?;
+    let relocated = aligned_subslice(relocated, ZIRCON_KERNEL_ALIGN)?;
+    let off = (SafeNum::from(relocated.len()) - reloc_size).round_down(ZIRCON_KERNEL_ALIGN);
+    let relocated = &mut relocated[off.try_into()?..];
+    relocate_kernel(original, relocated)?;
+    let reloc_addr = relocated.as_ptr() as usize;
+    Ok(kernel.split_at_mut(reloc_addr.checked_sub(kernel.as_ptr() as usize).unwrap()))
+}
+
+/// Helper for getting the slotted/slotless standard zircon partition name.
+fn zircon_part_name(slot: Option<SlotIndex>) -> &'static str {
+    match slot {
+        Some(slot) => match slot {
+            SlotIndex::A => "zircon_a",
+            SlotIndex::B => "zircon_b",
+            SlotIndex::R => "zircon_r",
+        },
+        _ => "zircon",
+    }
+}
+
+/// Gets the ZBI command line string for the current slot.
+fn slot_cmd_line(slot: SlotIndex) -> &'static str {
+    match slot {
+        SlotIndex::A => "zvb.current_slot=a",
+        SlotIndex::B => "zvb.current_slot=b",
+        SlotIndex::R => "zvb.current_slot=r",
+    }
+}
+
+/// Loads and verifies a kernel of the given slot or slotless.
+///
+/// # Args
+///
+/// * `ops`: A reference to an object that implements `GblOps`.
+/// * `slot`: None if slotless. Otherwise the target slot to boot.
+/// * `load`: Buffer for loading the kernel.
+///
+/// On success returns a pair containing: 1. the slice of the ZBI container with device ZBI items
+/// and 2. the slice of the relocated kernel at the tail.
+pub fn zircon_load_verify<'a>(
+    ops: &mut impl GblOps,
+    slot: Option<SlotIndex>,
+    load: &'a mut [u8],
+) -> GblResult<(&'a mut [u8], &'a mut [u8])> {
+    let load = aligned_subslice(load, ZIRCON_KERNEL_ALIGN)?;
+    let zircon_part = zircon_part_name(slot);
+
+    // Reads ZBI header to computes the total size of kernel.
+    let mut zbi_header: ZbiHeader = Default::default();
+    ops.read_from_partition_sync(zircon_part, 0, zbi_header.as_bytes_mut())?;
+    let image_length = SafeNum::from(zbi_header.as_bytes_mut().len()) + zbi_header.length;
+
+    // Reads the entire kernel
+    let kernel = load.get_mut(..image_length.try_into()?).ok_or(GblError::BufferTooSmall)?;
+    ops.read_from_partition_sync(zircon_part, 0, kernel)?;
+
+    // TODO(b/334962583): Perform AVB verification.
+
+    // Append additional ZBI items.
+    let mut zbi_kernel = ZbiContainer::parse(&mut load[..])?;
+
+    match slot {
+        Some(slot) => {
+            // Appends current slot item.
+            zbi_kernel.create_entry_with_payload(
+                ZbiType::CmdLine,
+                0,
+                ZbiFlags::default(),
+                slot_cmd_line(slot).as_bytes(),
+            )?;
+        }
+        _ => {}
+    }
+
+    // Relocates the kernel to the tail to reserved extra memory that the kernel may require.
+    let (zbi_items, relocated) = relocate_to_tail(&mut load[..])?;
+
+    // Appends device specific ZBI items.
+    ops.zircon_add_device_zbi_items(&mut ZbiContainer::parse(&mut zbi_items[..])?)?;
+
+    Ok((zbi_items, relocated))
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::{slots, BootImages, GblOpsError};
+    use std::{
+        collections::{BTreeSet, HashMap},
+        fmt::Write,
+        fs,
+        path::Path,
+    };
+
+    // The `reserve_memory_size` value in the test ZBI kernel.
+    // See `gen_zircon_test_images()` in libgbl/testdata/gen_test_data.py.
+    const TEST_KERNEL_RESERVED_MEMORY_SIZE: usize = 1024;
+
+    pub(crate) const ZIRCON_A_ZBI_FILE: &str = "zircon_a.zbi";
+    pub(crate) const ZIRCON_B_ZBI_FILE: &str = "zircon_b.zbi";
+    pub(crate) const ZIRCON_R_ZBI_FILE: &str = "zircon_r.zbi";
+    pub(crate) const ZIRCON_SLOTLESS_ZBI_FILE: &str = "zircon_slotless.zbi";
+
+    /// Reads a data file under libgbl/testdata/
+    pub(crate) fn read_test_data(file: &str) -> Vec<u8> {
+        fs::read(Path::new(format!("external/gbl/libgbl/testdata/{}", file).as_str())).unwrap()
+    }
+
+    /// `TestZirconBootGblOps` implements `GblOps` for test.
+    pub(crate) struct TestZirconBootGblOps {
+        pub(crate) partitions: HashMap<&'static str, Vec<u8>>,
+    }
+
+    impl Default for TestZirconBootGblOps {
+        fn default() -> Self {
+            let partitions = HashMap::from([
+                ("zircon_a", read_test_data(ZIRCON_A_ZBI_FILE)),
+                ("zircon_b", read_test_data(ZIRCON_B_ZBI_FILE)),
+                ("zircon_r", read_test_data(ZIRCON_R_ZBI_FILE)),
+                ("zircon", read_test_data(ZIRCON_SLOTLESS_ZBI_FILE)),
+            ]);
+            Self { partitions }
+        }
+    }
+
+    impl GblOps for TestZirconBootGblOps {
+        fn console_out(&mut self) -> Option<&mut dyn Write> {
+            unimplemented!();
+        }
+
+        fn should_stop_in_fastboot(&mut self) -> Result<bool, GblOpsError> {
+            unimplemented!();
+        }
+
+        fn preboot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError> {
+            unimplemented!();
+        }
+
+        async fn read_from_partition(
+            &mut self,
+            part: &str,
+            off: u64,
+            out: &mut [u8],
+        ) -> Result<(), GblOpsError> {
+            match self.partitions.get_mut(part) {
+                Some(v) => Ok(out.clone_from_slice(&v[off.try_into().unwrap()..][..out.len()])),
+                _ => Err(GblOpsError(Some("Test: No such partition"))),
+            }
+        }
+
+        async fn write_to_partition(
+            &mut self,
+            part: &str,
+            off: u64,
+            data: &mut [u8],
+        ) -> Result<(), GblOpsError> {
+            match self.partitions.get_mut(part) {
+                Some(v) => Ok(v[off.try_into().unwrap()..][..data.len()].clone_from_slice(data)),
+                _ => Err(GblOpsError(Some("Test: No such partition"))),
+            }
+        }
+
+        fn partition_size(&mut self, part: &str) -> Result<Option<u64>, GblOpsError> {
+            Ok(self.partitions.get_mut(part).map(|v| v.len().try_into().unwrap()))
+        }
+
+        fn zircon_add_device_zbi_items(
+            &mut self,
+            container: &mut ZbiContainer<&mut [u8]>,
+        ) -> Result<(), GblOpsError> {
+            container
+                .create_entry_with_payload(
+                    ZbiType::CmdLine,
+                    0,
+                    ZbiFlags::default(),
+                    b"test_zbi_item",
+                )
+                .unwrap();
+            Ok(())
+        }
+
+        fn do_fastboot<B: gbl_storage::AsBlockDevice>(
+            &self,
+            cursor: &mut slots::Cursor<B>,
+        ) -> GblResult<()> {
+            unimplemented!();
+        }
+
+        fn load_slot_interface<'a, B: gbl_storage::AsBlockDevice>(
+            &'a mut self,
+            block_device: &'a mut B,
+            boot_token: slots::BootToken,
+        ) -> GblResult<slots::Cursor<'a, B>> {
+            unimplemented!();
+        }
+    }
+
+    // Helper object for allocating aligned buffer.
+    pub(crate) struct AlignedBuffer {
+        buffer: Vec<u8>,
+        size: usize,
+        alignment: usize,
+    }
+
+    impl AlignedBuffer {
+        /// Allocates a buffer.
+        pub(crate) fn new(size: usize, alignment: usize) -> Self {
+            Self { buffer: vec![0u8; alignment + size - 1], size, alignment }
+        }
+
+        /// Allocates a buffer and initializes with data.
+        pub(crate) fn new_with_data(data: &[u8], alignment: usize) -> Self {
+            let mut res = Self::new(data.len(), alignment);
+            res.get().clone_from_slice(data);
+            res
+        }
+
+        /// Gets the buffer
+        pub(crate) fn get(&mut self) -> &mut [u8] {
+            &mut aligned_subslice(&mut self.buffer[..], self.alignment).unwrap()[..self.size]
+        }
+    }
+
+    /// Normalizes a ZBI container by converting each ZBI item into raw bytes and storing them in
+    /// an ordered set. The function is mainly used for comparing two ZBI containers have identical
+    /// set of items, disregarding order.
+    pub(crate) fn normalize_zbi(zbi: &[u8]) -> BTreeSet<Vec<u8>> {
+        let zbi = ZbiContainer::parse(zbi).unwrap();
+        BTreeSet::from_iter(zbi.iter().map(|v| {
+            let mut hdr = *v.header;
+            hdr.crc32 = 0; // ignores crc32 field.
+            hdr.flags &= !ZbiFlags::CRC32.bits();
+            [hdr.as_bytes(), v.payload.as_bytes()].concat()
+        }))
+    }
+
+    /// Helper to append a command line ZBI item to a ZBI container
+    pub(crate) fn append_cmd_line(zbi: &mut [u8], cmd: &[u8]) {
+        let mut container = ZbiContainer::parse(zbi).unwrap();
+        container.create_entry_with_payload(ZbiType::CmdLine, 0, ZbiFlags::default(), cmd).unwrap();
+    }
+
+    /// Helper for testing `zircon_load_verify`.
+    fn test_load_verify(
+        ops: &mut impl GblOps,
+        slot: Option<SlotIndex>,
+        expected_zbi_items: &[u8],
+        expected_kernel: &[u8],
+    ) {
+        // Test load buffer layout:
+        // |  zircon_x.zbi + items| ~~ |~64k~| relocated kernel + reserved |
+        // | ---------- 64K -----------|~~~~~| ----------------------------|
+        let sz = 2 * ZIRCON_KERNEL_ALIGN + expected_kernel.len() + TEST_KERNEL_RESERVED_MEMORY_SIZE;
+        let mut load = AlignedBuffer::new(sz, ZIRCON_KERNEL_ALIGN);
+        let (zbi_items, relocated) = zircon_load_verify(ops, slot, load.get()).unwrap();
+        // Verifies loaded ZBI kernel/items
+        assert_eq!(normalize_zbi(expected_zbi_items), normalize_zbi(zbi_items));
+        // Verifies relocated kernel
+        assert_eq!(normalize_zbi(expected_kernel), normalize_zbi(relocated));
+        // Relocated kernel is at the latest aligned address
+        let off = (relocated.as_ptr() as usize) - (load.get().as_ptr() as usize);
+        assert_eq!(off, 2 * ZIRCON_KERNEL_ALIGN);
+    }
+
+    #[test]
+    fn test_zircon_load_verify_slotless() {
+        let mut ops = TestZirconBootGblOps::default();
+        let zbi = &read_test_data(ZIRCON_SLOTLESS_ZBI_FILE);
+        let mut expected_kernel = AlignedBuffer::new_with_data(zbi, 8);
+        // Adds extra bytes for device ZBI items.
+        let mut expected_zbi_items = AlignedBuffer::new(zbi.len() + 1024, 8);
+        expected_zbi_items.get()[..zbi.len()].clone_from_slice(zbi);
+        append_cmd_line(expected_zbi_items.get(), b"test_zbi_item");
+        test_load_verify(&mut ops, None, expected_zbi_items.get(), expected_kernel.get());
+    }
+
+    /// Helper for testing `zircon_load_verify` using A/B/R.
+    fn test_load_verify_slotted_helper(
+        ops: &mut impl GblOps,
+        slot: SlotIndex,
+        zbi: &[u8],
+        slot_item: &str,
+    ) {
+        let mut expected_kernel = AlignedBuffer::new_with_data(zbi, 8);
+        // Adds extra bytes for device ZBI items.
+        let mut expected_zbi_items = AlignedBuffer::new(zbi.len() + 1024, 8);
+        expected_zbi_items.get()[..zbi.len()].clone_from_slice(zbi);
+        append_cmd_line(expected_zbi_items.get(), b"test_zbi_item");
+        append_cmd_line(expected_zbi_items.get(), slot_item.as_bytes());
+        test_load_verify(ops, Some(slot), expected_zbi_items.get(), expected_kernel.get());
+    }
+
+    #[test]
+    fn test_load_verify_slot_a() {
+        let mut ops = TestZirconBootGblOps::default();
+        let zircon_a_zbi = &read_test_data(ZIRCON_A_ZBI_FILE);
+        test_load_verify_slotted_helper(&mut ops, SlotIndex::A, zircon_a_zbi, "zvb.current_slot=a");
+    }
+
+    #[test]
+    fn test_load_verify_slot_b() {
+        let mut ops = TestZirconBootGblOps::default();
+        let zircon_b_zbi = &read_test_data(ZIRCON_B_ZBI_FILE);
+        test_load_verify_slotted_helper(&mut ops, SlotIndex::B, zircon_b_zbi, "zvb.current_slot=b");
+    }
+
+    #[test]
+    fn test_load_verify_slot_r() {
+        let mut ops = TestZirconBootGblOps::default();
+        let zircon_r_zbi = &read_test_data(ZIRCON_R_ZBI_FILE);
+        test_load_verify_slotted_helper(&mut ops, SlotIndex::R, zircon_r_zbi, "zvb.current_slot=r");
+    }
+
+    #[test]
+    fn test_not_enough_buffer_for_reserved_memory() {
+        let mut ops = TestZirconBootGblOps::default();
+        let zbi = &read_test_data(ZIRCON_A_ZBI_FILE);
+        let sz = ZIRCON_KERNEL_ALIGN + zbi.len() + TEST_KERNEL_RESERVED_MEMORY_SIZE - 1;
+        let mut load = AlignedBuffer::new(sz, ZIRCON_KERNEL_ALIGN);
+        assert!(zircon_load_verify(&mut ops, Some(SlotIndex::A), load.get()).is_err());
+    }
+}
diff --git a/gbl/libgbl/src/lib.rs b/gbl/libgbl/src/lib.rs
index a26fe45..e49a4b3 100644
--- a/gbl/libgbl/src/lib.rs
+++ b/gbl/libgbl/src/lib.rs
@@ -42,6 +42,7 @@
 pub mod boot_reason;
 pub mod error;
 pub mod fastboot;
+pub mod fuchsia_boot;
 pub mod ops;
 mod overlap;
 
@@ -59,7 +60,6 @@
     AndroidBootImages, BootImages, DefaultGblOps, FuchsiaBootImages, GblOps, GblOpsError,
 };
 
-use gbl_async::block_on;
 use overlap::is_overlap;
 
 // TODO: b/312607649 - Replace placeholders with actual structures: https://r.android.com/2721974, etc
@@ -515,25 +515,6 @@
 
         Ok((kernel_image, token))
     }
-
-    /// Loads and boots a Zircon kernel according to ABR + AVB.
-    pub fn zircon_load_and_boot(&mut self, load_buffer: &mut [u8]) -> Result<()> {
-        // TODO(b/334962583): Implement zircon ABR + AVB.
-        // The following are place holder for test of invocation in the integration test only.
-        let ptn_size = self
-            .ops
-            .partition_size("zircon_a")?
-            .ok_or(Error::MissingImage)?
-            .try_into()
-            .or(Err(Error::ArithmeticOverflow))?;
-        let (kernel, remains) = load_buffer.split_at_mut(ptn_size);
-        block_on(self.ops.read_from_partition("zircon_a", 0, kernel))?;
-        self.ops.boot(BootImages::Fuchsia(FuchsiaBootImages {
-            zbi_kernel: kernel,
-            zbi_items: &mut [],
-        }))?;
-        Err(Error::BootFailed.into())
-    }
 }
 
 #[cfg(test)]
@@ -548,7 +529,7 @@
 
     const TEST_ZIRCON_PARTITION_NAME: &str = "zircon_a";
     const TEST_ZIRCON_PARTITION_NAME_CSTR: &CStr = c"zircon_a";
-    const TEST_ZIRCON_IMAGE_PATH: &str = "zircon_a.bin";
+    const TEST_ZIRCON_IMAGE_PATH: &str = "zircon_a.zbi";
     const TEST_ZIRCON_VBMETA_PATH: &str = "zircon_a.vbmeta";
     const TEST_ZIRCON_VBMETA_CERT_PATH: &str = "zircon_a.vbmeta.cert";
     const TEST_PUBLIC_KEY_PATH: &str = "testkey_rsa4096_pub.bin";
diff --git a/gbl/libgbl/src/ops.rs b/gbl/libgbl/src/ops.rs
index 2bf4063..b1541f4 100644
--- a/gbl/libgbl/src/ops.rs
+++ b/gbl/libgbl/src/ops.rs
@@ -17,13 +17,15 @@
 #[cfg(feature = "alloc")]
 extern crate alloc;
 
-use crate::error::{Error, Result as GblResult};
+use crate::error::Result as GblResult;
 #[cfg(feature = "alloc")]
 use alloc::ffi::CString;
 use core::{
     fmt::{Debug, Write},
     result::Result,
 };
+use gbl_async::block_on;
+use zbi::ZbiContainer;
 
 use super::slots;
 
@@ -54,9 +56,15 @@
 }
 
 /// `GblOpsError` is the error type returned by required methods in `GblOps`.
-#[derive(Default, Debug, PartialEq, Eq)]
+#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
 pub struct GblOpsError(pub Option<&'static str>);
 
+impl From<&'static str> for GblOpsError {
+    fn from(val: &'static str) -> Self {
+        Self(Some(val))
+    }
+}
+
 // https://stackoverflow.com/questions/41081240/idiomatic-callbacks-in-rust
 // should we use traits for this? or optional/box FnMut?
 //
@@ -67,21 +75,15 @@
 */
 /// Trait that defines callbacks that can be provided to Gbl.
 pub trait GblOps {
-    /// Prints a ASCII character to the platform console.
-    fn console_put_char(&mut self, ch: u8) -> Result<(), GblOpsError> {
-        Err(GblOpsError(Some("not defined yet")))
-    }
+    /// Gets a console for logging messages.
+    fn console_out(&mut self) -> Option<&mut dyn Write>;
 
     /// 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>;
 
-    /// Platform specific kernel boot implementation.
-    ///
-    /// Implementation is not expected to return on success.
-    fn boot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError> {
-        Err(GblOpsError(Some("not defined yet")))
-    }
+    /// Platform specific processing of boot images before booting.
+    fn preboot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError>;
 
     /// Reads data from a partition.
     async fn read_from_partition(
@@ -89,8 +91,16 @@
         part: &str,
         off: u64,
         out: &mut [u8],
+    ) -> Result<(), GblOpsError>;
+
+    /// Reads data from a partition synchronously.
+    fn read_from_partition_sync(
+        &mut self,
+        part: &str,
+        off: u64,
+        out: &mut [u8],
     ) -> Result<(), GblOpsError> {
-        Err(GblOpsError(Some("not defined yet")))
+        block_on(self.read_from_partition(part, off, out))
     }
 
     /// Writes data to a partition.
@@ -99,14 +109,26 @@
         part: &str,
         off: u64,
         data: &mut [u8],
+    ) -> Result<(), GblOpsError>;
+
+    /// Writes data to a partition synchronously.
+    fn write_to_partition_sync(
+        &mut self,
+        part: &str,
+        off: u64,
+        data: &mut [u8],
     ) -> Result<(), GblOpsError> {
-        Err(GblOpsError(Some("not defined yet")))
+        block_on(self.write_to_partition(part, off, data))
     }
 
     /// Returns the size of a partiiton. Returns Ok(None) if partition doesn't exist.
-    fn partition_size(&mut self, part: &str) -> Result<Option<usize>, GblOpsError> {
-        Err(GblOpsError(Some("not defined yet")))
-    }
+    fn partition_size(&mut self, part: &str) -> Result<Option<u64>, GblOpsError>;
+
+    /// Adds device specific ZBI items to the given `container`
+    fn zircon_add_device_zbi_items(
+        &mut self,
+        container: &mut ZbiContainer<&mut [u8]>,
+    ) -> Result<(), GblOpsError>;
 
     // 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
@@ -121,15 +143,12 @@
     fn do_fastboot<B: gbl_storage::AsBlockDevice>(
         &self,
         cursor: &mut slots::Cursor<B>,
-    ) -> GblResult<()> {
-        Err(Error::NotImplemented.into())
-    }
+    ) -> GblResult<()>;
 
     /// TODO: b/312607649 - placeholder interface for Gbl specific callbacks that uses alloc.
     #[cfg(feature = "alloc")]
     fn gbl_alloc_extra_action(&mut self, s: &str) -> GblResult<()> {
-        let _c_string = CString::new(s);
-        Err(Error::NotImplemented.into())
+        unimplemented!();
     }
 
     /// Load and initialize a slot manager and return a cursor over the manager on success.
@@ -137,38 +156,7 @@
         &'a mut self,
         block_device: &'a mut B,
         boot_token: slots::BootToken,
-    ) -> GblResult<slots::Cursor<'a, B>> {
-        Err(Error::OperationProhibited.into())
-    }
-}
-
-/// `GblUtils` takes a reference to `GblOps` and implements various traits.
-pub(crate) struct GblUtils<'a, T: GblOps> {
-    ops: &'a mut T,
-}
-
-impl<'a, T: GblOps> GblUtils<'a, T> {
-    /// Create a new instance.
-    ///
-    /// # Args
-    ///
-    /// * `ops`: A reference to a `GblOps`,
-    ///
-    /// # Returns
-    ///
-    /// Returns a new instance and the trailing unused part of the input scratch buffer.
-    pub fn new(ops: &'a mut T) -> GblResult<Self> {
-        Ok(Self { ops })
-    }
-}
-
-impl<T: GblOps> Write for GblUtils<'_, T> {
-    fn write_str(&mut self, s: &str) -> core::fmt::Result {
-        for ch in s.as_bytes() {
-            self.ops.console_put_char(*ch).map_err(|_| core::fmt::Error {})?;
-        }
-        Ok(())
-    }
+    ) -> GblResult<slots::Cursor<'a, B>>;
 }
 
 /// Default [GblOps] implementation that returns errors and does nothing.
@@ -176,15 +164,59 @@
 pub struct DefaultGblOps {}
 
 impl GblOps for DefaultGblOps {
-    fn console_put_char(&mut self, ch: u8) -> Result<(), GblOpsError> {
-        Err(GblOpsError(Some("unimplemented")))
+    fn console_out(&mut self) -> Option<&mut dyn Write> {
+        unimplemented!();
     }
 
     fn should_stop_in_fastboot(&mut self) -> Result<bool, GblOpsError> {
-        Err(GblOpsError(Some("unimplemented")))
+        unimplemented!();
     }
 
-    fn boot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError> {
-        Err(GblOpsError(Some("unimplemented")))
+    fn preboot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    async fn read_from_partition(
+        &mut self,
+        part: &str,
+        off: u64,
+        out: &mut [u8],
+    ) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    async fn write_to_partition(
+        &mut self,
+        part: &str,
+        off: u64,
+        data: &mut [u8],
+    ) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    fn partition_size(&mut self, part: &str) -> Result<Option<u64>, GblOpsError> {
+        unimplemented!();
+    }
+
+    fn zircon_add_device_zbi_items(
+        &mut self,
+        container: &mut ZbiContainer<&mut [u8]>,
+    ) -> Result<(), GblOpsError> {
+        unimplemented!();
+    }
+
+    fn do_fastboot<B: gbl_storage::AsBlockDevice>(
+        &self,
+        cursor: &mut slots::Cursor<B>,
+    ) -> GblResult<()> {
+        unimplemented!();
+    }
+
+    fn load_slot_interface<'a, B: gbl_storage::AsBlockDevice>(
+        &'a mut self,
+        block_device: &'a mut B,
+        boot_token: slots::BootToken,
+    ) -> GblResult<slots::Cursor<'a, B>> {
+        unimplemented!();
     }
 }
diff --git a/gbl/libgbl/testdata/gen_test_data.py b/gbl/libgbl/testdata/gen_test_data.py
index 5a74fc3..bf94d88 100755
--- a/gbl/libgbl/testdata/gen_test_data.py
+++ b/gbl/libgbl/testdata/gen_test_data.py
@@ -15,6 +15,7 @@
 # limitations under the License.
 """Generate test data files for libgbl tests"""
 
+import argparse
 import os
 import pathlib
 import random
@@ -34,7 +35,7 @@
 # reproducibility as much as possible; this will prevent adding a bunch of
 # unnecessary test binaries to the git history.
 RNG_SEED_SPARSE_TEST_RAW = 1
-RNG_SEED_ZIRCON = {"a": 2, "b": 3, "r": 4}
+RNG_SEED_ZIRCON = {"a": 2, "b": 3, "r": 4, "slotless": 5}
 
 
 # A helper for writing bytes to a file at a given offset.
@@ -65,7 +66,9 @@
     # For now this requires that img2simg exists on $PATH.
     # It can be built from an Android checkout via `m img2simg`; the resulting
     # binary will be at out/host/linux-x86/bin/img2simg.
-    subprocess.run(["img2simg", "-s", out_file_raw, SCRIPT_DIR / "sparse_test.bin"])
+    subprocess.run(
+        ["img2simg", "-s", out_file_raw, SCRIPT_DIR / "sparse_test.bin"]
+    )
     subprocess.run(
         [
             "img2simg",
@@ -77,19 +80,37 @@
     )
 
 
-# Generates GPT disk, kernel data for Zircon tests
-def gen_zircon_gpt():
-    gen_gpt_args = []
-    for suffix in ["a", "b", "r"]:
-        random.seed(RNG_SEED_ZIRCON[suffix])
-        zircon = random.randbytes(16 * SZ_KB)
-        out_file = SCRIPT_DIR / f"zircon_{suffix}.bin"
-        out_file.write_bytes(zircon)
-        gen_gpt_args.append(f"--partition=zircon_{suffix},16K,{str(out_file)}")
+def gen_zircon_test_images(zbi_tool):
+    if not zbi_tool:
+        print(
+            "Warning: ZBI tool not provided. Skip regenerating zircon test images"
+        )
+        return
 
-    subprocess.run(
-        [GPT_TOOL, SCRIPT_DIR / "zircon_gpt.bin", "128K"] + gen_gpt_args, check=True
-    )
+    with tempfile.TemporaryDirectory() as temp_dir:
+        for suffix in ["a", "b", "r", "slotless"]:
+            temp_dir = pathlib.Path(temp_dir)
+            random.seed(RNG_SEED_ZIRCON[suffix])
+            out_kernel_bin_file = temp_dir / f"zircon_{suffix}.bin"
+            # The first 16 bytes are two u64 integers representing `entry` and
+            # `reserve_memory_size`.
+            # Set `entry` value to 2048 and `reserve_memory_size` to 1024.
+            kernel_bytes = int(2048).to_bytes(8, "little") + int(1024).to_bytes(
+                8, "little"
+            )
+            kernel_bytes += random.randbytes(8 * SZ_KB - 16)
+            out_kernel_bin_file.write_bytes(kernel_bytes)
+            out_zbi_file = SCRIPT_DIR / f"zircon_{suffix}.zbi"
+            # Put image in a zbi container.
+            subprocess.run(
+                [
+                    zbi_tool,
+                    "--output",
+                    out_zbi_file,
+                    "--type=KERNEL_X64",
+                    out_kernel_bin_file,
+                ]
+            )
 
 
 # Generates test data for A/B slot Manager writeback test
@@ -139,7 +160,9 @@
     # Also create a corrupted version of the permanent attributes to test failure.
     # This is a little bit of a pain but we don't have an easy way to do a SHA256 in Rust
     # at the moment so we can't generate it on the fly.
-    bad_attrs = bytearray((SCRIPT_DIR / "cert_permanent_attributes.bin").read_bytes())
+    bad_attrs = bytearray(
+        (SCRIPT_DIR / "cert_permanent_attributes.bin").read_bytes()
+    )
     bad_attrs[4] ^= 0x01  # Bytes 0-3 = version, byte 4 starts the public key.
     (SCRIPT_DIR / "cert_permanent_attributes.bad.bin").write_bytes(bad_attrs)
     hash_bytes = sha256_hash(SCRIPT_DIR / "cert_permanent_attributes.bad.bin")
@@ -173,7 +196,7 @@
                 "--partition_name",
                 "zircon_a",
                 "--image",
-                SCRIPT_DIR / "zircon_a.bin",
+                SCRIPT_DIR / "zircon_a.zbi",
                 "--output_vbmeta_image",
                 hash_descriptor_path,
                 "--salt",
@@ -218,8 +241,22 @@
         )
 
 
+def _parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description=__doc__,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+
+    parser.add_argument(
+        "--zbi_tool", default="", help="Path to the Fuchsia ZBI tool"
+    )
+
+    return parser.parse_args()
+
+
 if __name__ == "__main__":
+    args = _parse_args()
     gen_writeback_test_bin()
     gen_sparse_test_file()
-    gen_zircon_gpt()
+    gen_zircon_test_images(args.zbi_tool)
     gen_vbmeta()
diff --git a/gbl/libgbl/testdata/zircon_a.bin b/gbl/libgbl/testdata/zircon_a.bin
deleted file mode 100644
index 10e7ce4..0000000
--- a/gbl/libgbl/testdata/zircon_a.bin
+++ /dev/null
Binary files differ
diff --git a/gbl/libgbl/testdata/zircon_a.vbmeta b/gbl/libgbl/testdata/zircon_a.vbmeta
index 13e15d1..1078e98 100644
--- a/gbl/libgbl/testdata/zircon_a.vbmeta
+++ b/gbl/libgbl/testdata/zircon_a.vbmeta
Binary files differ
diff --git a/gbl/libgbl/testdata/zircon_a.vbmeta.cert b/gbl/libgbl/testdata/zircon_a.vbmeta.cert
index cb69e5c..f7449db 100644
--- a/gbl/libgbl/testdata/zircon_a.vbmeta.cert
+++ b/gbl/libgbl/testdata/zircon_a.vbmeta.cert
Binary files differ
diff --git a/gbl/libgbl/testdata/zircon_a.zbi b/gbl/libgbl/testdata/zircon_a.zbi
new file mode 100644
index 0000000..e2bbf5e
--- /dev/null
+++ b/gbl/libgbl/testdata/zircon_a.zbi
Binary files differ
diff --git a/gbl/libgbl/testdata/zircon_b.zbi b/gbl/libgbl/testdata/zircon_b.zbi
new file mode 100644
index 0000000..cad59d7
--- /dev/null
+++ b/gbl/libgbl/testdata/zircon_b.zbi
Binary files differ
diff --git a/gbl/libgbl/testdata/zircon_gpt.bin b/gbl/libgbl/testdata/zircon_gpt.bin
deleted file mode 100644
index f873c07..0000000
--- a/gbl/libgbl/testdata/zircon_gpt.bin
+++ /dev/null
Binary files differ
diff --git a/gbl/libgbl/testdata/zircon_r.zbi b/gbl/libgbl/testdata/zircon_r.zbi
new file mode 100644
index 0000000..e04847e
--- /dev/null
+++ b/gbl/libgbl/testdata/zircon_r.zbi
Binary files differ
diff --git a/gbl/libgbl/testdata/zircon_slotless.zbi b/gbl/libgbl/testdata/zircon_slotless.zbi
new file mode 100644
index 0000000..d10c149
--- /dev/null
+++ b/gbl/libgbl/testdata/zircon_slotless.zbi
Binary files differ
diff --git a/gbl/libgbl/tests/integration_tests.rs b/gbl/libgbl/tests/integration_tests.rs
deleted file mode 100644
index b7dcf29..0000000
--- a/gbl/libgbl/tests/integration_tests.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-// 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.
-
-use libgbl::{BootImages, FuchsiaBootImages, Gbl, GblOps, GblOpsError};
-use std::{
-    collections::{BTreeMap, VecDeque},
-    vec::Vec,
-};
-
-extern crate avb_sysdeps;
-
-/// `TestGblOps` provides mock implementation of GblOps for integration test.
-#[derive(Default)]
-struct TestGblOps<'a> {
-    console_out: VecDeque<u8>,
-    boot_cb: Option<MustUse<&'a mut dyn FnMut(BootImages)>>,
-    storage: BTreeMap<&'static str, Vec<u8>>,
-}
-
-impl GblOps for TestGblOps<'_> {
-    fn console_put_char(&mut self, ch: u8) -> Result<(), GblOpsError> {
-        Ok(self.console_out.push_back(ch))
-    }
-
-    fn should_stop_in_fastboot(&mut self) -> Result<bool, GblOpsError> {
-        Ok(false)
-    }
-
-    fn boot(&mut self, boot_images: BootImages) -> Result<(), GblOpsError> {
-        Ok((self.boot_cb.as_mut().unwrap().get())(boot_images))
-    }
-
-    async fn read_from_partition(
-        &mut self,
-        part: &str,
-        off: u64,
-        out: &mut [u8],
-    ) -> Result<(), GblOpsError> {
-        match self.storage.get_mut(part) {
-            Some(v) => Ok(out.clone_from_slice(&v[off.try_into().unwrap()..][..out.len()])),
-            _ => Err(GblOpsError(Some("Test: No such partition"))),
-        }
-    }
-
-    async fn write_to_partition(
-        &mut self,
-        part: &str,
-        off: u64,
-        data: &mut [u8],
-    ) -> Result<(), GblOpsError> {
-        match self.storage.get_mut(part) {
-            Some(v) => Ok(v[off.try_into().unwrap()..][..data.len()].clone_from_slice(data)),
-            _ => Err(GblOpsError(Some("Test: No such partition"))),
-        }
-    }
-
-    fn partition_size(&mut self, part: &str) -> Result<Option<usize>, GblOpsError> {
-        Ok(self.storage.get_mut(part).map(|v| v.len()))
-    }
-}
-
-/// `MustUse` wraps an object and checks that it is accessed at least once before it's dropped.
-/// In this integration test, it is mainly used to check that test provided ops callbacks are run.
-struct MustUse<T: ?Sized> {
-    used: bool,
-    val: T,
-}
-
-impl<T: ?Sized> MustUse<T> {
-    /// Create a new instance.
-    fn new(val: T) -> Self
-    where
-        T: Sized,
-    {
-        Self { used: false, val: val }
-    }
-
-    /// Returns a mutable reference to the object.
-    fn get(&mut self) -> &mut T {
-        self.used = true;
-        &mut self.val
-    }
-}
-
-impl<T: ?Sized> Drop for MustUse<T> {
-    fn drop(&mut self) {
-        assert!(self.used)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_zircon_load_and_boot() {
-        // TODO(b/334962583): Invocation test only. Update this test once
-        // `Gbl::zircon_load_and_boot()` is implemented.
-        let mut boot_cb = |boot_images: BootImages| {
-            let BootImages::Fuchsia(FuchsiaBootImages { zbi_kernel, zbi_items }) = boot_images
-            else {
-                panic!("Wrong image type");
-            };
-            assert_eq!(zbi_kernel, include_bytes!("../testdata/zircon_a.bin"));
-            assert_eq!(zbi_items, []);
-        };
-        let mut ops: TestGblOps = Default::default();
-        ops.storage =
-            BTreeMap::from([("zircon_a", include_bytes!("../testdata/zircon_a.bin").to_vec())]);
-        ops.boot_cb = Some(MustUse::new(&mut boot_cb));
-        let mut gbl = Gbl::new(&mut ops);
-        let mut load_buffer = vec![0u8; 64 * 1024];
-        let _ = gbl.zircon_load_and_boot(&mut load_buffer[..]);
-    }
-}
diff --git a/gbl/tests/BUILD b/gbl/tests/BUILD
index 705bff3..4a55b18 100644
--- a/gbl/tests/BUILD
+++ b/gbl/tests/BUILD
@@ -24,7 +24,6 @@
         "@gbl//libefi:libefi_test",
         "@gbl//libfastboot:libfastboot_test",
         "@gbl//libfdt:libfdt_test",
-        "@gbl//libgbl:integration_test",
         "@gbl//libgbl:libgbl_test",
         "@gbl//libsafemath:libsafemath_test",
         "@gbl//libstorage:libstorage_doc_test",
diff --git a/gbl/third_party/libzbi/src/lib.rs b/gbl/third_party/libzbi/src/lib.rs
index 529a87d..7774df2 100644
--- a/gbl/third_party/libzbi/src/lib.rs
+++ b/gbl/third_party/libzbi/src/lib.rs
@@ -303,6 +303,11 @@
         self.payload_length
     }
 
+    /// Returns the total size including the ZBI container header, payload length after padding.
+    pub fn container_size(&self) -> usize {
+        self.get_payload_length_usize() + size_of::<ZbiHeader>()
+    }
+
     /// Immutable iterator over ZBI elements. First element is first ZBI element after
     /// container header. Container header is not available via iterator.
     pub fn iter(&self) -> ZbiContainerIterator<impl ByteSlice + Default + Debug + PartialEq + '_> {
@@ -316,25 +321,56 @@
     ///
     /// # Returns
     ///
-    /// * `Ok(())` - if bootable
+    /// * `Ok(item)` - if bootable, where `item` is the ZBI kernel item.
     /// * Err([`ZbiError::IncompleteKernel`]) - if first element in container has type not bootable
     ///                                         on target platform.
     /// * Err([`ZbiError::Truncated`]) - if container is empty
-    pub fn is_bootable(&self) -> ZbiResult<()> {
+    pub fn is_bootable(
+        &self,
+    ) -> ZbiResult<ZbiItem<impl ByteSlice + Default + Debug + PartialEq + '_>> {
         let hdr = &self.header;
         if hdr.length == 0 {
             return Err(ZbiError::Truncated);
         }
 
         match self.iter().next() {
-            Some(ZbiItem { header, payload: _ }) if header.type_ == ZBI_ARCH_KERNEL_TYPE as u32 => {
-                Ok(())
-            }
+            Some(v) if v.header.type_ == ZBI_ARCH_KERNEL_TYPE as u32 => Ok(v),
             Some(_) => Err(ZbiError::IncompleteKernel),
             None => Err(ZbiError::Truncated),
         }
     }
 
+    /// Returns the ZBI kernel `entry` and `reserved_memory_size` field value if the container is a
+    /// bootable ZBI kernel.
+    ///
+    /// # Returns
+    ///
+    /// * Returns `Ok((entry, reserved_memory_size))` on success.
+    /// * Returns `Err` if container is not a bootable ZBI kernel or is truncated.
+    pub fn get_kernel_entry_and_reserved_memory_size(&self) -> ZbiResult<(u64, u64)> {
+        let kernel = self.is_bootable()?;
+        let vals = Ref::<_, [u64]>::new_slice_from_prefix(kernel.payload, 2)
+            .ok_or(ZbiError::IncompleteKernel)?
+            .0
+            .into_slice();
+        Ok((vals[0], vals[1]))
+    }
+
+    /// Computes the required buffer size needed for relocating this ZBI kernel.
+    ///
+    /// # Returns
+    ///
+    /// * Returns `Ok(size)` on success.
+    /// * Returns `Err` if container is not a valid bootable ZBI kernel.
+    pub fn get_buffer_size_for_kernel_relocation(&self) -> ZbiResult<usize> {
+        let kernel = self.is_bootable()?;
+        let (_, reserve_memory_size) = self.get_kernel_entry_and_reserved_memory_size()?;
+        let kernel_size = 2 * size_of::<ZbiHeader>() + kernel.payload.as_bytes().len();
+        let reserve_memory_size =
+            usize::try_from(reserve_memory_size).map_err(|_| ZbiError::LengthOverflow)?;
+        kernel_size.checked_add(reserve_memory_size).ok_or(ZbiError::LengthOverflow)
+    }
+
     /// Creates `ZbiContainer` from provided buffer.
     ///
     /// Buffer must be aligned to [`ZBI_ALIGNMENT_USIZE`] ([`align_buffer`] could be
@@ -1128,7 +1164,7 @@
 /// ```
 pub type ZbiKernel = zbi_kernel_t;
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Eq)]
 /// Error values that can be returned by function in this library
 pub enum ZbiError {
     /// Generic error
@@ -2056,6 +2092,10 @@
                     })
                     .sum::<usize>()
         );
+        assert_eq!(
+            container.container_size(),
+            container.get_payload_length_usize() + size_of::<ZbiHeader>()
+        );
 
         // Check if container elements match provided items
         let mut it = expected_items.iter();
@@ -2447,6 +2487,41 @@
     }
 
     #[test]
+    fn zbi_test_get_kernel_entry_and_reserved_memory_size() {
+        let mut buffer = ZbiAligned::default();
+        let mut container = ZbiContainer::new(&mut buffer.0[..]).unwrap();
+        let bytes = [1u64.to_le_bytes(), 2u64.to_le_bytes()].concat();
+        container
+            .create_entry_with_payload(ZBI_ARCH_KERNEL_TYPE, 0, ZbiFlags::default(), &bytes)
+            .unwrap();
+        assert_eq!(container.get_kernel_entry_and_reserved_memory_size().unwrap(), (1, 2));
+    }
+
+    #[test]
+    fn zbi_test_get_kernel_entry_and_reserved_memory_size_truncated() {
+        let mut buffer = ZbiAligned::default();
+        let mut container = ZbiContainer::new(&mut buffer.0[..]).unwrap();
+        container
+            .create_entry_with_payload(ZBI_ARCH_KERNEL_TYPE, 0, ZbiFlags::default(), &[])
+            .unwrap();
+        assert!(container.get_kernel_entry_and_reserved_memory_size().is_err());
+    }
+
+    #[test]
+    fn zbi_get_buffer_size_for_kernel_relocation() {
+        let mut buffer = ZbiAligned::default();
+        let mut container = ZbiContainer::new(&mut buffer.0[..]).unwrap();
+        let bytes = [0u64.to_le_bytes(), 1024u64.to_le_bytes()].concat();
+        container
+            .create_entry_with_payload(ZBI_ARCH_KERNEL_TYPE, 0, ZbiFlags::default(), &bytes)
+            .unwrap();
+        assert_eq!(
+            container.get_buffer_size_for_kernel_relocation().unwrap(),
+            container.container_size() + 1024
+        );
+    }
+
+    #[test]
     fn zbi_test_header_alignment() {
         assert_eq!(core::mem::size_of::<ZbiHeader>() & ZBI_ALIGNMENT_USIZE, 0);
     }