[PATCH v2 17/31] gpu: nova-core: add GMC transport receive path

From: John Hubbard

Date: Fri Aug 21 2026 - 21:57:49 EST


GSP-RM posts GMC and RPC messages on the same message queue. Both use
the same MCTP and NVDM transport headers, but RPC messages continue with
rpc_message_header_v and GMC messages continue with GmcApiHeader.
Existing RPC receive code cannot parse the GMC layout.

Add a GMC receive path. It checks the MCTP magic, the MCTP header
version and the NVIDIA vendor id, and requires the declared element
length to lie between the element header size and the maximum queue
element size, matching the checks Open RM makes on arriving elements.

Poison the queue on any of those failures. The read pointer advances by
the declared length, so without a trusted length the driver cannot move
past the element at all.

Assisted-by: Cursor:claude-opus-5
Reviewed-by: Timur Tabi <ttabi@xxxxxxxxxx>
Signed-off-by: John Hubbard <jhubbard@xxxxxxxxxx>
---
drivers/gpu/nova-core/gsp/cmdq.rs | 87 ++++++++++++++++++++++++++++++-
drivers/gpu/nova-core/gsp/fw.rs | 54 +++++++++++++++++++
drivers/gpu/nova-core/mctp.rs | 14 ++++-
3 files changed, 152 insertions(+), 3 deletions(-)

diff --git a/drivers/gpu/nova-core/gsp/cmdq.rs b/drivers/gpu/nova-core/gsp/cmdq.rs
index 6470b1b673b6..96128c418dc9 100644
--- a/drivers/gpu/nova-core/gsp/cmdq.rs
+++ b/drivers/gpu/nova-core/gsp/cmdq.rs
@@ -629,7 +629,7 @@ struct GspCommand<'a, H = GspMsgElement> {

/// A message ready to be processed from the message queue.
///
-/// This is the type returned by [`Cmdq::wait_for_msg`].
+/// This is the type returned by [`CmdqInner::wait_for_msg`].
struct GspMessage<'a> {
// Reference to the header of the message.
header: &'a GspMsgElement,
@@ -638,6 +638,18 @@ struct GspMessage<'a> {
contents: (&'a [u8], &'a [u8]),
}

+/// A GMC message ready to be processed from the message queue.
+///
+/// This is the type returned by [`CmdqInner::wait_for_gmc_msg`].
+#[expect(dead_code)]
+struct GmcMessage<'a> {
+ // Reference to the header of the message.
+ header: &'a GspGmcMsgElement,
+ // Slices of the payload following the `GmcApiHeader`. The second slice is empty unless the
+ // payload wraps around the end of the message queue.
+ contents: (&'a [u8], &'a [u8]),
+}
+
/// GSP command queue.
///
/// Provides the ability to send commands and receive messages from the GSP using a shared memory
@@ -1285,4 +1297,77 @@ fn drain(&mut self) -> Result {

Ok(())
}
+
+ /// Wait for a GMC message to become available on the message queue.
+ ///
+ /// This is the GMC counterpart to [`Self::wait_for_msg`] and reads the same queue. Like that
+ /// method it works purely at the transport layer, validating the MCTP framing and the
+ /// advertised length and nothing else.
+ ///
+ /// A [`GspGmcMsgElement`] and a [`GspMsgElement`] share every field through `nvdm_header`,
+ /// so a caller that may see either must check the NVDM type before reading `gmc`. Both
+ /// layouts put the element length in the same place, so the caller can advance the read
+ /// pointer past a returned message either way.
+ ///
+ /// # Errors
+ ///
+ /// - `ETIMEDOUT` if `timeout` has elapsed before any message becomes available.
+ /// - `EIO` if the framing is invalid, or the queue was already poisoned by an earlier such
+ /// failure. Either failure poisons the queue, so recovery requires a reset.
+ #[expect(dead_code)]
+ fn wait_for_gmc_msg(&self, timeout: Delta) -> Result<GmcMessage<'_>> {
+ if self.poisoned.get() {
+ return Err(EIO);
+ }
+
+ let (slice_1, slice_2) = read_poll_timeout(
+ || Ok(self.gsp_mem.driver_read_area()),
+ |driver_area| !driver_area.0.is_empty(),
+ Delta::from_millis(1),
+ timeout,
+ )
+ .map(|(slice_1, slice_2)| (slice_1.as_flattened(), slice_2.as_flattened()))?;
+
+ let Some((header, slice_1)) = GspGmcMsgElement::from_bytes_prefix(slice_1) else {
+ self.poisoned.set(true);
+ return Err(EIO);
+ };
+
+ // Checked before any length field is read, since bad framing leaves them untrusted.
+ if let Err(e) = header.validate_framing() {
+ dev_err!(
+ &self.dev,
+ "GSP GMC: receive: bad MCTP framing, declared length {}\n",
+ header.length(),
+ );
+ self.poisoned.set(true);
+ return Err(e);
+ }
+
+ let payload_length = header.payload_length();
+
+ // Check that the driver read area is large enough for the message.
+ if slice_1.len() + slice_2.len() < payload_length {
+ self.poisoned.set(true);
+ return Err(EIO);
+ }
+
+ // Cut the message slices down to the actual length of the message.
+ let (slice_1, slice_2) = if slice_1.len() > payload_length {
+ // PANIC: we checked above that `slice_1` is at least as long as `payload_length`.
+ (slice_1.split_at(payload_length).0, &slice_2[0..0])
+ } else {
+ (
+ slice_1,
+ // PANIC: we checked above that `slice_1.len() + slice_2.len()` is at least as
+ // large as `payload_length`.
+ slice_2.split_at(payload_length - slice_1.len()).0,
+ )
+ };
+
+ Ok(GmcMessage {
+ header,
+ contents: (slice_1, slice_2),
+ })
+ }
}
diff --git a/drivers/gpu/nova-core/gsp/fw.rs b/drivers/gpu/nova-core/gsp/fw.rs
index 2d7ea46332e6..bbcc2e2b8401 100644
--- a/drivers/gpu/nova-core/gsp/fw.rs
+++ b/drivers/gpu/nova-core/gsp/fw.rs
@@ -955,6 +955,39 @@ unsafe impl FromBytes for GspMsgElement {}
/// Magic value that opens every MCTP-framed queue element: `"MCTP"` in ASCII.
const MCTP_MAGIC: u32 = 0x4D43_5450;

