Re: [PATCH v5 3/8] gpu: nova-core: add TLV parser for firmware files

"Alexandre Courbot" <[email protected]> Mon, 27 Jul 2026 20:31:03 +0900
Newsgroups dev.linux.lists.nova-gpu,dev.linux.lists.driver-core,org.kernel.vger.rust-for-linux
Message-ID <[email protected]>
On Sat Jul 11, 2026 at 8:04 AM JST, Timur Tabi wrote:
> 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 <[email protected]>
> ---
>  Documentation/gpu/nova/core/tlv.rst   | 187 ++++++++++++++++++

We should add a link to this new document in
`Documentation/gpu/nova/index.rst`.

>  drivers/gpu/nova-core/firmware.rs     |   1 +
>  drivers/gpu/nova-core/firmware/tlv.rs | 274 ++++++++++++++++++++++++++
>  3 files changed, 462 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..9e8c08727977
> --- /dev/null
> +++ b/Documentation/gpu/nova/core/tlv.rst
> @@ -0,0 +1,187 @@
> +.. SPDX-License-Identifier: (GPL-2.0+ OR MIT)
> +
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +TLV Tags in Nova Firmware Images
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=
=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +
> +Nova firmware images use a Type-Length-Value (TLV) format to encapsulate
> +firmware components and metadata. The TLV file begins with a 4-byte "mag=
ic"
> +header that contains the string "NVFW".  Following the header is a seque=
nce 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, th=
e 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 p=
arser.
> +It is the responsibility of the parser to handle missing or malformed ta=
gs,
> +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
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +TLV tags are always four-character words, with all letters being upper c=
ase.
> +Duplicate tags are not allowed.
> +
> +Lengths of zero are allowed and indicate that the tag is a boolean.  Tha=
t is,

Let's mention "Payloads of length zero" to be precise what length this
is about.

> +presence of the tag indicates ``True`` and absence indicates ``False``.
> +
> +A TLV file may contain additional tags not described in this document.
> +
> +Values
> +=3D=3D=3D=3D=3D=3D
> +Values are one of three types.  The type is not encoded in the format; r=
ather,
> +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 ASCII only and without a nu=
ll terminator.
> +3) An array of bytes, for binary data.

I guess booleans adds a fourth value type?

> +
> +Common Tags
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +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 parti=
cular
> +firmware type.
> +
> +``VERS`` (string)
> +    Human-readable firmware version string, indicates the version of
> +    the firmware.  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 cont=
ains
> +    the actual firmware image bytes.
> +
> +``FILE`` (string)
> +    If the firmware binary is stored as a separate file, this tag contai=
ns 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 befor=
e
> +    loading the file.
> +
> +``SIZE`` (u32)
> +    Total size in bytes of the firmware image to be loaded from the comp=
anion
> +    file named by ``FILE``.  This tag is mandatory if ``FILE`` exists, s=
o the
> +    size of the firmware image must be known when the TLV is created.  I=
f the
> +    firmware image is updated and its size changes, then the TLV must be
> +    updated with it.
> +
> +GSP Firmware Tags
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +``SIGN`` (bytes)
> +    Cryptographic signature for the GSP firmware.
> +
> +``BLID`` (bytes)
> +    The build ID, extracted from the ".note.gnu.build-id" section.

Should this be a string by any chance?

> +
> +Booter Firmware Tags
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +``DAOF`` (u32) - ``os_data_offset``
> +    OS data section offset within the firmware image (absolute byte offs=
et).
> +    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 offs=
et).
> +    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 wh=
ere the
> +    selected signature should be written.
> +
> +``FUSE`` (u32) - ``fuse_version``
> +    Fuse version of the firmware, used with the hardware fuse register t=
o
> +    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 sign=
ature
> +    fuse registers.
> +
> +``A0CO`` (u32) - ``app0_code_offset``
> +    App0 code offset -- start of the secure code region within the firmw=
are
> +    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.  A value of 0 ind=
icates
> +    unsigned firmware and that there is no ``SIGN`` tag.

In patch 4 `booter.rs` says "Booter is always signed", which contradicts
this definition. We had a non-signed path in the original code; why not
preserve it? The current firmware files do not make use of it, but the
point of specifying things here is to make the code future-proof in case
we introduce or enable unsigned firmwares in the future; so let's make
the code religiously follow the spec.

