Skip to content

Fix system hang from DIFR prefetch GPFIFO exhaustion - #1286

Open
runiter wants to merge 2 commits into
NVIDIA:mainfrom
runiter:fix-unbounded-gpfifo-wait
Open

Fix system hang from DIFR prefetch GPFIFO exhaustion#1286
runiter wants to merge 2 commits into
NVIDIA:mainfrom
runiter:fix-unbounded-gpfifo-wait

Conversation

@runiter

@runiter runiter commented Aug 11, 2026

Copy link
Copy Markdown

Two related fixes for a deterministic system hang in the DIFR prefetch path. The first stops a stalled channel from taking down the machine; the second stops the channel getting into that state.

Context and the full per-boot data are in #1205.

1. nvidia-push: bound the wait for a free GPFIFO entry

nvWriteGpEntry() waits for a free GPFIFO entry here:

// Wait for a free entry in the buffer
while (nextGpPut == ReadGpGetOffset(push_buffer)) {
    if (nvPushCheckChannelError(push_buffer)) {
        nvAssert(!"A channel error occurred in nvWriteGpEntry()");
        return FALSE;
    }
}

No deadline, and the only exit is nvPushCheckChannelError(), which returns TRUE only once RM has written 0xFFFF into the error notifier. A channel that stalls without faulting — never serviced, no RC event, no notifier write — leaves that check returning FALSE forever. It is also a busy spin with no yield, so the calling kernel thread is lost and the watchdog escalates to a system hang.

Every other wait in this file is bounded: IdleChannel() takes a timeoutMSec, and the notifier wait has short and long timeouts plus an nvPushImportYield(). This loop is the exception.

The change gives it a deadline using IdleChannel()'s idiom, honours the existing noTimeout opt-out, yields between polls, and returns FALSE on expiry — a path Kickoff() already handles by leaving putOffset unchanged. No new helpers or constants.

I chose SHORT_TIMEOUT (3s) over LONG_TIMEOUT (10s) deliberately: the soft-lockup watchdog reports at ~20s, and I wanted this to surface as a logged error rather than a lockup splat. Happy to switch it for consistency with the other waits.

2. nvkms-difr: reset the prefetch channel after a CE fault

This is what makes the channel stall in the first place.

PrefetchSingleSurface() kicks off a copy and waits DIFR_PREFETCH_WAIT_PERIOD_US (10ms) for the semaphore. On expiry it returns FAIL_CE_HW_ERROR — but the GPFIFO entries it already wrote stay queued on a channel the CE never drained. Nothing reclaims them, so GET stops advancing and the channel permanently loses one kickoff's worth of ring.

That ring is small: pushBufferSizeInBytes = 1024 gives numGpFifoEntries = 16 (nvidia-push-init.c), 2 entries per kickoff, and InitGpFifoExtendedBase() consumes 2 more at alloc on Hopper+ without a matching progress-tracker release. So there is very little headroom, and a handful of faults exhausts it — after which the next kickoff waits on a ring that can never drain, which is fix 1's loop.

The change resets the channel on exactly that status, reusing the existing Free/Alloc helpers, so a fault costs nothing permanent and a later prefetch starts with GET and PUT in sync. Only FAIL_CE_HW_ERROR can leak — the other two failure codes return before any kickoff. If reallocation itself fails, that is remembered and FAIL_INSUFFICIENT_L2_SIZE is returned from then on (the code which, per the existing comment, tells RM and PMU to stop requesting prefetches) rather than leaving a freed channel for the next prefetch to dereference.

I did not attempt to make DIFR retry within a boot cycle. subdeviceCtrlCmdLpwrDifrCtrl_IMPL has no implementation in the open tree, so the "disable until next driver load" decision after a CE fault is yours, not NVKMS's, and I did not want to guess at it. In practice a resume re-enables DIFR anyway, so with this fix the next cycle gets a clean channel and can succeed if the stall was transient.