+/// Validates the MCTP and NVDM framing that opens an MCTP-framed queue element.
+///
+/// `element_size` is the size of the decoded element header, and is the smallest length a
+/// well-formed element can declare.
+///
+/// # Errors
+///
+/// - `EIO` if the magic, the MCTP version or the NVIDIA vendor id is wrong, or if the declared
+/// element length lies outside `element_size..=GSP_MSG_QUEUE_ELEMENT_SIZE_MAX`. The caller
+/// must treat every one of these as leaving the whole element untrusted, its length fields
+/// included.
+fn validate_mctp_framing(
+ magic: u32,
+ mctp_payload_size: u32,
+ mctp_header: MctpHeader,
+ nvdm_header: NvdmHeader,
+ element_size: usize,
+) -> Result {
+ if magic != MCTP_MAGIC
+ || !mctp_header.has_expected_version()
+ || !nvdm_header.has_nvidia_vendor()
+ {
+ return Err(EIO);
+ }
+
+ let length = num::u32_as_usize(mctp_payload_size);
+ if length < element_size || length > GSP_MSG_QUEUE_ELEMENT_SIZE_MAX {
+ return Err(EIO);
+ }
+
+ Ok(())
+}
+
/// GMC API message header.
///
/// Matches the `GMCAPI_HEADER` struct from Open RM. The `command` field carries
@@ -1075,11 +1108,32 @@ pub(crate) fn init(
})
}

+ /// Returns the length of the response payload (data after the [`GmcApiHeader`]).
+ pub(crate) fn payload_length(&self) -> usize {
+ num::u32_as_usize(self.nvdm_payload_size).saturating_sub(size_of::<GmcApiHeader>())
+ }
+
/// Returns the total length of the message, transport and GMC headers included.
pub(crate) fn length(&self) -> usize {
num::u32_as_usize(self.mctp_payload_size)
}

+ /// Validates the transport framing, before any length field in the element is trusted.
+ ///
+ /// # Errors
+ ///
+ /// - `EIO` if the MCTP magic, the MCTP version, the NVDM vendor id, or the declared element
+ /// length is not one this driver accepts.
+ pub(crate) fn validate_framing(&self) -> Result {
+ validate_mctp_framing(
+ self.mctp_magic,
+ self.mctp_payload_size,
+ self.mctp_header,
+ self.nvdm_header,
+ size_of::<Self>(),
+ )
+ }
+
/// Returns the number of elements (i.e. memory pages) used by this message.
pub(crate) fn element_count(&self) -> u32 {
self.mctp_payload_size
diff --git a/drivers/gpu/nova-core/mctp.rs b/drivers/gpu/nova-core/mctp.rs
index 0ae88bef2a05..5980c356e919 100644
--- a/drivers/gpu/nova-core/mctp.rs
+++ b/drivers/gpu/nova-core/mctp.rs
@@ -67,6 +67,11 @@ pub(crate) fn single_packet() -> Self {
pub(crate) fn is_single_packet(self) -> bool {
self.som().into_bool() && self.eom().into_bool()
}
+
+ /// Returns whether the header carries the MCTP version this driver speaks.
+ pub(crate) fn has_expected_version(self) -> bool {
+ u32::from(self.version()) == Self::VERSION
+ }
}

/// MCTP message type for PCI vendor-defined messages.
@@ -93,10 +98,15 @@ pub(crate) fn new(nvdm_type: NvdmType) -> Self {
.with_nvdm_type(nvdm_type)
}

- /// Validates this header against the expected NVIDIA NVDM format and type.
- pub(crate) fn validate(self, expected_type: NvdmType) -> bool {
+ /// Returns whether this is an NVIDIA vendor-defined MCTP message, whatever its NVDM type.
+ pub(crate) fn has_nvidia_vendor(self) -> bool {
u8::from(self.msg_type()) == MSG_TYPE_VENDOR_PCI
&& u16::from(self.vendor_id()) == Vendor::NVIDIA.as_raw()
+ }
+
+ /// Validates this header against the expected NVIDIA NVDM format and type.
+ pub(crate) fn validate(self, expected_type: NvdmType) -> bool {
+ self.has_nvidia_vendor()
&& matches!(self.nvdm_type(), Ok(nvdm_type) if nvdm_type == expected_type)
}
}
--
2.55.0