Linux driver-core infrastructure
 help / color / mirror / Atom feed
From: "Alexandre Courbot" <acourbot@nvidia.com>
To: "Timur Tabi" <ttabi@nvidia.com>
Cc: <driver-core@lists.linux.dev>, <nova-gpu@lists.linux.dev>,
	<rust-for-linux@vger.kernel.org>,
	"Danilo Krummrich" <dakr@kernel.org>,
	"Eliot Courtney" <ecourtney@nvidia.com>,
	"Zhi Wang" <zhiw@nvidia.com>,
	"John Hubbard" <jhubbard@nvidia.com>,
	"Luis Chamberlain" <mcgrof@kernel.org>,
	"Russ Weight" <russ.weight@linux.dev>,
	"Miguel Ojeda" <ojeda@kernel.org>, "Gary Guo" <gary@garyguo.net>
Subject: Re: [PATCH v3 2/7] gpu: nova-core: add TLV parser for firmware files
Date: Mon, 06 Jul 2026 15:31:22 +0900	[thread overview]
Message-ID: <DJR9ZWAN4OCT.3GRDS8LGOZ16Y@nvidia.com> (raw)
In-Reply-To: <20260702192712.3450652-3-ttabi@nvidia.com>

On Fri Jul 3, 2026 at 4:27 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 <ttabi@nvidia.com>
> ---
>  Documentation/gpu/nova/core/tlv.rst   | 182 +++++++++++++++++++++
>  drivers/gpu/nova-core/firmware.rs     |   1 +
>  drivers/gpu/nova-core/firmware/tlv.rs | 225 ++++++++++++++++++++++++++
>  3 files changed, 408 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..e4eb6ab6d02f
> --- /dev/null
> +++ b/Documentation/gpu/nova/core/tlv.rst
> @@ -0,0 +1,182 @@
> +.. 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.

I guess that last paragraph is kind of expected - spelling it out loud
without being specific as to how to handle missing or malformed tags is
just confusing. I think it can just be dropped?

> +
> +::
> +
> +    +------+------+------+------+
> +    |  '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.
> +
> +Lengths of zero are allowed and indicate that the tag is a boolean.  That is,
> +presence of the tag indicates ``True`` and absence indicates ``False``.
> +
> +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 expected to be ASCII only, without a null terminator.

"expected" leaves a window of interpretation - let's say "required" or
just "encoded in ASCII without a null terminator".

> +3) An array of bytes, for binary data.
> +
> +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, 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 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.
> +
> +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.  A value of 0 indicates
> +    unsigned firmware and that there is no ``SIGN`` tag.

Shall we say "A value of 0 or absence of this tag" for coverage? ... Or
not, see my review of patch 3.

> +
> +``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.
> \ No newline at end of file
> diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.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;
>  
>  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..56e0d5cab580
> --- /dev/null
> +++ b/drivers/gpu/nova-core/firmware/tlv.rs
> @@ -0,0 +1,225 @@
> +// 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;
> +
> +/// 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();
> +
> +    dev_dbg!(
> +        dev,
> +        "loading firmware image {}/gsp/{}.tlv\n",
> +        chip_name,
> +        name
> +    );
> +
> +    CString::try_from_fmt(fmt!("nvidia/{chip_name}/gsp/{name}.tlv"))
> +        .and_then(|path| firmware::Firmware::request(&path, dev))
> +}
> +
> +struct TlvBlock<'a> {
> +    tag: &'a [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<'a> {
> +    tag: &'a [u8; 4],

This struct doesn't need a lifetime - it can store `tag` as `[u8; 4]`.
Same for the `tag` member of `TlvBlock`. Everything above them (`find`,
`get_bytes`, etc) can still take a reference to the array for
convenience as we use the `b"ABCD"` syntax, but for these two the
reference is just inefficient without reason.

> +    length: usize,
> +}
> +
> +impl<'a> TlvBlockHeader<'a> {
> +    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: &'a [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::from_le_bytes(len_arr) as usize;

Let's avoid using the potentially lossy `as` keyword:

    let length = num::u32_as_usize(u32::from_le_bytes(len_arr));

(after importing `crate::num`).

> +        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 += advance;

Maybe use `checked_add` here as well for consistency.

> +
> +        Some(TlvBlock {
> +            tag: header.tag,
> +            value,
> +        })
> +    }
> +}
> +
> +/// The payload of a validated TLV (type, length, value) firmware image.