How I hit this

RTX 5070 Ti, Ubuntu 24.04, kernel 6.17.0-1028-oem, driver 595.71.05-open, GNOME Wayland. The machine hard-hangs on exactly the 6th suspend/resume cycle of every boot — 7 boots out of 7, on both deep (S3) and s2idle, never once surviving a 6th. Always:

watchdog: BUG: soft lockup - CPU#1 stuck for 26s! [nvidia-modeset/]
RIP: nvWriteGpEntry+0xf9
  nvPushKickoff
  PrefetchHelperSurfaceEvo
  nvDIFRPrefetchSurfaces
  DifrPrefetchEventDeferredWork
  nvkms_kthread_q_callback

The fixed count is what pointed at a small ring being consumed one kickoff per resume rather than anything probabilistic.

Worth noting the user-visible consequence of the leak beyond the hang: because a CE fault disables DIFR until the next driver load, and each resume re-enables it for exactly one more doomed attempt, DIFR appears to be silently non-functional on this machine after the first suspend of any boot — while still accumulating the damage that eventually hangs it.

#1205 also has a report of the same deadlock signature triggered by plain display idle with no S3 involved (RTX 4060 Max-Q, 580.173.02), which fits: any repeated prefetch failure fills the ring, and suspend/resume is just a reliable way to produce one.

Testing

Both patches are derived from source analysis and have been compile-tested only. They build clean against 6.17.0-1028-oem with no new warnings. I have not run them on hardware, so I can't claim they resolve the hang — only that fix 1 removes the code path's ability to spin forever, and fix 2 removes the accumulation that leads there.

Two review questions I can't answer from outside:

  1. nvPushImportYield() has precedent in nvidia-push.c, but you know better than I do whether every Kickoff() caller is in a context where yielding is legal. Happy to gate or drop the yield and keep only the deadline.
  2. Is channel alloc/free legal from DifrPrefetchEventDeferredWork()'s kthread-queue context? nvkms-difr.c already makes RM control calls from timer and kthread callbacks, but full channel reallocation from there is an assumption on my part. If it isn't safe, the reset would need deferring to a worker.

Both files are byte-identical between 595.71.05, 595.91.07 and 610.57.04, so nothing released contains a fix. I have a 100% reproducible case in six suspend cycles and am glad to build and test any alternative patch you'd prefer, on either branch.

nvWriteGpEntry() waits for the GPU to consume a GPFIFO entry in a loop
whose only exit is nvPushCheckChannelError(), which reports an error only
once RM has written 0xFFFF into the channel's error notifier. A channel
that stalls without faulting -- one that is simply never serviced --
leaves the notifier clean, so the loop never terminates. Because it is a
busy spin with no yield, this hangs the calling kernel thread, and with
it the machine.

Give the loop a deadline using the same idiom as IdleChannel(), honouring
the existing noTimeout opt-out, and yield between polls as the notifier
wait already does. On expiry return FALSE, which Kickoff() already
handles by leaving putOffset unchanged.
@CLAassistant

CLAassistant commented Aug 11, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

When PrefetchSingleSurface() gives up waiting for the prefetch semaphore
it returns FAIL_CE_HW_ERROR, but the GPFIFO entries it already wrote stay
queued on a channel the copy engine never drained. Nothing reclaims them,
so GET stops advancing and the channel permanently loses one kickoff's
worth of its ring. The DIFR channel's pushbuffer is 1024 bytes, giving
only 16 GPFIFO entries, so a handful of such faults exhausts it and the
next kickoff waits on a ring that can never drain.

Reset the channel on that status so a fault costs nothing permanent, and
a later prefetch starts with GET and PUT back in sync. Only
FAIL_CE_HW_ERROR can leak: the other failure paths return before any
kickoff. If the channel cannot be reallocated, remember that and report
FAIL_INSUFFICIENT_L2_SIZE from then on, which stops RM requesting further
prefetches, rather than leaving a freed channel for the next one to use.
@runiter runiter changed the title nvidia-push: bound the wait for a free GPFIFO entry Fix system hang from DIFR prefetch GPFIFO exhaustion Aug 11, 2026
@runiter

