* [PATCH v3 00/14] drm/panfrost: Resume long jobs instead of resetting, and mapping error path fixes
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
Fixes for problems found running the GLES, Vulkan and OpenCL conformance
tests with Mesa on an Orange Pi Zero2 (Allwinner H616, Mali-G31 MP2, one
shader core, 1 GB of RAM), and for problems the Sashiko reviews of v1
and v2 found in the code around them.
Patches 1-3: the reset work touches a runtime or system suspended GPU, a
CPU cache sync of a heap BO walks its sparse page array, and the GPU
fault handler revives a mapping that is being freed.
Patches 4-7, BO mapping error paths under memory pressure: a WARN on an
ordinary allocation failure, pages dropped while the cached sg table
still maps them, PTEs left behind by a partial map_pages(), folios of a
heap chunk kept after a failed fault, and a false WARN for folios larger
than the 2 MB chunk (16K and 64K pages).
Patches 8-11, races around panfrost_reset(): a job cancelled from _NEXT
is taken for done, a job that completes during the reset is reported as
cancelled, a job queued while a reset is pending drops a cycle counter
reference it never took, and such a job is taken for done without having
run. Patch 13 widens these windows.
Patches 12 and 13: a job that runs into the 500 ms timeout no longer
gets the GPU reset. On a small GPU OpenCL kernels and heavy fragment
jobs legitimately run longer: OpenCL CTS Integer Ops long math and
Thread Dimensions, KHR-GLES31 advanced blend mixedSequence and several
dEQP-VK spirv_assembly fragment cases fail that way. As in the Arm
driver, a job whose slot is still active is soft-stopped and resumed
from JS_TAIL, hard-stopped if it has not reached a task boundary after
5 s, and the GPU is reset only if the hard stop fails too. Patch 12
makes panfrost_reset() wait on the live slot status (JOB_INT_JS_STATE is
a snapshot); patch 13 needs it, and it fixes the reset path on its own.
Patch 14 reports the BOs around an unhandled page fault, which told a
texture over-read apart from a kernel problem.
Open points:
- A job that keeps reaching task boundaries is resumed after every
timeout and never killed. I can add a limit on its total run time.
- While a long job is being stopped (5.5 s at worst), patch 13 keeps the
job interrupts masked and all schedulers stopped, as panfrost_reset()
does today around a reset. The other slot starts no job in that time,
and a job that finishes on it is signalled only afterwards. Over two
runs of the tests below this took at most 85 ms for a compute job
(median 0.3 ms, 206 timeouts) and 349 ms for a fragment job (6
timeouts). Mainline resets the GPU at that point, under the jobs on
both slots. Stopping only the slot that timed out needs reset.pending,
the interrupt mask and the scheduler stop made per slot; I can do that
as a follow-up.
- I can split the independent fixes (1-11, 14) from the soft-stop rework
(12, 13).
Tested on the Orange Pi Zero2 with Mesa 26.2 and this series backported
to v7.3.3:
- OpenCL CTS Integer Ops quick_long_math/quick_ulong_math, Thread
Dimensions quick_2d_explicit_local and KHR-GLES31
blend_equation_advanced test_coherency.mixedSequence pass (failed
before); dEQP-VK spirv_assembly vec4 i8/u64 bit_field_insert count64
fragment cases pass in 4-6 s (timed out before). 107 job timeouts, all
soft-stopped and resumed, no GPU reset.
- A never-ending OpenCL kernel is hard-stopped after 5 s; with the hard
stop withheld (*), the GPU is reset instead. Both times a devcoredump
is taken and the GPU keeps working.
- The reset work queued (*) while the GPU is runtime suspended resumes
it before the reset (patch 1). Queued five times while Integer Ops and
Thread Dimensions run side by side: both pass. Most of these resets
found the GPU suspended between two jobs; the trace does not show
whether a job was on a slot at any of them.
- s2idle entered while the reset work runs (*), and resets requested (*)
during suspend, idle and under Integer Ops: suspend waits for the
work, the pending reset runs after resume, Integer Ops passes
(patch 1).
- With a 200 ms delay before the stop (*) and the cycle counter forced
on (*): 10 jobs queued while a reset was pending were drained without
a cycle counter reference (patch 10), one on a slot that had gone idle
was kept (patch 11), the use count ends at 0, and the tests pass.
- dEQP-GLES31 draw_indirect compute_interop large cases in memory
cgroups of 450-600 MB: allocation errors or OOM kills, no WARN, and
the GPU keeps working.
- A forced page fault prints the BO report.
- PANFROST_SYNC_BO fails with -EINVAL on a heap BO and succeeds on a
plain one.
- No regressions in dEQP-GLES3 draw/occlusion_query/shaders.loops (964
cases), dEQP-GLES31 compute (195), KHR-GLES31 compute_shader (37, two
not supported) and 9 OpenCL CTS groups.
(*) test-only module parameter, not part of this series.
The problems patches 1-3 and 8-11 fix were not reproduced. The fixed
paths of patches 1, 2, 10 and 11 ran in the tests above; patches 3, 8
and 9, the larger-folio cases of patches 6 and 7 (4K pages here) and the
races patch 13 handles rest on code review. Each patch builds without
warnings with W=1 for arm64 and arm.
These patches were developed with Claude (Anthropic) as a coding
assistant: it investigated the problems and wrote the code and the
commit messages. I reviewed them, and they were tested as described
above.
---
Changes in v3:
- Patch 1: disable the reset work across system sleep, as
pm_runtime_force_suspend() ignores the usage count, and queue it again
on resume if a reset is pending (Sashiko,
https://lore.kernel.org/all/sashiko-outbox-210830@kernel.org/).
- New patch 7 (Sashiko,
https://lore.kernel.org/all/sashiko-outbox-210837@kernel.org/).
- New patches 10 and 11 (Sashiko on v2 10/11,
https://lore.kernel.org/all/sashiko-outbox-210891@kernel.org/).
- Patch 13 (v2 10/11): rebased onto 10.
- All patches: shorter commit messages and comments; no other code
change.
- Renumbered: v2 7 and 8 are now 8 and 9, v2 9-11 are now 12-14.
- Rebased onto the current drm-misc-next.
- Link to v2: https://patch.msgid.link/20261112-panfrost-long-jobs-v2-0-a06830da3d99@gmail.com
Changes in v2:
- New patches 1-3, 7 and 8, for problems the Sashiko review of v1 found
in the code around the series (3: by analogy with a finding):
https://lore.kernel.org/all/sashiko-outbox-206548@kernel.org/
https://lore.kernel.org/all/sashiko-outbox-206561@kernel.org/
https://lore.kernel.org/all/sashiko-outbox-206570@kernel.org/
https://lore.kernel.org/all/sashiko-outbox-206581@kernel.org/
- Patch 6 (v1 3/7): clear the page array range the fault filled, whole
folios, which can exceed the 2 MB chunk, and leave folios owned by a
neighbouring chunk alone (Sashiko).
- Patch 10 (v1 6/7), after the Sashiko review: a job that does not stop
within 5 s gets karma and a devcoredump; a reset asked for while the
slots are being stopped is done, not dropped; jobs on other slots that
only had the 10 ms of an ordinary reset are restarted, not failed; a
job that completes just as its timeout fires no longer triggers a
reset.
- Patch 11 (v1 7/7): skip mappings whose last reference is gone, and
drop the "mapped" flag (Sashiko).
- Dropped v1 4/7 ("Don't resume a soft-stopped job after a GPU reset"):
I have no evidence that a reset loses results of a soft-stopped job,
and the Arm driver resumes soft-stopped atoms without such a check.
- Cover letter: which paths the tests did not run, what stopping a long
job costs the other slot, and tests of the GPU reset path.
- Renumbered: v1 1-3 are now 4-6, v1 5-7 are now 9-11.
- Rebased onto the current drm-misc-next.
- Link to v1: https://lore.kernel.org/r/20261110-panfrost-long-jobs-v1-0-24508939ae8d@gmail.com
---
Arun Iyengar (14):
drm/panfrost: Power the GPU up for a reset queued from the MMU code
drm/panfrost: Refuse to sync the CPU caches of a heap BO
drm/panfrost: Don't take a reference on a mapping that is being freed
drm/panfrost: Don't WARN when a GEM mapping cannot get its pages
drm/panfrost: Unwind a failed GPU mapping without dropping its pages
drm/panfrost: Give back the pages of a heap chunk that failed to map
drm/panfrost: Don't WARN for a heap folio a neighbouring chunk filled
drm/panfrost: Don't signal a job cancelled from _NEXT at a reset as done
drm/panfrost: Don't cancel a job that completed during a reset
drm/panfrost: Drop the cycle counter only for a job that holds it
drm/panfrost: Don't take a job that was never started for done
drm/panfrost: Wait for the soft-stopped slots with the live slot status
drm/panfrost: Soft-stop and resume a job that outlives the timeout
drm/panfrost: Name the BOs around an unhandled page fault
drivers/gpu/drm/panfrost/panfrost_device.c | 26 ++-
drivers/gpu/drm/panfrost/panfrost_gem.c | 4 +
drivers/gpu/drm/panfrost/panfrost_job.c | 272 +++++++++++++++++++++++------
drivers/gpu/drm/panfrost/panfrost_job.h | 4 +
drivers/gpu/drm/panfrost/panfrost_mmu.c | 159 +++++++++++++++--
5 files changed, 396 insertions(+), 69 deletions(-)
---
base-commit: c88084027d0e922dd925b96e5a0d184ba04a8f43
change-id: 20261110-panfrost-long-jobs-1643287a2ac1
Best regards,
--
Arun Iyengar <aiyengar1990@gmail.com>
^ permalink raw reply [flat|nested] 32+ messages in thread* [PATCH v3 01/14] drm/panfrost: Power the GPU up for a reset queued from the MMU code
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
panfrost_reset_work() calls panfrost_reset() without a runtime PM
reference. wait_ready() queues it when AS_ACTIVE is stuck, from MMU
operations whose callers drop their PM reference right after, and it
shares the ordered reset workqueue with the scheduler timeout handlers,
so it can run late. If the GPU was runtime suspended in between,
panfrost_reset() writes the registers of a powered-off GPU: a
synchronous external abort on arm64.
Take a runtime PM reference in the work. pm_runtime_force_suspend()
ignores the usage count, so also disable the work across system sleep
and queue it again on resume if a reset is still pending. If the GPU
cannot be resumed, no job is running on it and
panfrost_device_runtime_resume() resets it later, so just clear the
pending flag. Skipping the reset on a suspended GPU instead would leave
jobs that a concurrent resume queued behind the pending flag waiting.
Found by code review (Sashiko). Not reproduced.
Fixes: f9ab9c66f080 ("drm/panfrost: Reset the GPU when the AS_ACTIVE bit is stuck")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206548@kernel.org/
Closes: https://lore.kernel.org/all/sashiko-outbox-210830@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_device.c | 26 ++++++++++++++++++++++++--
drivers/gpu/drm/panfrost/panfrost_job.c | 11 +++++++++++
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_device.c b/drivers/gpu/drm/panfrost/panfrost_device.c
index 40eda3612..2c11a6721 100644
--- a/drivers/gpu/drm/panfrost/panfrost_device.c
+++ b/drivers/gpu/drm/panfrost/panfrost_device.c
@@ -567,7 +567,7 @@ static int panfrost_device_runtime_suspend(struct device *dev)
return 0;
}
-static int panfrost_device_resume(struct device *dev)
+static int panfrost_device_power_up(struct device *dev)
{
struct panfrost_device *pfdev = dev_get_drvdata(dev);
int ret;
@@ -604,14 +604,36 @@ static int panfrost_device_resume(struct device *dev)
return ret;
}
+static void panfrost_device_enable_reset(struct panfrost_device *pfdev)
+{
+ enable_work(&pfdev->reset.work);
+ if (atomic_read(&pfdev->reset.pending))
+ queue_work(pfdev->reset.wq, &pfdev->reset.work);
+}
+
+static int panfrost_device_resume(struct device *dev)
+{
+ int ret = panfrost_device_power_up(dev);
+
+ panfrost_device_enable_reset(dev_get_drvdata(dev));
+ return ret;
+}
+
static int panfrost_device_suspend(struct device *dev)
{
struct panfrost_device *pfdev = dev_get_drvdata(dev);
int ret;
+ /* pm_runtime_force_suspend() ignores the reset work's runtime PM
+ * reference: keep the work off the GPU until resume.
+ */
+ disable_work_sync(&pfdev->reset.work);
+
ret = pm_runtime_force_suspend(dev);
- if (ret)
+ if (ret) {
+ panfrost_device_enable_reset(pfdev);
return ret;
+ }
if (pfdev->comp->pm_features & BIT(GPU_PM_CLK_DIS))
panfrost_clks_disable(pfdev, false);
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 3a46dd61c..7319511db 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -810,7 +810,18 @@ static void panfrost_reset_work(struct work_struct *work)
struct panfrost_device *pfdev;
pfdev = container_of(work, struct panfrost_device, reset.work);
+
+ /* The GPU may have been runtime suspended since the work was queued.
+ * If it cannot be resumed, no job is on it, and
+ * panfrost_device_runtime_resume() resets it later.
+ */
+ if (pm_runtime_resume_and_get(pfdev->base.dev)) {
+ atomic_set(&pfdev->reset.pending, 0);
+ return;
+ }
+
panfrost_reset(pfdev, NULL);
+ pm_runtime_put_autosuspend(pfdev->base.dev);
}
static const struct drm_sched_backend_ops panfrost_sched_ops = {
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 01/14] drm/panfrost: Power the GPU up for a reset queued from the MMU code
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
panfrost_reset_work() calls panfrost_reset() without a runtime PM
reference. wait_ready() queues it when AS_ACTIVE is stuck, from MMU
operations whose callers drop their PM reference right after, and it
shares the ordered reset workqueue with the scheduler timeout handlers,
so it can run late. If the GPU was runtime suspended in between,
panfrost_reset() writes the registers of a powered-off GPU: a
synchronous external abort on arm64.
Take a runtime PM reference in the work. pm_runtime_force_suspend()
ignores the usage count, so also disable the work across system sleep
and queue it again on resume if a reset is still pending. If the GPU
cannot be resumed, no job is running on it and
panfrost_device_runtime_resume() resets it later, so just clear the
pending flag. Skipping the reset on a suspended GPU instead would leave
jobs that a concurrent resume queued behind the pending flag waiting.
Found by code review (Sashiko). Not reproduced.
Fixes: f9ab9c66f080 ("drm/panfrost: Reset the GPU when the AS_ACTIVE bit is stuck")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206548@kernel.org/
Closes: https://lore.kernel.org/all/sashiko-outbox-210830@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_device.c | 26 ++++++++++++++++++++++++--
drivers/gpu/drm/panfrost/panfrost_job.c | 11 +++++++++++
2 files changed, 35 insertions(+), 2 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_device.c b/drivers/gpu/drm/panfrost/panfrost_device.c
index 40eda3612..2c11a6721 100644
--- a/drivers/gpu/drm/panfrost/panfrost_device.c
+++ b/drivers/gpu/drm/panfrost/panfrost_device.c
@@ -567,7 +567,7 @@ static int panfrost_device_runtime_suspend(struct device *dev)
return 0;
}
-static int panfrost_device_resume(struct device *dev)
+static int panfrost_device_power_up(struct device *dev)
{
struct panfrost_device *pfdev = dev_get_drvdata(dev);
int ret;
@@ -604,14 +604,36 @@ static int panfrost_device_resume(struct device *dev)
return ret;
}
+static void panfrost_device_enable_reset(struct panfrost_device *pfdev)
+{
+ enable_work(&pfdev->reset.work);
+ if (atomic_read(&pfdev->reset.pending))
+ queue_work(pfdev->reset.wq, &pfdev->reset.work);
+}
+
+static int panfrost_device_resume(struct device *dev)
+{
+ int ret = panfrost_device_power_up(dev);
+
+ panfrost_device_enable_reset(dev_get_drvdata(dev));
+ return ret;
+}
+
static int panfrost_device_suspend(struct device *dev)
{
struct panfrost_device *pfdev = dev_get_drvdata(dev);
int ret;
+ /* pm_runtime_force_suspend() ignores the reset work's runtime PM
+ * reference: keep the work off the GPU until resume.
+ */
+ disable_work_sync(&pfdev->reset.work);
+
ret = pm_runtime_force_suspend(dev);
- if (ret)
+ if (ret) {
+ panfrost_device_enable_reset(pfdev);
return ret;
+ }
if (pfdev->comp->pm_features & BIT(GPU_PM_CLK_DIS))
panfrost_clks_disable(pfdev, false);
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 3a46dd61c..7319511db 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -810,7 +810,18 @@ static void panfrost_reset_work(struct work_struct *work)
struct panfrost_device *pfdev;
pfdev = container_of(work, struct panfrost_device, reset.work);
+
+ /* The GPU may have been runtime suspended since the work was queued.
+ * If it cannot be resumed, no job is on it, and
+ * panfrost_device_runtime_resume() resets it later.
+ */
+ if (pm_runtime_resume_and_get(pfdev->base.dev)) {
+ atomic_set(&pfdev->reset.pending, 0);
+ return;
+ }
+
panfrost_reset(pfdev, NULL);
+ pm_runtime_put_autosuspend(pfdev->base.dev);
}
static const struct drm_sched_backend_ops panfrost_sched_ops = {
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH v3 01/14] drm/panfrost: Power the GPU up for a reset queued from the MMU code
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
(?)
@ 2026-11-13 9:36 ` sashiko-bot
-1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-11-13 9:36 UTC (permalink / raw)
To: Arun Iyengar; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] drm/panfrost: unclocked MMIO access in
panfrost_device_resume() on error
- [Low] drm/panfrost: race when clearing reset.pending in
panfrost_reset_work()
--
commit 7ec017c5585451d1f5d87f4425f33e0c841194f8
Author: Arun Iyengar <aiyengar1990@gmail.com>
drm/panfrost: Power the GPU up for a reset queued from the MMU code
This commit takes a runtime PM reference in panfrost_reset_work() and
disables the reset work across system suspend and resume so GPU registers
are not accessed while the GPU is powered off.
> diff --git a/drivers/gpu/drm/panfrost/panfrost_device.c b/drivers/gpu/drm/panfrost/panfrost_device.c
> index 40eda3612f849..2c11a67216e0f 100644
> --- a/drivers/gpu/drm/panfrost/panfrost_device.c
> +++ b/drivers/gpu/drm/panfrost/panfrost_device.c
[ ... ]
> @@ -604,14 +604,36 @@ static int panfrost_device_resume(struct device *dev)
> return ret;
> }
>
> +static void panfrost_device_enable_reset(struct panfrost_device *pfdev)
> +{
> + enable_work(&pfdev->reset.work);
> + if (atomic_read(&pfdev->reset.pending))
> + queue_work(pfdev->reset.wq, &pfdev->reset.work);
> +}
> +
> +static int panfrost_device_resume(struct device *dev)
> +{
> + int ret = panfrost_device_power_up(dev);
> +
> + panfrost_device_enable_reset(dev_get_drvdata(dev));
> + return ret;
> +}
[Severity: Medium]
Can panfrost_device_resume() queue pfdev->reset.work onto an unclocked GPU
when panfrost_device_power_up() fails?
If pm_runtime_force_resume() fails in panfrost_device_power_up(), it has
already called pm_runtime_set_suspended() and pm_runtime_enable(), and the
err_resume path disables the GPU_PM_CLK_DIS clocks and GPU_PM_VREG_OFF
regulator. Calling panfrost_device_enable_reset() unconditionally then
queues panfrost_reset_work(), where pm_runtime_resume_and_get() runs
panfrost_device_runtime_resume() -> panfrost_device_reset() without
re-enabling those clocks or regulators, causing an unclocked MMIO access
and synchronous external abort on arm64.
> diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
> index 3a46dd61cf87f..7319511db19f5 100644
> --- a/drivers/gpu/drm/panfrost/panfrost_job.c
> +++ b/drivers/gpu/drm/panfrost/panfrost_job.c
> @@ -810,7 +810,18 @@ static void panfrost_reset_work(struct work_struct *work)
> struct panfrost_device *pfdev;
>
> pfdev = container_of(work, struct panfrost_device, reset.work);
> +
> + /* The GPU may have been runtime suspended since the work was queued.
> + * If it cannot be resumed, no job is on it, and
> + * panfrost_device_runtime_resume() resets it later.
> + */
> + if (pm_runtime_resume_and_get(pfdev->base.dev)) {
> + atomic_set(&pfdev->reset.pending, 0);
> + return;
> + }
[Severity: Low]
Can clearing pfdev->reset.pending after pm_runtime_resume_and_get() fails
race with a concurrent job submission and leave the job unstarted?
kworker CPU1
panfrost_reset_work()
pm_runtime_resume_and_get() -> fails
panfrost_job_run()
pm_runtime_get_sync() -> succeeds
panfrost_job_hw_submit()
panfrost_enqueue_job()
if (!atomic_read(&reset.pending))
/* sees 1, skips JS_COMMAND_START */
atomic_set(&pfdev->reset.pending, 0);
return; <-- job left unstarted until timeout
rpm_resume() does not latch runtime_error on callback or supplier failure,
so a concurrent panfrost_job_run() can resume the GPU after
pm_runtime_resume_and_get() drops dev->power.lock. Because
panfrost_device_runtime_resume() resets the GPU without clearing
pfdev->reset.pending, panfrost_job_hw_submit() sees reset.pending == 1 and
relies on panfrost_reset() to resubmit the job, which never happens once
panfrost_reset_work() clears reset.pending and returns.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113-panfrost-long-jobs-v3-0-80ae78d38f6e@gmail.com?part=1
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH v3 02/14] drm/panfrost: Refuse to sync the CPU caches of a heap BO
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
On a coherent GPU a heap BO is not write-combined, so PANFROST_SYNC_BO
on it reaches drm_gem_shmem_get_pages_sgt() with bo->base.pages being
the fault handler's sparse page array, and drm_prime_pages_to_sg()
dereferences the NULL entries of the chunks not faulted in yet. With no
chunk faulted in, it pins the whole heap and sets bo->base.pages without
bo->sgts, which the next GPU fault on the heap dereferences.
Heap BOs are not CPU-mappable (panfrost_ioctl_mmap_bo() refuses them,
panfrost_gem_create() refuses PANFROST_BO_WB_MMAP for them), so there is
nothing to sync: return -EINVAL, as for imported BOs.
Found by code review (Sashiko). Not tested on a coherent GPU: on the
Mali-G31 here a heap BO is write-combined and the sync returned early.
Fixes: 7be45f548976 ("drm/panfrost: Add a PANFROST_SYNC_BO ioctl")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206561@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_gem.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/drivers/gpu/drm/panfrost/panfrost_gem.c b/drivers/gpu/drm/panfrost/panfrost_gem.c
index 518f24549..f72edb65b 100644
--- a/drivers/gpu/drm/panfrost/panfrost_gem.c
+++ b/drivers/gpu/drm/panfrost/panfrost_gem.c
@@ -569,6 +569,10 @@ panfrost_gem_sync(struct drm_gem_object *obj, u32 type, u32 offset, u32 size)
if (drm_gem_is_imported(&shmem->base))
return -EINVAL;
+ /* A heap BO's pages and sg tables belong to the GPU fault handler. */
+ if (bo->is_heap)
+ return -EINVAL;
+
switch (type) {
case PANFROST_BO_SYNC_CPU_CACHE_FLUSH:
case PANFROST_BO_SYNC_CPU_CACHE_FLUSH_AND_INVALIDATE:
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 02/14] drm/panfrost: Refuse to sync the CPU caches of a heap BO
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
On a coherent GPU a heap BO is not write-combined, so PANFROST_SYNC_BO
on it reaches drm_gem_shmem_get_pages_sgt() with bo->base.pages being
the fault handler's sparse page array, and drm_prime_pages_to_sg()
dereferences the NULL entries of the chunks not faulted in yet. With no
chunk faulted in, it pins the whole heap and sets bo->base.pages without
bo->sgts, which the next GPU fault on the heap dereferences.
Heap BOs are not CPU-mappable (panfrost_ioctl_mmap_bo() refuses them,
panfrost_gem_create() refuses PANFROST_BO_WB_MMAP for them), so there is
nothing to sync: return -EINVAL, as for imported BOs.
Found by code review (Sashiko). Not tested on a coherent GPU: on the
Mali-G31 here a heap BO is write-combined and the sync returned early.
Fixes: 7be45f548976 ("drm/panfrost: Add a PANFROST_SYNC_BO ioctl")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206561@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_gem.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/drivers/gpu/drm/panfrost/panfrost_gem.c b/drivers/gpu/drm/panfrost/panfrost_gem.c
index 518f24549..f72edb65b 100644
--- a/drivers/gpu/drm/panfrost/panfrost_gem.c
+++ b/drivers/gpu/drm/panfrost/panfrost_gem.c
@@ -569,6 +569,10 @@ panfrost_gem_sync(struct drm_gem_object *obj, u32 type, u32 offset, u32 size)
if (drm_gem_is_imported(&shmem->base))
return -EINVAL;
+ /* A heap BO's pages and sg tables belong to the GPU fault handler. */
+ if (bo->is_heap)
+ return -EINVAL;
+
switch (type) {
case PANFROST_BO_SYNC_CPU_CACHE_FLUSH:
case PANFROST_BO_SYNC_CPU_CACHE_FLUSH_AND_INVALIDATE:
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 03/14] drm/panfrost: Don't take a reference on a mapping that is being freed
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
addr_to_mapping() takes a reference on the mapping it finds in the
address space's drm_mm with kref_get(). But the last reference is
dropped before panfrost_gem_teardown_mapping() removes the node from the
drm_mm under mm_lock, so the lookup can find a mapping whose refcount is
zero. kref_get() revives it, the release still frees it and drops the
BO, and the fault handler maps pages into a BO that is being freed.
This takes a GPU fault on a heap BO while its last handle and last job
reference go away, so a job that uses a heap BO it does not list;
userspace on the render node can submit that.
Use kref_get_unless_zero(); the fault is then reported as unhandled.
Found by code review, after Sashiko reported the same pattern in new
code of this series. Not reproduced.
Fixes: bdefca2d8dc0 ("drm/panfrost: Add the panfrost_gem_mapping concept")
Cc: stable@vger.kernel.org
Link: https://lore.kernel.org/all/sashiko-outbox-206570@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index faa62643c..d0c6b6635 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -576,7 +576,11 @@ addr_to_mapping(struct panfrost_device *pfdev, int as, u64 addr)
offset < (node->start + node->size)) {
mapping = drm_mm_node_to_panfrost_mapping(node);
- kref_get(&mapping->refcount);
+ /* The node stays in the mm after the last reference is
+ * dropped: don't revive a mapping that is being freed.
+ */
+ if (!kref_get_unless_zero(&mapping->refcount))
+ mapping = NULL;
break;
}
}
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 03/14] drm/panfrost: Don't take a reference on a mapping that is being freed
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
addr_to_mapping() takes a reference on the mapping it finds in the
address space's drm_mm with kref_get(). But the last reference is
dropped before panfrost_gem_teardown_mapping() removes the node from the
drm_mm under mm_lock, so the lookup can find a mapping whose refcount is
zero. kref_get() revives it, the release still frees it and drops the
BO, and the fault handler maps pages into a BO that is being freed.
This takes a GPU fault on a heap BO while its last handle and last job
reference go away, so a job that uses a heap BO it does not list;
userspace on the render node can submit that.
Use kref_get_unless_zero(); the fault is then reported as unhandled.
Found by code review, after Sashiko reported the same pattern in new
code of this series. Not reproduced.
Fixes: bdefca2d8dc0 ("drm/panfrost: Add the panfrost_gem_mapping concept")
Cc: stable@vger.kernel.org
Link: https://lore.kernel.org/all/sashiko-outbox-206570@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index faa62643c..d0c6b6635 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -576,7 +576,11 @@ addr_to_mapping(struct panfrost_device *pfdev, int as, u64 addr)
offset < (node->start + node->size)) {
mapping = drm_mm_node_to_panfrost_mapping(node);
- kref_get(&mapping->refcount);
+ /* The node stays in the mm after the last reference is
+ * dropped: don't revive a mapping that is being freed.
+ */
+ if (!kref_get_unless_zero(&mapping->refcount))
+ mapping = NULL;
break;
}
}
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 04/14] drm/panfrost: Don't WARN when a GEM mapping cannot get its pages
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
drm_gem_shmem_get_pages_sgt() fails with -ENOMEM when the caller's
memory cgroup is at its limit, or with -EINTR when the caller is being
killed. Userspace gets the error (Mesa: "DRM_IOCTL_PANFROST_CREATE_BO
failed (err=12)"), but the WARN_ON() in panfrost_mmu_map() also prints a
backtrace:
WARNING: drivers/gpu/drm/panfrost/panfrost_mmu.c:478 at panfrost_mmu_map+0xcc
dEQP-GLES31.functional.draw_indirect.compute_interop.large.* in a 600 MB
memory cgroup on a 1 GB Mali-G31 board prints it five times.
Drop the WARN_ON(). No pages are held at this point, so there is nothing
to undo.
Fixes: f3ba91228e8e ("drm/panfrost: Add initial panfrost driver")
Cc: stable@vger.kernel.org
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index d0c6b6635..74fd159de 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -480,7 +480,7 @@ int panfrost_mmu_map(struct panfrost_gem_mapping *mapping)
prot |= IOMMU_NOEXEC;
sgt = drm_gem_shmem_get_pages_sgt(shmem);
- if (WARN_ON(IS_ERR(sgt)))
+ if (IS_ERR(sgt))
return PTR_ERR(sgt);
ret = mmu_map_sg(pfdev, mapping->mmu, mapping->mmnode.start << PAGE_SHIFT,
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 04/14] drm/panfrost: Don't WARN when a GEM mapping cannot get its pages
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
drm_gem_shmem_get_pages_sgt() fails with -ENOMEM when the caller's
memory cgroup is at its limit, or with -EINTR when the caller is being
killed. Userspace gets the error (Mesa: "DRM_IOCTL_PANFROST_CREATE_BO
failed (err=12)"), but the WARN_ON() in panfrost_mmu_map() also prints a
backtrace:
WARNING: drivers/gpu/drm/panfrost/panfrost_mmu.c:478 at panfrost_mmu_map+0xcc
dEQP-GLES31.functional.draw_indirect.compute_interop.large.* in a 600 MB
memory cgroup on a 1 GB Mali-G31 board prints it five times.
Drop the WARN_ON(). No pages are held at this point, so there is nothing
to undo.
Fixes: f3ba91228e8e ("drm/panfrost: Add initial panfrost driver")
Cc: stable@vger.kernel.org
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index d0c6b6635..74fd159de 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -480,7 +480,7 @@ int panfrost_mmu_map(struct panfrost_gem_mapping *mapping)
prot |= IOMMU_NOEXEC;
sgt = drm_gem_shmem_get_pages_sgt(shmem);
- if (WARN_ON(IS_ERR(sgt)))
+ if (IS_ERR(sgt))
return PTR_ERR(sgt);
ret = mmu_map_sg(pfdev, mapping->mmu, mapping->mmnode.start << PAGE_SHIFT,
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 05/14] drm/panfrost: Unwind a failed GPU mapping without dropping its pages
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
Two error paths of a BO mapping leave wrong state behind when an
allocation fails part-way (memory cgroup limit, io-pgtable out of
memory):
- When mmu_map_sg() fails, panfrost_mmu_map() calls
drm_gem_shmem_put_pages_locked(), without the reservation lock it
asserts. The only page reference drm_gem_shmem_get_pages_sgt() took
belongs to the sg table cached in shmem->sgt, so this drops pages that
shmem->sgt still maps (for an imported BO it underflows
pages_use_count). Keep the pages and return the error.
- When ops->map_pages() fails, mmu_map_sg() does not unmap what the
failing call itself had mapped (reported in @mapped). Those PTEs stay
in a VA range that drm_mm hands out again. Unmap that part too, and
flush the unmapped range.
Fixes: 4da352cf8fb1 ("drm/panfrost: Handle page mapping failure")
Cc: stable@vger.kernel.org
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index 74fd159de..efa4a3bb1 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -442,8 +442,11 @@ static int mmu_map_sg(struct panfrost_device *pfdev, struct panfrost_mmu *mmu,
ret = ops->map_pages(ops, iova, paddr, pgsize, pgcount, prot,
GFP_KERNEL, &mapped);
- if (ret)
+ if (ret) {
+ /* Unwind the pages this call mapped too. */
+ total_mapped += mapped;
goto err_unmap_pages;
+ }
/* Don't get stuck if things have gone wrong */
mapped = max(mapped, pgsize);
@@ -459,7 +462,10 @@ static int mmu_map_sg(struct panfrost_device *pfdev, struct panfrost_mmu *mmu,
return 0;
err_unmap_pages:
- mmu_unmap_range(mmu, start_iova, total_mapped);
+ if (total_mapped) {
+ mmu_unmap_range(mmu, start_iova, total_mapped);
+ panfrost_mmu_flush_range(pfdev, mmu, start_iova, total_mapped);
+ }
return ret;
}
@@ -483,18 +489,15 @@ int panfrost_mmu_map(struct panfrost_gem_mapping *mapping)
if (IS_ERR(sgt))
return PTR_ERR(sgt);
+ /* On failure keep the pages: shmem->sgt holds their only reference. */
ret = mmu_map_sg(pfdev, mapping->mmu, mapping->mmnode.start << PAGE_SHIFT,
prot, sgt);
if (ret)
- goto err_put_pages;
+ return ret;
mapping->active = true;
return 0;
-
-err_put_pages:
- drm_gem_shmem_put_pages_locked(shmem);
- return ret;
}
void panfrost_mmu_unmap(struct panfrost_gem_mapping *mapping)
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 05/14] drm/panfrost: Unwind a failed GPU mapping without dropping its pages
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
Two error paths of a BO mapping leave wrong state behind when an
allocation fails part-way (memory cgroup limit, io-pgtable out of
memory):
- When mmu_map_sg() fails, panfrost_mmu_map() calls
drm_gem_shmem_put_pages_locked(), without the reservation lock it
asserts. The only page reference drm_gem_shmem_get_pages_sgt() took
belongs to the sg table cached in shmem->sgt, so this drops pages that
shmem->sgt still maps (for an imported BO it underflows
pages_use_count). Keep the pages and return the error.
- When ops->map_pages() fails, mmu_map_sg() does not unmap what the
failing call itself had mapped (reported in @mapped). Those PTEs stay
in a VA range that drm_mm hands out again. Unmap that part too, and
flush the unmapped range.
Fixes: 4da352cf8fb1 ("drm/panfrost: Handle page mapping failure")
Cc: stable@vger.kernel.org
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index 74fd159de..efa4a3bb1 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -442,8 +442,11 @@ static int mmu_map_sg(struct panfrost_device *pfdev, struct panfrost_mmu *mmu,
ret = ops->map_pages(ops, iova, paddr, pgsize, pgcount, prot,
GFP_KERNEL, &mapped);
- if (ret)
+ if (ret) {
+ /* Unwind the pages this call mapped too. */
+ total_mapped += mapped;
goto err_unmap_pages;
+ }
/* Don't get stuck if things have gone wrong */
mapped = max(mapped, pgsize);
@@ -459,7 +462,10 @@ static int mmu_map_sg(struct panfrost_device *pfdev, struct panfrost_mmu *mmu,
return 0;
err_unmap_pages:
- mmu_unmap_range(mmu, start_iova, total_mapped);
+ if (total_mapped) {
+ mmu_unmap_range(mmu, start_iova, total_mapped);
+ panfrost_mmu_flush_range(pfdev, mmu, start_iova, total_mapped);
+ }
return ret;
}
@@ -483,18 +489,15 @@ int panfrost_mmu_map(struct panfrost_gem_mapping *mapping)
if (IS_ERR(sgt))
return PTR_ERR(sgt);
+ /* On failure keep the pages: shmem->sgt holds their only reference. */
ret = mmu_map_sg(pfdev, mapping->mmu, mapping->mmnode.start << PAGE_SHIFT,
prot, sgt);
if (ret)
- goto err_put_pages;
+ return ret;
mapping->active = true;
return 0;
-
-err_put_pages:
- drm_gem_shmem_put_pages_locked(shmem);
- return ret;
}
void panfrost_mmu_unmap(struct panfrost_gem_mapping *mapping)
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 06/14] drm/panfrost: Give back the pages of a heap chunk that failed to map
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
panfrost_mmu_map_fault_addr() grows a heap BO by one 2 MB chunk per
fault. If reading the chunk's folios, building its sg table, DMA mapping
it or mapping it on the GPU fails, the folios already read stay in
bo->base.pages while the chunk has no sg table. The next fault in the
chunk reads them again, finds their pages owned and hits
if (drm_WARN_ON(&pfdev->base, already_owned))
folio_put(folio);
which commit b49c35ba0ce1 ("drm/panfrost: Fix a page leak in
panfrost_mmu_map_fault_addr() when THP is on") added; before it, pages
already filled in were skipped.
On those error paths, put the folios read and clear their entries. The
range to clear is what the fill loop wrote, whole folios: a folio larger
than 2 MB (possible with 16K or 64K pages) also fills entries of the
neighbouring chunks, and a folio the loop found already owned (by such a
neighbour) is not ours to put.
Tested with 4K pages, where a folio never crosses a chunk; the
larger-folio case is from reading the code.
Fixes: b49c35ba0ce1 ("drm/panfrost: Fix a page leak in panfrost_mmu_map_fault_addr() when THP is on")
Cc: stable@vger.kernel.org
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 45 ++++++++++++++++++++++++++++++---
1 file changed, 41 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index efa4a3bb1..e1c4e57d0 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -596,6 +596,30 @@ addr_to_mapping(struct panfrost_device *pfdev, int as, u64 addr)
#define NUM_FAULT_PAGES (SZ_2M / PAGE_SIZE)
+/*
+ * Put the folios a failed heap fault took and clear their page array entries.
+ * [start, end) covers whole folios, so it can reach past the 2 MB chunk.
+ */
+static void panfrost_heap_put_pages(struct page **pages, pgoff_t start,
+ pgoff_t end)
+{
+ while (start < end) {
+ struct folio *folio;
+
+ if (!pages[start]) {
+ start++;
+ continue;
+ }
+
+ folio = page_folio(pages[start]);
+ do {
+ pages[start++] = NULL;
+ } while (start < end && pages[start] &&
+ page_folio(pages[start]) == folio);
+ folio_put(folio);
+ }
+}
+
static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
u64 addr)
{
@@ -604,7 +628,7 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
struct panfrost_gem_object *bo;
struct address_space *mapping;
struct drm_gem_object *obj;
- pgoff_t page_offset, nr_pages;
+ pgoff_t page_offset, nr_pages, got_start, got_end;
struct sg_table *sgt;
struct page **pages;
@@ -663,17 +687,23 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
mapping = bo->base.base.filp->f_mapping;
mapping_set_unevictable(mapping);
+ /* The page array range whose folio references this fault takes. */
+ got_start = page_offset;
+ got_end = page_offset;
+
for (pgoff_t pg = page_offset; pg < page_offset + NUM_FAULT_PAGES;) {
bool already_owned = false;
struct folio *folio;
+ pgoff_t first;
folio = shmem_read_folio(mapping, pg);
if (IS_ERR(folio)) {
ret = PTR_ERR(folio);
- goto err_unlock;
+ goto err_put_chunk;
}
pg &= ~(folio_nr_pages(folio) - 1);
+ first = pg;
for (u32 i = 0; i < folio_nr_pages(folio) && pg < nr_pages; i++) {
if (pages[pg])
already_owned = true;
@@ -685,14 +715,18 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
* there's no valid reason for a folio range to be partially
* populated.
*/
- if (drm_WARN_ON(&pfdev->base, already_owned))
+ if (drm_WARN_ON(&pfdev->base, already_owned)) {
folio_put(folio);
+ } else {
+ got_start = min(got_start, first);
+ got_end = max(got_end, pg);
+ }
}
ret = sg_alloc_table_from_pages(sgt, pages + page_offset,
NUM_FAULT_PAGES, 0, SZ_2M, GFP_KERNEL);
if (ret)
- goto err_unlock;
+ goto err_put_chunk;
ret = dma_map_sgtable(pfdev->base.dev, sgt, DMA_BIDIRECTIONAL, 0);
if (ret)
@@ -719,6 +753,9 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
dma_unmap_sgtable(pfdev->base.dev, sgt, DMA_BIDIRECTIONAL, 0);
err_map:
sg_free_table(sgt);
+err_put_chunk:
+ /* The next fault in this chunk must find its page range empty. */
+ panfrost_heap_put_pages(pages, got_start, got_end);
err_unlock:
dma_resv_unlock(obj->resv);
err_bo:
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 06/14] drm/panfrost: Give back the pages of a heap chunk that failed to map
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
panfrost_mmu_map_fault_addr() grows a heap BO by one 2 MB chunk per
fault. If reading the chunk's folios, building its sg table, DMA mapping
it or mapping it on the GPU fails, the folios already read stay in
bo->base.pages while the chunk has no sg table. The next fault in the
chunk reads them again, finds their pages owned and hits
if (drm_WARN_ON(&pfdev->base, already_owned))
folio_put(folio);
which commit b49c35ba0ce1 ("drm/panfrost: Fix a page leak in
panfrost_mmu_map_fault_addr() when THP is on") added; before it, pages
already filled in were skipped.
On those error paths, put the folios read and clear their entries. The
range to clear is what the fill loop wrote, whole folios: a folio larger
than 2 MB (possible with 16K or 64K pages) also fills entries of the
neighbouring chunks, and a folio the loop found already owned (by such a
neighbour) is not ours to put.
Tested with 4K pages, where a folio never crosses a chunk; the
larger-folio case is from reading the code.
Fixes: b49c35ba0ce1 ("drm/panfrost: Fix a page leak in panfrost_mmu_map_fault_addr() when THP is on")
Cc: stable@vger.kernel.org
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 45 ++++++++++++++++++++++++++++++---
1 file changed, 41 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index efa4a3bb1..e1c4e57d0 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -596,6 +596,30 @@ addr_to_mapping(struct panfrost_device *pfdev, int as, u64 addr)
#define NUM_FAULT_PAGES (SZ_2M / PAGE_SIZE)
+/*
+ * Put the folios a failed heap fault took and clear their page array entries.
+ * [start, end) covers whole folios, so it can reach past the 2 MB chunk.
+ */
+static void panfrost_heap_put_pages(struct page **pages, pgoff_t start,
+ pgoff_t end)
+{
+ while (start < end) {
+ struct folio *folio;
+
+ if (!pages[start]) {
+ start++;
+ continue;
+ }
+
+ folio = page_folio(pages[start]);
+ do {
+ pages[start++] = NULL;
+ } while (start < end && pages[start] &&
+ page_folio(pages[start]) == folio);
+ folio_put(folio);
+ }
+}
+
static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
u64 addr)
{
@@ -604,7 +628,7 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
struct panfrost_gem_object *bo;
struct address_space *mapping;
struct drm_gem_object *obj;
- pgoff_t page_offset, nr_pages;
+ pgoff_t page_offset, nr_pages, got_start, got_end;
struct sg_table *sgt;
struct page **pages;
@@ -663,17 +687,23 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
mapping = bo->base.base.filp->f_mapping;
mapping_set_unevictable(mapping);
+ /* The page array range whose folio references this fault takes. */
+ got_start = page_offset;
+ got_end = page_offset;
+
for (pgoff_t pg = page_offset; pg < page_offset + NUM_FAULT_PAGES;) {
bool already_owned = false;
struct folio *folio;
+ pgoff_t first;
folio = shmem_read_folio(mapping, pg);
if (IS_ERR(folio)) {
ret = PTR_ERR(folio);
- goto err_unlock;
+ goto err_put_chunk;
}
pg &= ~(folio_nr_pages(folio) - 1);
+ first = pg;
for (u32 i = 0; i < folio_nr_pages(folio) && pg < nr_pages; i++) {
if (pages[pg])
already_owned = true;
@@ -685,14 +715,18 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
* there's no valid reason for a folio range to be partially
* populated.
*/
- if (drm_WARN_ON(&pfdev->base, already_owned))
+ if (drm_WARN_ON(&pfdev->base, already_owned)) {
folio_put(folio);
+ } else {
+ got_start = min(got_start, first);
+ got_end = max(got_end, pg);
+ }
}
ret = sg_alloc_table_from_pages(sgt, pages + page_offset,
NUM_FAULT_PAGES, 0, SZ_2M, GFP_KERNEL);
if (ret)
- goto err_unlock;
+ goto err_put_chunk;
ret = dma_map_sgtable(pfdev->base.dev, sgt, DMA_BIDIRECTIONAL, 0);
if (ret)
@@ -719,6 +753,9 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
dma_unmap_sgtable(pfdev->base.dev, sgt, DMA_BIDIRECTIONAL, 0);
err_map:
sg_free_table(sgt);
+err_put_chunk:
+ /* The next fault in this chunk must find its page range empty. */
+ panfrost_heap_put_pages(pages, got_start, got_end);
err_unlock:
dma_resv_unlock(obj->resv);
err_bo:
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 07/14] drm/panfrost: Don't WARN for a heap folio a neighbouring chunk filled
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
panfrost_mmu_map_fault_addr() fills the page array of a heap BO a whole
folio at a time and warns when a page of the folio is already filled in.
With 16K or 64K pages, the THP mount panfrost uses can return folios
larger than the 2 MB fault chunk (up to 32 MB and 128 MB). The first
fault in such a folio fills the entries of the neighbouring chunks too,
and every later fault in those chunks WARNs, although dropping the extra
folio reference, as it does, is all it has to do.
Warn only for a folio that fits in the chunk: only an earlier fault in
the same chunk can have filled its pages, which is a bug.
Not reproduced: with 4K pages, as here, a folio never crosses a chunk.
Fixes: b49c35ba0ce1 ("drm/panfrost: Fix a page leak in panfrost_mmu_map_fault_addr() when THP is on")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-210837@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index e1c4e57d0..2cf67d932 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -711,11 +711,13 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
pages[pg++] = folio_page(folio, i);
}
- /* We always fill the page array at a folio granularity so
- * there's no valid reason for a folio range to be partially
- * populated.
+ /* We always fill the page array at a folio granularity, so a
+ * folio is already owned only if it is larger than the chunk
+ * and the fault of a neighbouring chunk filled it.
*/
- if (drm_WARN_ON(&pfdev->base, already_owned)) {
+ if (already_owned) {
+ drm_WARN_ON(&pfdev->base,
+ folio_nr_pages(folio) <= NUM_FAULT_PAGES);
folio_put(folio);
} else {
got_start = min(got_start, first);
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 07/14] drm/panfrost: Don't WARN for a heap folio a neighbouring chunk filled
@ 2026-11-13 9:02 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:02 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
panfrost_mmu_map_fault_addr() fills the page array of a heap BO a whole
folio at a time and warns when a page of the folio is already filled in.
With 16K or 64K pages, the THP mount panfrost uses can return folios
larger than the 2 MB fault chunk (up to 32 MB and 128 MB). The first
fault in such a folio fills the entries of the neighbouring chunks too,
and every later fault in those chunks WARNs, although dropping the extra
folio reference, as it does, is all it has to do.
Warn only for a folio that fits in the chunk: only an earlier fault in
the same chunk can have filled its pages, which is a bug.
Not reproduced: with 4K pages, as here, a folio never crosses a chunk.
Fixes: b49c35ba0ce1 ("drm/panfrost: Fix a page leak in panfrost_mmu_map_fault_addr() when THP is on")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-210837@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index e1c4e57d0..2cf67d932 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -711,11 +711,13 @@ static int panfrost_mmu_map_fault_addr(struct panfrost_device *pfdev, int as,
pages[pg++] = folio_page(folio, i);
}
- /* We always fill the page array at a folio granularity so
- * there's no valid reason for a folio range to be partially
- * populated.
+ /* We always fill the page array at a folio granularity, so a
+ * folio is already owned only if it is larger than the chunk
+ * and the fault of a neighbouring chunk filled it.
*/
- if (drm_WARN_ON(&pfdev->base, already_owned)) {
+ if (already_owned) {
+ drm_WARN_ON(&pfdev->base,
+ folio_nr_pages(folio) <= NUM_FAULT_PAGES);
folio_put(folio);
} else {
got_start = min(got_start, first);
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 08/14] drm/panfrost: Don't signal a job cancelled from _NEXT at a reset as done
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
panfrost_reset() cancels the job in each slot's _NEXT registers with
JS_COMMAND_NOP and soft-stops the running one. If the running job
completes instead of stopping, panfrost_jm_handle_irq() dequeues it,
finds both subslots inactive and takes the cancelled job, now at the
head of the slot, for done as well. The cancelled job gets its jc
cleared and its done fence signalled: drm_sched_resubmit_jobs() does not
run it again, and it completes with -ECANCELED without having run.
The Arm driver reads JS_HEAD_NEXT after the NOP: if it still holds the
job, the job has not started and is evicted. Do the same: take an
evicted job off the slot while the remaining interrupts are handled and
put it back afterwards, so that it is restarted with the others.
Found by code review (Sashiko) of "Soft-stop and resume a job that
outlives the timeout", which widens the window. Not reproduced.
Fixes: 030761e097b4 ("drm/panfrost: Queue jobs on the hardware")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206581@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 7319511db..1bb70b07d 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -659,6 +659,7 @@ static void
panfrost_reset(struct panfrost_device *pfdev,
struct drm_sched_job *bad)
{
+ struct panfrost_job *evicted[NUM_JOB_SLOTS] = {};
u32 js_state, js_state_mask = 0xffffffff;
unsigned int i, j;
bool cookie;
@@ -694,11 +695,25 @@ panfrost_reset(struct panfrost_device *pfdev,
job_write(pfdev, JOB_INT_MASK, 0);
synchronize_irq(pfdev->js->irq);
+ spin_lock(&pfdev->js->job_lock);
for (i = 0; i < NUM_JOB_SLOTS; i++) {
/* Cancel the next job and soft-stop the running job. */
job_write(pfdev, JS_COMMAND_NEXT(i), JS_COMMAND_NOP);
+
+ /* A job still in _NEXT after the NOP never started. Take it
+ * off the slot, or panfrost_jm_handle_irq() takes it for done
+ * if the running job completes instead of stopping.
+ */
+ if (pfdev->jobs[i][1] &&
+ (job_read(pfdev, JS_HEAD_NEXT_LO(i)) ||
+ job_read(pfdev, JS_HEAD_NEXT_HI(i)))) {
+ evicted[i] = pfdev->jobs[i][1];
+ pfdev->jobs[i][1] = NULL;
+ }
+
job_write(pfdev, JS_COMMAND(i), JS_COMMAND_SOFT_STOP);
}
+ spin_unlock(&pfdev->js->job_lock);
/* Wait at most 10ms for soft-stops to complete */
ret = readl_poll_timeout(pfdev->iomem + JOB_INT_JS_STATE, js_state,
@@ -720,6 +735,10 @@ panfrost_reset(struct panfrost_device *pfdev,
*/
spin_lock(&pfdev->js->job_lock);
for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ /* Put the evicted job back, to be restarted. */
+ if (evicted[i])
+ pfdev->jobs[i][pfdev->jobs[i][0] ? 1 : 0] = evicted[i];
+
for (j = 0; j < ARRAY_SIZE(pfdev->jobs[0]) && pfdev->jobs[i][j]; j++) {
if (pfdev->jobs[i][j]->requirements & PANFROST_JD_REQ_CYCLE_COUNT ||
pfdev->jobs[i][j]->is_profiled)
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 08/14] drm/panfrost: Don't signal a job cancelled from _NEXT at a reset as done
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
panfrost_reset() cancels the job in each slot's _NEXT registers with
JS_COMMAND_NOP and soft-stops the running one. If the running job
completes instead of stopping, panfrost_jm_handle_irq() dequeues it,
finds both subslots inactive and takes the cancelled job, now at the
head of the slot, for done as well. The cancelled job gets its jc
cleared and its done fence signalled: drm_sched_resubmit_jobs() does not
run it again, and it completes with -ECANCELED without having run.
The Arm driver reads JS_HEAD_NEXT after the NOP: if it still holds the
job, the job has not started and is evicted. Do the same: take an
evicted job off the slot while the remaining interrupts are handled and
put it back afterwards, so that it is restarted with the others.
Found by code review (Sashiko) of "Soft-stop and resume a job that
outlives the timeout", which widens the window. Not reproduced.
Fixes: 030761e097b4 ("drm/panfrost: Queue jobs on the hardware")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206581@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 7319511db..1bb70b07d 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -659,6 +659,7 @@ static void
panfrost_reset(struct panfrost_device *pfdev,
struct drm_sched_job *bad)
{
+ struct panfrost_job *evicted[NUM_JOB_SLOTS] = {};
u32 js_state, js_state_mask = 0xffffffff;
unsigned int i, j;
bool cookie;
@@ -694,11 +695,25 @@ panfrost_reset(struct panfrost_device *pfdev,
job_write(pfdev, JOB_INT_MASK, 0);
synchronize_irq(pfdev->js->irq);
+ spin_lock(&pfdev->js->job_lock);
for (i = 0; i < NUM_JOB_SLOTS; i++) {
/* Cancel the next job and soft-stop the running job. */
job_write(pfdev, JS_COMMAND_NEXT(i), JS_COMMAND_NOP);
+
+ /* A job still in _NEXT after the NOP never started. Take it
+ * off the slot, or panfrost_jm_handle_irq() takes it for done
+ * if the running job completes instead of stopping.
+ */
+ if (pfdev->jobs[i][1] &&
+ (job_read(pfdev, JS_HEAD_NEXT_LO(i)) ||
+ job_read(pfdev, JS_HEAD_NEXT_HI(i)))) {
+ evicted[i] = pfdev->jobs[i][1];
+ pfdev->jobs[i][1] = NULL;
+ }
+
job_write(pfdev, JS_COMMAND(i), JS_COMMAND_SOFT_STOP);
}
+ spin_unlock(&pfdev->js->job_lock);
/* Wait at most 10ms for soft-stops to complete */
ret = readl_poll_timeout(pfdev->iomem + JOB_INT_JS_STATE, js_state,
@@ -720,6 +735,10 @@ panfrost_reset(struct panfrost_device *pfdev,
*/
spin_lock(&pfdev->js->job_lock);
for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ /* Put the evicted job back, to be restarted. */
+ if (evicted[i])
+ pfdev->jobs[i][pfdev->jobs[i][0] ? 1 : 0] = evicted[i];
+
for (j = 0; j < ARRAY_SIZE(pfdev->jobs[0]) && pfdev->jobs[i][j]; j++) {
if (pfdev->jobs[i][j]->requirements & PANFROST_JD_REQ_CYCLE_COUNT ||
pfdev->jobs[i][j]->is_profiled)
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 09/14] drm/panfrost: Don't cancel a job that completed during a reset
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
panfrost_reset() stops the schedulers, which detaches them from the done
fences of the jobs on the GPU, then soft-stops the slots and handles the
pending interrupts. A job that completes in that window gets its done
fence signalled and its jc cleared. drm_sched_resubmit_jobs() then calls
panfrost_job_run(), which returns NULL for a job with no jc, and since
commit 03877d621db0 ("drm/scheduler: mark jobs without fence as
canceled") drm_sched_start() completes it with -ECANCELED, although it
did complete.
Return the job's done fence instead: drm_sched_start() finds it
signalled and completes the job with its status, 0 or the error
panfrost_job_handle_err() set for a job that was hard-stopped or
faulted.
This depends on the previous patch: without it, a job cancelled from
_NEXT and taken for done would now be reported as completed.
Found by code review (Sashiko) of "Soft-stop and resume a job that
outlives the timeout", which widens the window. Not reproduced.
Fixes: 03877d621db0 ("drm/scheduler: mark jobs without fence as canceled")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206581@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 1bb70b07d..897743cb8 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -409,11 +409,11 @@ static struct dma_fence *panfrost_job_run(struct drm_sched_job *sched_job)
if (unlikely(job->base.s_fence->finished.error))
return NULL;
- /* Nothing to execute: can happen if the job has finished while
- * we were resetting the GPU.
+ /* The job has finished while we were resetting the GPU. Return its
+ * done fence: with no fence, drm_sched_start() cancels the job.
*/
if (!job->jc)
- return NULL;
+ return dma_fence_get(job->done_fence);
fence = panfrost_fence_create(pfdev, slot);
if (IS_ERR(fence))
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 09/14] drm/panfrost: Don't cancel a job that completed during a reset
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
panfrost_reset() stops the schedulers, which detaches them from the done
fences of the jobs on the GPU, then soft-stops the slots and handles the
pending interrupts. A job that completes in that window gets its done
fence signalled and its jc cleared. drm_sched_resubmit_jobs() then calls
panfrost_job_run(), which returns NULL for a job with no jc, and since
commit 03877d621db0 ("drm/scheduler: mark jobs without fence as
canceled") drm_sched_start() completes it with -ECANCELED, although it
did complete.
Return the job's done fence instead: drm_sched_start() finds it
signalled and completes the job with its status, 0 or the error
panfrost_job_handle_err() set for a job that was hard-stopped or
faulted.
This depends on the previous patch: without it, a job cancelled from
_NEXT and taken for done would now be reported as completed.
Found by code review (Sashiko) of "Soft-stop and resume a job that
outlives the timeout", which widens the window. Not reproduced.
Fixes: 03877d621db0 ("drm/scheduler: mark jobs without fence as canceled")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-206581@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 1bb70b07d..897743cb8 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -409,11 +409,11 @@ static struct dma_fence *panfrost_job_run(struct drm_sched_job *sched_job)
if (unlikely(job->base.s_fence->finished.error))
return NULL;
- /* Nothing to execute: can happen if the job has finished while
- * we were resetting the GPU.
+ /* The job has finished while we were resetting the GPU. Return its
+ * done fence: with no fence, drm_sched_start() cancels the job.
*/
if (!job->jc)
- return NULL;
+ return dma_fence_get(job->done_fence);
fence = panfrost_fence_create(pfdev, slot);
if (IS_ERR(fence))
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 10/14] drm/panfrost: Drop the cycle counter only for a job that holds it
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
panfrost_job_hw_submit() puts a job in pfdev->jobs[] but, while a reset
is pending, does not start it and takes no cycle counter reference.
panfrost_reset() drops one for every job in pfdev->jobs[] that wants the
counter, so use_count goes below zero. The same happens for a job that
was stopped (reference dropped at dequeue) and resubmitted while a reset
was pending: is_profiled is still set from its first run.
Today the GPU reset that follows WARNs in panfrost_gpu_soft_reset() and
sets use_count back to 0. Without a reset ("Soft-stop and resume a job
that outlives the timeout") the count stays wrong and
panfrost_cycle_counter_get() no longer starts the counter.
Record in the job whether it holds a reference, and drop it only then.
Found by code review (Sashiko). Not reproduced.
Fixes: f11b0417eec2 ("drm/panfrost: Add fdinfo support GPU load metrics")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-210891@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 19 +++++++++++++------
drivers/gpu/drm/panfrost/panfrost_job.h | 2 ++
2 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 897743cb8..df7fcc436 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -160,6 +160,14 @@ panfrost_get_job_chain_flag(const struct panfrost_job *job)
return (f->seqno & 1) ? JS_CONFIG_JOB_CHAIN_FLAG : 0;
}
+static void panfrost_job_put_cycle_counter(struct panfrost_job *job)
+{
+ if (job->cycle_counter_ref) {
+ panfrost_cycle_counter_put(job->pfdev);
+ job->cycle_counter_ref = false;
+ }
+}
+
static struct panfrost_job *
panfrost_dequeue_job(struct panfrost_device *pfdev, int slot)
{
@@ -174,8 +182,7 @@ panfrost_dequeue_job(struct panfrost_device *pfdev, int slot)
panfrost_cycle_counter_read(pfdev) - job->start_cycles;
}
- if (job->requirements & PANFROST_JD_REQ_CYCLE_COUNT || job->is_profiled)
- panfrost_cycle_counter_put(pfdev);
+ panfrost_job_put_cycle_counter(job);
pfdev->jobs[slot][0] = pfdev->jobs[slot][1];
pfdev->jobs[slot][1] = NULL;
@@ -259,8 +266,10 @@ static int panfrost_job_hw_submit(struct panfrost_job *job, int js)
job->is_profiled = pfdev->profile_mode;
if (job->requirements & PANFROST_JD_REQ_CYCLE_COUNT ||
- job->is_profiled)
+ job->is_profiled) {
panfrost_cycle_counter_get(pfdev);
+ job->cycle_counter_ref = true;
+ }
if (job->is_profiled) {
job->start_time = ktime_get();
@@ -740,9 +749,7 @@ panfrost_reset(struct panfrost_device *pfdev,
pfdev->jobs[i][pfdev->jobs[i][0] ? 1 : 0] = evicted[i];
for (j = 0; j < ARRAY_SIZE(pfdev->jobs[0]) && pfdev->jobs[i][j]; j++) {
- if (pfdev->jobs[i][j]->requirements & PANFROST_JD_REQ_CYCLE_COUNT ||
- pfdev->jobs[i][j]->is_profiled)
- panfrost_cycle_counter_put(pfdev->jobs[i][j]->pfdev);
+ panfrost_job_put_cycle_counter(pfdev->jobs[i][j]);
pm_runtime_put_noidle(pfdev->base.dev);
panfrost_devfreq_record_idle(&pfdev->pfdevfreq);
}
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.h b/drivers/gpu/drm/panfrost/panfrost_job.h
index 3e3b97178..cd82d7930 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.h
+++ b/drivers/gpu/drm/panfrost/panfrost_job.h
@@ -36,6 +36,8 @@ struct panfrost_job {
struct panfrost_engine_usage *engine_usage;
bool is_profiled;
+ /* Holds a reference on the cycle counter */
+ bool cycle_counter_ref;
ktime_t start_time;
u64 start_cycles;
};
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 10/14] drm/panfrost: Drop the cycle counter only for a job that holds it
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
panfrost_job_hw_submit() puts a job in pfdev->jobs[] but, while a reset
is pending, does not start it and takes no cycle counter reference.
panfrost_reset() drops one for every job in pfdev->jobs[] that wants the
counter, so use_count goes below zero. The same happens for a job that
was stopped (reference dropped at dequeue) and resubmitted while a reset
was pending: is_profiled is still set from its first run.
Today the GPU reset that follows WARNs in panfrost_gpu_soft_reset() and
sets use_count back to 0. Without a reset ("Soft-stop and resume a job
that outlives the timeout") the count stays wrong and
panfrost_cycle_counter_get() no longer starts the counter.
Record in the job whether it holds a reference, and drop it only then.
Found by code review (Sashiko). Not reproduced.
Fixes: f11b0417eec2 ("drm/panfrost: Add fdinfo support GPU load metrics")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-210891@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 19 +++++++++++++------
drivers/gpu/drm/panfrost/panfrost_job.h | 2 ++
2 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 897743cb8..df7fcc436 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -160,6 +160,14 @@ panfrost_get_job_chain_flag(const struct panfrost_job *job)
return (f->seqno & 1) ? JS_CONFIG_JOB_CHAIN_FLAG : 0;
}
+static void panfrost_job_put_cycle_counter(struct panfrost_job *job)
+{
+ if (job->cycle_counter_ref) {
+ panfrost_cycle_counter_put(job->pfdev);
+ job->cycle_counter_ref = false;
+ }
+}
+
static struct panfrost_job *
panfrost_dequeue_job(struct panfrost_device *pfdev, int slot)
{
@@ -174,8 +182,7 @@ panfrost_dequeue_job(struct panfrost_device *pfdev, int slot)
panfrost_cycle_counter_read(pfdev) - job->start_cycles;
}
- if (job->requirements & PANFROST_JD_REQ_CYCLE_COUNT || job->is_profiled)
- panfrost_cycle_counter_put(pfdev);
+ panfrost_job_put_cycle_counter(job);
pfdev->jobs[slot][0] = pfdev->jobs[slot][1];
pfdev->jobs[slot][1] = NULL;
@@ -259,8 +266,10 @@ static int panfrost_job_hw_submit(struct panfrost_job *job, int js)
job->is_profiled = pfdev->profile_mode;
if (job->requirements & PANFROST_JD_REQ_CYCLE_COUNT ||
- job->is_profiled)
+ job->is_profiled) {
panfrost_cycle_counter_get(pfdev);
+ job->cycle_counter_ref = true;
+ }
if (job->is_profiled) {
job->start_time = ktime_get();
@@ -740,9 +749,7 @@ panfrost_reset(struct panfrost_device *pfdev,
pfdev->jobs[i][pfdev->jobs[i][0] ? 1 : 0] = evicted[i];
for (j = 0; j < ARRAY_SIZE(pfdev->jobs[0]) && pfdev->jobs[i][j]; j++) {
- if (pfdev->jobs[i][j]->requirements & PANFROST_JD_REQ_CYCLE_COUNT ||
- pfdev->jobs[i][j]->is_profiled)
- panfrost_cycle_counter_put(pfdev->jobs[i][j]->pfdev);
+ panfrost_job_put_cycle_counter(pfdev->jobs[i][j]);
pm_runtime_put_noidle(pfdev->base.dev);
panfrost_devfreq_record_idle(&pfdev->pfdevfreq);
}
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.h b/drivers/gpu/drm/panfrost/panfrost_job.h
index 3e3b97178..cd82d7930 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.h
+++ b/drivers/gpu/drm/panfrost/panfrost_job.h
@@ -36,6 +36,8 @@ struct panfrost_job {
struct panfrost_engine_usage *engine_usage;
bool is_profiled;
+ /* Holds a reference on the cycle counter */
+ bool cycle_counter_ref;
ktime_t start_time;
u64 start_cycles;
};
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 11/14] drm/panfrost: Don't take a job that was never started for done
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
While a reset is pending, panfrost_job_hw_submit() puts the job in
pfdev->jobs[] without starting it; the reset resubmits it. But
reset.pending is set, by a timeout or a job fault, before
panfrost_reset() masks the job interrupt. If such a job waits behind a
running one that completes in that window, panfrost_jm_handle_irq()
finds both subslots inactive and takes the waiting job for done as well.
Its fence is signalled and its jc cleared, so it is never run.
Mark a job as started when JS_COMMAND_START is written for it, and leave
a job that was not started to the reset.
Found by code review (Sashiko). Not reproduced.
Fixes: 030761e097b4 ("drm/panfrost: Queue jobs on the hardware")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-210891@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 8 ++++++--
drivers/gpu/drm/panfrost/panfrost_job.h | 2 ++
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index df7fcc436..1b7becf7b 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -261,6 +261,7 @@ static int panfrost_job_hw_submit(struct panfrost_job *job, int js)
spin_lock(&pfdev->js->job_lock);
subslot = panfrost_enqueue_job(pfdev, js, job);
+ job->started = false;
/* Don't queue the job if a reset is in progress */
if (!atomic_read(&pfdev->reset.pending)) {
job->is_profiled = pfdev->profile_mode;
@@ -276,6 +277,7 @@ static int panfrost_job_hw_submit(struct panfrost_job *job, int js)
job->start_cycles = panfrost_cycle_counter_read(pfdev);
}
+ job->started = true;
job_write(pfdev, JS_COMMAND_NEXT(js), JS_COMMAND_START);
dev_dbg(pfdev->base.dev,
"JS: Submitting atom %p to js[%d][%d] with head=0x%llx AS %d",
@@ -586,7 +588,8 @@ static void panfrost_jm_handle_irq(struct panfrost_device *pfdev, u32 status)
if (failed[j]) {
panfrost_job_handle_err(pfdev, failed[j], j);
- } else if (pfdev->jobs[j][0] && !(js_state & MK_JS_MASK(j))) {
+ } else if (pfdev->jobs[j][0] && pfdev->jobs[j][0]->started &&
+ !(js_state & MK_JS_MASK(j))) {
/* When the current job doesn't fail, the JM dequeues
* the next job without waiting for an ACK, this means
* we can have 2 jobs dequeued and only catch the
@@ -594,7 +597,8 @@ static void panfrost_jm_handle_irq(struct panfrost_device *pfdev, u32 status)
* are inactive, but one job remains in pfdev->jobs[j],
* consider it done. Of course that doesn't apply if a
* failure happened since we cancelled execution of the
- * job in _NEXT (see above).
+ * job in _NEXT (see above), or if the job was queued
+ * while a reset was pending and never started.
*/
if (WARN_ON(!done[j][0]))
done[j][0] = panfrost_dequeue_job(pfdev, j);
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.h b/drivers/gpu/drm/panfrost/panfrost_job.h
index cd82d7930..4d1c54243 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.h
+++ b/drivers/gpu/drm/panfrost/panfrost_job.h
@@ -38,6 +38,8 @@ struct panfrost_job {
bool is_profiled;
/* Holds a reference on the cycle counter */
bool cycle_counter_ref;
+ /* JS_COMMAND_START was written for this submission */
+ bool started;
ktime_t start_time;
u64 start_cycles;
};
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 11/14] drm/panfrost: Don't take a job that was never started for done
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
While a reset is pending, panfrost_job_hw_submit() puts the job in
pfdev->jobs[] without starting it; the reset resubmits it. But
reset.pending is set, by a timeout or a job fault, before
panfrost_reset() masks the job interrupt. If such a job waits behind a
running one that completes in that window, panfrost_jm_handle_irq()
finds both subslots inactive and takes the waiting job for done as well.
Its fence is signalled and its jc cleared, so it is never run.
Mark a job as started when JS_COMMAND_START is written for it, and leave
a job that was not started to the reset.
Found by code review (Sashiko). Not reproduced.
Fixes: 030761e097b4 ("drm/panfrost: Queue jobs on the hardware")
Cc: stable@vger.kernel.org
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/all/sashiko-outbox-210891@kernel.org/
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 8 ++++++--
drivers/gpu/drm/panfrost/panfrost_job.h | 2 ++
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index df7fcc436..1b7becf7b 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -261,6 +261,7 @@ static int panfrost_job_hw_submit(struct panfrost_job *job, int js)
spin_lock(&pfdev->js->job_lock);
subslot = panfrost_enqueue_job(pfdev, js, job);
+ job->started = false;
/* Don't queue the job if a reset is in progress */
if (!atomic_read(&pfdev->reset.pending)) {
job->is_profiled = pfdev->profile_mode;
@@ -276,6 +277,7 @@ static int panfrost_job_hw_submit(struct panfrost_job *job, int js)
job->start_cycles = panfrost_cycle_counter_read(pfdev);
}
+ job->started = true;
job_write(pfdev, JS_COMMAND_NEXT(js), JS_COMMAND_START);
dev_dbg(pfdev->base.dev,
"JS: Submitting atom %p to js[%d][%d] with head=0x%llx AS %d",
@@ -586,7 +588,8 @@ static void panfrost_jm_handle_irq(struct panfrost_device *pfdev, u32 status)
if (failed[j]) {
panfrost_job_handle_err(pfdev, failed[j], j);
- } else if (pfdev->jobs[j][0] && !(js_state & MK_JS_MASK(j))) {
+ } else if (pfdev->jobs[j][0] && pfdev->jobs[j][0]->started &&
+ !(js_state & MK_JS_MASK(j))) {
/* When the current job doesn't fail, the JM dequeues
* the next job without waiting for an ACK, this means
* we can have 2 jobs dequeued and only catch the
@@ -594,7 +597,8 @@ static void panfrost_jm_handle_irq(struct panfrost_device *pfdev, u32 status)
* are inactive, but one job remains in pfdev->jobs[j],
* consider it done. Of course that doesn't apply if a
* failure happened since we cancelled execution of the
- * job in _NEXT (see above).
+ * job in _NEXT (see above), or if the job was queued
+ * while a reset was pending and never started.
*/
if (WARN_ON(!done[j][0]))
done[j][0] = panfrost_dequeue_job(pfdev, j);
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.h b/drivers/gpu/drm/panfrost/panfrost_job.h
index cd82d7930..4d1c54243 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.h
+++ b/drivers/gpu/drm/panfrost/panfrost_job.h
@@ -38,6 +38,8 @@ struct panfrost_job {
bool is_profiled;
/* Holds a reference on the cycle counter */
bool cycle_counter_ref;
+ /* JS_COMMAND_START was written for this submission */
+ bool started;
ktime_t start_time;
u64 start_cycles;
};
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 12/14] drm/panfrost: Wait for the soft-stopped slots with the live slot status
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
panfrost_reset() soft-stops the slots and waits for them to go idle by
polling JOB_INT_JS_STATE. That register is a snapshot of the slot states
taken when JOB_INT_CLEAR was last written (Arm driver: "status==active
and _next == busy snapshot from last JOB_IRQ_CLEAR"). For a job started
into an idle slot, the usual case for an OpenCL queue that submits one
kernel at a time, it shows the slot idle and the wait ends at once.
The GPU reset that follows hides this by killing the job. With the job
resumed instead (next patch), it was removed from pfdev->jobs[] and
resubmitted while its first run was still stopping. When the first run
reported DONE, the handler signalled the resubmission's fence, the job
manager started the resubmission from _NEXT, and its completion found no
job on the slot (Mali-G31, OpenCL CTS Integer Ops):
Unable to handle kernel NULL pointer dereference at virtual address 00000000000000f8
pc : panfrost_dequeue_job+0x2c/0xd8 [panfrost]
lr : panfrost_jm_handle_irqs+0x268/0x570 [panfrost]
Wait until no JS_STATUS register reads ACTIVE instead, so that the
pending DONE or STOPPED interrupt is handled before the queues are
touched. Also make panfrost_dequeue_job() return NULL for a slot without
a job: every caller copes with it, so a stray event is a WARN, not an
Oops.
Fixes: 030761e097b4 ("drm/panfrost: Queue jobs on the hardware")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 41 +++++++++++++++------------------
1 file changed, 19 insertions(+), 22 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 1b7becf7b..8235d182d 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -173,7 +173,8 @@ panfrost_dequeue_job(struct panfrost_device *pfdev, int slot)
{
struct panfrost_job *job = pfdev->jobs[slot][0];
- WARN_ON(!job);
+ if (WARN_ON(!job))
+ return NULL;
if (job->is_profiled && job->engine_usage) {
job->engine_usage->elapsed_ns[slot] +=
@@ -647,25 +648,20 @@ static void panfrost_jm_handle_irqs(struct panfrost_device *pfdev)
}
}
-static u32 panfrost_active_slots(struct panfrost_device *pfdev,
- u32 *js_state_mask, u32 js_state)
+/* Reads the live JS_STATUS: JOB_INT_JS_STATE is a snapshot from the last
+ * JOB_INT_CLEAR write and can show a busy slot as idle.
+ */
+static bool panfrost_slots_idle(struct panfrost_device *pfdev)
{
- u32 rawstat;
-
- if (!(js_state & *js_state_mask))
- return 0;
-
- rawstat = job_read(pfdev, JOB_INT_RAWSTAT);
- if (rawstat) {
- unsigned int i;
+ unsigned int i;
- for (i = 0; i < NUM_JOB_SLOTS; i++) {
- if (rawstat & MK_JS_MASK(i))
- *js_state_mask &= ~MK_JS_MASK(i);
- }
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (job_read(pfdev, JS_STATUS(i)) ==
+ DRM_PANFROST_EXCEPTION_ACTIVE)
+ return false;
}
- return js_state & *js_state_mask;
+ return true;
}
static void
@@ -673,9 +669,8 @@ panfrost_reset(struct panfrost_device *pfdev,
struct drm_sched_job *bad)
{
struct panfrost_job *evicted[NUM_JOB_SLOTS] = {};
- u32 js_state, js_state_mask = 0xffffffff;
unsigned int i, j;
- bool cookie;
+ bool cookie, idle;
int ret;
if (!atomic_read(&pfdev->reset.pending))
@@ -728,10 +723,12 @@ panfrost_reset(struct panfrost_device *pfdev,
}
spin_unlock(&pfdev->js->job_lock);
- /* Wait at most 10ms for soft-stops to complete */
- ret = readl_poll_timeout(pfdev->iomem + JOB_INT_JS_STATE, js_state,
- !panfrost_active_slots(pfdev, &js_state_mask, js_state),
- 10, 10000);
+ /* Wait at most 10ms for soft-stops to complete. No slot may be active
+ * when we touch the queues: a late interrupt would find the wrong job,
+ * or none, in pfdev->jobs[].
+ */
+ ret = read_poll_timeout(panfrost_slots_idle, idle, idle, 10, 10000,
+ false, pfdev);
if (ret)
dev_err(pfdev->base.dev, "Soft-stop failed\n");
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 12/14] drm/panfrost: Wait for the soft-stopped slots with the live slot status
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
panfrost_reset() soft-stops the slots and waits for them to go idle by
polling JOB_INT_JS_STATE. That register is a snapshot of the slot states
taken when JOB_INT_CLEAR was last written (Arm driver: "status==active
and _next == busy snapshot from last JOB_IRQ_CLEAR"). For a job started
into an idle slot, the usual case for an OpenCL queue that submits one
kernel at a time, it shows the slot idle and the wait ends at once.
The GPU reset that follows hides this by killing the job. With the job
resumed instead (next patch), it was removed from pfdev->jobs[] and
resubmitted while its first run was still stopping. When the first run
reported DONE, the handler signalled the resubmission's fence, the job
manager started the resubmission from _NEXT, and its completion found no
job on the slot (Mali-G31, OpenCL CTS Integer Ops):
Unable to handle kernel NULL pointer dereference at virtual address 00000000000000f8
pc : panfrost_dequeue_job+0x2c/0xd8 [panfrost]
lr : panfrost_jm_handle_irqs+0x268/0x570 [panfrost]
Wait until no JS_STATUS register reads ACTIVE instead, so that the
pending DONE or STOPPED interrupt is handled before the queues are
touched. Also make panfrost_dequeue_job() return NULL for a slot without
a job: every caller copes with it, so a stray event is a WARN, not an
Oops.
Fixes: 030761e097b4 ("drm/panfrost: Queue jobs on the hardware")
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 41 +++++++++++++++------------------
1 file changed, 19 insertions(+), 22 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 1b7becf7b..8235d182d 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -173,7 +173,8 @@ panfrost_dequeue_job(struct panfrost_device *pfdev, int slot)
{
struct panfrost_job *job = pfdev->jobs[slot][0];
- WARN_ON(!job);
+ if (WARN_ON(!job))
+ return NULL;
if (job->is_profiled && job->engine_usage) {
job->engine_usage->elapsed_ns[slot] +=
@@ -647,25 +648,20 @@ static void panfrost_jm_handle_irqs(struct panfrost_device *pfdev)
}
}
-static u32 panfrost_active_slots(struct panfrost_device *pfdev,
- u32 *js_state_mask, u32 js_state)
+/* Reads the live JS_STATUS: JOB_INT_JS_STATE is a snapshot from the last
+ * JOB_INT_CLEAR write and can show a busy slot as idle.
+ */
+static bool panfrost_slots_idle(struct panfrost_device *pfdev)
{
- u32 rawstat;
-
- if (!(js_state & *js_state_mask))
- return 0;
-
- rawstat = job_read(pfdev, JOB_INT_RAWSTAT);
- if (rawstat) {
- unsigned int i;
+ unsigned int i;
- for (i = 0; i < NUM_JOB_SLOTS; i++) {
- if (rawstat & MK_JS_MASK(i))
- *js_state_mask &= ~MK_JS_MASK(i);
- }
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (job_read(pfdev, JS_STATUS(i)) ==
+ DRM_PANFROST_EXCEPTION_ACTIVE)
+ return false;
}
- return js_state & *js_state_mask;
+ return true;
}
static void
@@ -673,9 +669,8 @@ panfrost_reset(struct panfrost_device *pfdev,
struct drm_sched_job *bad)
{
struct panfrost_job *evicted[NUM_JOB_SLOTS] = {};
- u32 js_state, js_state_mask = 0xffffffff;
unsigned int i, j;
- bool cookie;
+ bool cookie, idle;
int ret;
if (!atomic_read(&pfdev->reset.pending))
@@ -728,10 +723,12 @@ panfrost_reset(struct panfrost_device *pfdev,
}
spin_unlock(&pfdev->js->job_lock);
- /* Wait at most 10ms for soft-stops to complete */
- ret = readl_poll_timeout(pfdev->iomem + JOB_INT_JS_STATE, js_state,
- !panfrost_active_slots(pfdev, &js_state_mask, js_state),
- 10, 10000);
+ /* Wait at most 10ms for soft-stops to complete. No slot may be active
+ * when we touch the queues: a late interrupt would find the wrong job,
+ * or none, in pfdev->jobs[].
+ */
+ ret = read_poll_timeout(panfrost_slots_idle, idle, idle, 10, 10000,
+ false, pfdev);
if (ret)
dev_err(pfdev->base.dev, "Soft-stop failed\n");
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread
* [PATCH v3 13/14] drm/panfrost: Soft-stop and resume a job that outlives the timeout
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
When the 500 ms job timeout fires, panfrost resets the GPU and the job
is lost. But OpenCL kernels and heavy fragment jobs legitimately take
seconds on a small GPU. On a Mali-G31 (one shader core) this breaks,
among others:
- OpenCL CTS Integer Ops quick_long_math and quick_ulong_math (long16
64-bit division, > 500 ms per kernel): each subtest logs
panfrost 1800000.gpu: gpu sched timeout, js=1, config=0x7300, status=0x8, head=0xa0161c0, tail=0xa0161c0
(status 0x8 = ACTIVE) and fails with the work-items after the reset
never written; Thread Dimensions quick_2d/3d lose whole rows the same
way.
- KHR-GLES31.core.blend_equation_advanced.test_coherency.mixedSequence:
a 44-draw fragment job on a 1024x1024 target needs about 0.7 s, and
the tiles not reached at 500 ms read back as 0,0,0,0.
- dEQP-VK.spirv_assembly.type.vec4.*_frag cases (a loop over the whole
dataset in every fragment): "Incomplete job or timeout".
The Arm driver soft-stops a job after its time slice and resumes it
later, hard-stops it when it has not stopped after 5 s
(DEFAULT_JS_HARD_STOP_TICKS_SS = 50 ticks of 100 ms), and resets the GPU
when the hard stop has not taken effect 0.5 s later
(DEFAULT_JS_RESET_TICKS_SS = 55). Do the same from the timeout handler:
- A slot whose job is still ACTIVE at the timeout is soft-stopped and
its job resumed from JS_TAIL, without a GPU reset or karma. A
soft-stop takes effect at a task boundary only, so such a slot gets
JOB_SOFT_STOP_TIMEOUT_MS (5 s) to go idle; idle or faulted slots keep
the 10 ms.
- A slot still active after that is hard-stopped, which ends its job
with TERMINATED (-ECANCELED). That job is hung, so it gets karma and a
devcoredump, as before. The stopped jobs on the other slots are
resumed; nothing is reset.
- The GPU is reset when the hard stop has not taken effect after
JOB_HARD_STOP_TIMEOUT_MS (0.5 s). The job that did not stop is failed
with -ECANCELED rather than restarted, as it would only time out
again. Jobs that only had the 10 ms of a reset for another reason are
restarted, as before.
- The timeout marks reset.pending with PANFROST_RESET_STOP_ONLY. A reset
requested while the slots are being stopped (a job fault that needs
one, the MMU) overrides it, and the GPU is then reset.
- A job that completes between the timeout handler's fence check and its
JS_STATUS read has its DONE interrupt pending or being handled: the
timeout is spurious.
- Without a reset, the address space references of the jobs cancelled
from _NEXT are released (panfrost_mmu_reset() drops them otherwise),
and a runtime PM reference is held across the stop and the
resubmission, so that autosuspend cannot power the GPU off in between.
The timeout of a job that is merely long is a dev_dbg(); the other paths
report as before, with the status of every slot.
With this, on the Mali-G31 the Integer Ops, Thread Dimensions and
mixedSequence cases pass, and the two vec4 i8/u64 bit_field_insert
count64 fragment cases, which need several seconds to reach a task
boundary, pass in 4-6 s.
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 182 +++++++++++++++++++++++++++-----
1 file changed, 157 insertions(+), 25 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 8235d182d..6757d0fbf 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -27,6 +27,19 @@
#define MAX_JM_CTX_PER_FILE 64
#define JOB_TIMEOUT_MS 500
+/* Time a soft-stopped job gets to reach a task boundary before it is
+ * hard-stopped, and time the hard stop gets before the GPU is reset: the Arm
+ * driver's DEFAULT_JS_HARD_STOP_TICKS_SS (5 s) and DEFAULT_JS_RESET_TICKS_SS
+ * (5.5 s).
+ */
+#define JOB_SOFT_STOP_TIMEOUT_MS 5000
+#define JOB_HARD_STOP_TIMEOUT_MS 500
+
+/* reset.pending value for a stop of the slots without a GPU reset. A request
+ * for a reset (1) overrides it.
+ */
+#define PANFROST_RESET_STOP_ONLY 2
+
#define job_write(dev, reg, data) writel(data, dev->iomem + (reg))
#define job_read(dev, reg) readl(dev->iomem + (reg))
@@ -664,18 +677,35 @@ static bool panfrost_slots_idle(struct panfrost_device *pfdev)
return true;
}
+/* The running job: the second one if the first's DONE is still pending. */
+static struct panfrost_job *
+panfrost_running_job(struct panfrost_device *pfdev, unsigned int slot)
+{
+ if (pfdev->jobs[slot][1] &&
+ (job_read(pfdev, JOB_INT_RAWSTAT) & JOB_INT_MASK_DONE(slot)))
+ return pfdev->jobs[slot][1];
+
+ return pfdev->jobs[slot][0];
+}
+
static void
panfrost_reset(struct panfrost_device *pfdev,
- struct drm_sched_job *bad)
+ struct drm_sched_job *bad, bool job_active)
{
struct panfrost_job *evicted[NUM_JOB_SLOTS] = {};
+ struct panfrost_job *hung[NUM_JOB_SLOTS] = {};
+ bool cookie, idle, stop_failed = false, reset_gpu;
unsigned int i, j;
- bool cookie, idle;
int ret;
if (!atomic_read(&pfdev->reset.pending))
return;
+ /* The stopped jobs drop their runtime PM references before they are
+ * resubmitted: keep the GPU powered in between.
+ */
+ pm_runtime_get_noresume(pfdev->base.dev);
+
/* Stop the schedulers.
*
* FIXME: We temporarily get out of the dma_fence_signalling section
@@ -694,7 +724,8 @@ panfrost_reset(struct panfrost_device *pfdev,
cookie = dma_fence_begin_signalling();
- if (bad)
+ /* A job that is merely long is not guilty of anything. */
+ if (bad && !job_active)
drm_sched_increase_karma(bad);
/* Mask job interrupts and synchronize to make sure we won't be
@@ -723,23 +754,73 @@ panfrost_reset(struct panfrost_device *pfdev,
}
spin_unlock(&pfdev->js->job_lock);
- /* Wait at most 10ms for soft-stops to complete. No slot may be active
- * when we touch the queues: a late interrupt would find the wrong job,
- * or none, in pfdev->jobs[].
+ /* No slot may be active when we touch the queues: a late interrupt
+ * would find the wrong job, or none, in pfdev->jobs[]. If the job that
+ * timed out was active, wait JOB_SOFT_STOP_TIMEOUT_MS, then hard-stop;
+ * otherwise wait 10 ms.
*/
- ret = read_poll_timeout(panfrost_slots_idle, idle, idle, 10, 10000,
+ ret = read_poll_timeout(panfrost_slots_idle, idle, idle,
+ job_active ? 200 : 10,
+ job_active ? JOB_SOFT_STOP_TIMEOUT_MS * 1000 : 10000,
false, pfdev);
+ if (ret && job_active) {
+ dev_err(pfdev->base.dev,
+ "Soft-stop failed after %u ms (js status 0x%x 0x%x 0x%x), hard-stopping the active slots\n",
+ JOB_SOFT_STOP_TIMEOUT_MS,
+ job_read(pfdev, JS_STATUS(0)), job_read(pfdev, JS_STATUS(1)),
+ job_read(pfdev, JS_STATUS(2)));
+
+ /* No task boundary in all that time: the job is hung. */
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (job_read(pfdev, JS_STATUS(i)) ==
+ DRM_PANFROST_EXCEPTION_ACTIVE)
+ hung[i] = panfrost_running_job(pfdev, i);
+ if (hung[i])
+ drm_sched_increase_karma(&hung[i]->base);
+ }
+
+ /* The dump allocates memory: leave the signalling section. */
+ dma_fence_end_signalling(cookie);
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (hung[i])
+ panfrost_core_dump(hung[i]);
+ }
+ cookie = dma_fence_begin_signalling();
+
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (job_read(pfdev, JS_STATUS(i)) ==
+ DRM_PANFROST_EXCEPTION_ACTIVE)
+ job_write(pfdev, JS_COMMAND(i), JS_COMMAND_HARD_STOP);
+ }
+
+ ret = read_poll_timeout(panfrost_slots_idle, idle, idle, 200,
+ JOB_HARD_STOP_TIMEOUT_MS * 1000, false,
+ pfdev);
+ stop_failed = ret;
+ }
+
if (ret)
- dev_err(pfdev->base.dev, "Soft-stop failed\n");
+ dev_err(pfdev->base.dev,
+ "%s failed (js status 0x%x 0x%x 0x%x), resetting the GPU\n",
+ job_active ? "Hard-stop" : "Soft-stop",
+ job_read(pfdev, JS_STATUS(0)), job_read(pfdev, JS_STATUS(1)),
+ job_read(pfdev, JS_STATUS(2)));
/* Handle the remaining interrupts before we reset. */
panfrost_jm_handle_irqs(pfdev);
+ /* No reset if the slots only had to stop, did, and nobody (a job
+ * fault just handled, the MMU) asked for one meanwhile.
+ */
+ reset_gpu = !job_active || stop_failed ||
+ atomic_read(&pfdev->reset.pending) != PANFROST_RESET_STOP_ONLY;
+
/* Remaining interrupts have been handled, but we might still have
- * stuck jobs. Let's make sure the PM counters stay balanced by
+ * stuck jobs, and jobs that were waiting in _NEXT when it was
+ * cancelled. Let's make sure the PM counters stay balanced by
* manually calling pm_runtime_put_noidle() and
- * panfrost_devfreq_record_idle() for each stuck job.
+ * panfrost_devfreq_record_idle() for each of them.
* Let's also make sure the cycle counting register's refcnt is
* kept balanced to prevent it from running forever
*/
@@ -750,19 +831,38 @@ panfrost_reset(struct panfrost_device *pfdev,
pfdev->jobs[i][pfdev->jobs[i][0] ? 1 : 0] = evicted[i];
for (j = 0; j < ARRAY_SIZE(pfdev->jobs[0]) && pfdev->jobs[i][j]; j++) {
- panfrost_job_put_cycle_counter(pfdev->jobs[i][j]);
+ struct panfrost_job *job = pfdev->jobs[i][j];
+
+ panfrost_job_put_cycle_counter(job);
pm_runtime_put_noidle(pfdev->base.dev);
panfrost_devfreq_record_idle(&pfdev->pfdevfreq);
+
+ /* A GPU reset drops the AS references itself. */
+ if (!reset_gpu)
+ panfrost_mmu_as_put(pfdev, job->mmu);
+
+ /* Restarted, the job that forced the reset would only
+ * time out again: fail it.
+ */
+ if (stop_failed && job == hung[i])
+ dma_fence_set_error(&job->base.s_fence->finished,
+ -ECANCELED);
}
}
memset(pfdev->jobs, 0, sizeof(pfdev->jobs));
spin_unlock(&pfdev->js->job_lock);
/* Proceed with reset now. */
- panfrost_device_reset(pfdev, false);
+ if (reset_gpu) {
+ panfrost_device_reset(pfdev, false);
- /* GPU has been reset, we can clear the reset pending bit. */
- atomic_set(&pfdev->reset.pending, 0);
+ /* GPU has been reset, we can clear the reset pending bit. */
+ atomic_set(&pfdev->reset.pending, 0);
+ } else {
+ /* A reset requested since stays pending for its requester. */
+ atomic_cmpxchg(&pfdev->reset.pending,
+ PANFROST_RESET_STOP_ONLY, 0);
+ }
/* Now resubmit jobs that were previously queued but didn't have a
* chance to finish.
@@ -784,6 +884,8 @@ panfrost_reset(struct panfrost_device *pfdev,
panfrost_jm_enable_interrupts(pfdev);
dma_fence_end_signalling(cookie);
+
+ pm_runtime_put_autosuspend(pfdev->base.dev);
}
static enum drm_gpu_sched_stat panfrost_job_timedout(struct drm_sched_job
@@ -792,6 +894,8 @@ static enum drm_gpu_sched_stat panfrost_job_timedout(struct drm_sched_job
struct panfrost_job *job = to_panfrost_job(sched_job);
struct panfrost_device *pfdev = job->pfdev;
int js = panfrost_job_get_slot(job);
+ bool job_active;
+ u32 js_status;
/*
* If the GPU managed to complete this jobs fence, the timeout has
@@ -816,18 +920,46 @@ static enum drm_gpu_sched_stat panfrost_job_timedout(struct drm_sched_job
return DRM_GPU_SCHED_STAT_NO_HANG;
}
- dev_err(pfdev->base.dev, "gpu sched timeout, js=%d, config=0x%x, status=0x%x, head=0x%x, tail=0x%x, sched_job=%p",
- js,
- job_read(pfdev, JS_CONFIG(js)),
- job_read(pfdev, JS_STATUS(js)),
- job_read(pfdev, JS_HEAD_LO(js)),
- job_read(pfdev, JS_TAIL_LO(js)),
- sched_job);
+ /*
+ * A slot that is still ACTIVE runs a long job, not a hung one:
+ * soft-stop and resume it instead of resetting the GPU.
+ */
+ js_status = job_read(pfdev, JS_STATUS(js));
+ job_active = js_status == DRM_PANFROST_EXCEPTION_ACTIVE;
+
+ /* The job may have completed since the fence check above. */
+ if (!job_active) {
+ if (job_read(pfdev, JOB_INT_RAWSTAT) & JOB_INT_MASK_DONE(js))
+ return DRM_GPU_SCHED_STAT_NO_HANG;
+
+ synchronize_irq(pfdev->js->irq);
+ if (dma_fence_is_signaled(job->done_fence))
+ return DRM_GPU_SCHED_STAT_NO_HANG;
+ }
- panfrost_core_dump(job);
+ if (job_active) {
+ dev_dbg(pfdev->base.dev, "job still active after %u ms, js=%d, head=0x%x, sched_job=%p: soft-stopping it to resume",
+ JOB_TIMEOUT_MS, js, job_read(pfdev, JS_HEAD_LO(js)),
+ sched_job);
+ } else {
+ dev_err(pfdev->base.dev, "gpu sched timeout, js=%d, config=0x%x, status=0x%x, head=0x%x, tail=0x%x, sched_job=%p",
+ js,
+ job_read(pfdev, JS_CONFIG(js)),
+ js_status,
+ job_read(pfdev, JS_HEAD_LO(js)),
+ job_read(pfdev, JS_TAIL_LO(js)),
+ sched_job);
- atomic_set(&pfdev->reset.pending, 1);
- panfrost_reset(pfdev, sched_job);
+ panfrost_core_dump(job);
+ }
+
+ /* Ask for a reset, or only for the stop if no reset is pending. */
+ if (job_active)
+ atomic_cmpxchg(&pfdev->reset.pending, 0,
+ PANFROST_RESET_STOP_ONLY);
+ else
+ atomic_set(&pfdev->reset.pending, 1);
+ panfrost_reset(pfdev, sched_job, job_active);
return DRM_GPU_SCHED_STAT_RESET;
}
@@ -847,7 +979,7 @@ static void panfrost_reset_work(struct work_struct *work)
return;
}
- panfrost_reset(pfdev, NULL);
+ panfrost_reset(pfdev, NULL, false);
pm_runtime_put_autosuspend(pfdev->base.dev);
}
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 13/14] drm/panfrost: Soft-stop and resume a job that outlives the timeout
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
When the 500 ms job timeout fires, panfrost resets the GPU and the job
is lost. But OpenCL kernels and heavy fragment jobs legitimately take
seconds on a small GPU. On a Mali-G31 (one shader core) this breaks,
among others:
- OpenCL CTS Integer Ops quick_long_math and quick_ulong_math (long16
64-bit division, > 500 ms per kernel): each subtest logs
panfrost 1800000.gpu: gpu sched timeout, js=1, config=0x7300, status=0x8, head=0xa0161c0, tail=0xa0161c0
(status 0x8 = ACTIVE) and fails with the work-items after the reset
never written; Thread Dimensions quick_2d/3d lose whole rows the same
way.
- KHR-GLES31.core.blend_equation_advanced.test_coherency.mixedSequence:
a 44-draw fragment job on a 1024x1024 target needs about 0.7 s, and
the tiles not reached at 500 ms read back as 0,0,0,0.
- dEQP-VK.spirv_assembly.type.vec4.*_frag cases (a loop over the whole
dataset in every fragment): "Incomplete job or timeout".
The Arm driver soft-stops a job after its time slice and resumes it
later, hard-stops it when it has not stopped after 5 s
(DEFAULT_JS_HARD_STOP_TICKS_SS = 50 ticks of 100 ms), and resets the GPU
when the hard stop has not taken effect 0.5 s later
(DEFAULT_JS_RESET_TICKS_SS = 55). Do the same from the timeout handler:
- A slot whose job is still ACTIVE at the timeout is soft-stopped and
its job resumed from JS_TAIL, without a GPU reset or karma. A
soft-stop takes effect at a task boundary only, so such a slot gets
JOB_SOFT_STOP_TIMEOUT_MS (5 s) to go idle; idle or faulted slots keep
the 10 ms.
- A slot still active after that is hard-stopped, which ends its job
with TERMINATED (-ECANCELED). That job is hung, so it gets karma and a
devcoredump, as before. The stopped jobs on the other slots are
resumed; nothing is reset.
- The GPU is reset when the hard stop has not taken effect after
JOB_HARD_STOP_TIMEOUT_MS (0.5 s). The job that did not stop is failed
with -ECANCELED rather than restarted, as it would only time out
again. Jobs that only had the 10 ms of a reset for another reason are
restarted, as before.
- The timeout marks reset.pending with PANFROST_RESET_STOP_ONLY. A reset
requested while the slots are being stopped (a job fault that needs
one, the MMU) overrides it, and the GPU is then reset.
- A job that completes between the timeout handler's fence check and its
JS_STATUS read has its DONE interrupt pending or being handled: the
timeout is spurious.
- Without a reset, the address space references of the jobs cancelled
from _NEXT are released (panfrost_mmu_reset() drops them otherwise),
and a runtime PM reference is held across the stop and the
resubmission, so that autosuspend cannot power the GPU off in between.
The timeout of a job that is merely long is a dev_dbg(); the other paths
report as before, with the status of every slot.
With this, on the Mali-G31 the Integer Ops, Thread Dimensions and
mixedSequence cases pass, and the two vec4 i8/u64 bit_field_insert
count64 fragment cases, which need several seconds to reach a task
boundary, pass in 4-6 s.
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_job.c | 182 +++++++++++++++++++++++++++-----
1 file changed, 157 insertions(+), 25 deletions(-)
diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
index 8235d182d..6757d0fbf 100644
--- a/drivers/gpu/drm/panfrost/panfrost_job.c
+++ b/drivers/gpu/drm/panfrost/panfrost_job.c
@@ -27,6 +27,19 @@
#define MAX_JM_CTX_PER_FILE 64
#define JOB_TIMEOUT_MS 500
+/* Time a soft-stopped job gets to reach a task boundary before it is
+ * hard-stopped, and time the hard stop gets before the GPU is reset: the Arm
+ * driver's DEFAULT_JS_HARD_STOP_TICKS_SS (5 s) and DEFAULT_JS_RESET_TICKS_SS
+ * (5.5 s).
+ */
+#define JOB_SOFT_STOP_TIMEOUT_MS 5000
+#define JOB_HARD_STOP_TIMEOUT_MS 500
+
+/* reset.pending value for a stop of the slots without a GPU reset. A request
+ * for a reset (1) overrides it.
+ */
+#define PANFROST_RESET_STOP_ONLY 2
+
#define job_write(dev, reg, data) writel(data, dev->iomem + (reg))
#define job_read(dev, reg) readl(dev->iomem + (reg))
@@ -664,18 +677,35 @@ static bool panfrost_slots_idle(struct panfrost_device *pfdev)
return true;
}
+/* The running job: the second one if the first's DONE is still pending. */
+static struct panfrost_job *
+panfrost_running_job(struct panfrost_device *pfdev, unsigned int slot)
+{
+ if (pfdev->jobs[slot][1] &&
+ (job_read(pfdev, JOB_INT_RAWSTAT) & JOB_INT_MASK_DONE(slot)))
+ return pfdev->jobs[slot][1];
+
+ return pfdev->jobs[slot][0];
+}
+
static void
panfrost_reset(struct panfrost_device *pfdev,
- struct drm_sched_job *bad)
+ struct drm_sched_job *bad, bool job_active)
{
struct panfrost_job *evicted[NUM_JOB_SLOTS] = {};
+ struct panfrost_job *hung[NUM_JOB_SLOTS] = {};
+ bool cookie, idle, stop_failed = false, reset_gpu;
unsigned int i, j;
- bool cookie, idle;
int ret;
if (!atomic_read(&pfdev->reset.pending))
return;
+ /* The stopped jobs drop their runtime PM references before they are
+ * resubmitted: keep the GPU powered in between.
+ */
+ pm_runtime_get_noresume(pfdev->base.dev);
+
/* Stop the schedulers.
*
* FIXME: We temporarily get out of the dma_fence_signalling section
@@ -694,7 +724,8 @@ panfrost_reset(struct panfrost_device *pfdev,
cookie = dma_fence_begin_signalling();
- if (bad)
+ /* A job that is merely long is not guilty of anything. */
+ if (bad && !job_active)
drm_sched_increase_karma(bad);
/* Mask job interrupts and synchronize to make sure we won't be
@@ -723,23 +754,73 @@ panfrost_reset(struct panfrost_device *pfdev,
}
spin_unlock(&pfdev->js->job_lock);
- /* Wait at most 10ms for soft-stops to complete. No slot may be active
- * when we touch the queues: a late interrupt would find the wrong job,
- * or none, in pfdev->jobs[].
+ /* No slot may be active when we touch the queues: a late interrupt
+ * would find the wrong job, or none, in pfdev->jobs[]. If the job that
+ * timed out was active, wait JOB_SOFT_STOP_TIMEOUT_MS, then hard-stop;
+ * otherwise wait 10 ms.
*/
- ret = read_poll_timeout(panfrost_slots_idle, idle, idle, 10, 10000,
+ ret = read_poll_timeout(panfrost_slots_idle, idle, idle,
+ job_active ? 200 : 10,
+ job_active ? JOB_SOFT_STOP_TIMEOUT_MS * 1000 : 10000,
false, pfdev);
+ if (ret && job_active) {
+ dev_err(pfdev->base.dev,
+ "Soft-stop failed after %u ms (js status 0x%x 0x%x 0x%x), hard-stopping the active slots\n",
+ JOB_SOFT_STOP_TIMEOUT_MS,
+ job_read(pfdev, JS_STATUS(0)), job_read(pfdev, JS_STATUS(1)),
+ job_read(pfdev, JS_STATUS(2)));
+
+ /* No task boundary in all that time: the job is hung. */
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (job_read(pfdev, JS_STATUS(i)) ==
+ DRM_PANFROST_EXCEPTION_ACTIVE)
+ hung[i] = panfrost_running_job(pfdev, i);
+ if (hung[i])
+ drm_sched_increase_karma(&hung[i]->base);
+ }
+
+ /* The dump allocates memory: leave the signalling section. */
+ dma_fence_end_signalling(cookie);
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (hung[i])
+ panfrost_core_dump(hung[i]);
+ }
+ cookie = dma_fence_begin_signalling();
+
+ for (i = 0; i < NUM_JOB_SLOTS; i++) {
+ if (job_read(pfdev, JS_STATUS(i)) ==
+ DRM_PANFROST_EXCEPTION_ACTIVE)
+ job_write(pfdev, JS_COMMAND(i), JS_COMMAND_HARD_STOP);
+ }
+
+ ret = read_poll_timeout(panfrost_slots_idle, idle, idle, 200,
+ JOB_HARD_STOP_TIMEOUT_MS * 1000, false,
+ pfdev);
+ stop_failed = ret;
+ }
+
if (ret)
- dev_err(pfdev->base.dev, "Soft-stop failed\n");
+ dev_err(pfdev->base.dev,
+ "%s failed (js status 0x%x 0x%x 0x%x), resetting the GPU\n",
+ job_active ? "Hard-stop" : "Soft-stop",
+ job_read(pfdev, JS_STATUS(0)), job_read(pfdev, JS_STATUS(1)),
+ job_read(pfdev, JS_STATUS(2)));
/* Handle the remaining interrupts before we reset. */
panfrost_jm_handle_irqs(pfdev);
+ /* No reset if the slots only had to stop, did, and nobody (a job
+ * fault just handled, the MMU) asked for one meanwhile.
+ */
+ reset_gpu = !job_active || stop_failed ||
+ atomic_read(&pfdev->reset.pending) != PANFROST_RESET_STOP_ONLY;
+
/* Remaining interrupts have been handled, but we might still have
- * stuck jobs. Let's make sure the PM counters stay balanced by
+ * stuck jobs, and jobs that were waiting in _NEXT when it was
+ * cancelled. Let's make sure the PM counters stay balanced by
* manually calling pm_runtime_put_noidle() and
- * panfrost_devfreq_record_idle() for each stuck job.
+ * panfrost_devfreq_record_idle() for each of them.
* Let's also make sure the cycle counting register's refcnt is
* kept balanced to prevent it from running forever
*/
@@ -750,19 +831,38 @@ panfrost_reset(struct panfrost_device *pfdev,
pfdev->jobs[i][pfdev->jobs[i][0] ? 1 : 0] = evicted[i];
for (j = 0; j < ARRAY_SIZE(pfdev->jobs[0]) && pfdev->jobs[i][j]; j++) {
- panfrost_job_put_cycle_counter(pfdev->jobs[i][j]);
+ struct panfrost_job *job = pfdev->jobs[i][j];
+
+ panfrost_job_put_cycle_counter(job);
pm_runtime_put_noidle(pfdev->base.dev);
panfrost_devfreq_record_idle(&pfdev->pfdevfreq);
+
+ /* A GPU reset drops the AS references itself. */
+ if (!reset_gpu)
+ panfrost_mmu_as_put(pfdev, job->mmu);
+
+ /* Restarted, the job that forced the reset would only
+ * time out again: fail it.
+ */
+ if (stop_failed && job == hung[i])
+ dma_fence_set_error(&job->base.s_fence->finished,
+ -ECANCELED);
}
}
memset(pfdev->jobs, 0, sizeof(pfdev->jobs));
spin_unlock(&pfdev->js->job_lock);
/* Proceed with reset now. */
- panfrost_device_reset(pfdev, false);
+ if (reset_gpu) {
+ panfrost_device_reset(pfdev, false);
- /* GPU has been reset, we can clear the reset pending bit. */
- atomic_set(&pfdev->reset.pending, 0);
+ /* GPU has been reset, we can clear the reset pending bit. */
+ atomic_set(&pfdev->reset.pending, 0);
+ } else {
+ /* A reset requested since stays pending for its requester. */
+ atomic_cmpxchg(&pfdev->reset.pending,
+ PANFROST_RESET_STOP_ONLY, 0);
+ }
/* Now resubmit jobs that were previously queued but didn't have a
* chance to finish.
@@ -784,6 +884,8 @@ panfrost_reset(struct panfrost_device *pfdev,
panfrost_jm_enable_interrupts(pfdev);
dma_fence_end_signalling(cookie);
+
+ pm_runtime_put_autosuspend(pfdev->base.dev);
}
static enum drm_gpu_sched_stat panfrost_job_timedout(struct drm_sched_job
@@ -792,6 +894,8 @@ static enum drm_gpu_sched_stat panfrost_job_timedout(struct drm_sched_job
struct panfrost_job *job = to_panfrost_job(sched_job);
struct panfrost_device *pfdev = job->pfdev;
int js = panfrost_job_get_slot(job);
+ bool job_active;
+ u32 js_status;
/*
* If the GPU managed to complete this jobs fence, the timeout has
@@ -816,18 +920,46 @@ static enum drm_gpu_sched_stat panfrost_job_timedout(struct drm_sched_job
return DRM_GPU_SCHED_STAT_NO_HANG;
}
- dev_err(pfdev->base.dev, "gpu sched timeout, js=%d, config=0x%x, status=0x%x, head=0x%x, tail=0x%x, sched_job=%p",
- js,
- job_read(pfdev, JS_CONFIG(js)),
- job_read(pfdev, JS_STATUS(js)),
- job_read(pfdev, JS_HEAD_LO(js)),
- job_read(pfdev, JS_TAIL_LO(js)),
- sched_job);
+ /*
+ * A slot that is still ACTIVE runs a long job, not a hung one:
+ * soft-stop and resume it instead of resetting the GPU.
+ */
+ js_status = job_read(pfdev, JS_STATUS(js));
+ job_active = js_status == DRM_PANFROST_EXCEPTION_ACTIVE;
+
+ /* The job may have completed since the fence check above. */
+ if (!job_active) {
+ if (job_read(pfdev, JOB_INT_RAWSTAT) & JOB_INT_MASK_DONE(js))
+ return DRM_GPU_SCHED_STAT_NO_HANG;
+
+ synchronize_irq(pfdev->js->irq);
+ if (dma_fence_is_signaled(job->done_fence))
+ return DRM_GPU_SCHED_STAT_NO_HANG;
+ }
- panfrost_core_dump(job);
+ if (job_active) {
+ dev_dbg(pfdev->base.dev, "job still active after %u ms, js=%d, head=0x%x, sched_job=%p: soft-stopping it to resume",
+ JOB_TIMEOUT_MS, js, job_read(pfdev, JS_HEAD_LO(js)),
+ sched_job);
+ } else {
+ dev_err(pfdev->base.dev, "gpu sched timeout, js=%d, config=0x%x, status=0x%x, head=0x%x, tail=0x%x, sched_job=%p",
+ js,
+ job_read(pfdev, JS_CONFIG(js)),
+ js_status,
+ job_read(pfdev, JS_HEAD_LO(js)),
+ job_read(pfdev, JS_TAIL_LO(js)),
+ sched_job);
- atomic_set(&pfdev->reset.pending, 1);
- panfrost_reset(pfdev, sched_job);
+ panfrost_core_dump(job);
+ }
+
+ /* Ask for a reset, or only for the stop if no reset is pending. */
+ if (job_active)
+ atomic_cmpxchg(&pfdev->reset.pending, 0,
+ PANFROST_RESET_STOP_ONLY);
+ else
+ atomic_set(&pfdev->reset.pending, 1);
+ panfrost_reset(pfdev, sched_job, job_active);
return DRM_GPU_SCHED_STAT_RESET;
}
@@ -847,7 +979,7 @@ static void panfrost_reset_work(struct work_struct *work)
return;
}
- panfrost_reset(pfdev, NULL);
+ panfrost_reset(pfdev, NULL, false);
pm_runtime_put_autosuspend(pfdev->base.dev);
}
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* Re: [PATCH v3 13/14] drm/panfrost: Soft-stop and resume a job that outlives the timeout
2026-11-13 9:03 ` Arun Iyengar via B4 Relay
(?)
@ 2026-11-13 9:49 ` sashiko-bot
-1 siblings, 0 replies; 32+ messages in thread
From: sashiko-bot @ 2026-11-13 9:49 UTC (permalink / raw)
To: Arun Iyengar; +Cc: dri-devel
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [Medium] drm/panfrost: runtime suspend race in panfrost_job_timedout()
before reset
--
commit 3b989ea77e83f04bf915c182d8dabfa8f214f4fc
Author: Arun Iyengar <aiyengar1990@gmail.com>
drm/panfrost: Soft-stop and resume a job that outlives the timeout
This commit updates the Panfrost job timeout handler to soft-stop and resume
slots whose jobs are still active, falling back to a hard-stop after 5 seconds
and a full GPU reset only if the hard-stop fails or another reset is
requested.
> diff --git a/drivers/gpu/drm/panfrost/panfrost_job.c b/drivers/gpu/drm/panfrost/panfrost_job.c
> index 8235d182d8114..6757d0fbf63c9 100644
> --- a/drivers/gpu/drm/panfrost/panfrost_job.c
> +++ b/drivers/gpu/drm/panfrost/panfrost_job.c
[ ... ]
> @@ -664,18 +677,35 @@ static bool panfrost_slots_idle(struct panfrost_device *pfdev)
[ ... ]
> static void
> panfrost_reset(struct panfrost_device *pfdev,
> - struct drm_sched_job *bad)
> + struct drm_sched_job *bad, bool job_active)
> {
> struct panfrost_job *evicted[NUM_JOB_SLOTS] = {};
> + struct panfrost_job *hung[NUM_JOB_SLOTS] = {};
> + bool cookie, idle, stop_failed = false, reset_gpu;
> unsigned int i, j;
> - bool cookie, idle;
> int ret;
>
> if (!atomic_read(&pfdev->reset.pending))
> return;
>
> + /* The stopped jobs drop their runtime PM references before they are
> + * resubmitted: keep the GPU powered in between.
> + */
> + pm_runtime_get_noresume(pfdev->base.dev);
[ ... ]
> @@ -816,18 +920,46 @@ static enum drm_gpu_sched_stat panfrost_job_timedout(struct drm_sched_job
> return DRM_GPU_SCHED_STAT_NO_HANG;
> }
>
> - dev_err(pfdev->base.dev, "gpu sched timeout, js=%d, config=0x%x, status=0x%x, head=0x%x, tail=0x%x, sched_job=%p",
> - js,
> - job_read(pfdev, JS_CONFIG(js)),
> - job_read(pfdev, JS_STATUS(js)),
> - job_read(pfdev, JS_HEAD_LO(js)),
> - job_read(pfdev, JS_TAIL_LO(js)),
> - sched_job);
> + /*
> + * A slot that is still ACTIVE runs a long job, not a hung one:
> + * soft-stop and resume it instead of resetting the GPU.
> + */
> + js_status = job_read(pfdev, JS_STATUS(js));
> + job_active = js_status == DRM_PANFROST_EXCEPTION_ACTIVE;
> +
> + /* The job may have completed since the fence check above. */
> + if (!job_active) {
> + if (job_read(pfdev, JOB_INT_RAWSTAT) & JOB_INT_MASK_DONE(js))
> + return DRM_GPU_SCHED_STAT_NO_HANG;
> +
> + synchronize_irq(pfdev->js->irq);
> + if (dma_fence_is_signaled(job->done_fence))
> + return DRM_GPU_SCHED_STAT_NO_HANG;
> + }
>
> - panfrost_core_dump(job);
> + if (job_active) {
> + dev_dbg(pfdev->base.dev, "job still active after %u ms, js=%d, head=0x%x, sched_job=%p: soft-stopping it to resume",
> + JOB_TIMEOUT_MS, js, job_read(pfdev, JS_HEAD_LO(js)),
> + sched_job);
> + } else {
> + dev_err(pfdev->base.dev, "gpu sched timeout, js=%d, config=0x%x, status=0x%x, head=0x%x, tail=0x%x, sched_job=%p",
> + js,
> + job_read(pfdev, JS_CONFIG(js)),
> + js_status,
> + job_read(pfdev, JS_HEAD_LO(js)),
> + job_read(pfdev, JS_TAIL_LO(js)),
> + sched_job);
>
> - atomic_set(&pfdev->reset.pending, 1);
> - panfrost_reset(pfdev, sched_job);
> + panfrost_core_dump(job);
> + }
> +
> + /* Ask for a reset, or only for the stop if no reset is pending. */
> + if (job_active)
> + atomic_cmpxchg(&pfdev->reset.pending, 0,
> + PANFROST_RESET_STOP_ONLY);
> + else
> + atomic_set(&pfdev->reset.pending, 1);
> + panfrost_reset(pfdev, sched_job, job_active);
[Severity: Medium]
Can the GPU runtime-autosuspend in panfrost_job_timedout() before
panfrost_reset() calls pm_runtime_get_noresume(), causing MMIO accesses on
a powered-off GPU?
CPU0 (timeout worker) CPU1 (IRQ thread / PM worker)
panfrost_job_timedout()
js_status = job_read(JS_STATUS)
// reads ACTIVE
panfrost_jm_irq_handler_thread()
panfrost_jm_handle_done()
dma_fence_signal_locked(done_fence)
drm_sched_job_done_cb()
// credit_count -> 0
pm_runtime_put_autosuspend()
...
// 50 ms autosuspend expires
panfrost_device_runtime_suspend()
panfrost_gpu_power_off()
panfrost_reset()
pm_runtime_get_noresume()
job_write(JOB_INT_MASK, 0) <-- external abort (GPU powered off)
Job interrupts remain enabled until panfrost_reset() masks JOB_INT_MASK,
and the completion callback on job->done_fence remains attached until
drm_sched_stop(). If the only in-flight job completes right after
JS_STATUS(js) is read as DRM_PANFROST_EXCEPTION_ACTIVE (or faults just
before, setting JOB_INT_MASK_ERR(js) rather than JOB_INT_MASK_DONE(js),
and the IRQ thread finishes while panfrost_core_dump() is copying BOs),
the IRQ handler drops sched->credit_count to 0 and releases the job's
runtime PM reference. With panfrost_jm_is_idle() now true, runtime
autosuspend can power off the GPU and disable its clocks before
panfrost_reset() is reached, and pm_runtime_get_noresume() only increments
usage_count without resuming the device.
Acquiring the runtime PM reference at the start of panfrost_job_timedout()
(or using pm_runtime_resume_and_get()) before checking the fences and
reading registers would keep the GPU powered throughout the timeout and
reset path.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261113-panfrost-long-jobs-v3-0-80ae78d38f6e@gmail.com?part=13
^ permalink raw reply [flat|nested] 32+ messages in thread
* [PATCH v3 14/14] drm/panfrost: Name the BOs around an unhandled page fault
2026-11-13 9:02 ` Arun Iyengar via B4 Relay
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
-1 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
An unhandled GPU page fault reports only the faulting VA, which does not
tell an access just past the end of a buffer from a stale or garbage
pointer.
Also print the BO that contains the address, or else the nearest BO
below and above it: VA range, size, distance of the fault from it,
heap/noexec and the label userspace gave it (Mesa labels its BOs with
use, format and size). A freed BO has left the address space, so an
access to it shows up as a hole between its neighbours. Mappings whose
last reference is gone are skipped. For an OpenCL image read on a
Mali-G31 this gave (with an earlier version of this patch, which also
printed "mapped"):
panfrost 1800000.gpu: Unhandled Page fault in AS0 at VA 0x000000000A181000
...
panfrost 1800000.gpu: fault bo: below: VA [0xa041000-0xa181000) size 0x140000, fault at end+0x0, mapped, noexec, label "Texture format=r32g32b32_sint extent=25x128x32 array_size=1 mip_count=0 samples=0 modifier=0x0"
that is, the texture unit reading past the end of a 96-bit-per-texel
texture, which userspace can avoid by padding such textures.
Heap growth faults are not affected.
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 81 +++++++++++++++++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index 2cf67d932..29dd575f4 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -594,6 +594,86 @@ addr_to_mapping(struct panfrost_device *pfdev, int as, u64 addr)
return mapping;
}
+static void panfrost_mmu_print_fault_bo(struct panfrost_device *pfdev,
+ struct panfrost_gem_mapping *m,
+ u64 addr, const char *where)
+{
+ struct panfrost_gem_object *bo = m->obj;
+ u64 start = m->mmnode.start << PAGE_SHIFT;
+ u64 end = (m->mmnode.start + m->mmnode.size) << PAGE_SHIFT;
+ u64 dist = addr < start ? start - addr : addr >= end ? addr - end : addr - start;
+
+ mutex_lock(&bo->label.lock);
+ dev_err(pfdev->base.dev,
+ "fault bo: %s: VA [0x%llx-0x%llx) size 0x%zx, fault at %s0x%llx%s%s, label \"%s\"\n",
+ where, start, end, bo->base.base.size,
+ addr < start ? "-" : addr >= end ? "end+" : "start+", dist,
+ bo->is_heap ? ", heap" : "", bo->noexec ? ", noexec" : "",
+ bo->label.str ? bo->label.str : "");
+ mutex_unlock(&bo->label.lock);
+}
+
+/*
+ * Report the BO that contains an unhandled fault address, or else its nearest
+ * neighbours, to tell an overrun of a BO from a stale or garbage pointer.
+ */
+static void panfrost_mmu_report_fault_bos(struct panfrost_device *pfdev,
+ int as, u64 addr)
+{
+ struct panfrost_gem_mapping *below = NULL, *above = NULL;
+ u64 offset = addr >> PAGE_SHIFT;
+ struct drm_mm_node *node;
+ struct panfrost_mmu *mmu;
+ bool inside = false;
+
+ spin_lock(&pfdev->as_lock);
+ list_for_each_entry(mmu, &pfdev->as_lru_list, list) {
+ if (as == mmu->as)
+ goto found_mmu;
+ }
+ spin_unlock(&pfdev->as_lock);
+ dev_err(pfdev->base.dev, "fault bo: no context holds AS%d\n", as);
+ return;
+
+found_mmu:
+ spin_lock(&mmu->mm_lock);
+ /* The nodes are sorted by address. Skip mappings being freed. */
+ drm_mm_for_each_node(node, &mmu->mm) {
+ struct panfrost_gem_mapping *m = drm_mm_node_to_panfrost_mapping(node);
+
+ if (!kref_read(&m->refcount))
+ continue;
+ if (node->start > offset) {
+ above = m;
+ break;
+ }
+ below = m;
+ }
+ if (below) {
+ inside = offset < below->mmnode.start + below->mmnode.size;
+ if (inside)
+ above = NULL;
+ if (!kref_get_unless_zero(&below->refcount))
+ below = NULL;
+ }
+ if (above && !kref_get_unless_zero(&above->refcount))
+ above = NULL;
+ spin_unlock(&mmu->mm_lock);
+ spin_unlock(&pfdev->as_lock);
+
+ if (!below && !above)
+ dev_err(pfdev->base.dev, "fault bo: AS%d has no BO\n", as);
+ if (below) {
+ panfrost_mmu_print_fault_bo(pfdev, below, addr,
+ inside ? "inside" : "below");
+ panfrost_gem_mapping_put(below);
+ }
+ if (above) {
+ panfrost_mmu_print_fault_bo(pfdev, above, addr, "above");
+ panfrost_gem_mapping_put(above);
+ }
+}
+
#define NUM_FAULT_PAGES (SZ_2M / PAGE_SIZE)
/*
@@ -973,6 +1053,7 @@ static irqreturn_t panfrost_mmu_irq_handler_thread(int irq, void *data)
exception_type, panfrost_exception_name(exception_type),
access_type, access_type_name(pfdev, fault_status),
source_id);
+ panfrost_mmu_report_fault_bos(pfdev, as, addr);
spin_lock(&pfdev->as_lock);
/* Ignore MMU interrupts on this AS until it's been
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread* [PATCH v3 14/14] drm/panfrost: Name the BOs around an unhandled page fault
@ 2026-11-13 9:03 ` Arun Iyengar via B4 Relay
0 siblings, 0 replies; 32+ messages in thread
From: Arun Iyengar via B4 Relay @ 2026-11-13 9:03 UTC (permalink / raw)
To: Boris Brezillon, Rob Herring, Steven Price, Adrián Larumbe,
Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
Simona Vetter
Cc: dri-devel, linux-kernel, Arun Iyengar
From: Arun Iyengar <aiyengar1990@gmail.com>
An unhandled GPU page fault reports only the faulting VA, which does not
tell an access just past the end of a buffer from a stale or garbage
pointer.
Also print the BO that contains the address, or else the nearest BO
below and above it: VA range, size, distance of the fault from it,
heap/noexec and the label userspace gave it (Mesa labels its BOs with
use, format and size). A freed BO has left the address space, so an
access to it shows up as a hole between its neighbours. Mappings whose
last reference is gone are skipped. For an OpenCL image read on a
Mali-G31 this gave (with an earlier version of this patch, which also
printed "mapped"):
panfrost 1800000.gpu: Unhandled Page fault in AS0 at VA 0x000000000A181000
...
panfrost 1800000.gpu: fault bo: below: VA [0xa041000-0xa181000) size 0x140000, fault at end+0x0, mapped, noexec, label "Texture format=r32g32b32_sint extent=25x128x32 array_size=1 mip_count=0 samples=0 modifier=0x0"
that is, the texture unit reading past the end of a 96-bit-per-texel
texture, which userspace can avoid by padding such textures.
Heap growth faults are not affected.
Signed-off-by: Arun Iyengar <aiyengar1990@gmail.com>
---
drivers/gpu/drm/panfrost/panfrost_mmu.c | 81 +++++++++++++++++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/drivers/gpu/drm/panfrost/panfrost_mmu.c b/drivers/gpu/drm/panfrost/panfrost_mmu.c
index 2cf67d932..29dd575f4 100644
--- a/drivers/gpu/drm/panfrost/panfrost_mmu.c
+++ b/drivers/gpu/drm/panfrost/panfrost_mmu.c
@@ -594,6 +594,86 @@ addr_to_mapping(struct panfrost_device *pfdev, int as, u64 addr)
return mapping;
}
+static void panfrost_mmu_print_fault_bo(struct panfrost_device *pfdev,
+ struct panfrost_gem_mapping *m,
+ u64 addr, const char *where)
+{
+ struct panfrost_gem_object *bo = m->obj;
+ u64 start = m->mmnode.start << PAGE_SHIFT;
+ u64 end = (m->mmnode.start + m->mmnode.size) << PAGE_SHIFT;
+ u64 dist = addr < start ? start - addr : addr >= end ? addr - end : addr - start;
+
+ mutex_lock(&bo->label.lock);
+ dev_err(pfdev->base.dev,
+ "fault bo: %s: VA [0x%llx-0x%llx) size 0x%zx, fault at %s0x%llx%s%s, label \"%s\"\n",
+ where, start, end, bo->base.base.size,
+ addr < start ? "-" : addr >= end ? "end+" : "start+", dist,
+ bo->is_heap ? ", heap" : "", bo->noexec ? ", noexec" : "",
+ bo->label.str ? bo->label.str : "");
+ mutex_unlock(&bo->label.lock);
+}
+
+/*
+ * Report the BO that contains an unhandled fault address, or else its nearest
+ * neighbours, to tell an overrun of a BO from a stale or garbage pointer.
+ */
+static void panfrost_mmu_report_fault_bos(struct panfrost_device *pfdev,
+ int as, u64 addr)
+{
+ struct panfrost_gem_mapping *below = NULL, *above = NULL;
+ u64 offset = addr >> PAGE_SHIFT;
+ struct drm_mm_node *node;
+ struct panfrost_mmu *mmu;
+ bool inside = false;
+
+ spin_lock(&pfdev->as_lock);
+ list_for_each_entry(mmu, &pfdev->as_lru_list, list) {
+ if (as == mmu->as)
+ goto found_mmu;
+ }
+ spin_unlock(&pfdev->as_lock);
+ dev_err(pfdev->base.dev, "fault bo: no context holds AS%d\n", as);
+ return;
+
+found_mmu:
+ spin_lock(&mmu->mm_lock);
+ /* The nodes are sorted by address. Skip mappings being freed. */
+ drm_mm_for_each_node(node, &mmu->mm) {
+ struct panfrost_gem_mapping *m = drm_mm_node_to_panfrost_mapping(node);
+
+ if (!kref_read(&m->refcount))
+ continue;
+ if (node->start > offset) {
+ above = m;
+ break;
+ }
+ below = m;
+ }
+ if (below) {
+ inside = offset < below->mmnode.start + below->mmnode.size;
+ if (inside)
+ above = NULL;
+ if (!kref_get_unless_zero(&below->refcount))
+ below = NULL;
+ }
+ if (above && !kref_get_unless_zero(&above->refcount))
+ above = NULL;
+ spin_unlock(&mmu->mm_lock);
+ spin_unlock(&pfdev->as_lock);
+
+ if (!below && !above)
+ dev_err(pfdev->base.dev, "fault bo: AS%d has no BO\n", as);
+ if (below) {
+ panfrost_mmu_print_fault_bo(pfdev, below, addr,
+ inside ? "inside" : "below");
+ panfrost_gem_mapping_put(below);
+ }
+ if (above) {
+ panfrost_mmu_print_fault_bo(pfdev, above, addr, "above");
+ panfrost_gem_mapping_put(above);
+ }
+}
+
#define NUM_FAULT_PAGES (SZ_2M / PAGE_SIZE)
/*
@@ -973,6 +1053,7 @@ static irqreturn_t panfrost_mmu_irq_handler_thread(int irq, void *data)
exception_type, panfrost_exception_name(exception_type),
access_type, access_type_name(pfdev, fault_status),
source_id);
+ panfrost_mmu_report_fault_bos(pfdev, as, addr);
spin_lock(&pfdev->as_lock);
/* Ignore MMU interrupts on this AS until it's been
--
2.55.0
^ permalink raw reply related [flat|nested] 32+ messages in thread