Alexandre Courbot
2025-Aug-22 12:47 UTC
[PATCH 5/5] gpu: nova-core: firmware: process and prepare the GSP firmware
The GSP firmware is a binary blob that is verified, loaded, and run by the GSP bootloader. Its presentation is a bit peculiar as the GSP bootloader expects to be given a DMA address to a 3-levels page table mapping the GSP firmware at address 0 of its own address space. Prepare such a structure containing the DMA-mapped firmware as well as the DMA-mapped page tables, and a way to obtain the DMA handle of the level 0 page table. As we are performing the required ELF section parsing and radix3 page table building, remove these items from the TODO file. Signed-off-by: Alexandre Courbot <acourbot at nvidia.com> --- Documentation/gpu/nova/core/todo.rst | 17 ----- drivers/gpu/nova-core/firmware.rs | 108 ++++++++++++++++++++++++++++++- drivers/gpu/nova-core/firmware/gsp.rs | 116 ++++++++++++++++++++++++++++++++++ drivers/gpu/nova-core/gsp.rs | 4 ++ drivers/gpu/nova-core/nova_core.rs | 1 + 5 files changed, 226 insertions(+), 20 deletions(-) diff --git a/Documentation/gpu/nova/core/todo.rst b/Documentation/gpu/nova/core/todo.rst index 89431fec9041b1f35cc55799c91f48dc6bc918eb..0972cb905f7ae64dfbaef4808276757319009e9c 100644 --- a/Documentation/gpu/nova/core/todo.rst +++ b/Documentation/gpu/nova/core/todo.rst @@ -229,23 +229,6 @@ Rust abstraction for debugfs APIs. GPU (general) ============ -Parse firmware headers ----------------------- - -Parse ELF headers from the firmware files loaded from the filesystem. - -| Reference: ELF utils -| Complexity: Beginner -| Contact: Abdiel Janulgue - -Build radix3 page table ------------------------ - -Build the radix3 page table to map the firmware. - -| Complexity: Intermediate -| Contact: Abdiel Janulgue - Initial Devinit support ----------------------- diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs index 7006696bb8e8ec0d7fa3a94fb931d5f0b21fb79d..b97fe53487cab12069961b132ba989a88d3ace81 100644 --- a/drivers/gpu/nova-core/firmware.rs +++ b/drivers/gpu/nova-core/firmware.rs @@ -7,6 +7,7 @@ use core::mem::size_of; use booter::BooterFirmware; +use gsp::GspFirmware; use kernel::device; use kernel::firmware; use kernel::prelude::*; @@ -19,14 +20,98 @@ use crate::falcon::FalconFirmware; use crate::falcon::{sec2::Sec2, Falcon}; use crate::gpu; -use crate::gpu::Chipset; +use crate::gpu::{Architecture, Chipset}; pub(crate) mod booter; pub(crate) mod fwsec; +pub(crate) mod gsp; pub(crate) mod riscv; pub(crate) const FIRMWARE_VERSION: &str = "535.113.01"; +/// Ad-hoc and temporary module to extract sections from ELF images. +/// +/// Some firmware images are currently packaged as ELF files, where sections names are used as keys +/// to specific and related bits of data. Future firmware versions are scheduled to move away from +/// that scheme before nova-core becomes stable, which means this module will eventually be +/// removed. +mod elf { + use kernel::bindings; + use kernel::str::CStr; + use kernel::transmute::FromBytes; + + /// Newtype to provide a [`FromBytes`] implementation. + #[repr(transparent)] + struct Elf64Hdr(bindings::elf64_hdr); + + // SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability. + unsafe impl FromBytes for Elf64Hdr {} + + /// Tries to extract section with name `name` from the ELF64 image `elf`, and returns it. + pub(super) fn elf64_section<'a, 'b>(elf: &'a [u8], name: &'b str) -> Option<&'a [u8]> { + let hdr = &elf + .get(0..size_of::<bindings::elf64_hdr>()) + .and_then(Elf64Hdr::from_bytes)? + .0; + + let shdr_num = usize::from(hdr.e_shnum); + let shdr_start = usize::try_from(hdr.e_shoff).ok()?; + let shdr_end = shdr_num + .checked_mul(size_of::<bindings::elf64_shdr>()) + .and_then(|v| v.checked_add(shdr_start))?; + // Get all the section headers. + let shdr = elf + .get(shdr_start..shdr_end) + .map(|slice| slice.as_ptr()) + .filter(|ptr| ptr.align_offset(align_of::<bindings::elf64_shdr>()) == 0) + // `FromBytes::from_bytes` does not support slices yet, so build it manually. + // + // SAFETY: + // * `get` guarantees that the slice is within the bounds of `elf` and of size + // `elf64_shdr * shdr_num`. + // * We checked that `ptr` had the correct alignment for `elf64_shdr`. + .map(|ptr| unsafe { + core::slice::from_raw_parts(ptr.cast::<bindings::elf64_shdr>(), shdr_num) + })?; + + // Get the strings table. + let strhdr = shdr.get(usize::from(hdr.e_shstrndx))?; + + // Find the section which name matches `name` and return it. + shdr.iter() + .find(|sh| { + let Some(name_idx) = strhdr + .sh_offset + .checked_add(u64::from(sh.sh_name)) + .and_then(|idx| usize::try_from(idx).ok()) + else { + return false; + }; + + // Get the start of the name. + elf.get(name_idx..) + // Stop at the first `0`. + .and_then(|nstr| nstr.get(0..=nstr.iter().position(|b| *b == 0)?)) + // Convert into CStr. This should never fail because of the line above. + .and_then(|nstr| CStr::from_bytes_with_nul(nstr).ok()) + // Convert into str. + .and_then(|c_str| c_str.to_str().ok()) + // Check that the name matches. + .map(|str| str == name) + .unwrap_or(false) + }) + // Return the slice containing the section. + .and_then(|sh| { + let start = usize::try_from(sh.sh_offset).ok()?; + let end = usize::try_from(sh.sh_size) + .ok() + .and_then(|sh_size| start.checked_add(sh_size))?; + + elf.get(start..end) + }) + } +} + /// Structure encapsulating the firmware blobs required for the GPU to operate. #[expect(dead_code)] pub(crate) struct Firmware { @@ -36,7 +121,10 @@ pub(crate) struct Firmware { booter_unloader: BooterFirmware, /// GSP bootloader, verifies the GSP firmware before loading and running it. bootloader: RiscvFirmware, - gsp: firmware::Firmware, + /// GSP firmware. + gsp: GspFirmware, + /// GSP signatures, to be passed as parameter to the bootloader for validation. + gsp_sigs: DmaObject, } impl Firmware { @@ -56,13 +144,27 @@ pub(crate) fn new( .and_then(|path| firmware::Firmware::request(&path, dev)) }; + let gsp_fw = request("gsp")?; + let gsp = elf::elf64_section(gsp_fw.data(), ".fwimage") + .ok_or(EINVAL) + .and_then(|data| GspFirmware::new(dev, data))?; + + let gsp_sigs_section = match chipset.arch() { + Architecture::Ampere => ".fwsignature_ga10x", + _ => return Err(ENOTSUPP), + }; + let gsp_sigs = elf::elf64_section(gsp_fw.data(), gsp_sigs_section) + .ok_or(EINVAL) + .and_then(|data| DmaObject::from_data(dev, data))?; + Ok(Firmware { booter_loader: request("booter_load") .and_then(|fw| BooterFirmware::new(dev, &fw, sec2, bar))?, booter_unloader: request("booter_unload") .and_then(|fw| BooterFirmware::new(dev, &fw, sec2, bar))?, bootloader: request("bootloader").and_then(|fw| RiscvFirmware::new(dev, &fw))?, - gsp: request("gsp")?, + gsp, + gsp_sigs, }) } } diff --git a/drivers/gpu/nova-core/firmware/gsp.rs b/drivers/gpu/nova-core/firmware/gsp.rs new file mode 100644 index 0000000000000000000000000000000000000000..34714156e40c0b41e7d6f67b7abe9d76659b5d18 --- /dev/null +++ b/drivers/gpu/nova-core/firmware/gsp.rs @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-2.0 + +use kernel::device; +use kernel::dma::DataDirection; +use kernel::dma::DmaAddress; +use kernel::prelude::*; +use kernel::scatterlist::Owned; +use kernel::scatterlist::SGTable; + +use crate::dma::DmaObject; +use crate::gsp::GSP_PAGE_SIZE; + +/// A device-mapped firmware with a set of (also device-mapped) pages tables mapping the firmware +/// to the start of their own address space. +pub(crate) struct GspFirmware { + /// The GSP firmware inside a [`VVec`], device-mapped via a SG table. + #[expect(unused)] + fw: Pin<KBox<SGTable<Owned<VVec<u8>>>>>, + /// The level 2 page table, mapping [`Self::fw`] at its beginning. + #[expect(unused)] + lvl2: Pin<KBox<SGTable<Owned<VVec<u8>>>>>, + /// The level 1 page table, mapping [`Self::lvl2`] at its beginning. + #[expect(unused)] + lvl1: Pin<KBox<SGTable<Owned<VVec<u8>>>>>, + /// The level 0 page table, mapping [`Self::lvl1`] at its beginning. + lvl0: DmaObject, + /// Size in bytes of the firmware contained in [`Self::fw`]. + #[expect(unused)] + pub size: usize, +} + +impl GspFirmware { + pub(crate) fn new(dev: &device::Device<device::Bound>, fw: &[u8]) -> Result<Self> { + // Move the firmware into a vmalloc'd vector and map it into the device address space. + let fw_sg_table = VVec::with_capacity(fw.len(), GFP_KERNEL) + .and_then(|mut v| { + v.extend_from_slice(fw, GFP_KERNEL)?; + Ok(v) + }) + .map_err(|_| ENOMEM) + .and_then(|v| { + KBox::pin_init( + SGTable::new(dev, v, DataDirection::ToDevice, GFP_KERNEL), + GFP_KERNEL, + ) + })?; + + // Allocate the level 2 page table, map the firmware onto it, and map it into the device + // address space. + let lvl2_sg_table = VVec::<u8>::with_capacity( + fw_sg_table.into_iter().count() * core::mem::size_of::<u64>(), + GFP_KERNEL, + ) + .map_err(|_| ENOMEM) + .and_then(|lvl2| map_into_lvl(&fw_sg_table, lvl2)) + .and_then(|lvl2| { + KBox::pin_init( + SGTable::new(dev, lvl2, DataDirection::ToDevice, GFP_KERNEL), + GFP_KERNEL, + ) + })?; + + // Allocate the level 1 page table, map the level 2 page table onto it, and map it into the + // device address space. + let lvl1_sg_table = VVec::<u8>::with_capacity( + lvl2_sg_table.into_iter().count() * core::mem::size_of::<u64>(), + GFP_KERNEL, + ) + .map_err(|_| ENOMEM) + .and_then(|lvl1| map_into_lvl(&lvl2_sg_table, lvl1)) + .and_then(|lvl1| { + KBox::pin_init( + SGTable::new(dev, lvl1, DataDirection::ToDevice, GFP_KERNEL), + GFP_KERNEL, + ) + })?; + + // Allocate the level 0 page table as a device-visible DMA object, and map the level 1 page + // table onto it. + let mut lvl0 = DmaObject::new(dev, GSP_PAGE_SIZE)?; + // SAFETY: we are the only owner of this newly-created object, making races impossible. + let lvl0_slice = unsafe { lvl0.as_slice_mut(0, GSP_PAGE_SIZE) }?; + lvl0_slice[0..core::mem::size_of::<u64>()].copy_from_slice( + &(lvl1_sg_table.into_iter().next().unwrap().dma_address() as u64).to_le_bytes(), + ); + + Ok(Self { + fw: fw_sg_table, + lvl2: lvl2_sg_table, + lvl1: lvl1_sg_table, + lvl0, + size: fw.len(), + }) + } + + #[expect(unused)] + /// Returns the DMA handle of the level 0 page table. + pub(crate) fn lvl0_dma_handle(&self) -> DmaAddress { + self.lvl0.dma_handle() + } +} + +/// Create a linear mapping the device mapping of the buffer described by `sg_table` into `dst`. +fn map_into_lvl(sg_table: &SGTable<Owned<VVec<u8>>>, mut dst: VVec<u8>) -> Result<VVec<u8>> { + for sg_entry in sg_table.into_iter() { + // Number of pages we need to map. + let num_pages = (sg_entry.dma_len() as usize).div_ceil(GSP_PAGE_SIZE); + + for i in 0..num_pages { + let entry = sg_entry.dma_address() + (i as u64 * GSP_PAGE_SIZE as u64); + dst.extend_from_slice(&entry.to_le_bytes(), GFP_KERNEL)?; + } + } + + Ok(dst) +} diff --git a/drivers/gpu/nova-core/gsp.rs b/drivers/gpu/nova-core/gsp.rs new file mode 100644 index 0000000000000000000000000000000000000000..a0e7ec5f6c9c959d57540b3ebf4b782f2e002b08 --- /dev/null +++ b/drivers/gpu/nova-core/gsp.rs @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: GPL-2.0 + +pub(crate) const GSP_PAGE_SHIFT: usize = 12; +pub(crate) const GSP_PAGE_SIZE: usize = 1 << GSP_PAGE_SHIFT; diff --git a/drivers/gpu/nova-core/nova_core.rs b/drivers/gpu/nova-core/nova_core.rs index cb2bbb30cba142265b354c9acf70349a6e40759e..fffcaee2249fe6cd7f55a7291c1e44be42e791d9 100644 --- a/drivers/gpu/nova-core/nova_core.rs +++ b/drivers/gpu/nova-core/nova_core.rs @@ -9,6 +9,7 @@ mod firmware; mod gfw; mod gpu; +mod gsp; mod regs; mod util; mod vbios; -- 2.50.1
Danilo Krummrich
2025-Aug-22 12:57 UTC
[PATCH 5/5] gpu: nova-core: firmware: process and prepare the GSP firmware
Hi Alex, not a full review yet, but a few ad-hoc comments from skimming over it. On Fri Aug 22, 2025 at 2:47 PM CEST, Alexandre Courbot wrote:> +/// A device-mapped firmware with a set of (also device-mapped) pages tables mapping the firmware > +/// to the start of their own address space. > +pub(crate) struct GspFirmware { > + /// The GSP firmware inside a [`VVec`], device-mapped via a SG table. > + #[expect(unused)]Do we expect this to change? Otherwise, just prefix the field name with an underscore.> + fw: Pin<KBox<SGTable<Owned<VVec<u8>>>>>, > + /// The level 2 page table, mapping [`Self::fw`] at its beginning. > + #[expect(unused)] > + lvl2: Pin<KBox<SGTable<Owned<VVec<u8>>>>>, > + /// The level 1 page table, mapping [`Self::lvl2`] at its beginning. > + #[expect(unused)] > + lvl1: Pin<KBox<SGTable<Owned<VVec<u8>>>>>,Instead of creating three allocations, just make struct GspFirmware pin_data by itself. This should even propagate down to struct Gpu, which is pin_data. So everything can be in one single allocation.> + /// The level 0 page table, mapping [`Self::lvl1`] at its beginning. > + lvl0: DmaObject, > + /// Size in bytes of the firmware contained in [`Self::fw`]. > + #[expect(unused)] > + pub size: usize, > +}