runiter commented Aug 14, 2026

Copy link
Copy Markdown
Author

Good news, my proposed fix at PR below is working well in my own pc. No more hangs!

For those who like to try it, I built this easy script in attached zip file:
nvidia-difr-hang-fix.zip

Simply run these commands:

./build.sh
sudo ./install.sh 
sudo reboot

@Cartagines

Copy link
Copy Markdown

Independent reproduction on a different GPU generation, compositor and trigger,
on 610.57.04. Adding it here since you note the files are byte-identical across
595.71.05 / 595.91.07 / 610.57.04 — this confirms in the field that the newest
release still hangs.

Configuration

GPU RTX 4070 Max-Q / Mobile, AD106M [10de:2860] (Ada, not Blackwell)
Driver nvidia-open 610.57.04
Kernel 7.1.8-arch1-3 (Arch)
Session KWin 6.7.4 / Plasma 6, Wayland (not GNOME)
Display eDP-1 hangs off the dGPU (card1-eDP-1); the iGPU exposes no eDP connector

Trigger: display idle, with no suspend anywhere in the picture

You mention a second-hand report of the non-S3 case in #1205. This is a
first-hand one, and the journal is unambiguous: zero PM: suspend entry
events across the entire retained history.
This machine has never suspended.
The hang happens with the session sitting idle on the lock screen.

Four occurrences with an identical signature — 2026-08-06 18:09, 08-12 18:36,
08-13 19:56, 08-17 12:02 — each ending in a forced power-off. The nine boots
before 08-06 all shut down cleanly.

Stacks

Holder, spinning in R exactly as in your report:

task:nvidia-modeset/ state:R  running task
  ? nvWriteGpEntry            [nvidia_modeset]
  ? nvPushKickoff             [nvidia_modeset]
  ? PrefetchHelperSurfaceEvo  [nvidia_modeset]
  ? nvDIFRPrefetchSurfaces    [nvidia_modeset]
  ? DifrPrefetchEventDeferredWork [nvidia_modeset]
  ? nvkms_kthread_q_callback  [nvidia_modeset]

Waiter — and this is a different entry point from the atomic-modeset path
in the other reports:

INFO: task kwin_wayland:1343 blocked for more than 122 seconds.
INFO: task kwin_wayland:1343 blocked on a semaphore likely last held by task nvidia-modeset/:350
task:kwin_wayland    state:D
  down+0x53
  nvkms_ioctl_from_kapi+0x58     [nvidia_modeset]
  CreateSurface+0x214            [nvidia_modeset]
  nv_drm_framebuffer_create+0x365 [nvidia_drm]
  drm_internal_framebuffer_create
  drm_mode_addfb2

So the deadlock is reachable through plain ADDFB2, via
nvkms_ioctl_from_kapi() (not the ..._try_pmlock variant), which takes
nvkms_lock with an uninterruptible down(). That makes every blocked DRM
client unkillable — SIGKILL included — so there is no recovery short of a
hard reset.

Two details that support your "stall without faulting" reading:

  • No Xid, in any of the four occurrences. Consistent with
    nvPushCheckChannelError() never firing because RM never wrote 0xFFFF.
  • The kernel stays fully alive while the display is dead. In the 08-17 hang,
    an application completed an HTTPS request to a remote host 39 minutes
    after the compositor died
    — DNS, TCP, TLS and journald writes all working.
    Only the graphics path is gone.

Re: your first review question (is the yield legal?)

nvPushImportYield() has precedent in nvidia-push.c, but you know better
than I do whether every Kickoff() caller is in a context where yielding is
legal.

