[PATCH 2/3] gpu: nova-core: zero-pad radix3 page table levels to page boundary
From: John Hubbard
Date: Sun Sep 13 2026 - 15:54:59 EST
On Turing through Ada, the booter, a firmware stage that runs on the
GPU's SEC2 falcon, copies the GSP firmware image from system memory into
the framebuffer. It walks the radix3 table to find each page of the
image, and it reads each level of the table by DMA, a whole 4 KiB page
at a time. The last page of a level is only partly filled with entries,
so the bytes past the last entry reach the booter too.
Nova-core allocated each level at the size of its entries and wrote only
the entries, so the rest of the level's last page held whatever the
allocator left there, and the booter read that stale kernel memory.
Allocate each level zeroed and sized to a whole number of 4 KiB pages,
as Open RM does, and write the entries into it.
Assisted-by: LLM
Reviewed-by: Timur Tabi <ttabi@xxxxxxxxxx>
Signed-off-by: John Hubbard <jhubbard@xxxxxxxxxx>
---
drivers/gpu/nova-core/firmware/radix3.rs | 50 +++++++++++++++---------
1 file changed, 32 insertions(+), 18 deletions(-)
diff --git a/drivers/gpu/nova-core/firmware/radix3.rs b/drivers/gpu/nova-core/firmware/radix3.rs
index 6b8251ed871d..228ed54e6dad 100644
--- a/drivers/gpu/nova-core/firmware/radix3.rs
+++ b/drivers/gpu/nova-core/firmware/radix3.rs
@@ -63,22 +63,12 @@ pub(crate) fn new(
Ok(try_pin_init!(Self {
data <- SGTable::new(dev, data, DataDirection::ToDevice, GFP_KERNEL),
level2 <- {
- VVec::<u8>::with_capacity(
- data.iter().count() * core::mem::size_of::<u64>(),
- GFP_KERNEL,
- )
- .map_err(|_| ENOMEM)
- .and_then(|level2| map_into_lvl(&data, level2))
- .map(|level2| SGTable::new(dev, level2, DataDirection::ToDevice, GFP_KERNEL))?
+ build_lvl(&data)
+ .map(|l2| SGTable::new(dev, l2, DataDirection::ToDevice, GFP_KERNEL))?
},
level1 <- {
- VVec::<u8>::with_capacity(
- level2.iter().count() * core::mem::size_of::<u64>(),
- GFP_KERNEL,
- )
- .map_err(|_| ENOMEM)
- .and_then(|level1| map_into_lvl(&level2, level1))
- .map(|level1| SGTable::new(dev, level1, DataDirection::ToDevice, GFP_KERNEL))?
+ build_lvl(&level2)
+ .map(|l1| SGTable::new(dev, l1, DataDirection::ToDevice, GFP_KERNEL))?
},
level0: {
let level1_entry = level1.iter().next().ok_or(EINVAL)?;
@@ -109,16 +99,40 @@ pub(crate) fn size(&self) -> usize {
}
}
-/// Appends one level of the table to `dst`: one entry per [`GSP_PAGE_SIZE`] page of each
-/// DMA-mapped region of `sg_table`, in region order.
-fn map_into_lvl(sg_table: &SGTable<Owned<VVec<u8>>>, mut dst: VVec<u8>) -> Result<VVec<u8>> {
+/// Returns the size of the level that maps `sg_table`: one `u64` entry per [`GSP_PAGE_SIZE`]
+/// page of each DMA-mapped region, rounded up to whole pages.
+fn lvl_size(sg_table: &SGTable<Owned<VVec<u8>>>) -> usize {
+ let entries: usize = sg_table
+ .iter()
+ .map(|sg_entry| usize::from_safe_cast(sg_entry.dma_len()).div_ceil(GSP_PAGE_SIZE))
+ .sum();
+
+ (entries * size_of::<u64>()).next_multiple_of(GSP_PAGE_SIZE)
+}
+
+/// Builds one level of the table over `sg_table`: one entry per [`GSP_PAGE_SIZE`] page of each
+/// DMA-mapped region, in region order. The level is a whole number of pages and every byte past
+/// the last entry is zero, because the booter reads a level a whole page at a time.
+///
+/// # Errors
+///
+/// - `ENOMEM` if the level cannot be allocated.
+/// - `EINVAL` if `sg_table` yields more entries than [`lvl_size`] sized the level for.
+fn build_lvl(sg_table: &SGTable<Owned<VVec<u8>>>) -> Result<VVec<u8>> {
+ let mut dst = VVec::<u8>::zeroed(lvl_size(sg_table), GFP_KERNEL).map_err(|_| ENOMEM)?;
+ let mut entries = dst.chunks_exact_mut(size_of::<u64>());
+
for sg_entry in sg_table.iter() {
let num_pages = usize::from_safe_cast(sg_entry.dma_len()).div_ceil(GSP_PAGE_SIZE);
for i in 0..num_pages {
let entry = sg_entry.dma_address()
+ (u64::from_safe_cast(i) * u64::from_safe_cast(GSP_PAGE_SIZE));
- dst.extend_from_slice(&entry.to_le_bytes(), GFP_KERNEL)?;
+
+ entries
+ .next()
+ .ok_or(EINVAL)?
+ .copy_from_slice(&entry.to_le_bytes());
}
}
--
2.55.0