* [PATCH v2 0/3] drm/nouveau: nv04 FIFO cleanup + recovery for Tesla
@ 2026-08-06 8:52 ` Marek Czernohous
0 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
v1 of this series is here:
https://lore.kernel.org/nouveau/20260513175014.96599-1-marek@czernohous.de/
Please do not apply v1. While running it on the reference machine I found
three defects in my own patch, one of which is a guaranteed kernel panic on
exactly the hardware the series targets. v2 fixes all three and adds one new
patch as a prerequisite.
What went wrong in v1, and how v2 addresses it:
1) NULL pointer dereference in interrupt context (fixed in 3/3)
v1 called nvkm_chan_error(chan, true). nv50 and g84 channels have no
.preempt callback, and the call in nvkm_chan_error() is guarded only by
the preempt argument and not by a NULL check, so preempt=true dereferences
a NULL function pointer under chan->lock in interrupt context.
This is not theoretical. On 2026-06-02 it fired on the reference machine
(then running 7.0.10-p1) during a deliberate VRAM stress test, captured
over netconsole:
fifo: CACHE_ERROR - ch 2 [labwc[3950]] subc 3 mthd 0f00 data 0000007b
nv04_fifo_intr_cache_error+0x111
-> nv04_fifo_recover+0x80
-> nvkm_chan_error+0x99
RIP: 0010:0x0
The oops escalated to a full panic because nouveau's drm_panic scanout
path ioremaps in panic context:
nv50_wndw_get_scanout_buffer -> nouveau_bo_map -> ttm_bo_kmap
-> __ioremap_caller -> __get_vm_area_node
kernel BUG at mm/vmalloc.c:3212
That is a separate, pre-existing problem which I have not yet reported;
I will do so on its own. The hardware watchdog then reset the machine.
v2 uses preempt=false, which is what the existing caller in
nvkm_runl_rc() does as well.
2) Killing the channel on the first fault is wrong (fixed in 3/3)
v1 killed the channel on every fault. But there is a single PFIFO cache
puller, and on a fault it names the channel that is *resident*, not
necessarily the one that caused the fault; nv04_fifo_pause() documents
this ("incorrect instance offsets to PGRAPH"). On the reference machine
this killed the Wayland compositor twice for somebody else's fault, on
2026-06-02 and again on 2026-07-22.
v2 keeps mainline behaviour for the first faults (skip the method or drop
the push segment and resume) and only escalates to the kill once the same
channel object has faulted NVKM_FIFO_KILL_COUNT times inside
NVKM_FIFO_KILL_WINDOW_MS. The streak is keyed on the channel object
pointer as an identity token, never dereferenced, and dropped in
nvkm_chan_del() so a reused channel id cannot inherit it.
3) A killed channel is not survivable on Tesla (new patch 2/3)
This is the reason for the new patch, and it is the part I would most
like reviewed.
nouveau_channel_init() only subscribes to the channel-killed event for
FERMI_CHANNEL_GPFIFO and newer. On Tesla the ERRORED event is therefore
delivered into an empty notifier list, nouveau_fence_context_kill() never
runs, and the pending fences of the killed channel are never signalled.
Today that is harmless upstream, because nothing kills a Tesla channel.
Patch 3/3 introduces exactly such a caller, so without 2/3 the series
would trade a recoverable fault for an unrecoverable hang. On the
reference machine this presented twice as a frozen desktop on a machine
that was otherwise alive and reachable over ssh. The stacks as observed:
kworker/u8 events_unbound: dma_fence_default_wait
<- drm_atomic_helper_wait_for_fences <- nv50_disp_atomic_commit_tail
12x kworker/uN ttm: dma_fence_default_wait
<- dma_resv_wait_timeout <- ttm_bo_fini
The ttm frame is reported as ttm_bo_fini; the wait that actually holds
those workers is the MAX_SCHEDULE_TIMEOUT one in ttm_bo_delayed_delete,
which is static and in the same file, so the shortened frame is
consistent with inlining. Either way both waits are uninterruptible.
The desktop stayed frozen for minutes until the fences timed out, after
which a compositor respawn brought the display back; a reboot cleared it
immediately. Leaving a fence unsignalled also violates the dma-fence
contract, which is why I think the subscription belongs on NV50+ even
independently of this series.
Testing
Reference hardware: Apple Mac mini Late 2009, MCP79 / GeForce 9400M
(NVAC), Core 2 Duo, Wayland (labwc) on Mesa 26.0.8.
Fault injection. I restored the debugfs-style fault injector I used for the
original validation (as a pair of write-only module parameters this time)
and drove nv04_fifo_recover() directly on a throwaway GL process, so that
the compositor was never the target:
ch 5 fault 1/3 in 10000ms window, skipping method and resuming (Tier-0)
ch 5 fault 2/3 in 10000ms window, skipping method and resuming (Tier-0)
fifo:000000:0005:0005:[fbo-stress[13247]] errored - disabling channel
Xwayland[4931]: channel 5 killed!
(The last line is prefixed with the DRM client rather than the faulting
process because the test program is an X11 GL client; NV_PRINTK names the
client that owns the channel.)
That covers all three defects:
- The channel survives the first two faults and only the third one inside
the window escalates, so an unrelated process is no longer killed for a
fault the puller misattributed.
- A fault 18 seconds after the previous one counted as 1/3 again, so the
window expiry works.
- The "channel N killed!" line is the one that never appeared before patch
2/3. It proves the event is delivered and the fences of the killed
channel are signalled: no kworker ended up in dma_fence_default_wait,
no TTM worker was left in D state (there were twelve during the freeze),
the compositor kept running, and the display commit never had to fall
back to any timeout.
- The victim process died with a SIGSEGV inside libgallium. That is the
known Mesa nv50 limitation (no GL robustness on this generation), not a
consequence of these patches.
What the injection does not cover, to be explicit: it enters the recovery
path from process context, where the real fault arrives in hard interrupt
context. The locking is the same, but the context is not, so this is
functional coverage of the recovery path rather than of the interrupt path.
Soak. The v1 patches ran on that machine from 2026-05 onwards, which is how
defects 1 and 2 were found. The v2 code, including the escalation ladder and
the widened event subscription, has been running since 2026-07-25, twelve
days as of this posting, across a kernel bump from 7.1.5 to 7.1.6 with the patch
series unchanged. Since every channel now subscribes to the kill event, the
interesting long-run question is channel churn, so the soak is ordinary
daily driving with a browser and GL applications rather than a synthetic
loop.
One difference between the soaked code and the diff below, stated so it is
not a surprise: downstream I carry the widened subscription behind a
module parameter (default on) so I can fall back to the Fermi+ behaviour
without rebuilding. Patch 2/3 as posted does it unconditionally. At the
default the two are behaviourally identical, and that default is what ran
for those twelve days; the parameter itself is a local debugging aid and I did
not think it belonged upstream.
Compile coverage, precisely: the changed translation units build clean in a
downstream 7.1.6 tree that carries equivalent code. The series is rebased
onto the mainline base commit below, but I have not built it there, because
my mainline checkout is sparse. The full module, in the downstream form, is
built and booted on the reference machine.
A note on MAINTAINERS. Patch 3/3 adds a new file
drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c. The change is covered
by the existing nouveau MAINTAINERS section (drivers/gpu/drm/nouveau/),
so no MAINTAINERS update is included. checkpatch.pl flags this as a hint;
it is not load-bearing.
AI assistance
Lyude asked on the v1 thread whether these patches were written by a human
and pointed at Documentation/process/coding-assistants.rst. The answer,
repeated here for the archive: this work is AI assisted. I use Claude
(claude-opus-4-7 for v1, claude-opus-5 for v2) as a coding and analysis
assistant. Every patch carries an Assisted-by trailer accordingly, and no
Signed-off-by is added by the tool.
Nature of the assistance, so you can calibrate your review: the assistant
did most of the code archaeology and drafting. I described symptoms
("the desktop freezes but the machine is alive", "the compositor gets
killed for a fault it did not cause"), asked for the mechanism to be
traced in the source rather than guessed, and asked for each claim to be
backed by a file and line. The register dumps, kernel stacks and the
netconsole trace quoted above are measurements from the machine, not
model output. The assistant also reviewed its own earlier work
adversarially, which is how defects 2 and 3 were found. I reviewed the
result, I understand the code, and I take responsibility for it.
One consequence worth stating: a first draft of this cover letter
described the fence-context teardown ordering in nouveau_channel_del() as
a use-after-free worth its own patch. On closer inspection most of that
window is harmless, because nouveau_fence_context_del() empties the
pending list before the event is unsubscribed. I dropped that patch rather
than send an overstated claim. If you would still like the reordering as a
hardening change, I can send it separately.
Changes since v1
- new patch 2/3 (subscribe to channel-kill events on NV50+)
- 3/3: preempt=false instead of preempt=true
- 3/3: escalation ladder instead of kill on first fault
- 3/3: per-channel streak dropped in nvkm_chan_del()
- 3/3: chan_killed tracepoint moved to the actual kill
- 3/3: comments that had slipped through in German are now in English
- rebased onto current mainline
- Assisted-by trailers added, as offered on the v1 thread
- defects 2 and 3 reproduced and their fixes verified with fault
injection; defect 1 was reproduced organically before the fix and is
covered here by the absence of the panic under injection
Marek Czernohous (3):
drm/nouveau/fifo/nv04: filter benign CACHE_ERROR from Mesa NV50 bind
probe
drm/nouveau: subscribe to channel-kill events on NV50 and newer
drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher
.../drm/nouveau/include/nvkm/engine/fifo.h | 31 +++
.../include/trace/events/nouveau_fifo.h | 58 ++++++
drivers/gpu/drm/nouveau/nouveau_chan.c | 2 +-
drivers/gpu/drm/nouveau/nouveau_drm.c | 29 +++
.../gpu/drm/nouveau/nvkm/engine/fifo/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/fifo/base.c | 3 +
.../gpu/drm/nouveau/nvkm/engine/fifo/chan.c | 14 ++
.../gpu/drm/nouveau/nvkm/engine/fifo/nv04.c | 29 ++-
.../gpu/drm/nouveau/nvkm/engine/fifo/priv.h | 10 +
.../drm/nouveau/nvkm/engine/fifo/recover.c | 176 ++++++++++++++++++
10 files changed, 346 insertions(+), 7 deletions(-)
create mode 100644 drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
base-commit: c21bb4193868a8de71fc4693fa741e195fdf5d86
--
2.54.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 0/3] drm/nouveau: nv04 FIFO cleanup + recovery for Tesla
@ 2026-08-06 8:52 ` Marek Czernohous
0 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Lyude Paul, Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
v1 of this series is here:
https://lore.kernel.org/nouveau/20260513175014.96599-1-marek@czernohous.de/
Please do not apply v1. While running it on the reference machine I found
three defects in my own patch, one of which is a guaranteed kernel panic on
exactly the hardware the series targets. v2 fixes all three and adds one new
patch as a prerequisite.
What went wrong in v1, and how v2 addresses it:
1) NULL pointer dereference in interrupt context (fixed in 3/3)
v1 called nvkm_chan_error(chan, true). nv50 and g84 channels have no
.preempt callback, and the call in nvkm_chan_error() is guarded only by
the preempt argument and not by a NULL check, so preempt=true dereferences
a NULL function pointer under chan->lock in interrupt context.
This is not theoretical. On 2026-06-02 it fired on the reference machine
(then running 7.0.10-p1) during a deliberate VRAM stress test, captured
over netconsole:
fifo: CACHE_ERROR - ch 2 [labwc[3950]] subc 3 mthd 0f00 data 0000007b
nv04_fifo_intr_cache_error+0x111
-> nv04_fifo_recover+0x80
-> nvkm_chan_error+0x99
RIP: 0010:0x0
The oops escalated to a full panic because nouveau's drm_panic scanout
path ioremaps in panic context:
nv50_wndw_get_scanout_buffer -> nouveau_bo_map -> ttm_bo_kmap
-> __ioremap_caller -> __get_vm_area_node
kernel BUG at mm/vmalloc.c:3212
That is a separate, pre-existing problem which I have not yet reported;
I will do so on its own. The hardware watchdog then reset the machine.
v2 uses preempt=false, which is what the existing caller in
nvkm_runl_rc() does as well.
2) Killing the channel on the first fault is wrong (fixed in 3/3)
v1 killed the channel on every fault. But there is a single PFIFO cache
puller, and on a fault it names the channel that is *resident*, not
necessarily the one that caused the fault; nv04_fifo_pause() documents
this ("incorrect instance offsets to PGRAPH"). On the reference machine
this killed the Wayland compositor twice for somebody else's fault, on
2026-06-02 and again on 2026-07-22.
v2 keeps mainline behaviour for the first faults (skip the method or drop
the push segment and resume) and only escalates to the kill once the same
channel object has faulted NVKM_FIFO_KILL_COUNT times inside
NVKM_FIFO_KILL_WINDOW_MS. The streak is keyed on the channel object
pointer as an identity token, never dereferenced, and dropped in
nvkm_chan_del() so a reused channel id cannot inherit it.
3) A killed channel is not survivable on Tesla (new patch 2/3)
This is the reason for the new patch, and it is the part I would most
like reviewed.
nouveau_channel_init() only subscribes to the channel-killed event for
FERMI_CHANNEL_GPFIFO and newer. On Tesla the ERRORED event is therefore
delivered into an empty notifier list, nouveau_fence_context_kill() never
runs, and the pending fences of the killed channel are never signalled.
Today that is harmless upstream, because nothing kills a Tesla channel.
Patch 3/3 introduces exactly such a caller, so without 2/3 the series
would trade a recoverable fault for an unrecoverable hang. On the
reference machine this presented twice as a frozen desktop on a machine
that was otherwise alive and reachable over ssh. The stacks as observed:
kworker/u8 events_unbound: dma_fence_default_wait
<- drm_atomic_helper_wait_for_fences <- nv50_disp_atomic_commit_tail
12x kworker/uN ttm: dma_fence_default_wait
<- dma_resv_wait_timeout <- ttm_bo_fini
The ttm frame is reported as ttm_bo_fini; the wait that actually holds
those workers is the MAX_SCHEDULE_TIMEOUT one in ttm_bo_delayed_delete,
which is static and in the same file, so the shortened frame is
consistent with inlining. Either way both waits are uninterruptible.
The desktop stayed frozen for minutes until the fences timed out, after
which a compositor respawn brought the display back; a reboot cleared it
immediately. Leaving a fence unsignalled also violates the dma-fence
contract, which is why I think the subscription belongs on NV50+ even
independently of this series.
Testing
Reference hardware: Apple Mac mini Late 2009, MCP79 / GeForce 9400M
(NVAC), Core 2 Duo, Wayland (labwc) on Mesa 26.0.8.
Fault injection. I restored the debugfs-style fault injector I used for the
original validation (as a pair of write-only module parameters this time)
and drove nv04_fifo_recover() directly on a throwaway GL process, so that
the compositor was never the target:
ch 5 fault 1/3 in 10000ms window, skipping method and resuming (Tier-0)
ch 5 fault 2/3 in 10000ms window, skipping method and resuming (Tier-0)
fifo:000000:0005:0005:[fbo-stress[13247]] errored - disabling channel
Xwayland[4931]: channel 5 killed!
(The last line is prefixed with the DRM client rather than the faulting
process because the test program is an X11 GL client; NV_PRINTK names the
client that owns the channel.)
That covers all three defects:
- The channel survives the first two faults and only the third one inside
the window escalates, so an unrelated process is no longer killed for a
fault the puller misattributed.
- A fault 18 seconds after the previous one counted as 1/3 again, so the
window expiry works.
- The "channel N killed!" line is the one that never appeared before patch
2/3. It proves the event is delivered and the fences of the killed
channel are signalled: no kworker ended up in dma_fence_default_wait,
no TTM worker was left in D state (there were twelve during the freeze),
the compositor kept running, and the display commit never had to fall
back to any timeout.
- The victim process died with a SIGSEGV inside libgallium. That is the
known Mesa nv50 limitation (no GL robustness on this generation), not a
consequence of these patches.
What the injection does not cover, to be explicit: it enters the recovery
path from process context, where the real fault arrives in hard interrupt
context. The locking is the same, but the context is not, so this is
functional coverage of the recovery path rather than of the interrupt path.
Soak. The v1 patches ran on that machine from 2026-05 onwards, which is how
defects 1 and 2 were found. The v2 code, including the escalation ladder and
the widened event subscription, has been running since 2026-07-25, twelve
days as of this posting, across a kernel bump from 7.1.5 to 7.1.6 with the patch
series unchanged. Since every channel now subscribes to the kill event, the
interesting long-run question is channel churn, so the soak is ordinary
daily driving with a browser and GL applications rather than a synthetic
loop.
One difference between the soaked code and the diff below, stated so it is
not a surprise: downstream I carry the widened subscription behind a
module parameter (default on) so I can fall back to the Fermi+ behaviour
without rebuilding. Patch 2/3 as posted does it unconditionally. At the
default the two are behaviourally identical, and that default is what ran
for those twelve days; the parameter itself is a local debugging aid and I did
not think it belonged upstream.
Compile coverage, precisely: the changed translation units build clean in a
downstream 7.1.6 tree that carries equivalent code. The series is rebased
onto the mainline base commit below, but I have not built it there, because
my mainline checkout is sparse. The full module, in the downstream form, is
built and booted on the reference machine.
A note on MAINTAINERS. Patch 3/3 adds a new file
drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c. The change is covered
by the existing nouveau MAINTAINERS section (drivers/gpu/drm/nouveau/),
so no MAINTAINERS update is included. checkpatch.pl flags this as a hint;
it is not load-bearing.
AI assistance
Lyude asked on the v1 thread whether these patches were written by a human
and pointed at Documentation/process/coding-assistants.rst. The answer,
repeated here for the archive: this work is AI assisted. I use Claude
(claude-opus-4-7 for v1, claude-opus-5 for v2) as a coding and analysis
assistant. Every patch carries an Assisted-by trailer accordingly, and no
Signed-off-by is added by the tool.
Nature of the assistance, so you can calibrate your review: the assistant
did most of the code archaeology and drafting. I described symptoms
("the desktop freezes but the machine is alive", "the compositor gets
killed for a fault it did not cause"), asked for the mechanism to be
traced in the source rather than guessed, and asked for each claim to be
backed by a file and line. The register dumps, kernel stacks and the
netconsole trace quoted above are measurements from the machine, not
model output. The assistant also reviewed its own earlier work
adversarially, which is how defects 2 and 3 were found. I reviewed the
result, I understand the code, and I take responsibility for it.
One consequence worth stating: a first draft of this cover letter
described the fence-context teardown ordering in nouveau_channel_del() as
a use-after-free worth its own patch. On closer inspection most of that
window is harmless, because nouveau_fence_context_del() empties the
pending list before the event is unsubscribed. I dropped that patch rather
than send an overstated claim. If you would still like the reordering as a
hardening change, I can send it separately.
Changes since v1
- new patch 2/3 (subscribe to channel-kill events on NV50+)
- 3/3: preempt=false instead of preempt=true
- 3/3: escalation ladder instead of kill on first fault
- 3/3: per-channel streak dropped in nvkm_chan_del()
- 3/3: chan_killed tracepoint moved to the actual kill
- 3/3: comments that had slipped through in German are now in English
- rebased onto current mainline
- Assisted-by trailers added, as offered on the v1 thread
- defects 2 and 3 reproduced and their fixes verified with fault
injection; defect 1 was reproduced organically before the fix and is
covered here by the absence of the panic under injection
Marek Czernohous (3):
drm/nouveau/fifo/nv04: filter benign CACHE_ERROR from Mesa NV50 bind
probe
drm/nouveau: subscribe to channel-kill events on NV50 and newer
drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher
.../drm/nouveau/include/nvkm/engine/fifo.h | 31 +++
.../include/trace/events/nouveau_fifo.h | 58 ++++++
drivers/gpu/drm/nouveau/nouveau_chan.c | 2 +-
drivers/gpu/drm/nouveau/nouveau_drm.c | 29 +++
.../gpu/drm/nouveau/nvkm/engine/fifo/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/fifo/base.c | 3 +
.../gpu/drm/nouveau/nvkm/engine/fifo/chan.c | 14 ++
.../gpu/drm/nouveau/nvkm/engine/fifo/nv04.c | 29 ++-
.../gpu/drm/nouveau/nvkm/engine/fifo/priv.h | 10 +
.../drm/nouveau/nvkm/engine/fifo/recover.c | 176 ++++++++++++++++++
10 files changed, 346 insertions(+), 7 deletions(-)
create mode 100644 drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
base-commit: c21bb4193868a8de71fc4693fa741e195fdf5d86
--
2.54.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v2 1/3] drm/nouveau/fifo/nv04: filter benign CACHE_ERROR from Mesa NV50 bind probe
2026-08-06 8:52 ` Marek Czernohous
@ 2026-08-06 8:52 ` Marek Czernohous
-1 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
The Mesa userspace driver issues a method-0x0060 / data-0xbeef02xx
binding probe on Tesla GPUs that ends up triggering CACHE_ERROR in the
PFIFO interrupt handler. The probe is harmless and recovers cleanly,
but it floods dmesg at error level on every X/Wayland session start.
Filter that specific pattern down to debug level so dmesg stays clean
while real CACHE_ERROR conditions are still logged at error level.
Tested on an Apple Mac mini Late 2009 (MCP79, NVAC 0xac080b1): dmesg has
no CACHE_ERROR spam during normal operation, the previously visible
beef02xx pattern now only appears at debug level.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Marek Czernohous <marek@czernohous.de>
---
.../gpu/drm/nouveau/nvkm/engine/fifo/nv04.c | 25 ++++++++++++++-----
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
index c4b8e567d86f..fa13cd55b593 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
@@ -327,12 +327,25 @@ nv04_fifo_intr_cache_error(struct nvkm_fifo *fifo, u32 chid, u32 get)
if (!(pull0 & 0x00000100) ||
!nv04_fifo_swmthd(device, chid, mthd, data)) {
- chan = nvkm_chan_get_chid(&fifo->engine, chid, &flags);
- nvkm_error(subdev, "CACHE_ERROR - "
- "ch %d [%s] subc %d mthd %04x data %08x\n",
- chid, chan ? chan->name : "unknown",
- (mthd >> 13) & 7, mthd & 0x1ffc, data);
- nvkm_chan_put(&chan, flags);
+ /*
+ * Filter benign Mesa NV50 bind probe: mthd 0x0060 with
+ * data 0xbeef02xx is a harmless userspace probe on Tesla
+ * GPUs and does not indicate an actual error condition.
+ * Demote to debug to keep dmesg clean while still catching
+ * real CACHE_ERROR events.
+ */
+ if ((mthd & 0x1ffc) == 0x0060 &&
+ (data & 0xffffff00) == 0xbeef0200) {
+ nvkm_debug(subdev, "CACHE_ERROR - ch %d subc %d mthd %04x data %08x (benign, skipped)\n",
+ chid, (mthd >> 13) & 7, mthd & 0x1ffc, data);
+ } else {
+ chan = nvkm_chan_get_chid(&fifo->engine, chid, &flags);
+ nvkm_error(subdev, "CACHE_ERROR - "
+ "ch %d [%s] subc %d mthd %04x data %08x\n",
+ chid, chan ? chan->name : "unknown",
+ (mthd >> 13) & 7, mthd & 0x1ffc, data);
+ nvkm_chan_put(&chan, flags);
+ }
}
nvkm_wr32(device, NV04_PFIFO_CACHE1_DMA_PUSH, 0);
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 1/3] drm/nouveau/fifo/nv04: filter benign CACHE_ERROR from Mesa NV50 bind probe
@ 2026-08-06 8:52 ` Marek Czernohous
0 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Lyude Paul, Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
The Mesa userspace driver issues a method-0x0060 / data-0xbeef02xx
binding probe on Tesla GPUs that ends up triggering CACHE_ERROR in the
PFIFO interrupt handler. The probe is harmless and recovers cleanly,
but it floods dmesg at error level on every X/Wayland session start.
Filter that specific pattern down to debug level so dmesg stays clean
while real CACHE_ERROR conditions are still logged at error level.
Tested on an Apple Mac mini Late 2009 (MCP79, NVAC 0xac080b1): dmesg has
no CACHE_ERROR spam during normal operation, the previously visible
beef02xx pattern now only appears at debug level.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Marek Czernohous <marek@czernohous.de>
---
.../gpu/drm/nouveau/nvkm/engine/fifo/nv04.c | 25 ++++++++++++++-----
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
index c4b8e567d86f..fa13cd55b593 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
@@ -327,12 +327,25 @@ nv04_fifo_intr_cache_error(struct nvkm_fifo *fifo, u32 chid, u32 get)
if (!(pull0 & 0x00000100) ||
!nv04_fifo_swmthd(device, chid, mthd, data)) {
- chan = nvkm_chan_get_chid(&fifo->engine, chid, &flags);
- nvkm_error(subdev, "CACHE_ERROR - "
- "ch %d [%s] subc %d mthd %04x data %08x\n",
- chid, chan ? chan->name : "unknown",
- (mthd >> 13) & 7, mthd & 0x1ffc, data);
- nvkm_chan_put(&chan, flags);
+ /*
+ * Filter benign Mesa NV50 bind probe: mthd 0x0060 with
+ * data 0xbeef02xx is a harmless userspace probe on Tesla
+ * GPUs and does not indicate an actual error condition.
+ * Demote to debug to keep dmesg clean while still catching
+ * real CACHE_ERROR events.
+ */
+ if ((mthd & 0x1ffc) == 0x0060 &&
+ (data & 0xffffff00) == 0xbeef0200) {
+ nvkm_debug(subdev, "CACHE_ERROR - ch %d subc %d mthd %04x data %08x (benign, skipped)\n",
+ chid, (mthd >> 13) & 7, mthd & 0x1ffc, data);
+ } else {
+ chan = nvkm_chan_get_chid(&fifo->engine, chid, &flags);
+ nvkm_error(subdev, "CACHE_ERROR - "
+ "ch %d [%s] subc %d mthd %04x data %08x\n",
+ chid, chan ? chan->name : "unknown",
+ (mthd >> 13) & 7, mthd & 0x1ffc, data);
+ nvkm_chan_put(&chan, flags);
+ }
}
nvkm_wr32(device, NV04_PFIFO_CACHE1_DMA_PUSH, 0);
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 2/3] drm/nouveau: subscribe to channel-kill events on NV50 and newer
2026-08-06 8:52 ` Marek Czernohous
@ 2026-08-06 8:52 ` Marek Czernohous
-1 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
nouveau_channel_init() only subscribes to the channel-killed event for
FERMI_CHANNEL_GPFIFO and newer. On NV50/Tesla the subscription therefore
never happens, and nvkm_chan_error()'s NVKM_CHAN_EVENT_ERRORED is
delivered into an empty notifier list.
Today that is harmless, because nothing kills a channel on Tesla: the
only nvkm_chan_error() callers are the Fermi and newer recovery paths.
The next patch adds such a caller for Tesla, and without a subscriber
the consequences are severe. nouveau_channel_killed() never runs, so
nouveau_fence_context_kill() never runs either, and the pending fences
of the killed channel are never signalled. Everything waiting on them
waits forever: drm_atomic_helper_wait_for_fences() in the display commit
tail waits uninterruptibly and without a timeout, and the TTM delayed
delete workers wait in TASK_UNINTERRUPTIBLE. The user sees a frozen
desktop on a machine that is otherwise alive; it stays that way for
minutes until the fences time out, and a reboot clears it immediately.
That is also a dma-fence contract violation: a fence must always be
signalled, with an error if necessary.
Lower the class gate to NV50_CHANNEL_GPFIFO. The nvkm side is already
class neutral: the KILLED case hangs the notifier on runl->chid->event,
which every fifo owns since the runlist rework, and nvkm_uchan_uevent()
does not discriminate by class. Pre-NV50 chips keep the old behaviour,
so NV04 to NV40 are unaffected.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Marek Czernohous <marek@czernohous.de>
---
drivers/gpu/drm/nouveau/nouveau_chan.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/nouveau/nouveau_chan.c b/drivers/gpu/drm/nouveau/nouveau_chan.c
index 598513f60449..1db9fbd81f5c 100644
--- a/drivers/gpu/drm/nouveau/nouveau_chan.c
+++ b/drivers/gpu/drm/nouveau/nouveau_chan.c
@@ -363,7 +363,7 @@ nouveau_channel_init(struct nouveau_channel *chan, u32 vram, u32 gart)
if (ret)
return ret;
- if (chan->user.oclass >= FERMI_CHANNEL_GPFIFO) {
+ if (chan->user.oclass >= NV50_CHANNEL_GPFIFO) {
DEFINE_RAW_FLEX(struct nvif_event_v0, args, data,
sizeof(struct nvif_chan_event_v0));
struct nvif_chan_event_v0 *host =
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 2/3] drm/nouveau: subscribe to channel-kill events on NV50 and newer
@ 2026-08-06 8:52 ` Marek Czernohous
0 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Lyude Paul, Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
nouveau_channel_init() only subscribes to the channel-killed event for
FERMI_CHANNEL_GPFIFO and newer. On NV50/Tesla the subscription therefore
never happens, and nvkm_chan_error()'s NVKM_CHAN_EVENT_ERRORED is
delivered into an empty notifier list.
Today that is harmless, because nothing kills a channel on Tesla: the
only nvkm_chan_error() callers are the Fermi and newer recovery paths.
The next patch adds such a caller for Tesla, and without a subscriber
the consequences are severe. nouveau_channel_killed() never runs, so
nouveau_fence_context_kill() never runs either, and the pending fences
of the killed channel are never signalled. Everything waiting on them
waits forever: drm_atomic_helper_wait_for_fences() in the display commit
tail waits uninterruptibly and without a timeout, and the TTM delayed
delete workers wait in TASK_UNINTERRUPTIBLE. The user sees a frozen
desktop on a machine that is otherwise alive; it stays that way for
minutes until the fences time out, and a reboot clears it immediately.
That is also a dma-fence contract violation: a fence must always be
signalled, with an error if necessary.
Lower the class gate to NV50_CHANNEL_GPFIFO. The nvkm side is already
class neutral: the KILLED case hangs the notifier on runl->chid->event,
which every fifo owns since the runlist rework, and nvkm_uchan_uevent()
does not discriminate by class. Pre-NV50 chips keep the old behaviour,
so NV04 to NV40 are unaffected.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Marek Czernohous <marek@czernohous.de>
---
drivers/gpu/drm/nouveau/nouveau_chan.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/nouveau/nouveau_chan.c b/drivers/gpu/drm/nouveau/nouveau_chan.c
index 598513f60449..1db9fbd81f5c 100644
--- a/drivers/gpu/drm/nouveau/nouveau_chan.c
+++ b/drivers/gpu/drm/nouveau/nouveau_chan.c
@@ -363,7 +363,7 @@ nouveau_channel_init(struct nouveau_channel *chan, u32 vram, u32 gart)
if (ret)
return ret;
- if (chan->user.oclass >= FERMI_CHANNEL_GPFIFO) {
+ if (chan->user.oclass >= NV50_CHANNEL_GPFIFO) {
DEFINE_RAW_FLEX(struct nvif_event_v0, args, data,
sizeof(struct nvif_chan_event_v0));
struct nvif_chan_event_v0 *host =
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 3/3] drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher
2026-08-06 8:52 ` Marek Czernohous
@ 2026-08-06 8:52 ` Marek Czernohous
-1 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
On Tesla / NV50 family chipsets (nv50, g84, g94, g98, mcp77, mcp79), FIFO
fault handling in nv04_fifo_intr_cache_error() and
nv04_fifo_intr_dma_pusher() logs the fault and resets hardware registers
but leaves the offending channel running. Compared to Fermi and newer,
which call nvkm_chan_error() from nvkm_runl_rc(), Tesla has no
escalation at all: repeated faults on the same channel keep firing
forever and there is no telemetry beyond dmesg.
Add a shared recovery helper, nv04_fifo_recover(), that both interrupt
handlers call after the existing logging and reset sequence. It
implements two tiers:
Tier-1: kill the channel with nvkm_chan_error(), but only after it has
faulted NVKM_FIFO_KILL_COUNT times inside NVKM_FIFO_KILL_WINDOW_MS.
The single PFIFO cache puller names the channel that is *resident*
when the fault is noticed, not necessarily the one that caused it (see
the comment in nv04_fifo_pause() about incorrect instance offsets), so
one fault is not sufficient evidence to kill. Below the threshold the
behaviour is unchanged from mainline: the method is skipped or the
push segment dropped, and the channel resumes.
Tier-2: after a burst of faults within a sliding window, request a
device-wide drm_dev_wedged_event() so userspace can rebind the driver.
Tier-2 is fed by every fault, including those Tier-1 lets pass.
The per-channel streak is keyed on the channel object pointer, which is
used purely as an identity token and never dereferenced. It is dropped
in nvkm_chan_del() so that a channel id handed out again cannot inherit
the streak of its predecessor.
nvkm_chan_error() is called with preempt=false. nv50 and g84 channels
have no .preempt callback, so preempt=true would dereference a NULL
function pointer under chan->lock in interrupt context.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Marek Czernohous <marek@czernohous.de>
---
.../drm/nouveau/include/nvkm/engine/fifo.h | 31 +++
.../include/trace/events/nouveau_fifo.h | 58 ++++++
drivers/gpu/drm/nouveau/nouveau_drm.c | 29 +++
.../gpu/drm/nouveau/nvkm/engine/fifo/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/fifo/base.c | 3 +
.../gpu/drm/nouveau/nvkm/engine/fifo/chan.c | 14 ++
.../gpu/drm/nouveau/nvkm/engine/fifo/nv04.c | 4 +
.../gpu/drm/nouveau/nvkm/engine/fifo/priv.h | 10 +
.../drm/nouveau/nvkm/engine/fifo/recover.c | 176 ++++++++++++++++++
9 files changed, 326 insertions(+)
create mode 100644 drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
diff --git a/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h b/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h
index 96c16cfccf16..973c3ee445dc 100644
--- a/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h
+++ b/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h
@@ -55,6 +55,36 @@ void nvkm_chan_put(struct nvkm_chan **, unsigned long irqflags);
struct nvkm_chan *nvkm_uchan_chan(struct nvkm_object *);
+#define NVKM_FIFO_WEDGE_RING_MAX 32
+
+/*
+ * A channel is only killed once it has faulted NVKM_FIFO_KILL_COUNT times
+ * within NVKM_FIFO_KILL_WINDOW_MS. The PFIFO cache puller names the channel
+ * that is resident when the fault is noticed, which is not necessarily the one
+ * that caused it, so a single fault is not sufficient evidence to kill.
+ */
+#define NVKM_FIFO_KILL_COUNT 3
+#define NVKM_FIFO_KILL_WINDOW_MS 10000
+#define NVKM_FIFO_KILL_CHID_MAX 128
+
+struct nvkm_fifo_wedge {
+ spinlock_t lock;
+ u32 count; /* faults inside the window */
+ ktime_t ts[NVKM_FIFO_WEDGE_RING_MAX]; /* ring of fault timestamps */
+ u32 head; /* ring head */
+ struct work_struct work; /* schedules drm_dev_wedged_event */
+ atomic_t wedged; /* Tier-2 already fired? */
+
+ /* Per-channel fault streak for the Tier-1 escalation. owner is an
+ * identity token for the channel object and is never dereferenced.
+ */
+ struct {
+ void *owner;
+ ktime_t first;
+ u32 count;
+ } chfault[NVKM_FIFO_KILL_CHID_MAX];
+};
+
struct nvkm_fifo {
const struct nvkm_fifo_func *func;
struct nvkm_engine engine;
@@ -86,6 +116,7 @@ struct nvkm_fifo {
spinlock_t lock;
struct mutex mutex;
+ struct nvkm_fifo_wedge wedge;
};
void nvkm_fifo_fault(struct nvkm_fifo *, struct nvkm_fault_data *);
diff --git a/drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h b/drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
new file mode 100644
index 000000000000..46d043a82850
--- /dev/null
+++ b/drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: MIT */
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM nouveau
+
+#if !defined(_TRACE_NOUVEAU_FIFO_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_NOUVEAU_FIFO_H
+
+#include <linux/tracepoint.h>
+#include <drm/drm_device.h>
+
+TRACE_EVENT(nouveau_fifo_chan_killed,
+ TP_PROTO(struct drm_device *dev, u32 chid, u32 fault_type, u64 info),
+ TP_ARGS(dev, chid, fault_type, info),
+ TP_STRUCT__entry(
+ __string(devname, dev_name(dev->dev))
+ __field(u32, chid)
+ __field(u32, fault_type)
+ __field(u64, info)
+ ),
+ TP_fast_assign(
+ __assign_str(devname);
+ __entry->chid = chid;
+ __entry->fault_type = fault_type;
+ __entry->info = info;
+ ),
+ TP_printk("dev=%s chid=%u fault=%s info=0x%llx",
+ __get_str(devname),
+ __entry->chid,
+ __entry->fault_type == 0 ? "CACHE_ERROR" : "DMA_PUSHER",
+ __entry->info)
+);
+
+TRACE_EVENT(nouveau_fifo_dev_wedged,
+ TP_PROTO(struct drm_device *dev, u32 fault_count, u32 window_ms),
+ TP_ARGS(dev, fault_count, window_ms),
+ TP_STRUCT__entry(
+ __string(devname, dev_name(dev->dev))
+ __field(u32, fault_count)
+ __field(u32, window_ms)
+ ),
+ TP_fast_assign(
+ __assign_str(devname);
+ __entry->fault_count = fault_count;
+ __entry->window_ms = window_ms;
+ ),
+ TP_printk("dev=%s wedged after %u faults in %u ms",
+ __get_str(devname),
+ __entry->fault_count,
+ __entry->window_ms)
+);
+
+#endif /* _TRACE_NOUVEAU_FIFO_H */
+
+#undef TRACE_INCLUDE_PATH
+#define TRACE_INCLUDE_PATH ../../drivers/gpu/drm/nouveau/include/trace/events
+#undef TRACE_INCLUDE_FILE
+#define TRACE_INCLUDE_FILE nouveau_fifo
+#include <trace/define_trace.h>
diff --git a/drivers/gpu/drm/nouveau/nouveau_drm.c b/drivers/gpu/drm/nouveau/nouveau_drm.c
index e16f59b00f6f..9a9278589a3a 100644
--- a/drivers/gpu/drm/nouveau/nouveau_drm.c
+++ b/drivers/gpu/drm/nouveau/nouveau_drm.c
@@ -22,6 +22,8 @@
* Authors: Ben Skeggs
*/
+#define CREATE_TRACE_POINTS
+
#include <linux/aperture.h>
#include <linux/delay.h>
#include <linux/module.h>
@@ -74,6 +76,9 @@
#include "nouveau_uvmm.h"
#include "nouveau_sched.h"
+#include <engine/fifo.h>
+#include <trace/events/nouveau_fifo.h>
+
DECLARE_DYNDBG_CLASSMAP(drm_debug_classes, DD_CLASS_TYPE_DISJOINT_BITS, 0,
"DRM_UT_CORE",
"DRM_UT_DRIVER",
@@ -111,6 +116,18 @@ MODULE_PARM_DESC(runpm, "disable (0), force enable (1), optimus only default (-1
static int nouveau_runtime_pm = -1;
module_param_named(runpm, nouveau_runtime_pm, int, 0400);
+MODULE_PARM_DESC(fifo_wedge_count,
+ "FIFO faults within window before drm_dev_wedged_event "
+ "(0=disable Tier-2, max 32, default 10)");
+unsigned int nouveau_fifo_wedge_count = 10;
+module_param_named(fifo_wedge_count, nouveau_fifo_wedge_count, uint, 0400);
+
+MODULE_PARM_DESC(fifo_wedge_window_ms,
+ "Sliding-window width in milliseconds for fifo_wedge_count "
+ "(default 60000)");
+unsigned int nouveau_fifo_wedge_window_ms = 60000;
+module_param_named(fifo_wedge_window_ms, nouveau_fifo_wedge_window_ms, uint, 0400);
+
static struct drm_driver driver_stub;
static struct drm_driver driver_pci;
static struct drm_driver driver_platform;
@@ -1495,6 +1512,18 @@ nouveau_drm_init(void)
if (!nouveau_modeset)
return 0;
+ if (nouveau_fifo_wedge_count > NVKM_FIFO_WEDGE_RING_MAX) {
+ pr_warn("nouveau: fifo_wedge_count=%u exceeds max %u; clamping\n",
+ nouveau_fifo_wedge_count, NVKM_FIFO_WEDGE_RING_MAX);
+ nouveau_fifo_wedge_count = NVKM_FIFO_WEDGE_RING_MAX;
+ }
+ if (nouveau_fifo_wedge_window_ms < 100 ||
+ nouveau_fifo_wedge_window_ms > 600000) {
+ pr_warn("nouveau: fifo_wedge_window_ms=%u out of range; resetting to 60000\n",
+ nouveau_fifo_wedge_window_ms);
+ nouveau_fifo_wedge_window_ms = 60000;
+ }
+
nouveau_module_debugfs_init();
#ifdef CONFIG_NOUVEAU_PLATFORM_DRIVER
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild
index 376e9c3bcb1a..1ff29753731d 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild
@@ -5,6 +5,7 @@ nvkm-y += nvkm/engine/fifo/chan.o
nvkm-y += nvkm/engine/fifo/chid.o
nvkm-y += nvkm/engine/fifo/runl.o
nvkm-y += nvkm/engine/fifo/runq.o
+nvkm-y += nvkm/engine/fifo/recover.o
nvkm-y += nvkm/engine/fifo/nv04.o
nvkm-y += nvkm/engine/fifo/nv10.o
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c
index 9dd924694306..a61183fa38af 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c
@@ -337,6 +337,8 @@ nvkm_fifo_dtor(struct nvkm_engine *engine)
struct nvkm_runl *runl, *runt;
struct nvkm_runq *runq, *rtmp;
+ nv04_fifo_wedge_fini(fifo);
+
if (fifo->userd.bar1)
nvkm_vmm_put(nvkm_bar_bar1_vmm(engine->subdev.device), &fifo->userd.bar1);
nvkm_memory_unref(&fifo->userd.mem);
@@ -390,6 +392,7 @@ nvkm_fifo_new_(const struct nvkm_fifo_func *func, struct nvkm_device *device,
fifo->timeout.chan_msec = 10000;
spin_lock_init(&fifo->lock);
mutex_init(&fifo->mutex);
+ nv04_fifo_wedge_init(fifo);
return nvkm_engine_ctor(&nvkm_fifo, device, type, inst, true, &fifo->engine);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
index 418a8918bcb8..79774c6460f4 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
@@ -275,6 +275,20 @@ nvkm_chan_del(struct nvkm_chan **pchan)
nvkm_gpuobj_del(&chan->ramfc);
if (chan->cgrp) {
+ struct nvkm_fifo *fifo = chan->cgrp->runl->fifo;
+
+ /* Drop this channel's fault streak before the id is reused. */
+ if (chan->id >= 0) {
+ typeof(&fifo->wedge.chfault[0]) cf =
+ &fifo->wedge.chfault[chan->id % NVKM_FIFO_KILL_CHID_MAX];
+ unsigned long flags;
+
+ spin_lock_irqsave(&fifo->wedge.lock, flags);
+ if (cf->owner == chan)
+ cf->owner = NULL;
+ spin_unlock_irqrestore(&fifo->wedge.lock, flags);
+ }
+
nvkm_chid_put(chan->cgrp->runl->chid, chan->id, &chan->cgrp->lock);
nvkm_cgrp_unref(&chan->cgrp);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
index fa13cd55b593..cb81941ecccd 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
@@ -345,6 +345,8 @@ nv04_fifo_intr_cache_error(struct nvkm_fifo *fifo, u32 chid, u32 get)
chid, chan ? chan->name : "unknown",
(mthd >> 13) & 7, mthd & 0x1ffc, data);
nvkm_chan_put(&chan, flags);
+ nv04_fifo_recover(fifo, chid, NV04_FAULT_CACHE_ERROR,
+ ((u64)mthd << 32) | data);
}
}
@@ -410,6 +412,8 @@ nv04_fifo_intr_dma_pusher(struct nvkm_fifo *fifo, u32 chid)
}
nvkm_chan_put(&chan, flags);
+ nv04_fifo_recover(fifo, chid, NV04_FAULT_DMA_PUSHER, state);
+
nvkm_wr32(device, 0x003228, 0x00000000);
nvkm_wr32(device, 0x003220, 0x00000001);
nvkm_wr32(device, 0x002100, NV_PFIFO_INTR_DMA_PUSHER);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h
index fff1428ef267..bf551906dcd4 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h
@@ -83,6 +83,16 @@ void nv04_chan_start(struct nvkm_chan *);
void nv04_chan_stop(struct nvkm_chan *);
void nv04_eobj_ramht_del(struct nvkm_chan *, int);
+/* Recovery helper for Tesla cache_error/dma_pusher (recover.c). */
+#define NV04_FAULT_CACHE_ERROR 0
+#define NV04_FAULT_DMA_PUSHER 1
+
+void nv04_fifo_recover(struct nvkm_fifo *fifo, u32 chid, u32 fault_type, u64 info);
+void nv04_fifo_wedge_init(struct nvkm_fifo *fifo);
+void nv04_fifo_wedge_fini(struct nvkm_fifo *fifo);
+extern unsigned int nouveau_fifo_wedge_count;
+extern unsigned int nouveau_fifo_wedge_window_ms;
+
int nv10_fifo_chid_nr(struct nvkm_fifo *);
int nv50_fifo_chid_nr(struct nvkm_fifo *);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
new file mode 100644
index 000000000000..ea962ddf0bcb
--- /dev/null
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: MIT
+/*
+ * nv04_fifo_recover - shared recovery helper for Tesla cache_error and
+ * dma_pusher fault paths.
+ *
+ * Tier-1: kill the offending channel via nvkm_chan_error, but only once it
+ * has faulted repeatedly inside a short window.
+ * Tier-2: after a configurable burst of faults within a sliding time
+ * window, request a device-wide drm_dev_wedged_event so userspace
+ * can rebind the driver.
+ */
+
+#include "priv.h"
+#include "chan.h"
+
+#include <core/device.h>
+#include <subdev/timer.h>
+
+#include <linux/workqueue.h>
+#include <linux/jiffies.h>
+#include <linux/ktime.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_device.h>
+
+#include "nouveau_drv.h"
+#include <trace/events/nouveau_fifo.h>
+
+static struct drm_device *
+nv04_fifo_drm_device(struct nvkm_fifo *fifo)
+{
+ struct nvkm_device *device = fifo->engine.subdev.device;
+ struct nouveau_drm *drm = dev_get_drvdata(device->dev);
+
+ return (drm && drm->dev) ? drm->dev : NULL;
+}
+
+void
+nv04_fifo_recover(struct nvkm_fifo *fifo, u32 chid, u32 fault_type, u64 info)
+{
+ struct drm_device *drm_dev = nv04_fifo_drm_device(fifo);
+ struct nvkm_chan *chan;
+ unsigned long flags;
+ ktime_t now, cutoff;
+ u32 i, count;
+
+ chan = nvkm_chan_get_chid(&fifo->engine, chid, &flags);
+ if (chan) {
+ struct nvkm_fifo_wedge *w = &fifo->wedge;
+ typeof(&w->chfault[0]) cf =
+ &w->chfault[chid % NVKM_FIFO_KILL_CHID_MAX];
+ ktime_t tnow = ktime_get();
+ unsigned long wflags;
+ bool kill;
+ u32 seen;
+
+ /*
+ * wedge.lock nests inside chan->cgrp->lock, which
+ * nvkm_chan_get_chid() holds until nvkm_chan_put(). The other
+ * users of wedge.lock take no channel lock, so there is no
+ * reverse ordering.
+ */
+ if (atomic_read(&chan->errored)) {
+ /*
+ * The channel is already dead but can still be
+ * resident and fault again, because nv50 and g84 have
+ * no preempt and the handler re-enables the puller
+ * unconditionally. Do not restart its streak.
+ */
+ nvkm_chan_put(&chan, flags);
+ goto tier2;
+ }
+
+ spin_lock_irqsave(&w->lock, wflags);
+ if (cf->owner != chan ||
+ ktime_after(tnow, ktime_add_ms(cf->first,
+ NVKM_FIFO_KILL_WINDOW_MS))) {
+ cf->owner = chan;
+ cf->first = tnow;
+ cf->count = 0;
+ }
+ seen = ++cf->count;
+ kill = seen >= NVKM_FIFO_KILL_COUNT;
+ if (kill)
+ cf->owner = NULL;
+ spin_unlock_irqrestore(&w->lock, wflags);
+
+ if (kill) {
+ if (drm_dev)
+ trace_nouveau_fifo_chan_killed(drm_dev, chid,
+ fault_type, info);
+ /*
+ * preempt must stay false: nv50 and g84 channels have
+ * no .preempt callback, so nvkm_chan_error() would
+ * dereference a NULL function pointer under a spinlock
+ * in interrupt context.
+ */
+ nvkm_chan_error(chan, false);
+ } else {
+ nvkm_warn(&fifo->engine.subdev,
+ "ch %d fault %u/%u within %ums, resuming\n",
+ chid, seen, NVKM_FIFO_KILL_COUNT,
+ NVKM_FIFO_KILL_WINDOW_MS);
+ }
+ nvkm_chan_put(&chan, flags);
+ }
+
+tier2:
+ if (nouveau_fifo_wedge_count == 0)
+ return;
+
+ now = ktime_get();
+ cutoff = ktime_sub_ms(now, nouveau_fifo_wedge_window_ms);
+
+ spin_lock_irqsave(&fifo->wedge.lock, flags);
+
+ /* Insert current first, then purge expired and count survivors. */
+ fifo->wedge.ts[fifo->wedge.head] = now;
+ fifo->wedge.head = (fifo->wedge.head + 1) % NVKM_FIFO_WEDGE_RING_MAX;
+
+ count = 0;
+ for (i = 0; i < NVKM_FIFO_WEDGE_RING_MAX; i++) {
+ if (!ktime_to_ns(fifo->wedge.ts[i]))
+ continue;
+ if (ktime_before(fifo->wedge.ts[i], cutoff))
+ fifo->wedge.ts[i] = 0;
+ else
+ count++;
+ }
+ fifo->wedge.count = count;
+
+ if (count >= nouveau_fifo_wedge_count)
+ schedule_work(&fifo->wedge.work);
+
+ spin_unlock_irqrestore(&fifo->wedge.lock, flags);
+}
+
+static void
+nv04_fifo_wedge_work(struct work_struct *work)
+{
+ struct nvkm_fifo_wedge *w = container_of(work, struct nvkm_fifo_wedge, work);
+ struct nvkm_fifo *fifo = container_of(w, struct nvkm_fifo, wedge);
+ struct drm_device *drm_dev = nv04_fifo_drm_device(fifo);
+ u32 fault_count;
+
+ if (atomic_xchg(&w->wedged, 1) != 0)
+ return; /* already wedged this cycle */
+
+ if (!drm_dev)
+ return;
+
+ fault_count = w->count;
+
+ dev_info(drm_dev->dev,
+ "nouveau: fifo wedged after %u faults in %u ms\n",
+ fault_count, nouveau_fifo_wedge_window_ms);
+
+ trace_nouveau_fifo_dev_wedged(drm_dev, fault_count,
+ nouveau_fifo_wedge_window_ms);
+
+ drm_dev_wedged_event(drm_dev, DRM_WEDGE_RECOVERY_REBIND, NULL);
+}
+
+void
+nv04_fifo_wedge_init(struct nvkm_fifo *fifo)
+{
+ memset(fifo->wedge.chfault, 0, sizeof(fifo->wedge.chfault));
+ spin_lock_init(&fifo->wedge.lock);
+ INIT_WORK(&fifo->wedge.work, nv04_fifo_wedge_work);
+ atomic_set(&fifo->wedge.wedged, 0);
+}
+
+void
+nv04_fifo_wedge_fini(struct nvkm_fifo *fifo)
+{
+ cancel_work_sync(&fifo->wedge.work);
+}
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v2 3/3] drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher
@ 2026-08-06 8:52 ` Marek Czernohous
0 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 8:52 UTC (permalink / raw)
To: nouveau; +Cc: Lyude Paul, Danilo Krummrich, dri-devel, linux-kernel
From: Marek Czernohous <marek@czernohous.de>
On Tesla / NV50 family chipsets (nv50, g84, g94, g98, mcp77, mcp79), FIFO
fault handling in nv04_fifo_intr_cache_error() and
nv04_fifo_intr_dma_pusher() logs the fault and resets hardware registers
but leaves the offending channel running. Compared to Fermi and newer,
which call nvkm_chan_error() from nvkm_runl_rc(), Tesla has no
escalation at all: repeated faults on the same channel keep firing
forever and there is no telemetry beyond dmesg.
Add a shared recovery helper, nv04_fifo_recover(), that both interrupt
handlers call after the existing logging and reset sequence. It
implements two tiers:
Tier-1: kill the channel with nvkm_chan_error(), but only after it has
faulted NVKM_FIFO_KILL_COUNT times inside NVKM_FIFO_KILL_WINDOW_MS.
The single PFIFO cache puller names the channel that is *resident*
when the fault is noticed, not necessarily the one that caused it (see
the comment in nv04_fifo_pause() about incorrect instance offsets), so
one fault is not sufficient evidence to kill. Below the threshold the
behaviour is unchanged from mainline: the method is skipped or the
push segment dropped, and the channel resumes.
Tier-2: after a burst of faults within a sliding window, request a
device-wide drm_dev_wedged_event() so userspace can rebind the driver.
Tier-2 is fed by every fault, including those Tier-1 lets pass.
The per-channel streak is keyed on the channel object pointer, which is
used purely as an identity token and never dereferenced. It is dropped
in nvkm_chan_del() so that a channel id handed out again cannot inherit
the streak of its predecessor.
nvkm_chan_error() is called with preempt=false. nv50 and g84 channels
have no .preempt callback, so preempt=true would dereference a NULL
function pointer under chan->lock in interrupt context.
Assisted-by: Claude:claude-opus-5
Signed-off-by: Marek Czernohous <marek@czernohous.de>
---
.../drm/nouveau/include/nvkm/engine/fifo.h | 31 +++
.../include/trace/events/nouveau_fifo.h | 58 ++++++
drivers/gpu/drm/nouveau/nouveau_drm.c | 29 +++
.../gpu/drm/nouveau/nvkm/engine/fifo/Kbuild | 1 +
.../gpu/drm/nouveau/nvkm/engine/fifo/base.c | 3 +
.../gpu/drm/nouveau/nvkm/engine/fifo/chan.c | 14 ++
.../gpu/drm/nouveau/nvkm/engine/fifo/nv04.c | 4 +
.../gpu/drm/nouveau/nvkm/engine/fifo/priv.h | 10 +
.../drm/nouveau/nvkm/engine/fifo/recover.c | 176 ++++++++++++++++++
9 files changed, 326 insertions(+)
create mode 100644 drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
create mode 100644 drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
diff --git a/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h b/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h
index 96c16cfccf16..973c3ee445dc 100644
--- a/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h
+++ b/drivers/gpu/drm/nouveau/include/nvkm/engine/fifo.h
@@ -55,6 +55,36 @@ void nvkm_chan_put(struct nvkm_chan **, unsigned long irqflags);
struct nvkm_chan *nvkm_uchan_chan(struct nvkm_object *);
+#define NVKM_FIFO_WEDGE_RING_MAX 32
+
+/*
+ * A channel is only killed once it has faulted NVKM_FIFO_KILL_COUNT times
+ * within NVKM_FIFO_KILL_WINDOW_MS. The PFIFO cache puller names the channel
+ * that is resident when the fault is noticed, which is not necessarily the one
+ * that caused it, so a single fault is not sufficient evidence to kill.
+ */
+#define NVKM_FIFO_KILL_COUNT 3
+#define NVKM_FIFO_KILL_WINDOW_MS 10000
+#define NVKM_FIFO_KILL_CHID_MAX 128
+
+struct nvkm_fifo_wedge {
+ spinlock_t lock;
+ u32 count; /* faults inside the window */
+ ktime_t ts[NVKM_FIFO_WEDGE_RING_MAX]; /* ring of fault timestamps */
+ u32 head; /* ring head */
+ struct work_struct work; /* schedules drm_dev_wedged_event */
+ atomic_t wedged; /* Tier-2 already fired? */
+
+ /* Per-channel fault streak for the Tier-1 escalation. owner is an
+ * identity token for the channel object and is never dereferenced.
+ */
+ struct {
+ void *owner;
+ ktime_t first;
+ u32 count;
+ } chfault[NVKM_FIFO_KILL_CHID_MAX];
+};
+
struct nvkm_fifo {
const struct nvkm_fifo_func *func;
struct nvkm_engine engine;
@@ -86,6 +116,7 @@ struct nvkm_fifo {
spinlock_t lock;
struct mutex mutex;
+ struct nvkm_fifo_wedge wedge;
};
void nvkm_fifo_fault(struct nvkm_fifo *, struct nvkm_fault_data *);
diff --git a/drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h b/drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
new file mode 100644
index 000000000000..46d043a82850
--- /dev/null
+++ b/drivers/gpu/drm/nouveau/include/trace/events/nouveau_fifo.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: MIT */
+#undef TRACE_SYSTEM
+#define TRACE_SYSTEM nouveau
+
+#if !defined(_TRACE_NOUVEAU_FIFO_H) || defined(TRACE_HEADER_MULTI_READ)
+#define _TRACE_NOUVEAU_FIFO_H
+
+#include <linux/tracepoint.h>
+#include <drm/drm_device.h>
+
+TRACE_EVENT(nouveau_fifo_chan_killed,
+ TP_PROTO(struct drm_device *dev, u32 chid, u32 fault_type, u64 info),
+ TP_ARGS(dev, chid, fault_type, info),
+ TP_STRUCT__entry(
+ __string(devname, dev_name(dev->dev))
+ __field(u32, chid)
+ __field(u32, fault_type)
+ __field(u64, info)
+ ),
+ TP_fast_assign(
+ __assign_str(devname);
+ __entry->chid = chid;
+ __entry->fault_type = fault_type;
+ __entry->info = info;
+ ),
+ TP_printk("dev=%s chid=%u fault=%s info=0x%llx",
+ __get_str(devname),
+ __entry->chid,
+ __entry->fault_type == 0 ? "CACHE_ERROR" : "DMA_PUSHER",
+ __entry->info)
+);
+
+TRACE_EVENT(nouveau_fifo_dev_wedged,
+ TP_PROTO(struct drm_device *dev, u32 fault_count, u32 window_ms),
+ TP_ARGS(dev, fault_count, window_ms),
+ TP_STRUCT__entry(
+ __string(devname, dev_name(dev->dev))
+ __field(u32, fault_count)
+ __field(u32, window_ms)
+ ),
+ TP_fast_assign(
+ __assign_str(devname);
+ __entry->fault_count = fault_count;
+ __entry->window_ms = window_ms;
+ ),
+ TP_printk("dev=%s wedged after %u faults in %u ms",
+ __get_str(devname),
+ __entry->fault_count,
+ __entry->window_ms)
+);
+
+#endif /* _TRACE_NOUVEAU_FIFO_H */
+
+#undef TRACE_INCLUDE_PATH
+#define TRACE_INCLUDE_PATH ../../drivers/gpu/drm/nouveau/include/trace/events
+#undef TRACE_INCLUDE_FILE
+#define TRACE_INCLUDE_FILE nouveau_fifo
+#include <trace/define_trace.h>
diff --git a/drivers/gpu/drm/nouveau/nouveau_drm.c b/drivers/gpu/drm/nouveau/nouveau_drm.c
index e16f59b00f6f..9a9278589a3a 100644
--- a/drivers/gpu/drm/nouveau/nouveau_drm.c
+++ b/drivers/gpu/drm/nouveau/nouveau_drm.c
@@ -22,6 +22,8 @@
* Authors: Ben Skeggs
*/
+#define CREATE_TRACE_POINTS
+
#include <linux/aperture.h>
#include <linux/delay.h>
#include <linux/module.h>
@@ -74,6 +76,9 @@
#include "nouveau_uvmm.h"
#include "nouveau_sched.h"
+#include <engine/fifo.h>
+#include <trace/events/nouveau_fifo.h>
+
DECLARE_DYNDBG_CLASSMAP(drm_debug_classes, DD_CLASS_TYPE_DISJOINT_BITS, 0,
"DRM_UT_CORE",
"DRM_UT_DRIVER",
@@ -111,6 +116,18 @@ MODULE_PARM_DESC(runpm, "disable (0), force enable (1), optimus only default (-1
static int nouveau_runtime_pm = -1;
module_param_named(runpm, nouveau_runtime_pm, int, 0400);
+MODULE_PARM_DESC(fifo_wedge_count,
+ "FIFO faults within window before drm_dev_wedged_event "
+ "(0=disable Tier-2, max 32, default 10)");
+unsigned int nouveau_fifo_wedge_count = 10;
+module_param_named(fifo_wedge_count, nouveau_fifo_wedge_count, uint, 0400);
+
+MODULE_PARM_DESC(fifo_wedge_window_ms,
+ "Sliding-window width in milliseconds for fifo_wedge_count "
+ "(default 60000)");
+unsigned int nouveau_fifo_wedge_window_ms = 60000;
+module_param_named(fifo_wedge_window_ms, nouveau_fifo_wedge_window_ms, uint, 0400);
+
static struct drm_driver driver_stub;
static struct drm_driver driver_pci;
static struct drm_driver driver_platform;
@@ -1495,6 +1512,18 @@ nouveau_drm_init(void)
if (!nouveau_modeset)
return 0;
+ if (nouveau_fifo_wedge_count > NVKM_FIFO_WEDGE_RING_MAX) {
+ pr_warn("nouveau: fifo_wedge_count=%u exceeds max %u; clamping\n",
+ nouveau_fifo_wedge_count, NVKM_FIFO_WEDGE_RING_MAX);
+ nouveau_fifo_wedge_count = NVKM_FIFO_WEDGE_RING_MAX;
+ }
+ if (nouveau_fifo_wedge_window_ms < 100 ||
+ nouveau_fifo_wedge_window_ms > 600000) {
+ pr_warn("nouveau: fifo_wedge_window_ms=%u out of range; resetting to 60000\n",
+ nouveau_fifo_wedge_window_ms);
+ nouveau_fifo_wedge_window_ms = 60000;
+ }
+
nouveau_module_debugfs_init();
#ifdef CONFIG_NOUVEAU_PLATFORM_DRIVER
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild
index 376e9c3bcb1a..1ff29753731d 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/Kbuild
@@ -5,6 +5,7 @@ nvkm-y += nvkm/engine/fifo/chan.o
nvkm-y += nvkm/engine/fifo/chid.o
nvkm-y += nvkm/engine/fifo/runl.o
nvkm-y += nvkm/engine/fifo/runq.o
+nvkm-y += nvkm/engine/fifo/recover.o
nvkm-y += nvkm/engine/fifo/nv04.o
nvkm-y += nvkm/engine/fifo/nv10.o
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c
index 9dd924694306..a61183fa38af 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/base.c
@@ -337,6 +337,8 @@ nvkm_fifo_dtor(struct nvkm_engine *engine)
struct nvkm_runl *runl, *runt;
struct nvkm_runq *runq, *rtmp;
+ nv04_fifo_wedge_fini(fifo);
+
if (fifo->userd.bar1)
nvkm_vmm_put(nvkm_bar_bar1_vmm(engine->subdev.device), &fifo->userd.bar1);
nvkm_memory_unref(&fifo->userd.mem);
@@ -390,6 +392,7 @@ nvkm_fifo_new_(const struct nvkm_fifo_func *func, struct nvkm_device *device,
fifo->timeout.chan_msec = 10000;
spin_lock_init(&fifo->lock);
mutex_init(&fifo->mutex);
+ nv04_fifo_wedge_init(fifo);
return nvkm_engine_ctor(&nvkm_fifo, device, type, inst, true, &fifo->engine);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
index 418a8918bcb8..79774c6460f4 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
@@ -275,6 +275,20 @@ nvkm_chan_del(struct nvkm_chan **pchan)
nvkm_gpuobj_del(&chan->ramfc);
if (chan->cgrp) {
+ struct nvkm_fifo *fifo = chan->cgrp->runl->fifo;
+
+ /* Drop this channel's fault streak before the id is reused. */
+ if (chan->id >= 0) {
+ typeof(&fifo->wedge.chfault[0]) cf =
+ &fifo->wedge.chfault[chan->id % NVKM_FIFO_KILL_CHID_MAX];
+ unsigned long flags;
+
+ spin_lock_irqsave(&fifo->wedge.lock, flags);
+ if (cf->owner == chan)
+ cf->owner = NULL;
+ spin_unlock_irqrestore(&fifo->wedge.lock, flags);
+ }
+
nvkm_chid_put(chan->cgrp->runl->chid, chan->id, &chan->cgrp->lock);
nvkm_cgrp_unref(&chan->cgrp);
}
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
index fa13cd55b593..cb81941ecccd 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/nv04.c
@@ -345,6 +345,8 @@ nv04_fifo_intr_cache_error(struct nvkm_fifo *fifo, u32 chid, u32 get)
chid, chan ? chan->name : "unknown",
(mthd >> 13) & 7, mthd & 0x1ffc, data);
nvkm_chan_put(&chan, flags);
+ nv04_fifo_recover(fifo, chid, NV04_FAULT_CACHE_ERROR,
+ ((u64)mthd << 32) | data);
}
}
@@ -410,6 +412,8 @@ nv04_fifo_intr_dma_pusher(struct nvkm_fifo *fifo, u32 chid)
}
nvkm_chan_put(&chan, flags);
+ nv04_fifo_recover(fifo, chid, NV04_FAULT_DMA_PUSHER, state);
+
nvkm_wr32(device, 0x003228, 0x00000000);
nvkm_wr32(device, 0x003220, 0x00000001);
nvkm_wr32(device, 0x002100, NV_PFIFO_INTR_DMA_PUSHER);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h
index fff1428ef267..bf551906dcd4 100644
--- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/priv.h
@@ -83,6 +83,16 @@ void nv04_chan_start(struct nvkm_chan *);
void nv04_chan_stop(struct nvkm_chan *);
void nv04_eobj_ramht_del(struct nvkm_chan *, int);
+/* Recovery helper for Tesla cache_error/dma_pusher (recover.c). */
+#define NV04_FAULT_CACHE_ERROR 0
+#define NV04_FAULT_DMA_PUSHER 1
+
+void nv04_fifo_recover(struct nvkm_fifo *fifo, u32 chid, u32 fault_type, u64 info);
+void nv04_fifo_wedge_init(struct nvkm_fifo *fifo);
+void nv04_fifo_wedge_fini(struct nvkm_fifo *fifo);
+extern unsigned int nouveau_fifo_wedge_count;
+extern unsigned int nouveau_fifo_wedge_window_ms;
+
int nv10_fifo_chid_nr(struct nvkm_fifo *);
int nv50_fifo_chid_nr(struct nvkm_fifo *);
diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
new file mode 100644
index 000000000000..ea962ddf0bcb
--- /dev/null
+++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/recover.c
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: MIT
+/*
+ * nv04_fifo_recover - shared recovery helper for Tesla cache_error and
+ * dma_pusher fault paths.
+ *
+ * Tier-1: kill the offending channel via nvkm_chan_error, but only once it
+ * has faulted repeatedly inside a short window.
+ * Tier-2: after a configurable burst of faults within a sliding time
+ * window, request a device-wide drm_dev_wedged_event so userspace
+ * can rebind the driver.
+ */
+
+#include "priv.h"
+#include "chan.h"
+
+#include <core/device.h>
+#include <subdev/timer.h>
+
+#include <linux/workqueue.h>
+#include <linux/jiffies.h>
+#include <linux/ktime.h>
+#include <drm/drm_drv.h>
+#include <drm/drm_device.h>
+
+#include "nouveau_drv.h"
+#include <trace/events/nouveau_fifo.h>
+
+static struct drm_device *
+nv04_fifo_drm_device(struct nvkm_fifo *fifo)
+{
+ struct nvkm_device *device = fifo->engine.subdev.device;
+ struct nouveau_drm *drm = dev_get_drvdata(device->dev);
+
+ return (drm && drm->dev) ? drm->dev : NULL;
+}
+
+void
+nv04_fifo_recover(struct nvkm_fifo *fifo, u32 chid, u32 fault_type, u64 info)
+{
+ struct drm_device *drm_dev = nv04_fifo_drm_device(fifo);
+ struct nvkm_chan *chan;
+ unsigned long flags;
+ ktime_t now, cutoff;
+ u32 i, count;
+
+ chan = nvkm_chan_get_chid(&fifo->engine, chid, &flags);
+ if (chan) {
+ struct nvkm_fifo_wedge *w = &fifo->wedge;
+ typeof(&w->chfault[0]) cf =
+ &w->chfault[chid % NVKM_FIFO_KILL_CHID_MAX];
+ ktime_t tnow = ktime_get();
+ unsigned long wflags;
+ bool kill;
+ u32 seen;
+
+ /*
+ * wedge.lock nests inside chan->cgrp->lock, which
+ * nvkm_chan_get_chid() holds until nvkm_chan_put(). The other
+ * users of wedge.lock take no channel lock, so there is no
+ * reverse ordering.
+ */
+ if (atomic_read(&chan->errored)) {
+ /*
+ * The channel is already dead but can still be
+ * resident and fault again, because nv50 and g84 have
+ * no preempt and the handler re-enables the puller
+ * unconditionally. Do not restart its streak.
+ */
+ nvkm_chan_put(&chan, flags);
+ goto tier2;
+ }
+
+ spin_lock_irqsave(&w->lock, wflags);
+ if (cf->owner != chan ||
+ ktime_after(tnow, ktime_add_ms(cf->first,
+ NVKM_FIFO_KILL_WINDOW_MS))) {
+ cf->owner = chan;
+ cf->first = tnow;
+ cf->count = 0;
+ }
+ seen = ++cf->count;
+ kill = seen >= NVKM_FIFO_KILL_COUNT;
+ if (kill)
+ cf->owner = NULL;
+ spin_unlock_irqrestore(&w->lock, wflags);
+
+ if (kill) {
+ if (drm_dev)
+ trace_nouveau_fifo_chan_killed(drm_dev, chid,
+ fault_type, info);
+ /*
+ * preempt must stay false: nv50 and g84 channels have
+ * no .preempt callback, so nvkm_chan_error() would
+ * dereference a NULL function pointer under a spinlock
+ * in interrupt context.
+ */
+ nvkm_chan_error(chan, false);
+ } else {
+ nvkm_warn(&fifo->engine.subdev,
+ "ch %d fault %u/%u within %ums, resuming\n",
+ chid, seen, NVKM_FIFO_KILL_COUNT,
+ NVKM_FIFO_KILL_WINDOW_MS);
+ }
+ nvkm_chan_put(&chan, flags);
+ }
+
+tier2:
+ if (nouveau_fifo_wedge_count == 0)
+ return;
+
+ now = ktime_get();
+ cutoff = ktime_sub_ms(now, nouveau_fifo_wedge_window_ms);
+
+ spin_lock_irqsave(&fifo->wedge.lock, flags);
+
+ /* Insert current first, then purge expired and count survivors. */
+ fifo->wedge.ts[fifo->wedge.head] = now;
+ fifo->wedge.head = (fifo->wedge.head + 1) % NVKM_FIFO_WEDGE_RING_MAX;
+
+ count = 0;
+ for (i = 0; i < NVKM_FIFO_WEDGE_RING_MAX; i++) {
+ if (!ktime_to_ns(fifo->wedge.ts[i]))
+ continue;
+ if (ktime_before(fifo->wedge.ts[i], cutoff))
+ fifo->wedge.ts[i] = 0;
+ else
+ count++;
+ }
+ fifo->wedge.count = count;
+
+ if (count >= nouveau_fifo_wedge_count)
+ schedule_work(&fifo->wedge.work);
+
+ spin_unlock_irqrestore(&fifo->wedge.lock, flags);
+}
+
+static void
+nv04_fifo_wedge_work(struct work_struct *work)
+{
+ struct nvkm_fifo_wedge *w = container_of(work, struct nvkm_fifo_wedge, work);
+ struct nvkm_fifo *fifo = container_of(w, struct nvkm_fifo, wedge);
+ struct drm_device *drm_dev = nv04_fifo_drm_device(fifo);
+ u32 fault_count;
+
+ if (atomic_xchg(&w->wedged, 1) != 0)
+ return; /* already wedged this cycle */
+
+ if (!drm_dev)
+ return;
+
+ fault_count = w->count;
+
+ dev_info(drm_dev->dev,
+ "nouveau: fifo wedged after %u faults in %u ms\n",
+ fault_count, nouveau_fifo_wedge_window_ms);
+
+ trace_nouveau_fifo_dev_wedged(drm_dev, fault_count,
+ nouveau_fifo_wedge_window_ms);
+
+ drm_dev_wedged_event(drm_dev, DRM_WEDGE_RECOVERY_REBIND, NULL);
+}
+
+void
+nv04_fifo_wedge_init(struct nvkm_fifo *fifo)
+{
+ memset(fifo->wedge.chfault, 0, sizeof(fifo->wedge.chfault));
+ spin_lock_init(&fifo->wedge.lock);
+ INIT_WORK(&fifo->wedge.work, nv04_fifo_wedge_work);
+ atomic_set(&fifo->wedge.wedged, 0);
+}
+
+void
+nv04_fifo_wedge_fini(struct nvkm_fifo *fifo)
+{
+ cancel_work_sync(&fifo->wedge.work);
+}
--
2.54.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH v2 2/3] drm/nouveau: subscribe to channel-kill events on NV50 and newer
2026-08-06 8:52 ` Marek Czernohous
(?)
@ 2026-08-06 9:20 ` sashiko-bot
-1 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-08-06 9:20 UTC (permalink / raw)
To: Marek Czernohous; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
Pre-existing issues:
- [High] Asymmetric teardown of `chan->kill` event and `chan->fence` leads to a Use-After-Free if a channel kill event fires during channel destruction.
- [High] Channel kill event can fire before `chan->fence` is fully initialized, leading to uninitialized spinlock and list head access.
--
--- Patch [2]: [PATCH v2 2/3] drm/nouveau: subscribe to channel-kill events on NV50 and newer ---
commit 66f8740c572f7ea5ed5181be09ddea5061fdb648
Author: Marek Czernohous <marek@czernohous.de>
drm/nouveau: subscribe to channel-kill events on NV50 and newer
nouveau_channel_init() only subscribes to the channel-killed event for
FERMI_CHANNEL_GPFIFO and newer. This patch lowers the class gate to
NV50_CHANNEL_GPFIFO, ensuring that pending fences on a killed channel are
properly signalled for NV50/Tesla hardware.
> diff --git a/drivers/gpu/drm/nouveau/nouveau_chan.c b/drivers/gpu/drm/nouveau/nouveau_chan.c
> index 598513f604493..1db9fbd81f5c0 100644
> --- a/drivers/gpu/drm/nouveau/nouveau_chan.c
> +++ b/drivers/gpu/drm/nouveau/nouveau_chan.c
> @@ -363,7 +363,7 @@ nouveau_channel_init(struct nouveau_channel *chan, u32 vram, u32 gart)
> if (ret)
> return ret;
>
> - if (chan->user.oclass >= FERMI_CHANNEL_GPFIFO) {
> + if (chan->user.oclass >= NV50_CHANNEL_GPFIFO) {
[Severity: High]
This is a pre-existing issue, but does expanding this subscription to NV50
expose it to a use-after-free during channel teardown?
During teardown, nouveau_channel_del() destroys the fence context before
unregistering the channel kill event:
nouveau_channel_del() {
...
if (chan->fence)
nouveau_fence(chan->cli->drm)->context_del(chan);
...
nvif_event_dtor(&chan->kill);
}
The fence context is freed in nv10_fence_context_del() where chan->fence is
set to NULL non-atomically. If a channel kill event fires concurrently, could
the asynchronous handler nouveau_channel_kill() read chan->fence locklessly
just before it is set to NULL and dereference the freed fence context inside
nouveau_fence_context_kill()?
[Severity: High]
This isn't a bug introduced by this patch, but is there also a race during
initialization where the kill event can fire before the fence context is fully
initialized?
In nouveau_channel_init(), the kill event is enabled early:
nouveau_channel_init() {
...
if (ret == 0)
ret = nvif_event_allow(&chan->kill);
...
}
However, the fence context is created at the very end of the function:
nouveau_channel_init() {
...
return nouveau_fence(drm)->context_new(chan);
}
Inside nv50_fence_context_new(), chan->fence is assigned via kzalloc before
the underlying lists and spinlocks are initialized by
nouveau_fence_context_new():
nv50_fence_context_new() {
...
fctx = chan->fence = kzalloc_obj(*fctx);
if (!fctx)
return -ENOMEM;
nouveau_fence_context_new(chan, &fctx->base);
}
If a kill event fires during this window, could nouveau_fence_context_kill()
attempt to acquire an uninitialized spinlock and traverse an uninitialized
list?
> DEFINE_RAW_FLEX(struct nvif_event_v0, args, data,
> sizeof(struct nvif_chan_event_v0));
> struct nvif_chan_event_v0 *host =
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260806085228.1848994-1-mczernohous@gmail.com?part=2
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2 3/3] drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher
2026-08-06 8:52 ` Marek Czernohous
(?)
@ 2026-08-06 9:29 ` sashiko-bot
-1 siblings, 0 replies; 12+ messages in thread
From: sashiko-bot @ 2026-08-06 9:29 UTC (permalink / raw)
To: Marek Czernohous; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Race condition between nvkm_chan_del() and nv04_fifo_recover() leaves a dangling pointer in wedge.chfault, allowing a newly allocated channel to incorrectly inherit a fault streak.
--
--- Patch [3]: [PATCH v2 3/3] drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher ---
commit 673f2147a13507bd6f62792657604b061ac497d7
Author: Marek Czernohous <marek@czernohous.de>
drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher
This commit adds a shared recovery helper, nv04_fifo_recover, for Tesla
chipset FIFO fault handling. It implements a two-tier recovery mechanism
that kills channels after a burst of faults and requests a device-wide
wedge event if faults continue.
> diff --git a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
> index 418a8918bcb8f..79774c6460f4f 100644
> --- a/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
> +++ b/drivers/gpu/drm/nouveau/nvkm/engine/fifo/chan.c
[ ... ]
> @@ -275,6 +275,20 @@ nvkm_chan_del(struct nvkm_chan **pchan)
> nvkm_gpuobj_del(&chan->ramfc);
>
> if (chan->cgrp) {
> + struct nvkm_fifo *fifo = chan->cgrp->runl->fifo;
> +
> + /* Drop this channel's fault streak before the id is reused. */
> + if (chan->id >= 0) {
> + typeof(&fifo->wedge.chfault[0]) cf =
> + &fifo->wedge.chfault[chan->id % NVKM_FIFO_KILL_CHID_MAX];
> + unsigned long flags;
> +
> + spin_lock_irqsave(&fifo->wedge.lock, flags);
> + if (cf->owner == chan)
> + cf->owner = NULL;
> + spin_unlock_irqrestore(&fifo->wedge.lock, flags);
[Severity: Medium]
Can a concurrent nv04_fifo_recover() race with this teardown and leave a
dangling pointer in cf->owner?
Because this is cleared before nvkm_chid_put() unlinks the channel, and
before chan->cgrp->lock is acquired, is it possible for the following
sequence to occur:
CPU1 in nvkm_chan_del():
spin_lock_irqsave(&fifo->wedge.lock, flags);
if (cf->owner == chan)
cf->owner = NULL;
spin_unlock_irqrestore(&fifo->wedge.lock, flags);
CPU2 handling an interrupt in nv04_fifo_recover():
chan = nvkm_chan_get_chid(...)
...
spin_lock_irqsave(&w->lock, wflags);
if (cf->owner != chan || ...) {
cf->owner = chan;
}
CPU1 then calls nvkm_chid_put() unlinking the channel, and it is freed.
Would this leave the freed memory address in cf->owner, causing a newly
allocated channel that happens to reuse this address to incorrectly inherit
the previous channel's fault streak?
> + }
> +
> nvkm_chid_put(chan->cgrp->runl->chid, chan->id, &chan->cgrp->lock);
> nvkm_cgrp_unref(&chan->cgrp);
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260806085228.1848994-1-mczernohous@gmail.com?part=3
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2 0/3] drm/nouveau: nv04 FIFO cleanup + recovery for Tesla
2026-08-06 8:52 ` Marek Czernohous
@ 2026-08-06 9:59 ` Marek Czernohous
-1 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 9:59 UTC (permalink / raw)
To: nouveau; +Cc: Lyude Paul, Danilo Krummrich, dri-devel, linux-kernel
Both findings are correct, thank you.
I checked them against mainline rather than against my downstream tree:
1) nouveau_channel_del() frees the fence context first and only drops the
kill subscription later, among the nvif object teardown calls. The
subscribed handler reaches nouveau_fence_context_kill(chan->fence), so
a kill delivered in that window takes fctx->lock and walks
fctx->pending on a context that context_del() has already freed.
2) nouveau_channel_init() arms the subscription right after mapping userd
and creates the fence context at the end of the same function. The
backends publish the pointer before the context is usable:
fctx = chan->fence = kzalloc_obj(*fctx);
if (!fctx)
return -ENOMEM;
nouveau_fence_context_new(chan, &fctx->base);
and it is nouveau_fence_context_new() that runs spin_lock_init(&fctx->lock)
and INIT_LIST_HEAD(&fctx->pending). The NULL check in
nouveau_channel_kill() does not cover that window: chan->fence is
non-NULL and unusable, so the handler locks something that was never
initialised and walks a list head whose next pointer is still the NULL
left by kzalloc().
Both are unreachable below Fermi today, which is exactly why they belong
in this series: 2/3 lowers the gate to NV50 and 3/3 adds the caller that
kills Tesla channels. This series is what makes them reachable, so
shipping it without them would trade a recoverable fault for a
use-after-free.
I should have carried the first one from the start. It has been running
on the reference machine since 2026-07-25, and my v2 cover letter
described that same ordering and then dismissed it as "most of that
window is harmless". That judgement was wrong.
v3 will put both in front of the subscription change, as 1/5 and 2/5,
with the remaining three unchanged apart from the rebase.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v2 0/3] drm/nouveau: nv04 FIFO cleanup + recovery for Tesla
@ 2026-08-06 9:59 ` Marek Czernohous
0 siblings, 0 replies; 12+ messages in thread
From: Marek Czernohous @ 2026-08-06 9:59 UTC (permalink / raw)
To: nouveau; +Cc: Danilo Krummrich, dri-devel, linux-kernel
Both findings are correct, thank you.
I checked them against mainline rather than against my downstream tree:
1) nouveau_channel_del() frees the fence context first and only drops the
kill subscription later, among the nvif object teardown calls. The
subscribed handler reaches nouveau_fence_context_kill(chan->fence), so
a kill delivered in that window takes fctx->lock and walks
fctx->pending on a context that context_del() has already freed.
2) nouveau_channel_init() arms the subscription right after mapping userd
and creates the fence context at the end of the same function. The
backends publish the pointer before the context is usable:
fctx = chan->fence = kzalloc_obj(*fctx);
if (!fctx)
return -ENOMEM;
nouveau_fence_context_new(chan, &fctx->base);
and it is nouveau_fence_context_new() that runs spin_lock_init(&fctx->lock)
and INIT_LIST_HEAD(&fctx->pending). The NULL check in
nouveau_channel_kill() does not cover that window: chan->fence is
non-NULL and unusable, so the handler locks something that was never
initialised and walks a list head whose next pointer is still the NULL
left by kzalloc().
Both are unreachable below Fermi today, which is exactly why they belong
in this series: 2/3 lowers the gate to NV50 and 3/3 adds the caller that
kills Tesla channels. This series is what makes them reachable, so
shipping it without them would trade a recoverable fault for a
use-after-free.
I should have carried the first one from the start. It has been running
on the reference machine since 2026-07-25, and my v2 cover letter
described that same ordering and then dismissed it as "most of that
window is harmless". That judgement was wrong.
v3 will put both in front of the subscription change, as 1/5 and 2/5,
with the remaining three unchanged apart from the rebase.
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-08-06 10:00 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-06 8:52 [PATCH v2 0/3] drm/nouveau: nv04 FIFO cleanup + recovery for Tesla Marek Czernohous
2026-08-06 8:52 ` Marek Czernohous
2026-08-06 8:52 ` [PATCH v2 1/3] drm/nouveau/fifo/nv04: filter benign CACHE_ERROR from Mesa NV50 bind probe Marek Czernohous
2026-08-06 8:52 ` Marek Czernohous
2026-08-06 8:52 ` [PATCH v2 2/3] drm/nouveau: subscribe to channel-kill events on NV50 and newer Marek Czernohous
2026-08-06 8:52 ` Marek Czernohous
2026-08-06 9:20 ` sashiko-bot
2026-08-06 8:52 ` [PATCH v2 3/3] drm/nouveau/fifo: add recovery path for Tesla cache_error/dma_pusher Marek Czernohous
2026-08-06 8:52 ` Marek Czernohous
2026-08-06 9:29 ` sashiko-bot
2026-08-06 9:59 ` [PATCH v2 0/3] drm/nouveau: nv04 FIFO cleanup + recovery for Tesla Marek Czernohous
2026-08-06 9:59 ` Marek Czernohous
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.