For what it's worth from outside as well, this looks safe in the published
tree, on three grounds:

  1. nvkms is the only consumer of nvidia-push here. There is exactly one
    imports table — src/nvidia-modeset/src/nvkms-push.c:246. No other host
    driver in this tree can reach Kickoff() from an atomic context.

  2. The DIFR path is already in sleeping context.
    nvkms_kthread_q_callback() takes nvkms_read_lock_pm_lock() (line 1021)
    and then down(&nvkms_lock) (line 1023) before invoking timer->proc().
    Both are sleeping locks, and nvkms_yield() is just schedule()
    (nvidia-modeset-linux.c:735).

  3. Direct precedent for the same operation. nvkms-dma.c waits for the
    GPU to drain an EVO channel in nvEvoPollForEmptyChannel() and calls
    nvkms_yield() unconditionally in that loop (line 85), with
    nvEvoMakeRoom() bounding it at 5 s (line 97). Same semantics as this
    loop, bounded and yielding.

Only the two channels allocated through nvPushAllocChannel() are affected —
DIFR and HeadSurface-3D — so the blast radius of the yield is small.

Re: the timeout value

Happy to switch it for consistency with the other waits.

One data point for whichever you pick. The legitimate worst case on the DIFR
channel is bounded by the ring depth, not by how long the copy takes:

pushBufferSizeInBytes = 1024                       nvkms-difr.c:422
numGpFifoEntries      = 1024 / 64 = 16             nvidia-push-init.c:1285
                      / 2 per kickoff  =  8 kickoffs in flight

and PrefetchSingleSurface() documents each copy at a few hundred µs, "slightly
less than 2 milliseconds for a single 4k display". So ~16 ms is the realistic
ceiling for a healthy channel, and SHORT_TIMEOUT already carries ~190× margin.
HeadSurface's ring is 2048 entries, so filling it means a wedged GPU rather than
a loaded one either way.

Testing offer

I arrived at the same fix for the first hunk independently before finding this
PR, which is at least a second pair of eyes on that approach. What I have that
may be useful to you: a userspace regression test for the wait loop.

nvidia-push reaches the OS only through pDevice->pImports, and reads GP_GET
from plain memory at progressSemaphore.ptr[], so a stalled ring can be mocked
exactly — no GPU, no kernel headers, no root. Against the unpatched loop it
hangs; against the bounded one it passes in milliseconds, and a third case
covers the ring freeing up mid-wait so the normal kickoff path is not
regressed. It builds with gcc and clang, clean under ASan/UBSan.

Happy to post it if it would help, and equally happy to run either of your
patches on this hardware — my trigger is display idle rather than suspend, so
it exercises a different route into the same loop.

One caution on the sibling loop

__nvPushMakeRoom() has the same unbounded shape, and it would be natural to
give it the same treatment — but it returns void, and its callers write
unconditionally afterwards. nvPushHeader() does
__nvPushSetMethodDataSegment() and then freeDwords -= (count+1) with no
check, so an early return there would write past the available space and
underflow freeDwords (NvU32) into a self-amplifying overrun of a
DMA-mapped buffer. Bounding it safely needs the failure to be propagated
through the push macros first. Worth a comment there if this lands, so nobody
"fixes" it the obvious way.

@aritger

aritger commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

This is excellent debugging, @runiter. Thank you for the thorough analysis in #1205 and for the proposed fixes here. I need a little time to get more familiar with this particular code. But I wanted to acknowledge this pull request, and I'll try to answer your open questions, soon.

@Cartagines

Copy link
Copy Markdown

Let me know if i can help in any way @aritger @runiter

@runiter

runiter commented Aug 19, 2026

Copy link
Copy Markdown
Author

@aritger you're welcome. Glad to hear that the fix is being looked at seriously.
FYI the fix continues to work well in local machine. Before the fix I would get consistent hang at 6th s3 wakeup.
Now I'm on 15th s3 sleep and 2 weeks time and not a single hang so far.

@Cartagines thank you for confirming it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants