* [PATCH v6 1/8] rust: alloc: add Vec::zeroed method
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
2026-07-29 22:55 ` [PATCH v6 2/8] rust: firmware: add request_into_buf() Timur Tabi
` (6 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
Add a constructor for kernel Vec that allocates a vector of a given length
with all elements zero-initialized. Memory is allocated with the __GFP_ZERO
flag, matching the existing KBox::zeroed() pattern.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
Reviewed-by: Alexandre Courbot <acourbot@nvidia.com>
---
rust/kernel/alloc/kvec.rs | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/rust/kernel/alloc/kvec.rs b/rust/kernel/alloc/kvec.rs
index f7af62835aa8..c7546b9da4fa 100644
--- a/rust/kernel/alloc/kvec.rs
+++ b/rust/kernel/alloc/kvec.rs
@@ -9,6 +9,7 @@
Vmalloc,
VmallocPageIter, //
},
+ flags::__GFP_ZERO,
layout::ArrayLayout,
AllocError,
Allocator,
@@ -51,6 +52,8 @@
}, //
};
+use pin_init::Zeroable;
+
mod errors;
pub use self::errors::{InsertError, PushError, RemoveError};
@@ -532,6 +535,30 @@ pub fn with_capacity(capacity: usize, flags: Flags) -> Result<Self, AllocError>
Ok(v)
}
+ /// Creates a new [`Vec`] with `n` zero-initialized elements.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// let v = KVec::<u32>::zeroed(20, GFP_KERNEL)?;
+ ///
+ /// assert!(v.iter().all(|&x| x == 0));
+ /// # Ok::<(), Error>(())
+ /// ```
+ pub fn zeroed(n: usize, flags: Flags) -> Result<Self, AllocError>
+ where
+ T: Zeroable,
+ {
+ let mut v = Self::with_capacity(n, flags | __GFP_ZERO)?;
+
+ // SAFETY:
+ // - `n <= capacity - len`: `with_capacity(n)` guarantees capacity >= n, len is 0.
+ // - All elements in `[0, n)` are initialized: `__GFP_ZERO` zeroes the allocation,
+ // and `T: Zeroable` guarantees all-zeroes is a valid bit pattern.
+ unsafe { v.inc_len(n) };
+ Ok(v)
+ }
+
/// Creates a `Vec<T, A>` from a pointer, a length and a capacity using the allocator `A`.
///
/// # Examples
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v6 2/8] rust: firmware: add request_into_buf()
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
2026-07-29 22:55 ` [PATCH v6 1/8] rust: alloc: add Vec::zeroed method Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
2026-07-29 22:55 ` [PATCH v6 3/8] gpu: nova-core: add TLV parser for firmware files Timur Tabi
` (5 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
Add request_into_buf(), a Rust wrapper around the
request_firmware_into_buf() function. This variant loads the firmware
image directly into a caller-provided buffer rather than a
kernel-allocated one.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
Reviewed-by: Alexandre Courbot <acourbot@nvidia.com>
---
rust/kernel/firmware.rs | 48 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 46 insertions(+), 2 deletions(-)
diff --git a/rust/kernel/firmware.rs b/rust/kernel/firmware.rs
index 71168d8004e2..5b336bd1f09c 100644
--- a/rust/kernel/firmware.rs
+++ b/rust/kernel/firmware.rs
@@ -7,9 +7,9 @@
use crate::{
bindings,
device::Device,
- error::Error,
- error::Result,
+ error::to_result,
ffi,
+ prelude::*,
str::{CStr, CStrExt as _},
};
use core::ptr::NonNull;
@@ -120,6 +120,50 @@ fn drop(&mut self) {
}
}
+/// Load firmware directly into the caller-provided `buf`.
+///
+/// On success the firmware image has been copied into `buf`; the caller accesses the data
+/// through `buf` itself.
+///
+/// This is intentionally a stand-alone function rather than a `Firmware` constructor. For
+/// the `into_buf` path, the firmware data lives in the caller's `buf`, not in a
+/// kernel-owned buffer, so returning a `Firmware` would expose `Firmware::data()` as a
+/// second handle aliasing `buf` (and `release_firmware()` does not free `buf` anyway).
+pub fn request_into_buf(name: &CStr, dev: &Device, buf: &mut [u8]) -> Result {
+ // `as_mut_ptr()` on an empty slice returns a non-NULL pointer to
+ // memory which the loader does not own. Passing that pointer with `size == 0`
+ // makes the loader believe that it is buffer it allocated itself, so when
+ // `release_firmware()` is called, it will vfree the pointer and trigger a
+ // bug. Reject empty slices to avoid this situation.
+ if buf.is_empty() {
+ return Err(EINVAL);
+ }
+
+ let mut fw: *mut bindings::firmware = core::ptr::null_mut();
+ let pfw: *mut *mut bindings::firmware = &mut fw;
+ let pfw: *mut *const bindings::firmware = pfw.cast();
+
+ // SAFETY: `pfw` is a valid pointer to a NULL initialized `bindings::firmware` pointer.
+ // `name` and `dev` are valid as by their type invariants. `buf` is a valid writable
+ // buffer of `buf.len()` bytes.
+ to_result(unsafe {
+ bindings::request_firmware_into_buf(
+ pfw,
+ name.as_char_ptr(),
+ dev.as_raw(),
+ buf.as_mut_ptr().cast(),
+ buf.len(),
+ )
+ })?;
+
+ // The firmware bytes are now in `buf`, which the caller owns, so we don't need
+ // the kernel to hang on to it any more.
+ // SAFETY: `fw` is a valid pointer returned by `request_firmware_into_buf`.
+ unsafe { bindings::release_firmware(fw) };
+
+ Ok(())
+}
+
// SAFETY: `Firmware` only holds a pointer to a C `struct firmware`, which is safe to be used from
// any thread.
unsafe impl Send for Firmware {}
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v6 3/8] gpu: nova-core: add TLV parser for firmware files
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
2026-07-29 22:55 ` [PATCH v6 1/8] rust: alloc: add Vec::zeroed method Timur Tabi
2026-07-29 22:55 ` [PATCH v6 2/8] rust: firmware: add request_into_buf() Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
2026-07-29 22:55 ` [PATCH v6 4/8] gpu: nova-core: transition booter_load to TLV images Timur Tabi
` (4 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
TLV (type, length, value) files are the new image format used by Nova
to encapsulate firmware images and their metadata. Unlike the firmware
files for previous versions of the firmware, TLV filenames are not
versioned, and they have a .tlv suffix.
Add function request_tlv() to load TLV firmware images.
Add the Tlv struct and supporting types for parsing TLV (type, length,
value) firmware images. TLV files begin with a 4-byte magic header,
which must be "NVFW" for Nvidia firmware files. This is followed by a
sequence of blocks each containing a 4-byte ASCII tag, a 4-byte
little-endian length, and a payload padded to a 4-byte boundary.
Tlv::new() validates the entire image up front, so that the iterator can
subsequently yield blocks without fallible parsing.
Also add accessor methods for the various encoded types that will be used
by the driver.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
---
Documentation/gpu/nova/core/tlv.rst | 184 ++++++++++++++++++
Documentation/gpu/nova/index.rst | 1 +
drivers/gpu/nova-core/firmware.rs | 1 +
drivers/gpu/nova-core/firmware/tlv.rs | 267 ++++++++++++++++++++++++++
4 files changed, 453 insertions(+)
create mode 100644 Documentation/gpu/nova/core/tlv.rst
create mode 100644 drivers/gpu/nova-core/firmware/tlv.rs
diff --git a/Documentation/gpu/nova/core/tlv.rst b/Documentation/gpu/nova/core/tlv.rst
new file mode 100644
index 000000000000..2ddcff2744a7
--- /dev/null
+++ b/Documentation/gpu/nova/core/tlv.rst
@@ -0,0 +1,184 @@
+.. SPDX-License-Identifier: (GPL-2.0+ OR MIT)
+
+==================================
+TLV Tags in Nova Firmware Images
+==================================
+
+Nova firmware images use a Type-Length-Value (TLV) format to encapsulate
+firmware components and metadata. The TLV file begins with a 4-byte "magic"
+header that contains the string "NVFW". Following the header is a sequence of
+TLV blocks.
+
+Each block consists of a 4-byte tag of ASCII characters, a 4-byte length
+encoded as a little-endian unsigned integer, and a sequence of bytes, the size
+of which is equal to the length rounded up to the next multiple of 4.
+
+The driver code that reads the TLV and uses its contents is called the parser.
+It is the responsibility of the parser to handle missing or malformed tags,
+lengths, and values in the TLV.
+
+::
+
+ +------+------+------+------+
+ | 'N' | 'V' | 'F' | 'W' | Magic header
+ +------+------+------+------+
+ | Tag (4 bytes, ASCII) | TLV block 0
+ +---------------------------+
+ | Length (4 bytes, LE) |
+ +---------------------------+
+ | |
+ | Value (length bytes, |
+ | padded to 4-byte align) |
+ | |
+ +---------------------------+
+ | Tag (4 bytes, ASCII) | TLV block 1
+ +---------------------------+
+ | Length (4 bytes, LE) |
+ +---------------------------+
+ | |
+ | Value (length bytes, |
+ | padded to 4-byte align) |
+ | |
+ +---------------------------+
+ | ... | More TLV blocks
+ +---------------------------+
+
+Tags and Length
+===============
+TLV tags are always four-character words, with all letters being upper case.
+Duplicate tags are not allowed.
+
+A TLV file may contain additional tags not described in this document.
+
+Values
+======
+Values are one of three types. The type is not encoded in the format; rather,
+the parser expects a given tag to have a value of a given type.
+
+1) Integers, encoded in 32-bit or 64-bit little-endian format.
+2) Strings, encoded as-is and required to be only printable ASCII characters
+ and without a null terminator.
+3) An array of bytes, for binary data.
+4) Boolean, encoded as single byte, with a value of 0 for False or 1 for True.
+
+Common Tags
+===========
+These tags are shared across firmware types and carry the same meaning
+wherever they appear. Unlike the firmware-specific tags below, a common tag
+is reserved: its meaning is fixed and may never be redefined for a particular
+firmware type.
+
+``VERS`` (string)
+ Human-readable firmware version string. Present in all TLV files.
+
+A TLV image must contain either a single ``BLOB`` tag (firmware embedded
+inline) or a ``SIZE``/``FILE`` pair (firmware stored in a separate file).
+
+``BLOB`` (bytes)
+ If the firmware microcode binary is stored in the TLV, this tag contains
+ the actual firmware image bytes.
+
+``FILE`` (string)
+ If the firmware binary is stored as a separate file, this tag contains the
+ name of that file, which is required to be in the same directory as the TLV,
+ so no paths are allowed in the filename. This tag is always paired with
+ ``SIZE``, so as to allow the driver to pre-allocate the buffer before
+ loading the file.
+
+``SIZE`` (u32)
+ Total size in bytes of the firmware image to be loaded from the companion
+ file named by ``FILE``. This tag is mandatory if ``FILE`` exists, so the
+ size of the firmware image must be known when the TLV is created. If the
+ firmware image is updated and its size changes, then the TLV must be
+ updated with it.
+
+GSP Firmware Tags
+=================
+``SIGN`` (bytes)
+ Cryptographic signature for the GSP firmware.
+
+``BLID`` (string)
+ The build ID, extracted from the ".note.gnu.build-id" section.
+
+Booter Firmware Tags
+====================
+``DAOF`` (u32) - ``os_data_offset``
+ OS data section offset within the firmware image (absolute byte offset).
+ Maps to the DMEM load source.
+
+``DASZ`` (u32) - ``os_data_size``
+ OS data section size in bytes.
+
+``CDOF`` (u32) - ``os_code_offset``
+ OS code section offset within the firmware image (absolute byte offset).
+ Maps to the non-secure IMEM load source.
+
+``CDSZ`` (u32) - ``os_code_size``
+ OS code section size in bytes.
+
+``PLOC`` (u32) - ``patch_loc``
+ Signature patch location -- byte offset within the firmware image where the
+ selected signature should be written.
+
+``FUSE`` (u32) - ``fuse_version``
+ Fuse version of the firmware, used with the hardware fuse register to
+ select the correct signature index.
+
+``ENID`` (u32) - ``engine_id``
+ Engine ID mask identifying the falcon engine this firmware targets.
+
+``UCID`` (u32) - ``ucode_id``
+ Microcode ID used together with the engine ID to query hardware signature
+ fuse registers.
+
+``A0CO`` (u32) - ``app0_code_offset``
+ App0 code offset -- start of the secure code region within the firmware
+ image. Used as the IMEM secure section source.
+
+``A0CS`` (u32) - ``app0_code_size``
+ App0 code size in bytes.
+
+``NSIG`` (u32) - ``num_sigs``
+ Number of signatures included in the ``SIGN`` tag.
+
+``SIGN`` (bytes)
+ Concatenated array of firmware signatures. The size of each signature is
+ the total length of the ``SIGN`` value divided by ``NSIG``. The correct
+ signature is selected using the fuse-version-derived index.
+
+Generic Bootloader Tags
+=======================
+``CDSZ`` (u32) - ``code_size``
+ Size in bytes of the bootloader code to copy from the ``BLOB`` tag and
+ PIO-load into falcon IMEM.
+
+``STRT`` (u32) - ``start_tag``
+ Start tag identifying the IMEM block where execution begins. The falcon
+ boot address is derived as ``start_tag << 8``.
+
+GSP Bootloader Tags
+===================
+``CDOF`` (u32) - ``code_offset``
+ Offset within the firmware image at which the code section starts.
+
+``DAOF`` (u32) - ``data_offset``
+ Offset within the firmware image at which the data section starts.
+
+``MFOF`` (u32) - ``manifest_offset``
+ Offset within the firmware image at which the manifest starts.
+
+``APPV`` (u32) - ``app_version``
+ Application version of the firmware.
+
+FMC Firmware Tags
+=================
+``HASH`` (bytes)
+ SHA-384 hash of the FMC firmware, exactly 48 bytes long.
+
+``PKEY`` (bytes)
+ Public key used to verify the FMC firmware. At most 384 bytes (RSA-3072),
+ but may be shorter.
+
+``SIGN`` (bytes)
+ Signature of the FMC firmware. At most 384 bytes (RSA-3072), but may
+ be shorter.
diff --git a/Documentation/gpu/nova/index.rst b/Documentation/gpu/nova/index.rst
index 1783513cbd05..2afa58e8f08d 100644
--- a/Documentation/gpu/nova/index.rst
+++ b/Documentation/gpu/nova/index.rst
@@ -33,3 +33,4 @@ vGPU manager VFIO driver and the nova-drm driver.
core/fsp
core/fwsec
core/falcon
+ core/tlv
diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs
index 20eff987c5d6..2075b68b364a 100644
--- a/drivers/gpu/nova-core/firmware.rs
+++ b/drivers/gpu/nova-core/firmware.rs
@@ -33,6 +33,7 @@
pub(crate) mod fwsec;
pub(crate) mod gsp;
pub(crate) mod riscv;
+pub(crate) mod tlv;
pub(crate) const FIRMWARE_VERSION: &str = "570.144";
diff --git a/drivers/gpu/nova-core/firmware/tlv.rs b/drivers/gpu/nova-core/firmware/tlv.rs
new file mode 100644
index 000000000000..99b911cbf39f
--- /dev/null
+++ b/drivers/gpu/nova-core/firmware/tlv.rs
@@ -0,0 +1,267 @@
+// SPDX-License-Identifier: GPL-2.0
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+
+use kernel::{
+ device,
+ firmware,
+ prelude::*,
+ str::CString, //
+};
+
+use crate::{
+ gpu,
+ num::*, //
+};
+
+/// Requests the GPU firmware TLV `name` suitable for `chipset`.
+#[expect(dead_code)]
+pub(crate) fn request_tlv(
+ dev: &device::Device,
+ chipset: gpu::Chipset,
+ name: &str,
+) -> Result<firmware::Firmware> {
+ let chip_name = chipset.name();
+
+ let filename = CString::try_from_fmt(fmt!("nvidia/{chip_name}/gsp/{name}.tlv"))?;
+
+ dev_dbg!(dev, "loading firmware image {:?}\n", &filename);
+
+ firmware::Firmware::request(&filename, dev)
+}
+
+struct TlvBlock<'a> {
+ tag: [u8; 4],
+ value: &'a [u8],
+}
+
+/// On-wire TLV block header: 4-byte ASCII tag + little-endian payload length (bytes, excluding
+/// padding to a 4-byte boundary).
+struct TlvBlockHeader {
+ tag: [u8; 4],
+ length: usize,
+}
+
+impl TlvBlockHeader {
+ const SIZE: usize = size_of::<[u8; 4]>() + size_of::<u32>();
+
+ /// Parses the first [`Self::SIZE`] bytes of `hdr` (caller may pass a longer slice).
+ fn parse(hdr: &[u8]) -> Option<Self> {
+ let hdr = hdr.get(..Self::SIZE)?;
+ let tag = <[u8; 4]>::try_from(hdr.get(..4)?).ok()?;
+ if !tag.is_ascii() {
+ return None;
+ }
+ let len_arr = <[u8; 4]>::try_from(hdr.get(4..Self::SIZE)?).ok()?;
+ let length = u32_as_usize(u32::from_le_bytes(len_arr));
+ Some(Self { tag, length })
+ }
+}
+
+/// Iterator over the [`TlvBlock`]s of a [`Tlv`].
+///
+/// # Invariants
+///
+/// `pos` is a byte offset into `tlv.data` that always lies on a block boundary (in the sense
+/// of the [`Tlv`] invariant): it is either the start of a well-formed block, or equal to
+/// `tlv.data.len()` (end of iteration).
+struct TlvIter<'tlv, 'a> {
+ tlv: &'tlv Tlv<'a>,
+ pos: usize,
+}
+
+impl<'tlv, 'a> Iterator for TlvIter<'tlv, 'a> {
+ type Item = TlvBlock<'a>;
+
+ /// Returns the block starting at `self.pos` and advances the cursor past it, or [`None`]
+ /// once the cursor reaches the end of the data or encounters an error.
+ ///
+ /// Note that errors cannot actually occur because the data is validated in the constructor.
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.pos >= self.tlv.data.len() {
+ return None;
+ }
+
+ let tail = self.tlv.data.get(self.pos..)?;
+
+ let hdr = tail.get(..TlvBlockHeader::SIZE)?;
+ let header = TlvBlockHeader::parse(hdr)?;
+
+ let stored_size = header.length.checked_next_multiple_of(4)?;
+ let advance = TlvBlockHeader::SIZE.checked_add(stored_size)?;
+ let payload_end = TlvBlockHeader::SIZE.checked_add(header.length)?;
+
+ let value = tail
+ .get(..advance)?
+ .get(TlvBlockHeader::SIZE..payload_end)?;
+
+ // INVARIANT: by the `Tlv` invariant the block at `self.pos` occupies exactly `advance`
+ // bytes, so `self.pos + advance` is the next block boundary (or `data.len()`).
+ self.pos = self.pos.checked_add(advance)?;
+
+ Some(TlvBlock {
+ tag: header.tag,
+ value,
+ })
+ }
+}
+
+/// The post-header part of a validated TLV (type, length, value) firmware image.
+///
+/// TLV firmware images start with a 4-byte "NVFW" magic header, followed by a sequence of
+/// blocks. Each block has a 4-byte type tag, a 4-byte length field, and a data payload
+/// (value) whose stored size is the length rounded up to the nearest multiple of 4.
+///
+/// [`Self::new`] checks the magic header and walks every block: tags must be ASCII,
+/// lengths and padding must fit without overflow, and the byte stream after `NVFW` must
+/// be exactly partitionable into blocks (no trailing partial header or slack). After
+/// that, [`TlvIter`] only signals end-of-stream via [`None`], not parse failure.
+///
+/// Although the spec forbids duplicate tags, neither the constructor nor the iterator
+/// enforces this restriction. Instead, duplicate tags are simply ignored.
+///
+/// # Invariants
+///
+/// `data` is a validated TLV payload (the bytes *after* the `NVFW` magic): it is the exact
+/// concatenation of zero or more well-formed blocks, with no trailing partial header or slack.
+/// Consequently, any offset `o` into `data` that is a block boundary and satisfies
+/// `o < data.len()` is the start of a complete block whose header parses and whose stored
+/// extent (`TlvBlockHeader::SIZE + header.length.next_multiple_of(4)` bytes) lies within
+/// `data`. `data.len()` is itself a boundary.
+pub(crate) struct Tlv<'a> {
+ data: &'a [u8],
+}
+
+#[expect(dead_code)]
+impl<'a> Tlv<'a> {
+ const MAGIC: &'static [u8; 4] = b"NVFW";
+
+ /// Parses `data` as a TLV firmware image, returning [`EINVAL`] if the image is malformed.
+ pub(crate) fn new(data: &'a [u8]) -> Result<Self> {
+ // Verify that the magic bytes exist and are the correct value
+ let magic_len = Self::MAGIC.len();
+ if data
+ .get(..magic_len)
+ .is_none_or(|magic| magic != Self::MAGIC)
+ {
+ return Err(EINVAL);
+ }
+
+ // The payload is the contiguous sequence of TLV blocks after the magic.
+ let payload = data.get(magic_len..).ok_or(EINVAL)?;
+
+ if payload.is_empty() {
+ // Reject empty TLV files
+ return Err(ENODATA);
+ }
+
+ // The spec says every TLV must have a VERS tag.
+ let mut has_vers = false;
+
+ let mut rest = payload;
+ while !rest.is_empty() {
+ // Validate and extract the header (type, length).
+ let Some(header): Option<TlvBlockHeader> = rest
+ .get(..TlvBlockHeader::SIZE)
+ .and_then(TlvBlockHeader::parse)
+ else {
+ return Err(EINVAL);
+ };
+
+ has_vers |= header.tag == *b"VERS";
+
+ // The `length` field of a TLV block contains the actual byte length of the
+ // value, but each TLV block is aligned to a 4-byte boundary.
+ let Some(stored_size) = header.length.checked_next_multiple_of(4) else {
+ return Err(EINVAL);
+ };
+
+ let length = TlvBlockHeader::SIZE
+ .checked_add(stored_size)
+ .ok_or(EINVAL)?;
+
+ rest = rest.split_at_checked(length).ok_or(EINVAL)?.1;
+ }
+
+ if !has_vers {
+ return Err(EINVAL);
+ }
+
+ // INVARIANT: the loop above walked `payload` block-by-block. For each block, the
+ // header is parsed (`TlvBlockHeader::parse` rejects non-ASCII tags), and the
+ // stored extent (`SIZE + length.next_multiple_of(4)`) is computed without
+ // overflow and split off `rest` only when it fits. The loop ends only when `rest`
+ // is empty, so the byte stream is an exact concatenation of blocks with no
+ // trailing partial header or slack.
+ Ok(Self { data: payload })
+ }
+
+ fn iter(&self) -> TlvIter<'_, 'a> {
+ // INVARIANT: 0 is a block boundary, either the start of the first block,
+ // or `data.len()` when `data` is empty.
+ TlvIter { tlv: self, pos: 0 }
+ }
+
+ fn find(&self, tag: &[u8; 4]) -> Result<TlvBlock<'a>> {
+ self.iter().find(|b| b.tag == *tag).ok_or(EINVAL)
+ }
+
+ /// Return a slice of bytes. Returns ENODATA if the value is empty.
+ pub(crate) fn get_bytes(&self, tag: &[u8; 4]) -> Result<&'a [u8]> {
+ let tlv = self.find(tag)?;
+
+ // Treat empty value as an error, to avoid trying to parse nothing.
+ if tlv.value.is_empty() {
+ return Err(ENODATA);
+ }
+
+ Ok(tlv.value)
+ }
+
+ /// Return a little-endian u32.
+ pub(crate) fn get_u32(&self, tag: &[u8; 4]) -> Result<u32> {
+ let tlv = self.find(tag)?;
+
+ tlv.value
+ .try_into()
+ .ok()
+ .map(u32::from_le_bytes)
+ .ok_or(EINVAL)
+ }
+
+ /// Return a string value.
+ pub(crate) fn get_string(&self, tag: &[u8; 4]) -> Result<&'a str> {
+ let tlv = self.find(tag)?;
+
+ let bytes = tlv.value;
+
+ // Strings can only contain printable ASCII characters.
+ if bytes.iter().any(|&b| !(32..127).contains(&b)) {
+ return Err(EINVAL);
+ }
+
+ core::str::from_utf8(bytes).map_err(|_| EINVAL)
+ }
+
+ /// Obtain the nth signature from a SIGN tag. If `index` is None,
+ /// then return the last signature.
+ pub(crate) fn get_signature(&self, index: Option<usize>) -> Result<&'a [u8]> {
+ let num_sigs: usize = match self.get_u32(b"NSIG")? {
+ 0 => return Err(EINVAL),
+ n => n.into_safe_cast(),
+ };
+
+ let sig_bytes = self.get_bytes(b"SIGN")?;
+
+ // Ensure that sig_bytes can be divided evenly into chunks.
+ if sig_bytes.len() % num_sigs != 0 {
+ return Err(EINVAL);
+ }
+
+ // num_sigs cannot be 0, and sig_bytes cannot be empty, so this cannot panic.
+ let sig_size = sig_bytes.len() / num_sigs;
+
+ let index = index.unwrap_or(num_sigs - 1);
+
+ sig_bytes.chunks_exact(sig_size).nth(index).ok_or(EINVAL)
+ }
+}
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v6 4/8] gpu: nova-core: transition booter_load to TLV images
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
` (2 preceding siblings ...)
2026-07-29 22:55 ` [PATCH v6 3/8] gpu: nova-core: add TLV parser for firmware files Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
2026-07-29 22:55 ` [PATCH v6 5/8] gpu: nova-core: transition gsp " Timur Tabi
` (3 subsequent siblings)
7 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
Switch the booter firmware loader from the legacy binary format to the
TLV format. This change requires the new TLV versions of the r570.144
firmware images.
The new TLV format has all of the metadata needed by Nova encoded as
separate tags, eliminating the need to parse legacy firmware headers
such as HsHeaderV2 and HsSignatureParams. All of the structs and
code for parsing those headers is therefore deleted.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
---
drivers/gpu/nova-core/firmware.rs | 15 +-
drivers/gpu/nova-core/firmware/booter.rs | 331 ++++-------------------
drivers/gpu/nova-core/firmware/tlv.rs | 2 -
drivers/gpu/nova-core/gsp/hal/tu102.rs | 13 +-
4 files changed, 63 insertions(+), 298 deletions(-)
diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs
index 2075b68b364a..bd47ebbb013e 100644
--- a/drivers/gpu/nova-core/firmware.rs
+++ b/drivers/gpu/nova-core/firmware.rs
@@ -22,10 +22,7 @@
},
gpu,
gsp::boot_firmware_files,
- num::{
- FromSafeCast,
- IntoSafeCast, //
- },
+ num::IntoSafeCast, //
};
pub(crate) mod booter;
@@ -389,16 +386,6 @@ fn new(fw: &'a firmware::Firmware) -> Result<Self> {
.map(|hdr| Self { hdr, fw })
.ok_or(EINVAL)
}
-
- /// Returns the data payload of the firmware, or `None` if the data range is out of bounds of
- /// the firmware image.
- fn data(&self) -> Option<&[u8]> {
- let fw_start = usize::from_safe_cast(self.hdr.data_offset);
- let fw_size = usize::from_safe_cast(self.hdr.data_size);
- let fw_end = fw_start.checked_add(fw_size)?;
-
- self.fw.get(fw_start..fw_end)
- }
}
pub(crate) struct ModInfoBuilder<const N: usize>(firmware::ModInfoBuilder<N>);
diff --git a/drivers/gpu/nova-core/firmware/booter.rs b/drivers/gpu/nova-core/firmware/booter.rs
index acb7f4d8a532..e523667900be 100644
--- a/drivers/gpu/nova-core/firmware/booter.rs
+++ b/drivers/gpu/nova-core/firmware/booter.rs
@@ -10,8 +10,7 @@
use kernel::{
device,
dma::Coherent,
- prelude::*,
- transmute::FromBytes, //
+ prelude::*, //
};
use crate::{
@@ -24,224 +23,19 @@
FalconFirmware, //
},
firmware::{
- BinFirmware,
+ tlv::{
+ request_tlv, //
+ Tlv,
+ },
FirmwareObject,
FirmwareSignature,
Signed,
Unsigned, //
},
gpu::Chipset,
- num::{
- FromSafeCast,
- IntoSafeCast, //
- },
+ num::IntoSafeCast,
};
-/// Local convenience function to return a copy of `S` by reinterpreting the bytes starting at
-/// `offset` in `slice`.
-fn frombytes_at<S: FromBytes + Sized>(slice: &[u8], offset: usize) -> Result<S> {
- let end = offset.checked_add(size_of::<S>()).ok_or(EINVAL)?;
- slice
- .get(offset..end)
- .and_then(S::from_bytes_copy)
- .ok_or(EINVAL)
-}
-
-/// Heavy-Secured firmware header.
-///
-/// Such firmwares have an application-specific payload that needs to be patched with a given
-/// signature.
-#[repr(C)]
-#[derive(Debug, Clone)]
-struct HsHeaderV2 {
- /// Offset to the start of the signatures.
- sig_prod_offset: u32,
- /// Size in bytes of the signatures.
- sig_prod_size: u32,
- /// Offset to a `u32` containing the location at which to patch the signature in the microcode
- /// image.
- patch_loc_offset: u32,
- /// Offset to a `u32` containing the index of the signature to patch.
- patch_sig_offset: u32,
- /// Start offset to the signature metadata.
- meta_data_offset: u32,
- /// Size in bytes of the signature metadata.
- meta_data_size: u32,
- /// Offset to a `u32` containing the number of signatures in the signatures section.
- num_sig_offset: u32,
- /// Offset of the application-specific header.
- header_offset: u32,
- /// Size in bytes of the application-specific header.
- header_size: u32,
-}
-
-// SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
-unsafe impl FromBytes for HsHeaderV2 {}
-
-/// Heavy-Secured Firmware image container.
-///
-/// This provides convenient access to the fields of [`HsHeaderV2`] that are actually indices to
-/// read from in the firmware data.
-struct HsFirmwareV2<'a> {
- hdr: HsHeaderV2,
- fw: &'a [u8],
-}
-
-impl<'a> HsFirmwareV2<'a> {
- /// Interprets the header of `bin_fw` as a [`HsHeaderV2`] and returns an instance of
- /// `HsFirmwareV2` for further parsing.
- ///
- /// Fails if the header pointed at by `bin_fw` is not within the bounds of the firmware image.
- fn new(bin_fw: &BinFirmware<'a>) -> Result<Self> {
- frombytes_at::<HsHeaderV2>(bin_fw.fw, bin_fw.hdr.header_offset.into_safe_cast())
- .map(|hdr| Self { hdr, fw: bin_fw.fw })
- }
-
- /// Returns the location at which the signatures should be patched in the microcode image.
- ///
- /// Fails if the offset of the patch location is outside the bounds of the firmware
- /// image.
- fn patch_location(&self) -> Result<u32> {
- frombytes_at::<u32>(self.fw, self.hdr.patch_loc_offset.into_safe_cast())
- }
-
- /// Returns an iterator to the signatures of the firmware. The iterator can be empty if the
- /// firmware is unsigned.
- ///
- /// Fails if the pointed signatures are outside the bounds of the firmware image.
- fn signatures_iter(&'a self) -> Result<impl Iterator<Item = BooterSignature<'a>>> {
- let num_sig = frombytes_at::<u32>(self.fw, self.hdr.num_sig_offset.into_safe_cast())?;
- let iter = match self.hdr.sig_prod_size.checked_div(num_sig) {
- // If there are no signatures, return an iterator that will yield zero elements.
- None => (&[] as &[u8]).chunks_exact(1),
- Some(sig_size) => {
- let patch_sig =
- frombytes_at::<u32>(self.fw, self.hdr.patch_sig_offset.into_safe_cast())?;
-
- let signatures_start = self
- .hdr
- .sig_prod_offset
- .checked_add(patch_sig)
- .map(usize::from_safe_cast)
- .ok_or(EINVAL)?;
-
- let signatures_end = signatures_start
- .checked_add(usize::from_safe_cast(self.hdr.sig_prod_size))
- .ok_or(EINVAL)?;
-
- self.fw
- // Get signatures range.
- .get(signatures_start..signatures_end)
- .ok_or(EINVAL)?
- .chunks_exact(sig_size.into_safe_cast())
- }
- };
-
- // Map the byte slices into signatures.
- Ok(iter.map(BooterSignature))
- }
-}
-
-/// Signature parameters, as defined in the firmware.
-#[repr(C)]
-struct HsSignatureParams {
- /// Fuse version to use.
- fuse_ver: u32,
- /// Mask of engine IDs this firmware applies to.
- engine_id_mask: u32,
- /// ID of the microcode.
- ucode_id: u32,
-}
-
-// SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
-unsafe impl FromBytes for HsSignatureParams {}
-
-impl HsSignatureParams {
- /// Returns the signature parameters contained in `hs_fw`.
- ///
- /// Fails if the meta data parameter of `hs_fw` is outside the bounds of the firmware image, or
- /// if its size doesn't match that of [`HsSignatureParams`].
- fn new(hs_fw: &HsFirmwareV2<'_>) -> Result<Self> {
- let start = usize::from_safe_cast(hs_fw.hdr.meta_data_offset);
- let end = start
- .checked_add(hs_fw.hdr.meta_data_size.into_safe_cast())
- .ok_or(EINVAL)?;
-
- hs_fw
- .fw
- .get(start..end)
- .and_then(Self::from_bytes_copy)
- .ok_or(EINVAL)
- }
-}
-
-/// Header for code and data load offsets.
-#[repr(C)]
-#[derive(Debug, Clone)]
-struct HsLoadHeaderV2 {
- // Offset at which the code starts.
- os_code_offset: u32,
- // Total size of the code, for all apps.
- os_code_size: u32,
- // Offset at which the data starts.
- os_data_offset: u32,
- // Size of the data.
- os_data_size: u32,
- // Number of apps following this header. Each app is described by a [`HsLoadHeaderV2App`].
- num_apps: u32,
-}
-
-// SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
-unsafe impl FromBytes for HsLoadHeaderV2 {}
-
-impl HsLoadHeaderV2 {
- /// Returns the load header contained in `hs_fw`.
- ///
- /// Fails if the header pointed at by `hs_fw` is not within the bounds of the firmware image.
- fn new(hs_fw: &HsFirmwareV2<'_>) -> Result<Self> {
- frombytes_at::<Self>(hs_fw.fw, hs_fw.hdr.header_offset.into_safe_cast())
- }
-}
-
-/// Header for app code loader.
-#[repr(C)]
-#[derive(Debug, Clone)]
-struct HsLoadHeaderV2App {
- /// Offset at which to load the app code.
- offset: u32,
- /// Length in bytes of the app code.
- len: u32,
-}
-
-// SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
-unsafe impl FromBytes for HsLoadHeaderV2App {}
-
-impl HsLoadHeaderV2App {
- /// Returns the [`HsLoadHeaderV2App`] for app `idx` of `hs_fw`.
- ///
- /// Fails if `idx` is larger than the number of apps declared in `hs_fw`, or if the header is
- /// not within the bounds of the firmware image.
- fn new(hs_fw: &HsFirmwareV2<'_>, idx: u32) -> Result<Self> {
- let load_hdr = HsLoadHeaderV2::new(hs_fw)?;
- if idx >= load_hdr.num_apps {
- Err(EINVAL)
- } else {
- frombytes_at::<Self>(
- hs_fw.fw,
- usize::from_safe_cast(hs_fw.hdr.header_offset)
- // Skip the load header...
- .checked_add(size_of::<HsLoadHeaderV2>())
- // ... and jump to app header `idx`.
- .and_then(|offset| {
- offset
- .checked_add(usize::from_safe_cast(idx).checked_mul(size_of::<Self>())?)
- })
- .ok_or(EINVAL)?,
- )
- }
- }
-}
-
/// Signature for Booter firmware. Their size is encoded into the header and not known a compile
/// time, so we just wrap a byte slices on which we can implement [`FirmwareSignature`].
struct BooterSignature<'a>(&'a [u8]);
@@ -291,102 +85,93 @@ pub(crate) fn new(
dev: &device::Device<device::Bound>,
kind: BooterKind,
chipset: Chipset,
- ver: &str,
falcon: &Falcon<'_, <Self as FalconFirmware>::Target>,
) -> Result<Self> {
let fw_name = match kind {
BooterKind::Loader => "booter_load",
BooterKind::Unloader => "booter_unload",
};
- let fw = super::request_firmware(dev, chipset, fw_name, ver)?;
- let bin_fw = BinFirmware::new(&fw)?;
-
- // The binary firmware embeds a Heavy-Secured firmware.
- let hs_fw = HsFirmwareV2::new(&bin_fw)?;
+ let fw = request_tlv(dev, chipset, fw_name)?;
+ let tlv = Tlv::new(fw.data())?;
+ dev_dbg!(
+ dev,
+ "loaded {} firmware v{}\n",
+ fw_name,
+ tlv.get_string(b"VERS")?
+ );
+
+ let os_data_offset = tlv.get_u32(b"DAOF")?;
+ let os_data_size = tlv.get_u32(b"DASZ")?;
+ let os_code_offset = tlv.get_u32(b"CDOF")?;
+ let os_code_size = tlv.get_u32(b"CDSZ")?;
+ let patch_loc = tlv.get_u32(b"PLOC")?;
+ let fuse_version: usize = tlv.get_u32(b"FUSE")?.into_safe_cast();
+ let engine_id = tlv.get_u32(b"ENID")?;
+ let ucode_id = tlv.get_u32(b"UCID")?;
+ let app0_code_offset = tlv.get_u32(b"A0CO")?;
+ let app0_code_size = tlv.get_u32(b"A0CS")?;
- // The Heavy-Secured firmware embeds a firmware load descriptor.
- let load_hdr = HsLoadHeaderV2::new(&hs_fw)?;
-
- // Offset in `ucode` where to patch the signature.
- let patch_loc = hs_fw.patch_location()?;
-
- let sig_params = HsSignatureParams::new(&hs_fw)?;
let brom_params = FalconBromParams {
- // `load_hdr.os_data_offset` is an absolute index, but `pkc_data_offset` is from the
+ // `os_data_offset` is an absolute index, but `pkc_data_offset` is from the
// signature patch location.
- pkc_data_offset: patch_loc
- .checked_sub(load_hdr.os_data_offset)
- .ok_or(EINVAL)?,
- engine_id_mask: u16::try_from(sig_params.engine_id_mask).map_err(|_| EINVAL)?,
- ucode_id: u8::try_from(sig_params.ucode_id).map_err(|_| EINVAL)?,
+ pkc_data_offset: patch_loc.checked_sub(os_data_offset).ok_or(EINVAL)?,
+ engine_id_mask: u16::try_from(engine_id).map_err(|_| EINVAL)?,
+ ucode_id: u8::try_from(ucode_id).map_err(|_| EINVAL)?,
};
- let app0 = HsLoadHeaderV2App::new(&hs_fw, 0)?;
- // Object containing the firmware microcode to be signature-patched.
- let ucode = bin_fw
- .data()
- .ok_or(EINVAL)
+ let ucode = tlv
+ .get_bytes(b"BLOB")
.and_then(FirmwareObject::<Self, _>::new_booter)?;
- let ucode_signed = {
- let mut signatures = hs_fw.signatures_iter()?.peekable();
-
- if signatures.peek().is_none() {
- // If there are no signatures, then the firmware is unsigned.
- ucode.no_patch_signature()
- } else {
- // Obtain the version from the fuse register, and extract the corresponding
- // signature.
- let reg_fuse_version = falcon
- .signature_reg_fuse_version(brom_params.engine_id_mask, brom_params.ucode_id)?;
-
- // `0` means the last signature should be used.
- const FUSE_VERSION_USE_LAST_SIG: u32 = 0;
- let signature = match reg_fuse_version {
- FUSE_VERSION_USE_LAST_SIG => signatures.last(),
- // Otherwise hardware fuse version needs to be subtracted to obtain the index.
- reg_fuse_version => {
- let Some(idx) = sig_params.fuse_ver.checked_sub(reg_fuse_version) else {
- dev_err!(dev, "invalid fuse version for Booter firmware\n");
- return Err(EINVAL);
- };
- signatures.nth(idx.into_safe_cast())
- }
- }
- .ok_or(EINVAL)?;
-
- ucode.patch_signature(&signature, patch_loc.into_safe_cast())?
- }
+ // Obtain the version from the fuse register, and extract the corresponding
+ // signature.
+ let reg_fuse_version: usize = falcon
+ .signature_reg_fuse_version(brom_params.engine_id_mask, brom_params.ucode_id)?
+ .into_safe_cast();
+
+ const FUSE_VERSION_USE_LAST_SIG: usize = 0;
+
+ let index = match reg_fuse_version {
+ // `0` means the last signature should be used.
+ FUSE_VERSION_USE_LAST_SIG => None,
+ // Otherwise, hardware fuse version needs to be subtracted to obtain the index.
+ _ => Some(fuse_version.checked_sub(reg_fuse_version).ok_or(EINVAL)?),
};
+ // Extract the nth signature. Booter is always signed.
+ let sig_chunk = tlv.get_signature(index)?;
+
+ let signature = BooterSignature(sig_chunk);
+ let ucode_signed = ucode.patch_signature(&signature, patch_loc.into_safe_cast())?;
+
// There are two versions of Booter, one for Turing/GA100, and another for
// GA102+. The extraction of the IMEM sections differs between the two
// versions. Unfortunately, the file names are the same, and the headers
// don't indicate the versions. The only way to differentiate is by the Chipset.
let (imem_sec_dst_start, imem_ns_load_target) = if chipset <= Chipset::GA100 {
(
- app0.offset,
+ app0_code_offset,
Some(FalconDmaLoadTarget {
src_start: 0,
- dst_start: load_hdr.os_code_offset,
- len: load_hdr.os_code_size,
+ dst_start: os_code_offset,
+ len: os_code_size,
}),
)
} else {
- (0, None)
+ (os_code_offset, None)
};
Ok(Self {
imem_sec_load_target: FalconDmaLoadTarget {
- src_start: app0.offset,
+ src_start: app0_code_offset,
dst_start: imem_sec_dst_start,
- len: app0.len,
+ len: app0_code_size,
},
imem_ns_load_target,
dmem_load_target: FalconDmaLoadTarget {
- src_start: load_hdr.os_data_offset,
+ src_start: os_data_offset,
dst_start: 0,
- len: load_hdr.os_data_size,
+ len: os_data_size,
},
brom_params,
ucode: ucode_signed,
diff --git a/drivers/gpu/nova-core/firmware/tlv.rs b/drivers/gpu/nova-core/firmware/tlv.rs
index 99b911cbf39f..6c9c355b4034 100644
--- a/drivers/gpu/nova-core/firmware/tlv.rs
+++ b/drivers/gpu/nova-core/firmware/tlv.rs
@@ -14,7 +14,6 @@
};
/// Requests the GPU firmware TLV `name` suitable for `chipset`.
-#[expect(dead_code)]
pub(crate) fn request_tlv(
dev: &device::Device,
chipset: gpu::Chipset,
@@ -131,7 +130,6 @@ pub(crate) struct Tlv<'a> {
data: &'a [u8],
}
-#[expect(dead_code)]
impl<'a> Tlv<'a> {
const MAGIC: &'static [u8; 4] = b"NVFW";
diff --git a/drivers/gpu/nova-core/gsp/hal/tu102.rs b/drivers/gpu/nova-core/gsp/hal/tu102.rs
index 648657e248da..647abfbedae5 100644
--- a/drivers/gpu/nova-core/gsp/hal/tu102.rs
+++ b/drivers/gpu/nova-core/gsp/hal/tu102.rs
@@ -28,8 +28,7 @@
FwsecCommand,
FwsecFirmware, //
},
- gsp::GspFirmware,
- FIRMWARE_VERSION, //
+ gsp::GspFirmware, //
},
gpu::Chipset,
gsp::{
@@ -243,7 +242,6 @@ fn build_unload_bundle(
dev,
BooterKind::Unloader,
chipset,
- FIRMWARE_VERSION,
sec2_falcon,
)?,
},
@@ -302,14 +300,11 @@ fn boot(
"Using SEC2 to load and run the booter_load firmware...\n"
);
- BooterFirmware::new(
+ BooterFirmware::new(dev, BooterKind::Loader, chipset, sec2_falcon)?.run(
dev,
- BooterKind::Loader,
- chipset,
- FIRMWARE_VERSION,
sec2_falcon,
- )?
- .run(dev, sec2_falcon, wpr_meta)?;
+ wpr_meta,
+ )?;
Ok(unload_guard.dismiss())
}
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v6 5/8] gpu: nova-core: transition gsp to TLV images
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
` (3 preceding siblings ...)
2026-07-29 22:55 ` [PATCH v6 4/8] gpu: nova-core: transition booter_load to TLV images Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
2026-07-29 23:24 ` Timur Tabi
2026-07-29 22:55 ` [PATCH v6 6/8] gpu: nova-core: transition gen_bootloader " Timur Tabi
` (2 subsequent siblings)
7 siblings, 1 reply; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
Switch the GSP firmware loader from the legacy binary format to the
TLV format. This change requires the new TLV versions of the r570.144
firmware images.
Unlike the other TLV firmware images, gsp.tlv contains a pointer to
the actual GSP firmware file instead of its contents. This allows
each small gsp.tlv file to contain the distinct metadata for each GPU
while still allowing the very large gsp.bin to be shared by all
GPUs.
One key piece of metadata is the signature. The legacy GSP firmware
image is an ELF file that contains multiple sections that needed
to be parsed, and the driver needed to determine which section is
relevant for the GPU. Instead, gsp.tlv contains the pre-processed
metadata, so all the driver needs to do is to extract it.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
---
drivers/gpu/nova-core/firmware.rs | 23 --------
drivers/gpu/nova-core/firmware/gsp.rs | 61 +++++++-------------
drivers/gpu/nova-core/firmware/riscv.rs | 75 ++++++-------------------
drivers/gpu/nova-core/gsp/boot.rs | 7 +--
4 files changed, 40 insertions(+), 126 deletions(-)
diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs
index bd47ebbb013e..3aa0137cba53 100644
--- a/drivers/gpu/nova-core/firmware.rs
+++ b/drivers/gpu/nova-core/firmware.rs
@@ -365,29 +365,6 @@ struct BinHdr {
// SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
unsafe impl FromBytes for BinHdr {}
-// A firmware blob starting with a `BinHdr`.
-struct BinFirmware<'a> {
- hdr: BinHdr,
- fw: &'a [u8],
-}
-
-impl<'a> BinFirmware<'a> {
- /// Interpret `fw` as a firmware image starting with a [`BinHdr`], and returns the
- /// corresponding [`BinFirmware`] that can be used to extract its payload.
- fn new(fw: &'a firmware::Firmware) -> Result<Self> {
- const BIN_MAGIC: u32 = 0x10de;
- let fw = fw.data();
-
- fw.get(0..size_of::<BinHdr>())
- // Extract header.
- .and_then(BinHdr::from_bytes_copy)
- // Validate header.
- .filter(|hdr| hdr.bin_magic == BIN_MAGIC)
- .map(|hdr| Self { hdr, fw })
- .ok_or(EINVAL)
- }
-}
-
pub(crate) struct ModInfoBuilder<const N: usize>(firmware::ModInfoBuilder<N>);
impl<const N: usize> ModInfoBuilder<N> {
diff --git a/drivers/gpu/nova-core/firmware/gsp.rs b/drivers/gpu/nova-core/firmware/gsp.rs
index 99a302bae567..f240c1c2e5d9 100644
--- a/drivers/gpu/nova-core/firmware/gsp.rs
+++ b/drivers/gpu/nova-core/firmware/gsp.rs
@@ -8,22 +8,24 @@
DataDirection,
DmaAddress, //
},
+ firmware,
prelude::*,
scatterlist::{
Owned,
SGTable, //
},
+ str::CString,
};
use crate::{
firmware::{
- elf,
riscv::RiscvFirmware, //
+ tlv::{
+ request_tlv, //
+ Tlv,
+ },
},
- gpu::{
- Architecture,
- Chipset, //
- },
+ gpu::Chipset,
gsp::GSP_PAGE_SIZE,
num::FromSafeCast,
};
@@ -63,43 +65,28 @@ pub(crate) struct GspFirmware {
}
impl GspFirmware {
- fn find_gsp_sigs_section(chipset: Chipset) -> &'static str {
- match chipset.arch() {
- Architecture::Turing if matches!(chipset, Chipset::TU116 | Chipset::TU117) => {
- ".fwsignature_tu11x"
- }
- Architecture::Turing => ".fwsignature_tu10x",
- Architecture::Ampere if chipset == Chipset::GA100 => ".fwsignature_ga100",
- Architecture::Ampere => ".fwsignature_ga10x",
- Architecture::Ada => ".fwsignature_ad10x",
- Architecture::Hopper => ".fwsignature_gh10x",
- Architecture::BlackwellGB10x => ".fwsignature_gb10x",
- Architecture::BlackwellGB20x => ".fwsignature_gb20x",
- }
- }
-
/// Loads the GSP firmware binaries, map them into `dev`'s address-space, and creates the page
/// tables expected by the GSP bootloader to load it.
pub(crate) fn new<'a>(
dev: &'a device::Device<device::Bound>,
chipset: Chipset,
- ver: &'a str,
) -> impl PinInit<Self, Error> + 'a {
pin_init::pin_init_scope(move || {
- let firmware = super::request_firmware(dev, chipset, "gsp", ver)?;
+ let firmware = request_tlv(dev, chipset, "gsp")?;
+ let tlv = Tlv::new(firmware.data())?;
+ dev_dbg!(dev, "loaded gsp firmware v{}\n", tlv.get_string(b"VERS")?);
- let fw_section = elf::elf_section(firmware.data(), ".fwimage").ok_or(EINVAL)?;
+ let size = usize::from_safe_cast(tlv.get_u32(b"SIZE")?);
+ let mut fw_vvec = VVec::zeroed(size, GFP_KERNEL).map_err(|_| ENOMEM)?;
- let size = fw_section.len();
+ assert!(fw_vvec.iter().all(|&x| x == 0));
- // Move the firmware into a vmalloc'd vector and map it into the device address
- // space.
- let fw_vvec = VVec::with_capacity(fw_section.len(), GFP_KERNEL)
- .and_then(|mut v| {
- v.extend_from_slice(fw_section, GFP_KERNEL)?;
- Ok(v)
- })
- .map_err(|_| ENOMEM)?;
+ let chip_name = chipset.name();
+ let file = tlv.get_string(b"FILE")?;
+ let filename = CString::try_from_fmt(fmt!("nvidia/{chip_name}/gsp/{file}"))?;
+ firmware::request_into_buf(&filename, dev, fw_vvec.as_mut_slice())?;
+
+ let signatures = Coherent::from_slice(dev, tlv.get_bytes(b"SIGN")?, GFP_KERNEL)?;
Ok(try_pin_init!(Self {
fw <- SGTable::new(dev, fw_vvec, DataDirection::ToDevice, GFP_KERNEL),
@@ -145,15 +132,9 @@ pub(crate) fn new<'a>(
level0.into()
},
size,
- signatures: {
- let sigs_section = Self::find_gsp_sigs_section(chipset);
-
- elf::elf_section(firmware.data(), sigs_section)
- .ok_or(EINVAL)
- .and_then(|data| Coherent::from_slice(dev, data, GFP_KERNEL))?
- },
+ signatures,
bootloader: {
- let bl = super::request_firmware(dev, chipset, "bootloader", ver)?;
+ let bl = request_tlv(dev, chipset, "gsp_bootloader")?;
RiscvFirmware::new(dev, &bl)?
},
diff --git a/drivers/gpu/nova-core/firmware/riscv.rs b/drivers/gpu/nova-core/firmware/riscv.rs
index 2afa7f36404e..1403f05a7305 100644
--- a/drivers/gpu/nova-core/firmware/riscv.rs
+++ b/drivers/gpu/nova-core/firmware/riscv.rs
@@ -7,53 +7,10 @@
device,
dma::Coherent,
firmware::Firmware,
- prelude::*,
- transmute::FromBytes, //
+ prelude::*, //
};
-use crate::{
- firmware::BinFirmware,
- num::FromSafeCast, //
-};
-
-/// Descriptor for microcode running on a RISC-V core.
-#[repr(C)]
-#[derive(Debug)]
-struct RmRiscvUCodeDesc {
- version: u32,
- bootloader_offset: u32,
- bootloader_size: u32,
- bootloader_param_offset: u32,
- bootloader_param_size: u32,
- riscv_elf_offset: u32,
- riscv_elf_size: u32,
- app_version: u32,
- manifest_offset: u32,
- manifest_size: u32,
- monitor_data_offset: u32,
- monitor_data_size: u32,
- monitor_code_offset: u32,
- monitor_code_size: u32,
-}
-
-// SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
-unsafe impl FromBytes for RmRiscvUCodeDesc {}
-
-impl RmRiscvUCodeDesc {
- /// Interprets the header of `bin_fw` as a [`RmRiscvUCodeDesc`] and returns it.
- ///
- /// Fails if the header pointed at by `bin_fw` is not within the bounds of the firmware image.
- fn new(bin_fw: &BinFirmware<'_>) -> Result<Self> {
- let offset = usize::from_safe_cast(bin_fw.hdr.header_offset);
- let end = offset.checked_add(size_of::<Self>()).ok_or(EINVAL)?;
-
- bin_fw
- .fw
- .get(offset..end)
- .and_then(Self::from_bytes_copy)
- .ok_or(EINVAL)
- }
-}
+use crate::firmware::tlv::Tlv;
/// A parsed firmware for a RISC-V core, ready to be loaded and run.
pub(crate) struct RiscvFirmware {
@@ -72,24 +29,26 @@ pub(crate) struct RiscvFirmware {
impl RiscvFirmware {
/// Parses the RISC-V firmware image contained in `fw`.
pub(crate) fn new(dev: &device::Device<device::Bound>, fw: &Firmware) -> Result<Self> {
- let bin_fw = BinFirmware::new(fw)?;
-
- let riscv_desc = RmRiscvUCodeDesc::new(&bin_fw)?;
+ let tlv = Tlv::new(fw.data())?;
+ dev_dbg!(
+ dev,
+ "loaded gsp bootloader firmware v{}\n",
+ tlv.get_string(b"VERS")?
+ );
- let ucode = {
- let start = usize::from_safe_cast(bin_fw.hdr.data_offset);
- let len = usize::from_safe_cast(bin_fw.hdr.data_size);
- let end = start.checked_add(len).ok_or(EINVAL)?;
+ let code_offset = tlv.get_u32(b"CDOF")?;
+ let data_offset = tlv.get_u32(b"DAOF")?;
+ let manifest_offset = tlv.get_u32(b"MFOF")?;
+ let app_version = tlv.get_u32(b"APPV")?;
- Coherent::from_slice(dev, fw.data().get(start..end).ok_or(EINVAL)?, GFP_KERNEL)?
- };
+ let ucode = Coherent::from_slice(dev, tlv.get_bytes(b"BLOB")?, GFP_KERNEL)?;
Ok(Self {
ucode,
- code_offset: riscv_desc.monitor_code_offset,
- data_offset: riscv_desc.monitor_data_offset,
- manifest_offset: riscv_desc.manifest_offset,
- app_version: riscv_desc.app_version,
+ code_offset,
+ data_offset,
+ manifest_offset,
+ app_version,
})
}
}
diff --git a/drivers/gpu/nova-core/gsp/boot.rs b/drivers/gpu/nova-core/gsp/boot.rs
index 727b8ae4bcb7..1f1c419b9302 100644
--- a/drivers/gpu/nova-core/gsp/boot.rs
+++ b/drivers/gpu/nova-core/gsp/boot.rs
@@ -17,10 +17,7 @@
Falcon, //
},
fb::FbLayout,
- firmware::{
- gsp::GspFirmware,
- FIRMWARE_VERSION, //
- },
+ firmware::gsp::GspFirmware,
gsp::{
cmdq::Cmdq,
commands,
@@ -48,7 +45,7 @@ pub(crate) fn boot(
let dev = pdev.as_ref();
let hal = super::hal::gsp_hal(chipset);
- let gsp_fw = KBox::pin_init(GspFirmware::new(dev, chipset, FIRMWARE_VERSION), GFP_KERNEL)?;
+ let gsp_fw = KBox::pin_init(GspFirmware::new(dev, chipset), GFP_KERNEL)?;
let fb_layout = FbLayout::new(chipset, bar, &gsp_fw, ctx.vgpu.state())?;
dev_dbg!(dev, "{:#x?}\n", fb_layout);
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* Re: [PATCH v6 5/8] gpu: nova-core: transition gsp to TLV images
2026-07-29 22:55 ` [PATCH v6 5/8] gpu: nova-core: transition gsp " Timur Tabi
@ 2026-07-29 23:24 ` Timur Tabi
0 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 23:24 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard,
Zhi Wang, rust-for-linux@vger.kernel.org,
driver-core@lists.linux.dev, nova-gpu@lists.linux.dev
On 7/29/26 5:55 PM, Timur Tabi wrote:
> /// Loads the GSP firmware binaries, map them into `dev`'s address-space, and creates the page
> /// tables expected by the GSP bootloader to load it.
> pub(crate) fn new<'a>(
> dev: &'a device::Device<device::Bound>,
> chipset: Chipset,
> - ver: &'a str,
> ) -> impl PinInit<Self, Error> + 'a {
> pin_init::pin_init_scope(move || {
> - let firmware = super::request_firmware(dev, chipset, "gsp", ver)?;
> + let firmware = request_tlv(dev, chipset, "gsp")?;
> + let tlv = Tlv::new(firmware.data())?;
> + dev_dbg!(dev, "loaded gsp firmware v{}\n", tlv.get_string(b"VERS")?);
>
> - let fw_section = elf::elf_section(firmware.data(), ".fwimage").ok_or(EINVAL)?;
> + let size = usize::from_safe_cast(tlv.get_u32(b"SIZE")?);
> + let mut fw_vvec = VVec::zeroed(size, GFP_KERNEL).map_err(|_| ENOMEM)?;
>
> - let size = fw_section.len();
> + assert!(fw_vvec.iter().all(|&x| x == 0));
>
Oops, that assert should not be there.
^ permalink raw reply [flat|nested] 10+ messages in thread
* [PATCH v6 6/8] gpu: nova-core: transition gen_bootloader to TLV images
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
` (4 preceding siblings ...)
2026-07-29 22:55 ` [PATCH v6 5/8] gpu: nova-core: transition gsp " Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
2026-07-29 22:55 ` [PATCH v6 7/8] gpu: nova-core: transition fsp " Timur Tabi
2026-07-29 22:55 ` [PATCH v6 8/8] gpu: nova-core: update firmware module info for " Timur Tabi
7 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
Switch the generic bootloader firmware loader from the legacy binary
format to the TLV format. This change requires the new TLV versions
of the r570.144 firmware images.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
---
drivers/gpu/nova-core/firmware.rs | 24 +-----
.../nova-core/firmware/fwsec/bootloader.rs | 75 +++++--------------
2 files changed, 20 insertions(+), 79 deletions(-)
diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs
index 3aa0137cba53..c285e57268f0 100644
--- a/drivers/gpu/nova-core/firmware.rs
+++ b/drivers/gpu/nova-core/firmware.rs
@@ -11,8 +11,7 @@
device,
firmware,
prelude::*,
- str::CString,
- transmute::FromBytes, //
+ str::CString, //
};
use crate::{
@@ -344,27 +343,6 @@ fn no_patch_signature(self) -> FirmwareObject<F, Signed> {
}
}
-/// Header common to most firmware files.
-#[repr(C)]
-#[derive(Debug, Clone)]
-struct BinHdr {
- /// Magic number, must be `0x10de`.
- bin_magic: u32,
- /// Version of the header.
- bin_ver: u32,
- /// Size in bytes of the binary (to be ignored).
- bin_size: u32,
- /// Offset of the start of the application-specific header.
- header_offset: u32,
- /// Offset of the start of the data payload.
- data_offset: u32,
- /// Size in bytes of the data payload.
- data_size: u32,
-}
-
-// SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
-unsafe impl FromBytes for BinHdr {}
-
pub(crate) struct ModInfoBuilder<const N: usize>(firmware::ModInfoBuilder<N>);
impl<const N: usize> ModInfoBuilder<N> {
diff --git a/drivers/gpu/nova-core/firmware/fwsec/bootloader.rs b/drivers/gpu/nova-core/firmware/fwsec/bootloader.rs
index d9fafd2eea5b..30d247b59848 100644
--- a/drivers/gpu/nova-core/firmware/fwsec/bootloader.rs
+++ b/drivers/gpu/nova-core/firmware/fwsec/bootloader.rs
@@ -19,10 +19,7 @@
Alignment, //
},
sizes,
- transmute::{
- AsBytes,
- FromBytes, //
- },
+ transmute::AsBytes,
};
use crate::{
@@ -42,38 +39,16 @@
},
firmware::{
fwsec::FwsecFirmware,
- request_firmware,
- BinHdr,
- FIRMWARE_VERSION, //
+ tlv::{
+ request_tlv, //
+ Tlv,
+ },
},
gpu::Chipset,
num::FromSafeCast, //
regs,
};
-/// Descriptor used by RM to figure out the requirements of the boot loader.
-///
-/// Most of its fields appear to be legacy and carry incorrect values, so they are left unused.
-#[repr(C)]
-#[derive(Debug, Clone)]
-struct BootloaderDesc {
- /// Starting tag of bootloader.
- start_tag: u32,
- /// DMEM load offset - unused here as we always load at offset `0`.
- _dmem_load_off: u32,
- /// Offset of code section in the image. Unused as there is only one section in the bootloader
- /// binary.
- _code_off: u32,
- /// Size of code section in the image.
- code_size: u32,
- /// Offset of data section in the image. Unused as we build the data section ourselves.
- _data_off: u32,
- /// Size of data section in the image. Unused as we build the data section ourselves.
- _data_size: u32,
-}
-// SAFETY: any byte sequence is valid for this struct.
-unsafe impl FromBytes for BootloaderDesc {}
-
/// Structure used by the boot-loader to load the rest of the code.
///
/// This has to be filled by the GPU driver and copied into DMEM at offset
@@ -146,38 +121,24 @@ pub(crate) fn new(
dev: &Device<device::Bound>,
chipset: Chipset,
) -> Result<Self> {
- let fw = request_firmware(dev, chipset, "gen_bootloader", FIRMWARE_VERSION)?;
- let hdr = fw
- .data()
- .get(0..size_of::<BinHdr>())
- .and_then(BinHdr::from_bytes_copy)
- .ok_or(EINVAL)?;
-
- let desc = {
- let desc_offset = usize::from_safe_cast(hdr.header_offset);
-
- fw.data()
- .get(desc_offset..)
- .and_then(BootloaderDesc::from_bytes_copy_prefix)
- .ok_or(EINVAL)?
- .0
- };
+ let fw = request_tlv(dev, chipset, "gen_bootloader")?;
+ let tlv = Tlv::new(fw.data())?;
+ dev_dbg!(
+ dev,
+ "loaded generic bootloader firmware v{}\n",
+ tlv.get_string(b"VERS")?
+ );
let ucode = {
- let ucode_start = usize::from_safe_cast(hdr.data_offset);
- let code_size = usize::from_safe_cast(desc.code_size);
- // Align to falcon block size (256 bytes).
+ let blob = tlv.get_bytes(b"BLOB")?;
+ let code_size = usize::from_safe_cast(tlv.get_u32(b"CDSZ")?);
+ let code = blob.get(..code_size).ok_or(EINVAL)?;
let aligned_code_size = code_size
.align_up(Alignment::new::<{ falcon::MEM_BLOCK_ALIGNMENT }>())
.ok_or(EINVAL)?;
let mut ucode = KVec::with_capacity(aligned_code_size, GFP_KERNEL)?;
- ucode.extend_from_slice(
- fw.data()
- .get(ucode_start..ucode_start + code_size)
- .ok_or(EINVAL)?,
- GFP_KERNEL,
- )?;
+ ucode.extend_from_slice(code, GFP_KERNEL)?;
ucode.resize(aligned_code_size, 0, GFP_KERNEL)?;
ucode
@@ -258,13 +219,15 @@ pub(crate) fn new(
.checked_sub(ucode.len())
.ok_or(EOVERFLOW)?;
+ let start_tag = u16::try_from(tlv.get_u32(b"STRT")?)?;
+
Ok(Self {
_firmware_dma: firmware_dma,
ucode,
dmem_desc,
brom_params: firmware.brom_params(),
imem_dst_start: u16::try_from(imem_dst_start)?,
- start_tag: u16::try_from(desc.start_tag)?,
+ start_tag,
})
}
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v6 7/8] gpu: nova-core: transition fsp to TLV images
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
` (5 preceding siblings ...)
2026-07-29 22:55 ` [PATCH v6 6/8] gpu: nova-core: transition gen_bootloader " Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
2026-07-29 22:55 ` [PATCH v6 8/8] gpu: nova-core: update firmware module info for " Timur Tabi
7 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
Switch the FSP firmware loaders from the legacy ELF32 format to the new
TLV format. This change requires the new TLV versions of the r570.144
firmware images.
Because we are no longer loading ELF images, we can also delete the ELF
parser.
Also remove function request_firmware() as this was the last user.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
---
drivers/gpu/nova-core/firmware.rs | 222 +-------------------------
drivers/gpu/nova-core/firmware/fsp.rs | 88 +++++-----
drivers/gpu/nova-core/fsp.rs | 11 +-
3 files changed, 46 insertions(+), 275 deletions(-)
diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs
index c285e57268f0..d58a3d5770d5 100644
--- a/drivers/gpu/nova-core/firmware.rs
+++ b/drivers/gpu/nova-core/firmware.rs
@@ -8,10 +8,8 @@
use core::ops::Deref;
use kernel::{
- device,
firmware,
- prelude::*,
- str::CString, //
+ prelude::*, //
};
use crate::{
@@ -33,19 +31,6 @@
pub(crate) const FIRMWARE_VERSION: &str = "570.144";
-/// Requests the GPU firmware `name` suitable for `chipset`, with version `ver`.
-fn request_firmware(
- dev: &device::Device,
- chipset: gpu::Chipset,
- name: &str,
- ver: &str,
-) -> Result<firmware::Firmware> {
- let chip_name = chipset.name();
-
- CString::try_from_fmt(fmt!("nvidia/{chip_name}/gsp/{name}-{ver}.bin"))
- .and_then(|path| firmware::Firmware::request(&path, dev))
-}
-
/// Structure used to describe some firmwares, notably FWSEC-FRTS.
#[repr(C)]
#[derive(Debug, Clone, FromBytes)]
@@ -393,208 +378,3 @@ pub(crate) const fn create(
this.0
}
}
-
-/// 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,
- prelude::*,
- transmute::FromBytes, //
- };
-
- /// Trait to abstract over ELF header differences.
- trait ElfHeader: FromBytes {
- fn shnum(&self) -> u16;
- fn shoff(&self) -> u64;
- fn shstrndx(&self) -> u16;
- }
-
- /// Trait to abstract over ELF section-header differences.
- trait ElfSectionHeader: FromBytes {
- fn name(&self) -> u32;
- fn offset(&self) -> u64;
- fn size(&self) -> u64;
- }
-
- /// Trait describing a matching ELF header and section-header format.
- trait ElfFormat {
- type Header: ElfHeader;
- type SectionHeader: ElfSectionHeader;
- }
-
- /// 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 {}
-
- impl ElfHeader for Elf64Hdr {
- fn shnum(&self) -> u16 {
- self.0.e_shnum
- }
-
- fn shoff(&self) -> u64 {
- self.0.e_shoff
- }
-
- fn shstrndx(&self) -> u16 {
- self.0.e_shstrndx
- }
- }
-
- #[repr(transparent)]
- struct Elf64SHdr(bindings::elf64_shdr);
- // SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
- unsafe impl FromBytes for Elf64SHdr {}
-
- impl ElfSectionHeader for Elf64SHdr {
- fn name(&self) -> u32 {
- self.0.sh_name
- }
-
- fn offset(&self) -> u64 {
- self.0.sh_offset
- }
-
- fn size(&self) -> u64 {
- self.0.sh_size
- }
- }
-
- struct Elf64Format;
-
- impl ElfFormat for Elf64Format {
- type Header = Elf64Hdr;
- type SectionHeader = Elf64SHdr;
- }
-
- /// Newtype to provide [`FromBytes`] and [`ElfHeader`] implementations for ELF32.
- #[repr(transparent)]
- struct Elf32Hdr(bindings::elf32_hdr);
- // SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
- unsafe impl FromBytes for Elf32Hdr {}
-
- impl ElfHeader for Elf32Hdr {
- fn shnum(&self) -> u16 {
- self.0.e_shnum
- }
-
- fn shoff(&self) -> u64 {
- u64::from(self.0.e_shoff)
- }
-
- fn shstrndx(&self) -> u16 {
- self.0.e_shstrndx
- }
- }
-
- /// Newtype to provide [`FromBytes`] and [`ElfSectionHeader`] implementations for ELF32.
- #[repr(transparent)]
- struct Elf32SHdr(bindings::elf32_shdr);
- // SAFETY: all bit patterns are valid for this type, and it doesn't use interior mutability.
- unsafe impl FromBytes for Elf32SHdr {}
-
- impl ElfSectionHeader for Elf32SHdr {
- fn name(&self) -> u32 {
- self.0.sh_name
- }
-
- fn offset(&self) -> u64 {
- u64::from(self.0.sh_offset)
- }
-
- fn size(&self) -> u64 {
- u64::from(self.0.sh_size)
- }
- }
-
- struct Elf32Format;
-
- impl ElfFormat for Elf32Format {
- type Header = Elf32Hdr;
- type SectionHeader = Elf32SHdr;
- }
-
- /// Returns a NULL-terminated string from the ELF image at `offset`.
- fn elf_str(elf: &[u8], offset: u64) -> Option<&str> {
- let idx = usize::try_from(offset).ok()?;
- let bytes = elf.get(idx..)?;
- CStr::from_bytes_until_nul(bytes).ok()?.to_str().ok()
- }
-
- fn elf_section_generic<'a, F>(elf: &'a [u8], name: &str) -> Option<&'a [u8]>
- where
- F: ElfFormat,
- {
- let hdr = F::Header::from_bytes(elf.get(0..size_of::<F::Header>())?)?;
-
- let shdr_num = usize::from(hdr.shnum());
- let shdr_start = usize::try_from(hdr.shoff()).ok()?;
- let shdr_end = shdr_num
- .checked_mul(size_of::<F::SectionHeader>())
- .and_then(|v| v.checked_add(shdr_start))?;
-
- // Get all the section headers as an iterator over byte chunks.
- let shdr_bytes = elf.get(shdr_start..shdr_end)?;
- let mut shdr_iter = shdr_bytes.chunks_exact(size_of::<F::SectionHeader>());
-
- // Get the strings table.
- let strhdr = shdr_iter
- .clone()
- .nth(usize::from(hdr.shstrndx()))
- .and_then(F::SectionHeader::from_bytes)?;
-
- // Find the section which name matches `name` and return it.
- shdr_iter.find_map(|sh_bytes| {
- let sh = F::SectionHeader::from_bytes(sh_bytes)?;
- let name_offset = strhdr.offset().checked_add(u64::from(sh.name()))?;
- let section_name = elf_str(elf, name_offset)?;
-
- if section_name != name {
- return None;
- }
-
- let start = usize::try_from(sh.offset()).ok()?;
- let end = usize::try_from(sh.size())
- .ok()
- .and_then(|sz| start.checked_add(sz))?;
-
- elf.get(start..end)
- })
- }
-
- /// Extract the section with name `name` from the ELF64 image `elf`.
- fn elf64_section<'a>(elf: &'a [u8], name: &str) -> Option<&'a [u8]> {
- elf_section_generic::<Elf64Format>(elf, name)
- }
-
- /// Extract the section with name `name` from the ELF32 image `elf`.
- fn elf32_section<'a>(elf: &'a [u8], name: &str) -> Option<&'a [u8]> {
- elf_section_generic::<Elf32Format>(elf, name)
- }
-
- /// Automatically detects ELF32 vs ELF64 based on the ELF header.
- pub(super) fn elf_section<'a>(elf: &'a [u8], name: &str) -> Option<&'a [u8]> {
- // ELF identification: a 4-byte magic followed by a class byte (32- vs 64-bit).
- const ELFMAG: &[u8] = b"\x7fELF";
- const SELFMAG: usize = ELFMAG.len();
- const EI_CLASS: usize = 4;
- const ELFCLASS32: u8 = 1;
- const ELFCLASS64: u8 = 2;
-
- if elf.get(0..SELFMAG) != Some(ELFMAG) {
- return None;
- }
-
- match *elf.get(EI_CLASS)? {
- ELFCLASS32 => elf32_section(elf, name),
- ELFCLASS64 => elf64_section(elf, name),
- _ => None,
- }
- }
-}
diff --git a/drivers/gpu/nova-core/firmware/fsp.rs b/drivers/gpu/nova-core/firmware/fsp.rs
index 6eaf1c684b9d..5462e318410a 100644
--- a/drivers/gpu/nova-core/firmware/fsp.rs
+++ b/drivers/gpu/nova-core/firmware/fsp.rs
@@ -6,12 +6,14 @@
use kernel::{
device,
dma::Coherent,
- firmware::Firmware,
prelude::*, //
};
use crate::{
- firmware::elf,
+ firmware::tlv::{
+ request_tlv, //
+ Tlv,
+ },
gpu::Chipset, //
};
@@ -19,11 +21,11 @@
const FSP_HASH_SIZE: usize = 48;
/// Maximum size of the FSP public key (RSA-3072), in bytes.
///
-/// The FMC ELF `publickey` section may be shorter, so the remaining bytes are zero-padded.
+/// The FMC `PKEY` tag may be shorter, so the remaining bytes are zero-padded.
const FSP_PKEY_SIZE: usize = 384;
/// Maximum size of the FSP signature (RSA-3072), in bytes.
///
-/// The FMC ELF `signature` section may be shorter, so the remaining bytes are zero-padded.
+/// The FMC `SIGN` tag may be shorter, so the remaining bytes are zero-padded.
const FSP_SIG_SIZE: usize = 384;
/// Structure to hold FMC signatures.
@@ -38,64 +40,35 @@ pub(crate) struct FmcSignatures {
}
pub(crate) struct FspFirmware {
- /// FMC firmware image data (only the "image" ELF section).
+ /// FMC firmware image data
pub(crate) fmc_image: Coherent<[u8]>,
/// FMC firmware signatures.
pub(crate) fmc_sigs: KBox<FmcSignatures>,
}
impl FspFirmware {
- pub(crate) fn new(
- dev: &device::Device<device::Bound>,
- chipset: Chipset,
- ver: &str,
- ) -> Result<Self> {
- let fw = super::request_firmware(dev, chipset, "fmc", ver)?;
+ pub(crate) fn new(dev: &device::Device<device::Bound>, chipset: Chipset) -> Result<Self> {
+ let fw = request_tlv(dev, chipset, "fmc")?;
+ let tlv = Tlv::new(fw.data())?;
+ dev_dbg!(dev, "loaded fsp firmware v{}\n", tlv.get_string(b"VERS")?);
- // FSP expects only the "image" section, not the entire ELF file.
- let fmc_image_data = elf::elf_section(fw.data(), "image").ok_or_else(|| {
- dev_err!(dev, "FMC ELF file missing 'image' section\n");
- EINVAL
- })?;
+ let fmc_image_data = tlv.get_bytes(b"BLOB")?;
let fmc_image = Coherent::from_slice(dev, fmc_image_data, GFP_KERNEL)?;
Ok(Self {
fmc_image,
- fmc_sigs: Self::extract_fmc_signatures(&fw, dev)?,
+ fmc_sigs: Self::extract_fmc_signatures(&tlv, dev)?,
})
}
/// Extract FMC firmware signatures for Chain of Trust verification.
///
- /// Extracts real cryptographic signatures from FMC ELF32 firmware sections.
+ /// Extracts real cryptographic signatures from FMC TLV firmware tags.
/// Returns signatures in a heap-allocated structure to prevent stack overflow.
- fn extract_fmc_signatures(
- fmc_fw: &Firmware,
- dev: &device::Device,
- ) -> Result<KBox<FmcSignatures>> {
- let get_section = |name: &str, max_len: usize| {
- elf::elf_section(fmc_fw.data(), name)
- .ok_or(EINVAL)
- .inspect_err(|_| dev_err!(dev, "FMC firmware missing '{}' section\n", name))
- .and_then(|section| {
- if section.len() > max_len {
- dev_err!(
- dev,
- "FMC {} section size {} > maximum {}\n",
- name,
- section.len(),
- max_len
- );
- Err(EINVAL)
- } else {
- Ok(section)
- }
- })
- };
-
- let hash_section = get_section("hash", FSP_HASH_SIZE)?;
- let pkey_section = get_section("publickey", FSP_PKEY_SIZE)?;
- let sig_section = get_section("signature", FSP_SIG_SIZE)?;
+ fn extract_fmc_signatures(tlv: &Tlv<'_>, dev: &device::Device) -> Result<KBox<FmcSignatures>> {
+ let hash_section = tlv.get_bytes(b"HASH")?;
+ let pkey_section = tlv.get_bytes(b"PKEY")?;
+ let sig_section = tlv.get_bytes(b"SIGN")?;
// The hash section is a SHA-384 output: it must be exactly FSP_HASH_SIZE bytes.
if hash_section.len() != FSP_HASH_SIZE {
@@ -108,15 +81,36 @@ fn extract_fmc_signatures(
return Err(EINVAL);
}
+ // The key and signature sections are zero-padded to a fixed maximum, so they may be
+ // shorter, but must not exceed the destination buffers.
+ if pkey_section.len() > FSP_PKEY_SIZE {
+ dev_err!(
+ dev,
+ "FMC public key section size {} > maximum {}\n",
+ pkey_section.len(),
+ FSP_PKEY_SIZE
+ );
+ return Err(EINVAL);
+ }
+ if sig_section.len() > FSP_SIG_SIZE {
+ dev_err!(
+ dev,
+ "FMC signature section size {} > maximum {}\n",
+ sig_section.len(),
+ FSP_SIG_SIZE
+ );
+ return Err(EINVAL);
+ }
+
// Initialize the signatures in place to avoid building the large `FmcSignatures` on the
// stack, then fill each section from the firmware.
let signatures = KBox::init(
pin_init::init_zeroed::<FmcSignatures>().chain(|sigs| {
// PANIC: src and dst lengths are both FSP_HASH_SIZE (verified above).
sigs.hash384.copy_from_slice(hash_section);
- // PANIC: dst is sliced to src.len(); src.len() <= FSP_PKEY_SIZE per `get_section`.
+ // PANIC: dst is sliced to src.len(); src.len() <= FSP_PKEY_SIZE (verified above).
sigs.public_key[..pkey_section.len()].copy_from_slice(pkey_section);
- // PANIC: dst is sliced to src.len(); src.len() <= FSP_SIG_SIZE per `get_section`.
+ // PANIC: dst is sliced to src.len(); src.len() <= FSP_SIG_SIZE (verified above).
sigs.signature[..sig_section.len()].copy_from_slice(sig_section);
Ok(())
}),
diff --git a/drivers/gpu/nova-core/fsp.rs b/drivers/gpu/nova-core/fsp.rs
index ba4544210e40..29c809e1557e 100644
--- a/drivers/gpu/nova-core/fsp.rs
+++ b/drivers/gpu/nova-core/fsp.rs
@@ -32,12 +32,9 @@
Falcon, //
},
fb::FbLayout,
- firmware::{
- fsp::{
- FmcSignatures,
- FspFirmware, //
- },
- FIRMWARE_VERSION, //
+ firmware::fsp::{
+ FmcSignatures,
+ FspFirmware, //
},
gpu::Chipset,
gsp::{
@@ -413,7 +410,7 @@ fn wait_secure_boot(
const FSP_SECURE_BOOT_TIMEOUT_MS: i64 = 5000;
let falcon = Falcon::<FspEngine>::new(dev, chipset, bar)?;
- let fsp_fw = FspFirmware::new(dev, chipset, FIRMWARE_VERSION)?;
+ let fsp_fw = FspFirmware::new(dev, chipset)?;
read_poll_timeout(
|| Ok(hal.fsp_boot_status(bar)),
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread* [PATCH v6 8/8] gpu: nova-core: update firmware module info for TLV images
2026-07-29 22:55 [PATCH v6 0/8] Transition Nova Core to TLV firmare images Timur Tabi
` (6 preceding siblings ...)
2026-07-29 22:55 ` [PATCH v6 7/8] gpu: nova-core: transition fsp " Timur Tabi
@ 2026-07-29 22:55 ` Timur Tabi
7 siblings, 0 replies; 10+ messages in thread
From: Timur Tabi @ 2026-07-29 22:55 UTC (permalink / raw)
To: Danilo Krummrich, Miguel Ojeda, Luis Chamberlain, Russ Weight,
Gary Guo, Alexandre Courbot, Eliot Courtney, John Hubbard, zhiw,
rust-for-linux, driver-core, nova-gpu
Now that nova-core loads the TLV firmware images, update the firmware
module info to specify those files.
Also remove FIRMWARE_VERSION as it is no longer used.
Signed-off-by: Timur Tabi <ttabi@nvidia.com>
---
drivers/gpu/nova-core/firmware.rs | 12 ++++--------
drivers/gpu/nova-core/gsp/hal.rs | 8 ++++----
2 files changed, 8 insertions(+), 12 deletions(-)
diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs
index d58a3d5770d5..b49613a90bf0 100644
--- a/drivers/gpu/nova-core/firmware.rs
+++ b/drivers/gpu/nova-core/firmware.rs
@@ -29,8 +29,6 @@
pub(crate) mod riscv;
pub(crate) mod tlv;
-pub(crate) const FIRMWARE_VERSION: &str = "570.144";
-
/// Structure used to describe some firmwares, notably FWSEC-FRTS.
#[repr(C)]
#[derive(Debug, Clone, FromBytes)]
@@ -338,10 +336,7 @@ const fn make_entry_file(self, chipset: &str, fw: &str) -> Self {
.push("nvidia/")
.push(chipset)
.push("/gsp/")
- .push(fw)
- .push("-")
- .push(FIRMWARE_VERSION)
- .push(".bin"),
+ .push(fw),
)
}
@@ -350,8 +345,9 @@ const fn make_entry_chipset(self, chipset: gpu::Chipset) -> Self {
// GSP firmware files are always present.
let mut this = self
- .make_entry_file(name, "bootloader")
- .make_entry_file(name, "gsp");
+ .make_entry_file(name, "gsp_bootloader.tlv")
+ .make_entry_file(name, "gsp.tlv")
+ .make_entry_file(name, "gsp.bin");
// Add the firmware files specific to the GSP boot method of `chipset`.
let boot_files = boot_firmware_files(chipset);
diff --git a/drivers/gpu/nova-core/gsp/hal.rs b/drivers/gpu/nova-core/gsp/hal.rs
index 34b4bb82a999..2177ea17dee6 100644
--- a/drivers/gpu/nova-core/gsp/hal.rs
+++ b/drivers/gpu/nova-core/gsp/hal.rs
@@ -67,16 +67,16 @@ fn post_boot(
pub(crate) const fn boot_firmware_files(chipset: Chipset) -> &'static [&'static str] {
match chipset.arch() {
// Turing chipsets boot the GSP via the SEC2 Booter, and require the FWSEC bootloader.
- Architecture::Turing => &["booter_load", "booter_unload", "gen_bootloader"],
+ Architecture::Turing => &["booter_load.tlv", "booter_unload.tlv", "gen_bootloader.tlv"],
// GA100 also requires the FWSEC bootloader.
Architecture::Ampere if matches!(chipset, Chipset::GA100) => {
- &["booter_load", "booter_unload", "gen_bootloader"]
+ &["booter_load.tlv", "booter_unload.tlv", "gen_bootloader.tlv"]
}
// Other Ampere chipsets, as well as Ada chipsets, run FWSEC directly.
- Architecture::Ampere | Architecture::Ada => &["booter_load", "booter_unload"],
+ Architecture::Ampere | Architecture::Ada => &["booter_load.tlv", "booter_unload.tlv"],
// Hopper and later chipsets boot the GSP via the FMC image loaded by FSP.
Architecture::Hopper | Architecture::BlackwellGB10x | Architecture::BlackwellGB20x => {
- &["fmc"]
+ &["fmc.tlv"]
}
}
}
--
2.54.0
^ permalink raw reply related [flat|nested] 10+ messages in thread