* [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
* 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
* [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 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.