* [PATCH v4 0/3] gpu: nova-core: retain the GSP-RM log buffers
@ 2026-09-13 18:37 Vladislav Zaharov
2026-09-13 18:37 ` [PATCH v4 1/3] gpu: nova-core: move the debugfs root into the module data Vladislav Zaharov
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Vladislav Zaharov @ 2026-09-13 18:37 UTC (permalink / raw)
To: dakr, jhubbard
Cc: acourbot, aliceryhl, ttabi, gary, nova-gpu, dri-devel,
linux-kernel, linux-doc, Vladislav Zaharov
The GSP-RM log buffers are exposed through debugfs, but the entries are
owned by the Gpu that probe() builds, and the buffers themselves are DMA
allocations that cannot outlive the device. They are therefore gone as
soon as the GPU is unbound, and in particular as soon as probe() fails -
which is the case todo.rst singled out, and the one where a GSP log is
worth having.
Patch 1 gives the module data a DebugfsData that owns the debugfs root,
replacing the static that held it. Patch 2 adds a gsp_keep_logs module
parameter: when it is set, whatever the GSP wrote is copied into that
same DebugfsData and exposed under a "retained" directory until the
module is unloaded. Patch 3 drops the now completed task from todo.rst.
Changes since v3:
- keep the copies in the module data rather than in a second global.
The debugfs root moves there too, so one pointer now replaces both
statics, and the global lock and its unsafe initialization are gone
(Gary Guo)
- with that, the module data owns the root and is dropped after the
registration, so a module init that fails can no longer leave the
directory behind. v3 fixed that by ordering the fields of the module
data; it now follows from where the root lives, and the guard type
is gone
- keep a copy of the device name with the retained copies rather than
a reference to the device, so that a GPU that is gone does not stay
allocated until the module is unloaded
- drop a re-check of the retained directory that could not fire
The pointer is published from inside the initializer of DebugfsData,
through the `&this in` form, and it is a raw pointer rather than a
reference: pin-init does not allow references to fields to be created
inside an initializer. Readers turn it back into a reference in one
place.
Testing was done on top of drm-rust-next with the TLV firmware images
installed. On a GB203:
- with gsp_keep_logs unset, no "retained" directory is created and the
entries disappear on unbind, as before;
- with gsp_keep_logs=1, retained/<BDF>/{loginit,logintr,logrm} hold
the contents the live entries had, all 64 KiB of each readable;
- binding the GPU again recreates the live entries without disturbing
the copies, and unbinding it a second time replaces them, leaving
exactly one set behind;
- with a failure injected after the GSP has booted, probe() fails with
-EINVAL, the driver stays unbound, and the logs of that attempt are
still readable;
- with a failure injected into the Registration instead, the module
fails to load, leaves no debugfs directory behind, and the next load
comes up with its directory intact;
- the copies are released on module unload, and three load/unload
cycles leave nothing behind.
The parameter does need a value: the Rust bool param ops do not set
KERNEL_PARAM_OPS_FL_NOARG, so a bare gsp_keep_logs is refused, where the
C bool would have taken it.
No warnings, oopses or refcount complaints in dmesg throughout. Built and
checked with CLIPPY=1 and rustfmtcheck. checkpatch --strict is clean
apart from the MAINTAINERS note for the new file, which is already
covered by the existing "F: drivers/gpu/nova-core/" pattern.
John Hubbard's r000 series adds three more log buffers in gsp.rs.
Whichever of the two lands second needs a small rebase; the retained
copies extend to the new buffers by adding them to RetainedLogBuffers.
v3: https://lore.kernel.org/nova-gpu/20260912071842.622696-1-vladazaharova2018@gmail.com/
v2: https://lore.kernel.org/nova-gpu/20260815050826.306717-1-vladazaharova2018@gmail.com/
Vladislav Zaharov (3):
gpu: nova-core: move the debugfs root into the module data
gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind
Documentation: nova: remove completed GSP log buffer task
Documentation/gpu/nova/core/todo.rst | 12 --
drivers/gpu/nova-core/gsp.rs | 100 ++--------
drivers/gpu/nova-core/gsp/logbuffer.rs | 253 +++++++++++++++++++++++++
drivers/gpu/nova-core/nova_core.rs | 109 +++++++++--
4 files changed, 362 insertions(+), 112 deletions(-)
create mode 100644 drivers/gpu/nova-core/gsp/logbuffer.rs
base-commit: 66a2c223b620d844fe26c6bd4844d2a6a8c9dffc
--
2.55.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v4 1/3] gpu: nova-core: move the debugfs root into the module data
2026-09-13 18:37 [PATCH v4 0/3] gpu: nova-core: retain the GSP-RM log buffers Vladislav Zaharov
@ 2026-09-13 18:37 ` Vladislav Zaharov
2026-09-13 18:47 ` sashiko-bot
2026-09-13 18:37 ` [PATCH v4 2/3] gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind Vladislav Zaharov
2026-09-13 18:37 ` [PATCH v4 3/3] Documentation: nova: remove completed GSP log buffer task Vladislav Zaharov
2 siblings, 1 reply; 6+ messages in thread
From: Vladislav Zaharov @ 2026-09-13 18:37 UTC (permalink / raw)
To: dakr, jhubbard
Cc: acourbot, aliceryhl, ttabi, gary, nova-gpu, dri-devel,
linux-kernel, linux-doc, Vladislav Zaharov
The debugfs root lives in a static that init() fills in and a guard
field of the module data clears again. That costs a `static mut`, an
unsafe write on each side and a guard type whose only job is to undo
the write.
It also leaks. try_pin_init! drops only the fields it has already
built, and the guard is written after the Registration, so a
registration that fails leaves the guard unbuilt and the static set.
Statics are never dropped, and the module is unloaded right away, so
the directory outlives everything that could remove it. The next load
then finds the name taken: debugfs_create_dir() returns -EEXIST, which
Entry keeps as it would any other pointer, and the driver comes up with
no debugfs at all until the machine is rebooted.
Have the module data own a DebugfsData instead, built before the
registration and dropped after it, and keep only a pointer to it in the
static, for devices that have no other way to reach the data of their
module. What is left is one unsafe read for the users and one write on
each side, with no guard type. A registration that fails now drops the
data that was built before it, and the directory goes with it.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Vladislav Zaharov <vladazaharova2018@gmail.com>
---
drivers/gpu/nova-core/gsp.rs | 15 +++---
drivers/gpu/nova-core/nova_core.rs | 82 +++++++++++++++++++++++-------
2 files changed, 69 insertions(+), 28 deletions(-)
diff --git a/drivers/gpu/nova-core/gsp.rs b/drivers/gpu/nova-core/gsp.rs
index 25ea43f1cbe9..f29e601e6753 100644
--- a/drivers/gpu/nova-core/gsp.rs
+++ b/drivers/gpu/nova-core/gsp.rs
@@ -196,15 +196,12 @@ pub(crate) fn new(pdev: &'gsp pci::Device<device::Bound>) -> impl PinInit<Self,
logrm,
};
- #[allow(static_mut_refs)]
- // SAFETY: `DEBUGFS_ROOT` is created before driver registration and cleared
- // after driver unregistration, so no probe() can race with its modification.
- //
- // PANIC: `DEBUGFS_ROOT` cannot be `None` here. It is set before driver
- // registration and cleared after driver unregistration, so it is always
- // `Some` for the entire lifetime that probe() can be called.
- let log_parent: &debugfs::Dir = unsafe { crate::DEBUGFS_ROOT.as_ref() }
- .expect("DEBUGFS_ROOT not initialized");
+ // PANIC: The module data cannot be gone here. It is published before the
+ // driver is registered and taken down after it is unregistered, so it is
+ // there for as long as probe() can be called.
+ let log_parent: &debugfs::Dir = crate::debugfs_data()
+ .expect("module data not initialized")
+ .root();
log_parent.scope(log_buffers, dev.name(), |logs, dir| {
dir.read_binary_file(c"loginit", &logs.loginit.0);
diff --git a/drivers/gpu/nova-core/nova_core.rs b/drivers/gpu/nova-core/nova_core.rs
index 1133c6ce5c55..0f8501c26e05 100644
--- a/drivers/gpu/nova-core/nova_core.rs
+++ b/drivers/gpu/nova-core/nova_core.rs
@@ -30,40 +30,84 @@
pub(crate) const MODULE_NAME: &core::ffi::CStr = <LocalModule as kernel::ModuleMetadata>::NAME;
-// TODO: Move this into per-module data once that exists.
-static mut DEBUGFS_ROOT: Option<debugfs::Dir> = None;
+/// Pointer to the [`DebugfsData`] the module owns.
+///
+/// A device has no way to reach the data of its module, so probe() goes through here instead.
+// TODO: Drop this once devices can reach the data of their module.
+static mut DEBUGFS_DATA: *const DebugfsData = core::ptr::null();
-/// Guard that clears `DEBUGFS_ROOT` when dropped.
-struct DebugfsRootGuard;
+/// Data the module shares with every GPU it drives.
+///
+/// # Invariants
+///
+/// A non-null `DEBUGFS_DATA` points at a live, pinned instance of this type that outlives the
+/// driver registration.
+#[pin_data(PinnedDrop)]
+pub(crate) struct DebugfsData {
+ /// Root directory of the driver in debugfs.
+ root: debugfs::Dir,
+}
+
+impl DebugfsData {
+ /// Creates the shared data and publishes it, so that [`debugfs_data()`] can hand it out.
+ fn new() -> impl PinInit<Self> {
+ pin_init!(&this in Self {
+ root: debugfs::Dir::new(c"nova-core"),
+ _: {
+ // SAFETY: Module initialization runs once and before the driver is registered, so
+ // nothing can be reading `DEBUGFS_DATA` while it is written here. `this` is where
+ // the data is being built, and it stays there: the module data never moves.
+ unsafe { DEBUGFS_DATA = this.as_ptr() };
+ },
+ })
+ }
+
+ /// Returns the root directory of the driver in debugfs.
+ pub(crate) fn root(&self) -> &debugfs::Dir {
+ &self.root
+ }
+}
-impl Drop for DebugfsRootGuard {
- fn drop(&mut self) {
- // SAFETY: This guard is dropped after `_driver` (due to field order),
- // so the driver is unregistered and no probe() can be running.
- unsafe { DEBUGFS_ROOT = None };
+#[pinned_drop]
+impl PinnedDrop for DebugfsData {
+ fn drop(self: Pin<&mut Self>) {
+ // SAFETY: This runs after the registration is dropped, as the fields of `NovaCoreModule`
+ // are dropped in declaration order, so the driver is unregistered and neither a probe()
+ // nor the teardown of a device can be reading `DEBUGFS_DATA`.
+ unsafe { DEBUGFS_DATA = core::ptr::null() };
}
}
+/// Returns the data the module shares with its devices, or [`None`] if there is none yet.
+///
+/// Only ever call this while the driver is registered, which is to say from probe() or from the
+/// teardown of a device that is bound: the data is built before the registration and dropped
+/// after it, and nothing else keeps what is returned here alive.
+pub(crate) fn debugfs_data() -> Option<&'static DebugfsData> {
+ // SAFETY: `DEBUGFS_DATA` is written while the module data is initialized, before the driver
+ // is registered, and again when that data is dropped, after the driver is unregistered. Both
+ // happen with no device bound, so a caller in probe() or in the teardown of a device cannot
+ // race with either, and by the type invariant what it gets points at live data that outlives
+ // the device it is used from.
+ unsafe { DEBUGFS_DATA.as_ref() }
+}
+
#[pin_data]
struct NovaCoreModule {
- // Fields are dropped in declaration order, so `_driver` is dropped first,
- // then `_debugfs_guard` clears `DEBUGFS_ROOT`.
+ // Fields are dropped in declaration order, so the registration goes first and no probe() can
+ // still be running once the shared data is torn down. `init()` builds them the other way
+ // round, as the data has to be there before the first probe() reaches for it.
#[pin]
_driver: Registration<pci::Adapter<driver::NovaCoreDriver>>,
- _debugfs_guard: DebugfsRootGuard,
+ #[pin]
+ _debugfs: DebugfsData,
}
impl InPlaceModule for NovaCoreModule {
fn init(module: &'static kernel::ThisModule) -> impl PinInit<Self, Error> {
- let dir = debugfs::Dir::new(c"nova-core");
-
- // SAFETY: We are the only driver code running during init, so there
- // cannot be any concurrent access to `DEBUGFS_ROOT`.
- unsafe { DEBUGFS_ROOT = Some(dir) };
-
try_pin_init!(Self {
+ _debugfs <- DebugfsData::new(),
_driver <- Registration::new(MODULE_NAME, module),
- _debugfs_guard: DebugfsRootGuard,
})
}
}
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH v4 2/3] gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind
2026-09-13 18:37 [PATCH v4 0/3] gpu: nova-core: retain the GSP-RM log buffers Vladislav Zaharov
2026-09-13 18:37 ` [PATCH v4 1/3] gpu: nova-core: move the debugfs root into the module data Vladislav Zaharov
@ 2026-09-13 18:37 ` Vladislav Zaharov
2026-09-13 18:47 ` sashiko-bot
2026-09-13 18:37 ` [PATCH v4 3/3] Documentation: nova: remove completed GSP log buffer task Vladislav Zaharov
2 siblings, 1 reply; 6+ messages in thread
From: Vladislav Zaharov @ 2026-09-13 18:37 UTC (permalink / raw)
To: dakr, jhubbard
Cc: acourbot, aliceryhl, ttabi, gary, nova-gpu, dri-devel,
linux-kernel, linux-doc, Vladislav Zaharov
The GSP-RM log buffers are exposed through debugfs, but the Scope that
owns them lives in Gsp, inside GspResources, inside the Gpu built by
probe(). They are DMA allocations of the device and cannot outlive it,
so the entries go away as soon as the GPU is unbound - and, more to the
point, as soon as probe() fails, which is exactly when the log of a GSP
that did not come up is the thing one wants to read.
Add a gsp_keep_logs module parameter. When it is set, dropping the log
buffers copies whatever the GSP wrote into memory owned by the module
and exposes the copies until the module is unloaded. A buffer whose
"put" pointer is still zero was never written to and is skipped.
The GSP has normally been stopped by the time the buffers are dropped,
but a boot that timed out can leave it still appending, so a DMA read
barrier orders the read of the "put" pointer before the copy.
The copies belong to the module data next to the debugfs root, and live
in a "retained" directory created during module init rather than on
first use, which keeps the teardown path of a device from having to
create anything. Keeping them out of the directory used by bound GPUs
also means a device coming back does not find its debugfs name taken by
its own history; nouveau, which recreates the entries under the name of
the GPU that just went away, has that problem.
While at it, move the log buffer code out of gsp.rs into gsp/logbuffer.rs.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Vladislav Zaharov <vladazaharova2018@gmail.com>
---
drivers/gpu/nova-core/gsp.rs | 97 ++--------
drivers/gpu/nova-core/gsp/logbuffer.rs | 253 +++++++++++++++++++++++++
drivers/gpu/nova-core/nova_core.rs | 29 ++-
3 files changed, 300 insertions(+), 79 deletions(-)
create mode 100644 drivers/gpu/nova-core/gsp/logbuffer.rs
diff --git a/drivers/gpu/nova-core/gsp.rs b/drivers/gpu/nova-core/gsp.rs
index f29e601e6753..1a1eb7f37075 100644
--- a/drivers/gpu/nova-core/gsp.rs
+++ b/drivers/gpu/nova-core/gsp.rs
@@ -12,11 +12,7 @@
CoherentView,
DmaAddress, //
},
- io::{
- io_project,
- io_write,
- Io, //
- },
+ io::io_write,
pci,
prelude::*, //
};
@@ -24,9 +20,13 @@
pub(crate) mod cmdq;
pub(crate) mod commands;
mod fw;
+mod logbuffer;
mod regs;
mod sequencer;
+use logbuffer::LogBuffers;
+pub(crate) use logbuffer::RetainedLogs;
+
pub(crate) use fw::{
GspFmcBootParams,
GspFwWprMeta,
@@ -77,10 +77,6 @@ pub(crate) fn dev(&self) -> &'gpu device::Device<device::Bound> {
}
}
-/// Number of GSP pages to use in a RM log buffer.
-const RM_LOG_BUFFER_NUM_PAGES: usize = 0x10;
-const LOG_BUFFER_SIZE: usize = RM_LOG_BUFFER_NUM_PAGES * GSP_PAGE_SIZE;
-
/// Array of page table entries, as understood by the GSP bootloader.
#[repr(C)]
#[derive(FromBytes, IntoBytes)]
@@ -101,49 +97,6 @@ fn init(view: CoherentView<'_, Self>, start: DmaAddress) -> Result<()> {
}
}
-/// The logging buffers are byte queues that contain encoded printf-like
-/// messages from GSP-RM. They need to be decoded by a special application
-/// that can parse the buffers.
-///
-/// The 'loginit' buffer contains logs from early GSP-RM init and
-/// exception dumps. The 'logrm' buffer contains the subsequent logs. Both are
-/// written to directly by GSP-RM and can be any multiple of GSP_PAGE_SIZE.
-///
-/// The physical address map for the log buffer is stored in the buffer
-/// itself, starting with offset 1. Offset 0 contains the "put" pointer (pp).
-/// Initially, pp is equal to 0. If the buffer has valid logging data in it,
-/// then pp points to index into the buffer where the next logging entry will
-/// be written. Therefore, the logging data is valid if:
-/// 1 <= pp < sizeof(buffer)/sizeof(u64)
-struct LogBuffer<'a>(Coherent<'a, [u8; LOG_BUFFER_SIZE]>);
-
-impl<'a> LogBuffer<'a> {
- /// Creates a new `LogBuffer` mapped on `dev`.
- fn new(dev: &'a device::Device<device::Bound>) -> Result<Self> {
- let obj = Self(Coherent::zeroed(dev, GFP_KERNEL)?);
-
- let start_addr = obj.0.dma_address();
-
- let pte_view = io_project!(
- obj.0,
- [build: size_of::<u64>()..][build: ..RM_LOG_BUFFER_NUM_PAGES * size_of::<u64>()]
- )
- .try_cast::<PteArray<RM_LOG_BUFFER_NUM_PAGES>>()?;
- PteArray::init(pte_view, start_addr)?;
-
- Ok(obj)
- }
-}
-
-struct LogBuffers<'a> {
- /// Init log buffer.
- loginit: LogBuffer<'a>,
- /// Interrupts log buffer.
- logintr: LogBuffer<'a>,
- /// RM log buffer.
- logrm: LogBuffer<'a>,
-}
-
/// GSP runtime data.
#[pin_data]
pub(crate) struct Gsp<'gsp> {
@@ -165,9 +118,7 @@ pub(crate) fn new(pdev: &'gsp pci::Device<device::Bound>) -> impl PinInit<Self,
pin_init::pin_init_scope(move || {
let dev = pdev.as_ref();
- let loginit = LogBuffer::new(dev)?;
- let logintr = LogBuffer::new(dev)?;
- let logrm = LogBuffer::new(dev)?;
+ let log_buffers = LogBuffers::new(dev)?;
// Initialise the logging structures. The OpenRM equivalents are in:
// _kgspInitLibosLoggingStructures (allocates memory for buffers)
@@ -182,33 +133,23 @@ pub(crate) fn new(pdev: &'gsp pci::Device<device::Bound>) -> impl PinInit<Self,
GFP_KERNEL,
)?;
- libos.init_at(0, LibosMemoryRegionInitArgument::new("LOGINIT", &loginit.0))?;
- libos.init_at(1, LibosMemoryRegionInitArgument::new("LOGINTR", &logintr.0))?;
- libos.init_at(2, LibosMemoryRegionInitArgument::new("LOGRM", &logrm.0))?;
+ libos.init_at(
+ 0,
+ LibosMemoryRegionInitArgument::new("LOGINIT", &log_buffers.loginit.0),
+ )?;
+ libos.init_at(
+ 1,
+ LibosMemoryRegionInitArgument::new("LOGINTR", &log_buffers.logintr.0),
+ )?;
+ libos.init_at(
+ 2,
+ LibosMemoryRegionInitArgument::new("LOGRM", &log_buffers.logrm.0),
+ )?;
libos.init_at(3, LibosMemoryRegionInitArgument::new("RMARGS", rmargs))?;
libos.into()
},
- logs <- {
- let log_buffers = LogBuffers {
- loginit,
- logintr,
- logrm,
- };
-
- // PANIC: The module data cannot be gone here. It is published before the
- // driver is registered and taken down after it is unregistered, so it is
- // there for as long as probe() can be called.
- let log_parent: &debugfs::Dir = crate::debugfs_data()
- .expect("module data not initialized")
- .root();
-
- log_parent.scope(log_buffers, dev.name(), |logs, dir| {
- dir.read_binary_file(c"loginit", &logs.loginit.0);
- dir.read_binary_file(c"logintr", &logs.logintr.0);
- dir.read_binary_file(c"logrm", &logs.logrm.0);
- })
- },
+ logs <- log_buffers.scope(),
}))
})
}
diff --git a/drivers/gpu/nova-core/gsp/logbuffer.rs b/drivers/gpu/nova-core/gsp/logbuffer.rs
new file mode 100644
index 000000000000..890aa2f9e38e
--- /dev/null
+++ b/drivers/gpu/nova-core/gsp/logbuffer.rs
@@ -0,0 +1,253 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! GSP-RM log buffers, and the debugfs entries exposing them.
+
+use core::convert::Infallible;
+
+use kernel::{
+ debugfs,
+ device,
+ dma::Coherent,
+ io::{
+ io_project,
+ Io, //
+ },
+ prelude::*,
+ str::CString,
+ sync::barrier::{
+ dma_mb,
+ Read, //
+ }, //
+};
+
+use crate::gsp::{
+ PteArray,
+ GSP_PAGE_SIZE, //
+};
+
+/// Number of GSP pages to use in a RM log buffer.
+const RM_LOG_BUFFER_NUM_PAGES: usize = 0x10;
+const LOG_BUFFER_SIZE: usize = RM_LOG_BUFFER_NUM_PAGES * GSP_PAGE_SIZE;
+
+/// The logging buffers are byte queues that contain encoded printf-like
+/// messages from GSP-RM. They need to be decoded by a special application
+/// that can parse the buffers.
+///
+/// The 'loginit' buffer contains logs from early GSP-RM init and
+/// exception dumps. The 'logrm' buffer contains the subsequent logs. Both are
+/// written to directly by GSP-RM and can be any multiple of GSP_PAGE_SIZE.
+///
+/// The physical address map for the log buffer is stored in the buffer
+/// itself, starting with offset 1. Offset 0 contains the "put" pointer (pp).
+/// Initially, pp is equal to 0. If the buffer has valid logging data in it,
+/// then pp points to index into the buffer where the next logging entry will
+/// be written. Therefore, the logging data is valid if:
+/// 1 <= pp < sizeof(buffer)/sizeof(u64)
+pub(super) struct LogBuffer<'a>(pub(super) Coherent<'a, [u8; LOG_BUFFER_SIZE]>);
+
+impl<'a> LogBuffer<'a> {
+ /// Creates a new `LogBuffer` mapped on `dev`.
+ fn new(dev: &'a device::Device<device::Bound>) -> Result<Self> {
+ let obj = Self(Coherent::zeroed(dev, GFP_KERNEL)?);
+
+ let start_addr = obj.0.dma_address();
+
+ let pte_view = io_project!(
+ obj.0,
+ [build: size_of::<u64>()..][build: ..RM_LOG_BUFFER_NUM_PAGES * size_of::<u64>()]
+ )
+ .try_cast::<PteArray<RM_LOG_BUFFER_NUM_PAGES>>()?;
+ PteArray::init(pte_view, start_addr)?;
+
+ Ok(obj)
+ }
+
+ /// Copies the contents of this buffer into memory that does not belong to the device.
+ ///
+ /// A buffer the GSP never wrote to yields an empty vector, as it holds nothing worth keeping.
+ fn snapshot(&self) -> Result<VVec<u8>> {
+ // Offset 0 holds the "put" pointer, which the GSP advances as it appends entries. It is
+ // still zero if nothing was ever logged, which is all that is tested here: a buffer that
+ // was written to is copied whole, and making sense of "put" is left to the decoder.
+ let put = io_project!(self.0, [build: ..size_of::<u64>()]).try_cast::<u64>()?;
+ if put.read_val() == 0 {
+ return Ok(VVec::new());
+ }
+
+ // ORDERING: LOAD->LOAD ordering needed to order the "put" read before the data read. The
+ // GSP has normally been stopped by the time this runs, but a boot that timed out can leave
+ // it still appending.
+ dma_mb(Read);
+
+ let mut snapshot = VVec::zeroed(LOG_BUFFER_SIZE, GFP_KERNEL)?;
+ io_project!(self.0, [build: ..]).copy_to_slice(&mut snapshot);
+
+ Ok(snapshot)
+ }
+}
+
+/// The log buffers of a GPU, for as long as it is bound to the driver.
+pub(super) struct LogBuffers<'a> {
+ /// Device the buffers belong to. Also names their debugfs directory.
+ dev: &'a device::Device<device::Bound>,
+ /// Init log buffer.
+ pub(super) loginit: LogBuffer<'a>,
+ /// Interrupts log buffer.
+ pub(super) logintr: LogBuffer<'a>,
+ /// RM log buffer.
+ pub(super) logrm: LogBuffer<'a>,
+}
+
+impl<'a> LogBuffers<'a> {
+ /// Allocates the three log buffers of `dev`.
+ pub(super) fn new(dev: &'a device::Device<device::Bound>) -> Result<Self> {
+ Ok(Self {
+ dev,
+ loginit: LogBuffer::new(dev)?,
+ logintr: LogBuffer::new(dev)?,
+ logrm: LogBuffer::new(dev)?,
+ })
+ }
+
+ /// Creates an initializer exposing these buffers under a directory named after their device.
+ pub(super) fn scope(self) -> impl PinInit<debugfs::Scope<Self>, Infallible> + 'a {
+ let dev = self.dev;
+
+ // PANIC: The module data cannot be gone here. It is published before the driver is
+ // registered and taken down after it is unregistered, so it is there for as long as
+ // probe() can be called.
+ let log_parent: &debugfs::Dir = crate::debugfs_data()
+ .expect("module data not initialized")
+ .root();
+
+ log_parent.scope(self, dev.name(), |logs, dir| {
+ dir.read_binary_file(c"loginit", &logs.loginit.0);
+ dir.read_binary_file(c"logintr", &logs.logintr.0);
+ dir.read_binary_file(c"logrm", &logs.logrm.0);
+ })
+ }
+
+ /// Preserves whatever the GSP logged, so it can still be read once the GPU is gone.
+ ///
+ /// The buffers are DMA allocations of the device and cannot outlive it, so their contents are
+ /// copied into memory owned by the module and exposed through fresh debugfs entries. Those
+ /// live until the module is unloaded.
+ ///
+ /// Does nothing if `gsp_keep_logs` was not set when the module was loaded, as there is then
+ /// no directory to put the copies in.
+ fn retain(&self) -> Result {
+ // Copying is only worth it if there is somewhere to put the result, but the lock is
+ // dropped right away: what follows allocates 64 KiB three times, and no other device
+ // should have to wait for that.
+ let Some(data) = crate::debugfs_data() else {
+ return Ok(());
+ };
+
+ // The directory is taken here and the lock dropped again right away: what follows
+ // allocates 64 KiB three times, and no other device should have to wait for that.
+ let Some(dir) = data.retained_logs().lock().dir.clone() else {
+ return Ok(());
+ };
+
+ let logs = RetainedLogBuffers {
+ name: CString::try_from_fmt(fmt!("{}", self.dev.name()))?,
+ loginit: self.loginit.snapshot()?,
+ logintr: self.logintr.snapshot()?,
+ logrm: self.logrm.snapshot()?,
+ };
+
+ // Nothing was ever logged, so there is nothing to keep. A copy from an earlier run of
+ // this device is deliberately left alone: logs from a run that failed are worth more
+ // than the silence of one that did not.
+ if logs.loginit.is_empty() && logs.logintr.is_empty() && logs.logrm.is_empty() {
+ return Ok(());
+ }
+
+ // Take every allocation that can fail before the previous copy of this device is
+ // dropped, so that running out of memory here cannot leave it with no logs at all.
+ let scope = KBox::<debugfs::Scope<RetainedLogBuffers>>::new_uninit(GFP_KERNEL)?;
+
+ let mut retained = data.retained_logs().lock();
+
+ retained.gpus.reserve(1, GFP_KERNEL)?;
+
+ // An earlier run of the same device may have left a copy behind, and its directory
+ // carries the name about to be used again, so it has to go first. Nothing below can
+ // fail, so the replacement is guaranteed to take its place.
+ retained.gpus.retain(|gpu| *gpu.name != *logs.name);
+
+ let scope = scope.write_pin_init(dir.scope(logs, self.dev.name(), |logs, dir| {
+ if !logs.loginit.is_empty() {
+ dir.read_binary_file(c"loginit", &logs.loginit);
+ }
+ if !logs.logintr.is_empty() {
+ dir.read_binary_file(c"logintr", &logs.logintr);
+ }
+ if !logs.logrm.is_empty() {
+ dir.read_binary_file(c"logrm", &logs.logrm);
+ }
+ }))?;
+
+ retained.gpus.push(scope, GFP_KERNEL)?;
+
+ dev_dbg!(self.dev, "GSP-RM log buffers retained\n");
+
+ Ok(())
+ }
+}
+
+impl Drop for LogBuffers<'_> {
+ fn drop(&mut self) {
+ if let Err(e) = self.retain() {
+ dev_warn!(self.dev, "failed to retain GSP-RM log buffers: {:?}\n", e);
+ }
+ }
+}
+
+/// Copies of the log buffers of a GPU that is no longer around.
+struct RetainedLogBuffers {
+ /// Name of the device the buffers came from, which also names their directory.
+ ///
+ /// A copy rather than a reference to the device, so that a GPU that is gone does not stay
+ /// allocated for as long as its logs are kept.
+ name: CString,
+ /// Contents of the init log buffer, empty if it was never written to.
+ loginit: VVec<u8>,
+ /// Contents of the interrupts log buffer, empty if it was never written to.
+ logintr: VVec<u8>,
+ /// Contents of the RM log buffer, empty if it was never written to.
+ logrm: VVec<u8>,
+}
+
+/// Log buffers of GPUs that are gone, and the debugfs entries exposing them.
+///
+/// The copies live under a `retained` directory of their own instead of next to the entries of
+/// the GPUs that are actually bound, so that a device coming back does not find its name taken.
+pub(crate) struct RetainedLogs {
+ /// Parent directory of all copies. `None` unless retaining was asked for.
+ dir: Option<debugfs::Dir>,
+ /// One entry per GPU.
+ gpus: KVec<Pin<KBox<debugfs::Scope<RetainedLogBuffers>>>>,
+}
+
+impl RetainedLogs {
+ /// Creates an empty set of retained log buffers, retaining disabled.
+ pub(crate) const fn new() -> Self {
+ Self {
+ dir: None,
+ gpus: KVec::new(),
+ }
+ }
+
+ /// Creates the directory the copies will live in, enabling retaining.
+ ///
+ /// Does nothing without `CONFIG_DEBUG_FS`, where a [`debugfs::Dir`] is a zero-sized type and
+ /// the copies could never be read back.
+ pub(crate) fn enable(&mut self, parent: &debugfs::Dir) {
+ if !cfg!(CONFIG_DEBUG_FS) {
+ return;
+ }
+
+ self.dir = Some(parent.subdir(c"retained"));
+ }
+}
diff --git a/drivers/gpu/nova-core/nova_core.rs b/drivers/gpu/nova-core/nova_core.rs
index 0f8501c26e05..1ea61a2ebf4f 100644
--- a/drivers/gpu/nova-core/nova_core.rs
+++ b/drivers/gpu/nova-core/nova_core.rs
@@ -7,6 +7,7 @@
driver::Registration,
pci,
prelude::*,
+ sync::Mutex,
InPlaceModule, //
};
@@ -44,6 +45,11 @@
/// driver registration.
#[pin_data(PinnedDrop)]
pub(crate) struct DebugfsData {
+ /// Copies of the log buffers of GPUs that are gone.
+ ///
+ /// Declared before `root`, as the copies live below it.
+ #[pin]
+ retained_logs: Mutex<gsp::RetainedLogs>,
/// Root directory of the driver in debugfs.
root: debugfs::Dir,
}
@@ -51,8 +57,18 @@ pub(crate) struct DebugfsData {
impl DebugfsData {
/// Creates the shared data and publishes it, so that [`debugfs_data()`] can hand it out.
fn new() -> impl PinInit<Self> {
+ let root = debugfs::Dir::new(c"nova-core");
+
+ // Deciding here, rather than when the first GPU goes away, keeps the teardown path of a
+ // device from having to create anything.
+ let mut retained_logs = gsp::RetainedLogs::new();
+ if module_parameters::gsp_keep_logs.value() {
+ retained_logs.enable(&root);
+ }
+
pin_init!(&this in Self {
- root: debugfs::Dir::new(c"nova-core"),
+ retained_logs <- kernel::new_mutex!(retained_logs),
+ root,
_: {
// SAFETY: Module initialization runs once and before the driver is registered, so
// nothing can be reading `DEBUGFS_DATA` while it is written here. `this` is where
@@ -66,6 +82,11 @@ fn new() -> impl PinInit<Self> {
pub(crate) fn root(&self) -> &debugfs::Dir {
&self.root
}
+
+ /// Returns the copies of the log buffers of GPUs that are gone.
+ pub(crate) fn retained_logs(&self) -> &Mutex<gsp::RetainedLogs> {
+ &self.retained_logs
+ }
}
#[pinned_drop]
@@ -119,6 +140,12 @@ fn init(module: &'static kernel::ThisModule) -> impl PinInit<Self, Error> {
description: "Nova Core GPU driver",
license: "GPL v2",
firmware: [],
+ params: {
+ gsp_keep_logs: bool {
+ default: false,
+ description: "Keep the GSP-RM log buffers in debugfs after their GPU is gone",
+ },
+ },
}
kernel::module_firmware!(firmware::ModInfoBuilder);
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH v4 3/3] Documentation: nova: remove completed GSP log buffer task
2026-09-13 18:37 [PATCH v4 0/3] gpu: nova-core: retain the GSP-RM log buffers Vladislav Zaharov
2026-09-13 18:37 ` [PATCH v4 1/3] gpu: nova-core: move the debugfs root into the module data Vladislav Zaharov
2026-09-13 18:37 ` [PATCH v4 2/3] gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind Vladislav Zaharov
@ 2026-09-13 18:37 ` Vladislav Zaharov
2 siblings, 0 replies; 6+ messages in thread
From: Vladislav Zaharov @ 2026-09-13 18:37 UTC (permalink / raw)
To: dakr, jhubbard
Cc: acourbot, aliceryhl, ttabi, gary, nova-gpu, dri-devel,
linux-kernel, linux-doc, Vladislav Zaharov
Exposing the GSP-RM log buffers through debugfs is implemented, and with
the gsp_keep_logs module parameter they now also survive a failed probe,
which was the part of the task that was still missing.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Vladislav Zaharov <vladazaharova2018@gmail.com>
---
Documentation/gpu/nova/core/todo.rst | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/Documentation/gpu/nova/core/todo.rst b/Documentation/gpu/nova/core/todo.rst
index a01c362b1be0..28fc7329d56b 100644
--- a/Documentation/gpu/nova/core/todo.rst
+++ b/Documentation/gpu/nova/core/todo.rst
@@ -141,18 +141,6 @@ Implement support for instmem (bar2) used to store page tables.
GPU System Processor (GSP)
==========================
-Export GSP log buffers
-----------------------
-
-Recent patches from Timur Tabi [1] added support to expose GSP-RM log buffers
-(even after failure to probe the driver) through debugfs.
-
-This is also an interesting feature for nova-core, especially in the early days.
-
-| Link: https://lore.kernel.org/nouveau/20241030202952.694055-2-ttabi@nvidia.com/ [1]
-| Reference: Debugfs abstractions
-| Complexity: Intermediate
-
GSP firmware abstraction
------------------------
--
2.55.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH v4 1/3] gpu: nova-core: move the debugfs root into the module data
2026-09-13 18:37 ` [PATCH v4 1/3] gpu: nova-core: move the debugfs root into the module data Vladislav Zaharov
@ 2026-09-13 18:47 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-09-13 18:47 UTC (permalink / raw)
To: Vladislav Zaharov; +Cc: Danilo Krummrich, dri-devel, Alexandre Courbot
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Missing invariant comment for `DebugfsData` construction
--
commit f5ca13df2d92dfa80fcdd81e199934e5ce48a7c2
Author: Vladislav Zaharov <vladazaharova2018@gmail.com>
gpu: nova-core: move the debugfs root into the module data
This commit moves the debugfs root directory into a new DebugfsData
structure owned by the module, instead of using a static mut with a guard
type. This prevents leaks where a failing driver registration would leave
the directory lingering.
> diff --git a/drivers/gpu/nova-core/nova_core.rs b/drivers/gpu/nova-core/nova_core.rs
> --- a/drivers/gpu/nova-core/nova_core.rs
> +++ b/drivers/gpu/nova-core/nova_core.rs
[ ... ]
> +impl DebugfsData {
> + /// Creates the shared data and publishes it, so that [`debugfs_data()`] can hand it out.
> + fn new() -> impl PinInit<Self> {
> + pin_init!(&this in Self {
> + root: debugfs::Dir::new(c"nova-core"),
> + _: {
> + // SAFETY: Module initialization runs once and before the driver is registered, so
> + // nothing can be reading `DEBUGFS_DATA` while it is written here. `this` is where
> + // the data is being built, and it stays there: the module data never moves.
> + unsafe { DEBUGFS_DATA = this.as_ptr() };
> + },
> + })
> + }
[Severity: Low]
Is an // INVARIANT: comment missing here? DebugfsData has a documented
invariant, and is being constructed here via pin_init!, but there is no
comment explaining how the invariants are satisfied.
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260913183734.134307-1-vladazaharova2018@gmail.com?part=1
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v4 2/3] gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind
2026-09-13 18:37 ` [PATCH v4 2/3] gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind Vladislav Zaharov
@ 2026-09-13 18:47 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-09-13 18:47 UTC (permalink / raw)
To: Vladislav Zaharov; +Cc: Alexandre Courbot, dri-devel, Danilo Krummrich
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The `dma_mb(Read)` memory barrier does not protect against data races because the snapshot relies on the `put` pointer re-read by the unordered `copy_to_slice()`.
--
commit cb68e0aae9b03cb04a10d2043892acfc8a1fe581
Author: Vladislav Zaharov <vladazaharova2018@gmail.com>
gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind
Adds a module parameter to retain GSP-RM log buffers after unbind, making
them available through debugfs. This copies the contents into module-owned
memory since the original buffers are DMA allocations that do not outlive
the device.
> diff --git a/drivers/gpu/nova-core/gsp/logbuffer.rs b/drivers/gpu/nova-core/gsp/logbuffer.rs
> new file mode 100644
> index 0000000000000..890aa2f9e38e7
> --- /dev/null
> +++ b/drivers/gpu/nova-core/gsp/logbuffer.rs
[ ... ]
> + fn snapshot(&self) -> Result<VVec<u8>> {
> + // Offset 0 holds the "put" pointer, which the GSP advances as it appends entries. It is
> + // still zero if nothing was ever logged, which is all that is tested here: a buffer that
> + // was written to is copied whole, and making sense of "put" is left to the decoder.
> + let put = io_project!(self.0, [build: ..size_of::<u64>()]).try_cast::<u64>()?;
> + if put.read_val() == 0 {
> + return Ok(VVec::new());
> + }
> +
> + // ORDERING: LOAD->LOAD ordering needed to order the "put" read before the data read. The
> + // GSP has normally been stopped by the time this runs, but a boot that timed out can leave
> + // it still appending.
> + dma_mb(Read);
> +
> + let mut snapshot = VVec::zeroed(LOG_BUFFER_SIZE, GFP_KERNEL)?;
> + io_project!(self.0, [build: ..]).copy_to_slice(&mut snapshot);
[Severity: Medium]
Does the unordered copy_to_slice() at the end of this function negate the
effect of dma_mb(Read)?
If a boot times out, the GSP may still be appending logs when
LogBuffer::snapshot() is called. The safely ordered "put" value is dropped
here, and the snapshot relies on the "put" pointer being re-read by the
unordered copy_to_slice() operation.
This means copy_to_slice() can read the log payload before reading the "put"
pointer at offset 0. If the GSP concurrently writes new logs and updates the
"put" pointer, the snapshot could capture the updated "put" pointer but stale
log payload data, causing the user-space decoder to parse garbage data.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260913183734.134307-1-vladazaharova2018@gmail.com?part=2
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-09-13 20:54 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-13 18:37 [PATCH v4 0/3] gpu: nova-core: retain the GSP-RM log buffers Vladislav Zaharov
2026-09-13 18:37 ` [PATCH v4 1/3] gpu: nova-core: move the debugfs root into the module data Vladislav Zaharov
2026-09-13 18:47 ` sashiko-bot
2026-09-13 18:37 ` [PATCH v4 2/3] gpu: nova-core: gsp: retain the GSP-RM log buffers after unbind Vladislav Zaharov
2026-09-13 18:47 ` sashiko-bot
2026-09-13 18:37 ` [PATCH v4 3/3] Documentation: nova: remove completed GSP log buffer task Vladislav Zaharov
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox