// 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, prelude::*, ptr::{ Alignable, Alignment, // }, sizes::SZ_2M, time::Delta, transmute::{ AsBytes, FromBytes, // }, }; use crate::{ driver::Bar0, falcon::{ fsp::Fsp as FspEngine, Falcon, // }, fb::FbLayout, firmware::fsp::{ FmcSignatures, FspFirmware, // }, gpu::Chipset, gsp::GspFmcBootParams, mctp::{ MctpHeader, NvdmHeader, NvdmType, // }, num, regs, // }; mod hal; /// 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, } /// Complete FSP response structure with MCTP and NVDM headers. #[repr(C, packed)] #[derive(Clone, Copy)] struct FspResponse { mctp_header: MctpHeader, nvdm_header: NvdmHeader, response: NvdmPayloadCommandResponse, } // SAFETY: FspResponse is a packed C struct with only integral fields. unsafe impl FromBytes for FspResponse {} /// 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 message structure with MCTP and NVDM headers. #[repr(C)] #[derive(Clone, Copy)] struct FspMessage { mctp_header: MctpHeader, nvdm_header: NvdmHeader, cot: NvdmPayloadCot, } impl FspMessage { /// Returns an in-place initializer for [`FspMessage`]. fn new<'a>( fb_layout: &FbLayout, fsp_fw: &'a FspFirmware, args: &'a FmcBootArgs, ) -> Result + 'a> { // frts_offset is relative to FB end: FRTS_location = FB_END - frts_offset let frts_vidmem_offset = if !args.resume { let frts_reserved_size = fb_layout.heap.len() + u64::from(fb_layout.pmu_reserved_size); frts_reserved_size .align_up(Alignment::new::()) .ok_or(EINVAL)? } else { 0 }; let frts_size: u32 = if !args.resume { fb_layout.frts.len().try_into()? } else { 0 }; let version = hal::fsp_hal(args.chipset).ok_or(ENOTSUPP)?.cot_version(); let size = num::usize_into_u16::<{ core::mem::size_of::() }>(); Ok(init!(Self { mctp_header: MctpHeader::single_packet(), nvdm_header: NvdmHeader::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_handle(); msg.cot.frts_vidmem_offset = frts_vidmem_offset; msg.cot.frts_vidmem_size = frts_size; // frts_sysmem_* intentionally left at zero for now, but will be needed for e.g. // systems without VRAM. msg.cot.gsp_boot_args_sysmem_offset = args.fmc_boot_params.dma_handle(); msg.cot.sigs = *fsp_fw.fmc_sigs; Ok(()) })) } } // SAFETY: `FspMessage` is `#[repr(C)]` with no padding, so all of its // bytes are initialized. unsafe impl AsBytes for FspMessage {} impl MessageToFsp for FspMessage { const NVDM_TYPE: NvdmType = NvdmType::Cot; } /// Bundled arguments for FMC boot via FSP Chain of Trust. pub(crate) struct FmcBootArgs { chipset: Chipset, fmc_boot_params: Coherent, resume: bool, } impl FmcBootArgs { /// Builds FMC boot arguments, allocating the DMA-coherent boot parameter /// structure that FSP will read. pub(crate) fn new( dev: &device::Device, chipset: Chipset, wpr_meta_addr: u64, libos_addr: u64, resume: bool, ) -> Result { let init = GspFmcBootParams::new(wpr_meta_addr, libos_addr); Ok(Self { chipset, fmc_boot_params: Coherent::::init(dev, GFP_KERNEL, init)?, resume, }) } /// DMA address of the FMC boot parameters, needed after boot for lockdown /// release polling. pub(crate) fn boot_params_dma_handle(&self) -> u64 { self.fmc_boot_params.dma_handle() } } /// 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 { falcon: Falcon, fsp_fw: FspFirmware, } impl Fsp { /// 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. pub(crate) fn wait_secure_boot( dev: &device::Device, bar: Bar0<'_>, chipset: Chipset, fsp_fw: FspFirmware, ) -> Result { /// FSP secure boot completion timeout in milliseconds. const FSP_SECURE_BOOT_TIMEOUT_MS: i64 = 5000; let hal = hal::fsp_hal(chipset).ok_or(ENOTSUPP)?; let falcon = Falcon::::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. fn send_sync_fsp(&mut self, dev: &device::Device, bar: Bar0<'_>, msg: &M) -> Result where M: MessageToFsp, { self.falcon.send_msg(bar, msg.as_bytes())?; let response_buf = self.falcon.recv_msg(bar).inspect_err(|e| { dev_err!(dev, "FSP response error: {:?}\n", e); })?; let (response, _) = FspResponse::from_bytes_prefix(&response_buf[..]).ok_or_else(|| { dev_err!(dev, "FSP response too small: {}\n", response_buf.len()); EIO })?; let mctp_header = response.mctp_header; let nvdm_header = response.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 != u8::from(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(()) } /// 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, bar: Bar0<'_>, fb_layout: &FbLayout, args: &FmcBootArgs, ) -> Result { dev_dbg!(dev, "Starting FSP boot sequence for {}\n", args.chipset); let msg = KBox::init(FspMessage::new(fb_layout, &self.fsp_fw, args)?, GFP_KERNEL)?; self.send_sync_fsp(dev, bar, &*msg)?; dev_dbg!(dev, "FSP Chain of Trust completed successfully\n"); Ok(()) } }