blob: ab685fb4168f2f1a9e61b57c7de39200e5984a9a [file] [edit]
// SPDX-License-Identifier: GPL-2.0
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
//! FSP (Foundation Security Processor) interface for Hopper/Blackwell GPUs.
//!
//! Hopper/Blackwell use a simplified firmware boot sequence: FMC, then FSP, then GSP.
//! Unlike Turing/Ampere/Ada, there is no SEC2 (Security Engine 2) usage.
//! FSP handles secure boot directly using FMC firmware and Chain of Trust.
use kernel::{
device,
dma::Coherent,
io::poll::read_poll_timeout,
num::TryIntoBounded,
prelude::*,
ptr::{
Alignable,
Alignment, //
},
sizes::SZ_2M,
time::Delta,
transmute::{
AsBytes,
FromBytes, //
},
};
use crate::{
driver::Bar0,
falcon::{
fsp::Fsp as FspEngine,
Falcon, //
},
fb::FbSizes,
firmware::fsp::{
FmcSignatures,
FspFirmware, //
},
gpu::Chipset,
gsp::{
GspFmcBootParams,
GspFwWprMeta,
LibosMemoryRegionInitArgument, //
},
mctp::{
MctpHeader,
NvdmHeader,
NvdmType, //
},
num,
regs, //
};
mod hal;
/// PRC message sub-command.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum PrcMessageSubcmd {
/// Read a PRC knob value.
Read = 0x0c,
}
impl From<PrcMessageSubcmd> for u8 {
fn from(value: PrcMessageSubcmd) -> Self {
value as u8
}
}
/// PRC object identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum PrcObjectId {
/// vGPU mode configuration knob.
VgpuMode = 0x29,
}
impl From<PrcObjectId> for u8 {
fn from(value: PrcObjectId) -> Self {
value as u8
}
}
kernel::impl_flags!(
/// PRC request flags.
#[derive(Clone, Copy, Default, PartialEq, Eq)]
struct PrcFlags(u8);
/// Individual PRC request flag.
#[derive(Clone, Copy, PartialEq, Eq)]
enum PrcFlag {
/// Request the active knob value for the current boot.
Active = 1 << 1,
}
);
/// vGPU operating mode as reported by FSP via the PRC protocol.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VgpuMode {
/// vGPU support is disabled on this GPU.
Disabled,
/// vGPU support is enabled on this GPU.
Enabled,
}
/// FSP command response payload (`NVDM_PAYLOAD_COMMAND_RESPONSE`).
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct NvdmPayloadCommandResponse {
task_id: u32,
command_nvdm_type: u32,
error_code: u32,
}
/// PRC message payload.
///
/// Sent to FSP to query or modify a device configuration knob.
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct NvdmPayloadPrc {
sub_message_id: u8,
flags: u8,
object_id: u8,
reserved: u8,
}
impl NvdmPayloadPrc {
/// Constructs a PRC payload from typed protocol fields.
fn new(subcmd: PrcMessageSubcmd, object_id: PrcObjectId, flags: PrcFlags) -> Self {
Self {
sub_message_id: subcmd.into(),
flags: flags.into(),
object_id: object_id.into(),
reserved: 0,
}
}
}
// SAFETY: NvdmPayloadPrc is a packed C struct with only integral fields.
unsafe impl AsBytes for NvdmPayloadPrc {}
/// PRC response payload containing the knob state value.
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct NvdmPayloadPrcResponse {
value_low: u8,
value_high: u8,
reserved1: u8,
reserved2: u8,
}
impl NvdmPayloadPrcResponse {
/// Returns the PRC knob value as a little-endian 16-bit integer.
fn value(self) -> u16 {
u16::from(self.value_low) | (u16::from(self.value_high) << 8)
}
}
impl TryFrom<NvdmPayloadPrcResponse> for VgpuMode {
type Error = kernel::error::Error;
fn try_from(value: NvdmPayloadPrcResponse) -> Result<Self> {
match value.value() {
0 => Ok(VgpuMode::Disabled),
1 => Ok(VgpuMode::Enabled),
_ => Err(EINVAL),
}
}
}
/// Common MCTP and NVDM headers shared by all FSP messages.
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct FspMessageHeader {
mctp_header: MctpHeader,
nvdm_header: NvdmHeader,
}
// SAFETY: FspMessageHeader is a packed C struct with only integral fields.
unsafe impl AsBytes for FspMessageHeader {}
// SAFETY: FspMessageHeader is a packed C struct with only integral fields.
unsafe impl FromBytes for FspMessageHeader {}
impl FspMessageHeader {
/// Construct a standard FSP message header for the given NVDM type.
fn new(nvdm_type: NvdmType) -> Self {
Self {
mctp_header: MctpHeader::single_packet(),
nvdm_header: NvdmHeader::new(nvdm_type),
}
}
}
/// Common FSP response header with MCTP, NVDM and command response payloads.
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct FspResponseHeader {
header: FspMessageHeader,
response: NvdmPayloadCommandResponse,
}
// SAFETY: FspResponseHeader is a packed C struct with only integral fields.
unsafe impl FromBytes for FspResponseHeader {}
/// Complete FSP PRC response including the knob state payload.
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct FspPrcResponse {
header: FspResponseHeader,
prc_data: NvdmPayloadPrcResponse,
}
// SAFETY: FspPrcResponse is a packed C struct with only integral fields.
unsafe impl FromBytes for FspPrcResponse {}
/// Trait implemented by types representing a message to send to FSP.
///
/// This provides [`Fsp::send_sync_fsp`] with the information it needs to send
/// a given message, following the same pattern as GSP's `CommandToGsp`.
trait MessageToFsp: AsBytes {
/// NVDM type identifying this message to FSP.
const NVDM_TYPE: NvdmType;
}
/// NVDM (NVIDIA Data Model) CoT (Chain of Trust) payload, the main
/// message body sent to FSP for Chain of Trust boot.
#[repr(C, packed)]
#[derive(Clone, Copy, Zeroable)]
struct NvdmPayloadCot {
version: u16,
size: u16,
gsp_fmc_sysmem_offset: u64,
frts_sysmem_offset: u64,
frts_sysmem_size: u32,
frts_vidmem_offset: u64,
frts_vidmem_size: u32,
sigs: FmcSignatures,
gsp_boot_args_sysmem_offset: u64,
}
/// Complete FSP COT (Chain of Trust) message structure.
#[repr(C)]
#[derive(Clone, Copy)]
struct FspCotMessage {
header: FspMessageHeader,
cot: NvdmPayloadCot,
}
impl FspCotMessage {
/// Computes the FRTS vidmem offset for the Chain-of-Trust message. It is measured backwards
/// from the end of the framebuffer.
fn frts_vidmem_offset(hal: &dyn hal::FspHal, fb_info: &FbSizes) -> Result<u64> {
let mut offset = hal.fb_end_reserved_size();
// As per OpenRM's `kfspPrepareBootCommands_GH100`.
if fb_info.pmu_reserved_size != 0 {
offset = (offset + u64::from(fb_info.pmu_reserved_size))
// The 2 MiB alignment is r570-specific.
.align_up(Alignment::new::<SZ_2M>())
.ok_or(EINVAL)?;
}
Ok(offset)
}
/// Returns an in-place initializer for [`FspCotMessage`].
fn new<'a>(
fb_info: &FbSizes,
fsp_fw: &'a FspFirmware,
args: &'a FmcBootArgs<'_>,
) -> Result<impl Init<Self> + 'a> {
let hal = hal::fsp_hal(args.chipset).ok_or(ENOTSUPP)?;
let frts_vidmem_offset = if !args.resume {
Self::frts_vidmem_offset(hal, fb_info)?
} else {
0
};
let frts_size: u32 = if !args.resume {
fb_info.frts_size.try_into()?
} else {
0
};
let version = hal.cot_version();
let size = num::usize_into_u16::<{ core::mem::size_of::<NvdmPayloadCot>() }>();
Ok(init!(Self {
header: FspMessageHeader::new(NvdmType::Cot),
// The payload is packed, so we cannot use `init!`. Initialize it member-by-member using
// `chain`.
cot <- pin_init::init_zeroed(),
})
.chain(move |msg| {
msg.cot.version = version;
msg.cot.size = size;
msg.cot.gsp_fmc_sysmem_offset = fsp_fw.fmc_image.dma_address();
msg.cot.frts_vidmem_offset = frts_vidmem_offset;
msg.cot.frts_vidmem_size = frts_size;
// frts_sysmem_* are left at zero because this path places FRTS in vidmem. The sysmem
// fields point to an FRTS buffer in sysmem instead, for systems without VRAM.
msg.cot.gsp_boot_args_sysmem_offset = args.fmc_boot_params.dma_address();
msg.cot.sigs = *fsp_fw.fmc_sigs;
Ok(())
}))
}
}
// SAFETY: `FspCotMessage` is `#[repr(C)]` with no padding, so all of its
// bytes are initialized.
unsafe impl AsBytes for FspCotMessage {}
/// Complete FSP PRC message.
#[repr(C, packed)]
#[derive(Clone, Copy)]
struct FspPrcMessage {
header: FspMessageHeader,
prc: NvdmPayloadPrc,
}
impl FspPrcMessage {
/// Constructs a PRC message.
fn new(subcmd: PrcMessageSubcmd, object_id: PrcObjectId, flags: PrcFlags) -> Self {
Self {
header: FspMessageHeader::new(NvdmType::Prc),
prc: NvdmPayloadPrc::new(subcmd, object_id, flags),
}
}
}
// SAFETY: FspPrcMessage is a packed C struct with only integral fields.
unsafe impl AsBytes for FspPrcMessage {}
impl MessageToFsp for FspCotMessage {
const NVDM_TYPE: NvdmType = NvdmType::Cot;
}
impl MessageToFsp for FspPrcMessage {
const NVDM_TYPE: NvdmType = NvdmType::Prc;
}
/// Bundled arguments for FMC boot via FSP Chain of Trust.
pub(crate) struct FmcBootArgs<'a> {
chipset: Chipset,
fmc_boot_params: Coherent<GspFmcBootParams>,
resume: bool,
// Additional dependencies required to be kept alive for FMC boot.
_wpr_meta: Coherent<GspFwWprMeta>,
_libos: &'a Coherent<[LibosMemoryRegionInitArgument]>,
}
impl<'a> FmcBootArgs<'a> {
/// Builds FMC boot arguments, allocating the DMA-coherent boot parameter
/// structure that FSP will read.
pub(crate) fn new(
dev: &device::Device<device::Bound>,
chipset: Chipset,
wpr_meta: Coherent<GspFwWprMeta>,
libos: &'a Coherent<[LibosMemoryRegionInitArgument]>,
resume: bool,
) -> Result<Self> {
let init = GspFmcBootParams::new(wpr_meta.dma_address(), libos.dma_address());
Ok(Self {
chipset,
fmc_boot_params: Coherent::<GspFmcBootParams>::init(dev, GFP_KERNEL, init)?,
resume,
_wpr_meta: wpr_meta,
_libos: libos,
})
}
/// Returns the FMC boot parameters allocation.
pub(crate) fn boot_params(&self) -> &Coherent<GspFmcBootParams> {
&self.fmc_boot_params
}
}
/// FSP interface for Hopper/Blackwell GPUs.
///
/// An `Fsp` is produced by [`Fsp::wait_secure_boot`], which only returns once FSP secure boot
/// has completed. It owns the FSP falcon and the FMC firmware, which are used for the subsequent
/// Chain of Trust boot.
pub(crate) struct Fsp<'a> {
falcon: Falcon<'a, FspEngine>,
fsp_fw: FspFirmware,
}
impl<'a> Fsp<'a> {
/// Attempts to create a `Fsp` instance.
///
/// This can involve waiting for FSP secure boot completion, but should be instantaneous in
/// practice.
///
/// If `chipset` doesn't support FSP, `Ok(None)` is returned.
pub(crate) fn try_new(
dev: &'a device::Device<device::Bound>,
bar: Bar0<'a>,
chipset: Chipset,
) -> Result<Option<Self>> {
match hal::fsp_hal(chipset) {
None => Ok(None),
Some(hal) => Self::wait_secure_boot(dev, bar, chipset, hal).map(Option::Some),
}
}
/// Waits for FSP secure boot completion, then returns the [`Fsp`] interface.
///
/// Polls the thermal scratch register until FSP signals boot completion or the timeout
/// elapses. Returning an [`Fsp`] only on success guarantees, at the API level, that the
/// interface is not used before secure boot has completed.
fn wait_secure_boot(
dev: &'a device::Device<device::Bound>,
bar: Bar0<'a>,
chipset: Chipset,
hal: &'static dyn hal::FspHal,
) -> Result<Fsp<'a>> {
/// FSP secure boot completion timeout in milliseconds.
const FSP_SECURE_BOOT_TIMEOUT_MS: i64 = 5000;
let falcon = Falcon::<FspEngine>::new(dev, chipset, bar)?;
let fsp_fw = FspFirmware::new(dev, chipset)?;
read_poll_timeout(
|| Ok(hal.fsp_boot_status(bar)),
|&status| status == regs::NV_THERM_I2CS_SCRATCH_FSP_BOOT_COMPLETE_STATUS_SUCCESS,
Delta::from_millis(10),
Delta::from_millis(FSP_SECURE_BOOT_TIMEOUT_MS),
)
.inspect_err(|e| {
dev_err!(dev, "FSP secure boot completion error: {:?}\n", e);
})?;
Ok(Fsp { falcon, fsp_fw })
}
/// Sends a message to FSP and waits for the response.
/// Returns the full response buffer on success.
fn send_sync_fsp<M>(&mut self, dev: &device::Device, msg: &M) -> Result<KVec<u8>>
where
M: MessageToFsp,
{
self.falcon.send_msg(msg.as_bytes())?;
let response_buf = self.falcon.recv_msg().inspect_err(|e| {
dev_err!(dev, "FSP response error: {:?}\n", e);
})?;
let (response, _) =
FspResponseHeader::from_bytes_prefix(&response_buf[..]).ok_or_else(|| {
dev_err!(dev, "FSP response too small: {}\n", response_buf.len());
EIO
})?;
let mctp_header = response.header.mctp_header;
let nvdm_header = response.header.nvdm_header;
let command_nvdm_type = response.response.command_nvdm_type;
let error_code = response.response.error_code;
if !mctp_header.is_single_packet() {
dev_err!(
dev,
"Unexpected MCTP header in FSP reply: {:x?}\n",
mctp_header,
);
return Err(EIO);
}
if !nvdm_header.validate(NvdmType::FspResponse) {
dev_err!(
dev,
"Unexpected NVDM header in FSP reply: {:x?}\n",
nvdm_header,
);
return Err(EIO);
}
if command_nvdm_type.try_into_bounded() != Some(M::NVDM_TYPE.into()) {
dev_err!(
dev,
"Expected NVDM type {:?} in reply, got {:#x}\n",
M::NVDM_TYPE,
command_nvdm_type
);
return Err(EIO);
}
if error_code != 0 {
dev_err!(
dev,
"NVDM command {:?} failed with error {:#x}\n",
M::NVDM_TYPE,
error_code
);
return Err(EIO);
}
Ok(response_buf)
}
/// Reads the active vGPU mode from FSP using the PRC protocol.
///
/// Queries FSP's Management Partition for the active vGPU mode knob value.
pub(crate) fn read_vgpu_mode(
&mut self,
dev: &device::Device<device::Bound>,
) -> Result<VgpuMode> {
let msg = FspPrcMessage::new(
PrcMessageSubcmd::Read,
PrcObjectId::VgpuMode,
PrcFlags::from(PrcFlag::Active),
);
let response_buf = self.send_sync_fsp(dev, &msg)?;
let (prc_response, _) =
FspPrcResponse::from_bytes_prefix(&response_buf[..]).ok_or_else(|| {
dev_err!(dev, "PRC response too small: {}\n", response_buf.len());
EIO
})?;
let prc_data = prc_response.prc_data;
VgpuMode::try_from(prc_data).inspect_err(|_| {
dev_err!(dev, "Unexpected vGPU mode value: {:#x}\n", prc_data.value());
})
}
/// Boots GSP FMC via FSP Chain of Trust.
///
/// Builds the CoT message from the pre-configured [`FmcBootArgs`], sends it
/// to FSP, and waits for the response.
pub(crate) fn boot_fmc(
&mut self,
dev: &device::Device<device::Bound>,
fb_info: &FbSizes,
args: &FmcBootArgs<'_>,
) -> Result {
dev_dbg!(dev, "Starting FSP boot sequence for {}\n", args.chipset);
let msg = KBox::init(FspCotMessage::new(fb_info, &self.fsp_fw, args)?, GFP_KERNEL)?;
let _response_buf = self.send_sync_fsp(dev, &*msg)?;
dev_dbg!(dev, "FSP Chain of Trust completed successfully\n");
Ok(())
}
}