> +
> +``SIGN`` (bytes)
> +    Concatenated array of firmware signatures. The size of each signatur=
e is
> +    the total length of the ``SIGN`` value divided by ``NSIG``. The corr=
ect
> +    signature is selected using the fuse-version-derived index.
> +
> +Generic Bootloader Tags
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +``CDSZ`` (u32) - ``code_size``
> +    Size in bytes of the bootloader code to copy from the ``BLOB`` tag a=
nd
> +    PIO-load into falcon IMEM.
> +
> +``STRT`` (u32) - ``start_tag``
> +    Start tag identifying the IMEM block where execution begins.  The fa=
lcon
> +    boot address is derived as ``start_tag << 8``.
> +
> +GSP Bootloader Tags
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +``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
> +=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D
> +``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-3=
072),
> +    but may be shorter.
> +
> +``SIGN`` (bytes)
> +    Signature of the FMC firmware. At most 384 bytes (RSA-3072), but may
> +    be shorter.
> \ No newline at end of file
> diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/fi=
rmware.rs
> index a94820a3b335..c0cd06579643 100644
> --- a/drivers/gpu/nova-core/firmware.rs
> +++ b/drivers/gpu/nova-core/firmware.rs
> @@ -32,6 +32,7 @@
>  pub(crate) mod fwsec;
>  pub(crate) mod gsp;
>  pub(crate) mod riscv;
> +pub(crate) mod tlv;
> =20
>  pub(crate) const FIRMWARE_VERSION: &str =3D "570.144";
> =20
> diff --git a/drivers/gpu/nova-core/firmware/tlv.rs b/drivers/gpu/nova-cor=
e/firmware/tlv.rs
> new file mode 100644
> index 000000000000..5dfd2dd814f8
> --- /dev/null
> +++ b/drivers/gpu/nova-core/firmware/tlv.rs
> @@ -0,0 +1,274 @@
> +// SPDX-License-Identifier: GPL-2.0
> +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFIL=
IATES. 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 =3D chipset.name();
> +
> +    dev_dbg!(
> +        dev,
> +        "loading firmware image {}/gsp/{}.tlv\n",

This is missing `nvidia` in the path. To avoid issue I would suggest
building the `CString` earlier and using it both here and in the
`try_from_fmt` statement below.

<...>
> +    /// 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 =3D self.find(tag)?;
> +
> +        // Treat empty value as an error, to avoid trying to parse nothi=
ng.
> +        if tlv.value.is_empty() {
> +            return Err(ENODATA);
> +        }
> +
> +        Ok(tlv.value)
> +    }
> +
> +    // Return a little-endian u32.

Doccomment should use `///`.

> +    pub(crate) fn get_u32(&self, tag: &[u8; 4]) -> Result<u32> {
> +        let tlv =3D 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 =3D self.find(tag)?;
> +
> +        let bytes =3D tlv.value;
> +
> +        // To make sure the value actually is a string, ensure it's all =
ASCII.
> +        if !bytes.is_ascii() {
> +            return Err(EINVAL);
> +        }

The spec also says that NULL characters are invalid; we should test for
`|| bytes.contains(&0)` as well here.

> +
> +        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 =3D match self.get_u32(b"NSIG")? {
> +            0 =3D> return Err(EINVAL),
> +            n =3D> n.into_safe_cast(),
> +        };
> +
> +        let sig_bytes =3D self.get_bytes(b"SIGN")?;
> +
> +        // Ensure that sig_bytes can be divided evenly into chunks.
> +        if sig_bytes.len() % num_sigs !=3D 0 {
> +            return Err(EINVAL);
> +        }
> +
> +        // num_sigs cannot be 0, and sig_bytes cannot be empty, so this =
cannot panic.
> +        let sig_size =3D sig_bytes.len() / num_sigs;
> +
> +        let index: usize =3D match index {
> +            None =3D> num_sigs - 1,
> +            Some(index) =3D> index,
> +        };

This can be a one-liner:

  let index =3D index.unwrap_or(num_sigs - 1);