From: "Danilo Krummrich" <dakr@kernel.org>
To: "Timur Tabi" <ttabi@nvidia.com>
Cc: "Gary Guo" <gary@garyguo.net>,
"Alexandre Courbot" <acourbot@nvidia.com>,
<nova-gpu@lists.linux.dev>,
"Eliot Courtney" <ecourtney@nvidia.com>,
"John Hubbard" <jhubbard@nvidia.com>, <zhiw@nvidia.com>
Subject: Re: [PATCH 3/8] gpu: nova-core: add TLV parser for firmware files
Date: Thu, 11 Jun 2026 00:37:35 +0200 [thread overview]
Message-ID: <DJ5Q9J19NVQO.15OGQVSCIKO06@kernel.org> (raw)
In-Reply-To: <20260610174929.744477-4-ttabi@nvidia.com>
On Wed Jun 10, 2026 at 7:49 PM CEST, Timur Tabi wrote:
> diff --git a/Documentation/gpu/nova/core/tlv.rst b/Documentation/gpu/nova/core/tlv.rst
> new file mode 100644
> index 000000000000..8223605daa6b
> --- /dev/null
> +++ b/Documentation/gpu/nova/core/tlv.rst
> @@ -0,0 +1,172 @@
> +.. SPDX-License-Identifier: (GPL-2.0+ OR MIT)
> +
> +==================================
> +TLV Tags in Nova Firmware Images
> +==================================
Thanks for adding this!
> diff --git a/drivers/gpu/nova-core/firmware.rs b/drivers/gpu/nova-core/firmware.rs
> index 2749c196416d..888a426b7b41 100644
> --- a/drivers/gpu/nova-core/firmware.rs
> +++ b/drivers/gpu/nova-core/firmware.rs
> @@ -11,7 +11,7 @@
> device,
> firmware,
> prelude::*,
> - str::CString,
> + str::{CStr, CString},
NIT: format
> transmute::FromBytes, //
> };
>
> @@ -684,3 +684,240 @@ pub(super) fn elf_section<'a>(elf: &'a [u8], name: &str) -> Option<&'a [u8]> {
> }
> }
> }
> +
> +pub(crate) struct TlvBlock<'a> {
> + pub(crate) tag: &'a str,
> + pub(crate) 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 str,
> + 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_bytes = hdr.get(..4)?;
> + let tag = core::str::from_utf8(tag_bytes).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;
Here and in a couple other cases, nova-core depends on !CPU_BIG_ENDIAN, so that
shouldn't be necessary.
I'm not concerned to keep it for documentation purposes, but it may be a bit
inconsistent with the rest of the driver.
> + Some(Self { tag, length })
> + }
> +}
> +
> +/// Sequential scan over [`Tlv`]. Unlike [`Tlv`], this type carries a cursor (`pos`) into
> +/// the parent blob. It is not interchangeable with a fresh [`Tlv`] view of the same
> +/// bytes.
> +///
> +/// # 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.
> + ///
> + /// The body relies on a number of `unsafe` operations, because [`Iterator::next`] can
> + /// only return [`Option`], not [`Result`], so there is no way to report a parse error
> + /// from here. That is why the entire TLV stream is validated up front in the
> + /// constructor. By this type's invariants, `self.pos` is always on a block boundary
> + /// into already-validated data, so each block is known to be well-formed, and
> + /// therefore the unchecked operations cannot fail.
> + fn next(&mut self) -> Option<Self::Item> {
> + if self.pos >= self.tlv.data.len() {
> + return None;
> + }
> +
> + let tail = &self.tlv.data[self.pos..];
> +
> + // SAFETY: `self.pos` is on a block boundary (`TlvIter` invariant) and the check
> + // above gives `self.pos < data.len()`, so by the `Tlv` invariant a complete
> + // well-formed block starts at `tail`.
> + let hdr = unsafe { tail.get_unchecked(..TlvBlockHeader::SIZE) };
> +
> + // SAFETY: same header bytes as validated in `Tlv::new` for this offset.
> + let header = unsafe { TlvBlockHeader::parse(hdr).unwrap_unchecked() };
> +
> + let stored_size = header.length.next_multiple_of(4);
> + let advance = TlvBlockHeader::SIZE + stored_size;
> + let payload_end = TlvBlockHeader::SIZE + header.length;
> +
> + // SAFETY: `advance` and `payload_end` are exactly the stored and logical payload extents
> + // `Tlv::new` accepted for this block.
> + let value = unsafe {
> + let block = tail.get_unchecked(..advance);
> + block.get_unchecked(TlvBlockHeader::SIZE..payload_end)
> + };
Given that this method returns an Option anyways, this could just use the
corresponding safe accessors, e.g.
let hdr = tail.get(..TlvBlockHeader::SIZE)?;
Alternatively, we can just use panicking accessors with a '// PANIC' comment,
but again, since the function already returns an Option, this isn't a hot path,
and the panic comments aren't obviously trivial, it might not be worth it in
this case.
That said, I'm fine with either safe or panicking, but I don't like the unsafe
accessors.
> +
> + // 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;
> +
> + Some(TlvBlock {
> + tag: header.tag,
> + value,
> + })
> + }
> +}
> +
> +/// The payload 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
> +/// 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.
> +#[allow(dead_code)]
> +pub(crate) struct Tlv<'a> {
> + data: &'a [u8],
> +}
> +
> +#[allow(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)?;
> +
> + let mut pos = 0usize;
> + while pos < payload.len() {
> + // Get the next TLV block.
> + let Some(rest) = payload.get(pos..) else {
This check doesn't seem to be needed because the loop constraints it directly
above, so I'd use let rest = &payload[pos..] instead in this case.
> + return Err(EINVAL);
> + };
> + // 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 end = pos
> + .checked_add(TlvBlockHeader::SIZE)
> + .and_then(|p| p.checked_add(stored_size))
> + .ok_or(EINVAL)?;
> + if end > payload.len() {
> + return Err(EINVAL);
> + }
> + pos = end;
> + }
> +
> + // INVARIANT: the loop above walked `payload` from offset 0 and only stopped once `pos`
> + // reached `payload.len()` exactly, rejecting any block whose header failed to parse or
> + // whose stored extent overran. So `payload` is an exact concatenation of well-formed
> + // blocks with no partial header or trailing 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 }
> + }
> +
> + pub(crate) fn len(&self, tag: &str) -> Result<usize> {
> + let tlv = self.iter().find(|b| b.tag == tag).ok_or(EINVAL)?;
> +
> + Ok(tlv.value.len())
> + }
> +
> + pub(crate) fn get_bytes(&self, tag: &str) -> Result<&'a [u8]> {
> + let tlv = self.iter().find(|b| b.tag == tag).ok_or(EINVAL)?;
> +
> + Ok(tlv.value)
> + }
> +
> + pub(crate) fn get_u32(&self, tag: &str) -> Result<u32> {
> + let tlv = self.iter().find(|b| b.tag == tag).ok_or(EINVAL)?;
> +
> + tlv.value
> + .try_into()
> + .ok()
> + .map(u32::from_le_bytes)
> + .ok_or(EINVAL)
> + }
> +
> + pub(crate) fn get_string(&self, tag: &str) -> Result<&'a str> {
> + let tlv = self.iter().find(|b| b.tag == tag).ok_or(EINVAL)?;
> +
> + // Handle the possibility that the value is null-terminated.
> + let bytes = match CStr::from_bytes_until_nul(tlv.value) {
> + Ok(cstr) => cstr.to_bytes(),
> + Err(_) => tlv.value,
> + };
> +
> + // But do require it to be all ASCII.
> + if !bytes.is_ascii() {
> + return Err(EINVAL);
> + }
> +
> + core::str::from_utf8(bytes).map_err(|_| EINVAL)
> + }
Would it make sense to provide those accessors on TlvBlock, so we don't have to
create a fresh TlvIter and scan from the start every time?
> +
> + /// Returns the `n`-th fixed-size chunk from the value of the TLV entry matching
> + /// `tag`.
> + ///
> + /// The value is treated as an array of contiguous, equally-sized chunks, each
> + /// `sig_size` bytes long. `sig_size` is the size of one signature. The returned
> + /// slice is the `n`-th chunk.
> + ///
> + /// Returns [`EINVAL`] if no entry matches `tag`, if computing the chunk's offset
> + /// overflows, or if the value is too short to contain the requested chunk.
> + pub(crate) fn get_nth_chunk(&self, tag: &str, sig_size: usize, n: usize) -> Result<&'a [u8]> {
> + let sigs = self
> + .iter()
> + .find(|b| b.tag == tag)
> + .map(|b| b.value)
> + .ok_or(EINVAL)?;
> +
> + let start = sig_size.checked_mul(n).ok_or(EINVAL)?;
> + let end = start.checked_add(sig_size).ok_or(EINVAL)?;
> +
> + sigs.get(start..end).ok_or(EINVAL)
> + }
> +}
> --
> 2.54.0
next prev parent reply other threads:[~2026-06-10 22:37 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-10 17:49 [PATCH 0/8] Transition Nova Core to TLV firmware images Timur Tabi
2026-06-10 17:49 ` [PATCH 1/8] rust: firmware: add request_into_buf() Timur Tabi
2026-06-10 18:03 ` Gary Guo
2026-06-10 18:24 ` Timur Tabi
2026-06-10 20:26 ` Gary Guo
2026-06-10 20:28 ` Timur Tabi
2026-06-10 21:46 ` Danilo Krummrich
2026-06-11 4:58 ` Alexandre Courbot
2026-06-11 6:49 ` Alexandre Courbot
2026-06-10 17:49 ` [PATCH 2/8] gpu: nova-core: add request_tlv to load TLV images Timur Tabi
2026-06-10 22:00 ` Danilo Krummrich
2026-06-10 17:49 ` [PATCH 3/8] gpu: nova-core: add TLV parser for firmware files Timur Tabi
2026-06-10 22:37 ` Danilo Krummrich [this message]
2026-06-11 6:51 ` Alexandre Courbot
2026-06-10 17:49 ` [PATCH 4/8] gpu: nova-core: transition booter_load to TLV images Timur Tabi
2026-06-10 17:49 ` [PATCH 5/8] gpu: nova-core: transition gsp " Timur Tabi
2026-06-10 17:49 ` [PATCH 6/8] gpu: nova-core: transition gen_bootloader " Timur Tabi
2026-06-10 17:49 ` [PATCH 7/8] gpu: nova-core: transition fsp " Timur Tabi
2026-06-10 17:49 ` [PATCH 8/8] gpu: nova-core: update firmware module info for " Timur Tabi
2026-06-10 18:16 ` [PATCH 0/8] Transition Nova Core to TLV firmware images John Hubbard
2026-06-10 18:19 ` Timur Tabi
2026-06-10 18:59 ` Timur Tabi
2026-06-10 21:21 ` John Hubbard
2026-06-10 21:43 ` 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=DJ5Q9J19NVQO.15OGQVSCIKO06@kernel.org \
--to=dakr@kernel.org \
--cc=acourbot@nvidia.com \
--cc=ecourtney@nvidia.com \
--cc=gary@garyguo.net \
--cc=jhubbard@nvidia.com \
--cc=nova-gpu@lists.linux.dev \
--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