I think we can just say "A validated TLV", as payload makes it sound
like this is the payload of a given key within the TLV firmware.

> +///
> +/// 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
> +/// 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.
> +///
> +/// # 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(EINVAL);
> +        }
> +
> +        let mut rest = payload;
> +        while !rest.is_empty() {
> +            // Validate and extract the header (type, length).
> +            let Some(header) = rest
> +                .get(..TlvBlockHeader::SIZE)
> +                .and_then(TlvBlockHeader::parse)
> +            else {
> +                return Err(EINVAL);
> +            };
> +            // 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)?;
> +
> +            if length > rest.len() {
> +                return Err(EINVAL);
> +            }
> +
> +            // Advance to the next block. `length <= rest.len()` was just checked, so this
> +            // slice is always in bounds and lands on the next block boundary (or empties
> +            // `rest` after the final block).
> +            rest = &rest[length..];

The check on length and slice split can be factored out into this
one-liner:

    rest = rest.split_at_checked(length).ok_or(EINVAL)?.1;

It is preferable not only because it is shorter; the guarantee of
correctness is carried by the split operation itself, and as a result
you also don't need a comment justifying that you just performed the
check.

> +        }
> +
> +        Ok(Self { data: payload })

As mentioned by Sashiko, this needs an `INVARIANT:` comment.

> +    }
> +
> +    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)
> +    }
> +
> +    pub(crate) fn get_bytes(&self, tag: &[u8; 4]) -> Result<&'a [u8]> {

These methods are simple, but let's grant them a one-line doccomment for
consistency.

One thing I'd like to eventually add (for the record; as a follow-up is
ok) is an abstraction of the `BLOB`/`FILE` distinction: maybe a
`get_payload` method that returns either a reference to the `BLOB`
section, or creates a `VVec` to load the references firmware file. Its
return type would be an `enum` with an `AsRef<[u8]>` implementation and
a `to_owned(self) -> VVec<u8>` method so individual firmware
implementations can get the representation they need without caring
about the underlying storage type.

  parent reply	other threads:[~2026-07-06  6:31 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-02 19:27 [PATCH v3 0/7] Transition Nova Core to TLV firmare images Timur Tabi
2026-07-02 19:27 ` [PATCH v3 1/7] rust: firmware: add request_into_buf() Timur Tabi
2026-07-03  2:51   ` Alvin Sun
2026-07-03  3:06     ` Timur Tabi
2026-07-03 13:51       ` Danilo Krummrich
2026-07-06  6:30   ` Alexandre Courbot
2026-07-06 10:50     ` Danilo Krummrich
2026-07-02 19:27 ` [PATCH v3 2/7] gpu: nova-core: add TLV parser for firmware files Timur Tabi
2026-07-02 19:45   ` Timur Tabi
2026-07-06  6:31     ` Alexandre Courbot
2026-07-06  6:31   ` Alexandre Courbot [this message]
2026-07-02 19:27 ` [PATCH v3 3/7] gpu: nova-core: transition booter_load to TLV images Timur Tabi
2026-07-06  6:31   ` Alexandre Courbot
2026-07-02 19:27 ` [PATCH v3 4/7] gpu: nova-core: transition gsp " Timur Tabi
2026-07-02 19:27 ` [PATCH v3 5/7] gpu: nova-core: transition gen_bootloader " Timur Tabi
2026-07-06  6:31   ` Alexandre Courbot
2026-07-02 19:27 ` [PATCH v3 6/7] gpu: nova-core: transition fsp " Timur Tabi
2026-07-02 19:27 ` [PATCH v3 7/7] gpu: nova-core: update firmware module info for " Timur Tabi

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=DJR9ZWAN4OCT.3GRDS8LGOZ16Y@nvidia.com \
    --to=acourbot@nvidia.com \
    --cc=dakr@kernel.org \
    --cc=driver-core@lists.linux.dev \
    --cc=ecourtney@nvidia.com \
    --cc=gary@garyguo.net \
    --cc=jhubbard@nvidia.com \
    --cc=mcgrof@kernel.org \
    --cc=nova-gpu@lists.linux.dev \
    --cc=ojeda@kernel.org \
    --cc=russ.weight@linux.dev \
    --cc=rust-for-linux@vger.kernel.org \
    --cc=ttabi@nvidia.com \
    --cc=zhiw@nvidia.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox