All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v3 00/17] drm/panthor: Fix the unplug logic
@ 2026-08-13 10:56 Boris Brezillon
  2026-08-13 10:56 ` [PATCH v3 01/17] drm/panthor: Disable reset work before unplug Boris Brezillon
                   ` (17 more replies)
  0 siblings, 18 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:56 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon,
	sashiko-bot

The current unplug logic is broken in multiple ways. This is an attempt
at addressing the various problems found along the way (some were
reported by Sashiko, others have been found while trying to address
Sashiko's concerns).

Sending a new version even though v2 didn't receive any human review
just to try and address the new stuff pointed out by Sashiko.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
Changes in v3:
- Fix a race in the reset reschedule logic we added to
  panthor_device_resume() (missing smp_mb__after_atomic())
- Fix a VM leak when reset and suspend are racing with each other
- Add missing drm_dev_enter/exit() sections
- Insert the groups in the user_owned list even if the group creation
  happens during a reset
- Try to document why some of the issues pointed out by Sashiko are
  either not real issues, or are expected (either fixed in a later
  commits, or just expected behavior)
- Fix a race between panthor_device_unplug() and vm_prep_for_cleanup()
  (introduced in v2)
- Link to v2: https://patch.msgid.link/20260811-panthor-unplug-fixes-v2-0-6b583e37f9ae@collabora.com

Changes in v2:
- Fix UAFs caused by deferred cleanup works
- Fix UAFs caused by open FDs closed after unplug
- Fix deadlock when device_unplug() is called from the reset work
- Make sure reset requests are not lost in the resume and post_reset
  paths
- Fix a deadlock in the suspend path
- Fix a clk prepare_enable leak in the unplug path
- Don't use a drmm_action to flush the cleanup queue (this could cause
  UAFs)
- Drop the now unused panthor_vm::unusable field
- Keep track of user owned resources to prevent leaks and/or UAFs
- Link to v1: https://patch.msgid.link/20260804-panthor-unplug-fixes-v1-0-abbbd2d41b13@collabora.com

---
Boris Brezillon (17):
      drm/panthor: Disable reset work before unplug
      drm/panthor: Further delay reset work enablement
      drm/panthor: Make sure reset requests in the resume path are not lost
      drm/panthor: Make sure reset requests in the post reset path are not lost
      drm/panthor: Flush the cleanup_wq in the unplug path
      drm/panthor: Drop unused vm argument passed to panthor_vm_prepare_sync_only_op_ctx()
      drm/panthor: Move the debugfs initialization to panthor_device.c
      drm/panthor: Split panthor_vm
      drm/panthor: Add fine-grained restrictions on VMs
      drm/panthor: Check AS state before disabling
      drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap
      drm/panthor: Make the VM cleanup path more robust against UAF
      drm/panthor: Track user owned VMs
      drm/panthor: Track user owned groups
      drm/panthor: Fix the unplug logic
      drm/panthor: Add a debugfs knob to simulate unplug failures
      drm/panthor: Add a debugfs knobs to simulate reset failures

 drivers/gpu/drm/panthor/panthor_device.c |  189 +++-
 drivers/gpu/drm/panthor/panthor_device.h |   38 +
 drivers/gpu/drm/panthor/panthor_drv.c    |  132 ++-
 drivers/gpu/drm/panthor/panthor_fw.c     |    9 +-
 drivers/gpu/drm/panthor/panthor_mmu.c    | 1493 ++++++++++++++++++------------
 drivers/gpu/drm/panthor/panthor_mmu.h    |    4 +-
 drivers/gpu/drm/panthor/panthor_sched.c  |  132 ++-
 7 files changed, 1344 insertions(+), 653 deletions(-)
---
base-commit: 44e9eb5a762142a4aa46c0b5da7c39bfeb78910e
change-id: 20260804-panthor-unplug-fixes-7927b3ddc2f9

Best regards,
--  
Boris Brezillon <boris.brezillon@collabora.com>


^ permalink raw reply	[flat|nested] 30+ messages in thread

* [PATCH v3 01/17] drm/panthor: Disable reset work before unplug
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
@ 2026-08-13 10:56 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 02/17] drm/panthor: Further delay reset work enablement Boris Brezillon
                   ` (16 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:56 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

Unplug is supposed to be the end of the road, so we need to make sure
reset works won't execute while we're cleaning up everything as part
of the unplug, otherwise it would mess up the internal state.

In order to be able to call disable_work_sync() in the unplug
path, we need to defer the unplug triggered by the reset logic,
otherwise we would deadlock.

Fixes: 5fe909cae118 ("drm/panthor: Add the device logical block")
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c | 21 ++++++++++++++++++++-
 drivers/gpu/drm/panthor/panthor_device.h |  3 +++
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index 0b25abebb803..7c55d0c45cfd 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -79,6 +79,9 @@ void panthor_device_unplug(struct panthor_device *ptdev)
 		return;
 	}
 
+	/* Make sure we're not interrupted by resets while we're unplugging. */
+	disable_work_sync(&ptdev->reset.work);
+
 	drm_WARN_ON(&ptdev->base, pm_runtime_get_sync(ptdev->base.dev) < 0);
 
 	/* Call drm_dev_unplug() so any access to HW blocks happening after
@@ -91,6 +94,13 @@ void panthor_device_unplug(struct panthor_device *ptdev)
 	 */
 	mutex_unlock(&ptdev->unplug.lock);
 
+	/* Unplug triggered by a device removal might race with the deferred
+	 * one queued by the reset work. The function covers this concurrent
+	 * unplug situation, but if we can disable the work before its
+	 * execution, that's still better.
+	 */
+	disable_work(&ptdev->unplug.work);
+
 	/* Now, try to cleanly shutdown the GPU before the device resources
 	 * get reclaimed.
 	 */
@@ -114,6 +124,13 @@ void panthor_device_unplug(struct panthor_device *ptdev)
 	complete_all(&ptdev->unplug.done);
 }
 
+static void panthor_device_unplug_work(struct work_struct *work)
+{
+	struct panthor_device *ptdev = container_of(work, struct panthor_device, unplug.work);
+
+	panthor_device_unplug(ptdev);
+}
+
 static void panthor_device_reset_cleanup(struct drm_device *ddev, void *data)
 {
 	struct panthor_device *ptdev = container_of(ddev, struct panthor_device, base);
@@ -148,8 +165,9 @@ static void panthor_device_reset_work(struct work_struct *work)
 	drm_dev_exit(cookie);
 
 	if (ret) {
-		panthor_device_unplug(ptdev);
+		disable_work(&ptdev->reset.work);
 		drm_err(&ptdev->base, "Failed to boot MCU after reset, making device unusable.");
+		queue_work(ptdev->reset.wq, &ptdev->unplug.work);
 	}
 }
 
@@ -206,6 +224,7 @@ int panthor_device_init(struct panthor_device *ptdev)
 	 */
 	*dummy_page_virt = 1;
 
+	INIT_WORK(&ptdev->unplug.work, panthor_device_unplug_work);
 	INIT_WORK(&ptdev->reset.work, panthor_device_reset_work);
 	disable_work(&ptdev->reset.work);
 	ptdev->reset.wq = alloc_ordered_workqueue("panthor-reset-wq", 0);
diff --git a/drivers/gpu/drm/panthor/panthor_device.h b/drivers/gpu/drm/panthor/panthor_device.h
index 0fda64fbe5f2..ea23dde90fea 100644
--- a/drivers/gpu/drm/panthor/panthor_device.h
+++ b/drivers/gpu/drm/panthor/panthor_device.h
@@ -264,6 +264,9 @@ struct panthor_device {
 		 * operation is done.
 		 */
 		struct completion done;
+
+		/** @work: Unplug work. */
+		struct work_struct work;
 	} unplug;
 
 	/** @reset: Reset related fields. */

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 02/17] drm/panthor: Further delay reset work enablement
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
  2026-08-13 10:56 ` [PATCH v3 01/17] drm/panthor: Disable reset work before unplug Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 03/17] drm/panthor: Make sure reset requests in the resume path are not lost Boris Brezillon
                   ` (15 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

There's no point re-enabling the reset work before the DRM device
registration succeeds, so move the enable_work() after the point where
nothing can fail anymore, and in the unlikely event where a reset was
pending, reschedule it.

Fixes: 1b8d771fb214 ("drm/panthor: Keep the reset work disabled until everything is initialized")
Closes: https://sashiko.dev/#/patchset/20260625-panthor-misc-fixes-v1-0-b67ed973fea6@collabora.com?part=2
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index 7c55d0c45cfd..47dc7faf9728 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -305,9 +305,6 @@ int panthor_device_init(struct panthor_device *ptdev)
 
 	panthor_gem_init(ptdev);
 
-	/* Now that everything is initialized, we can enable the reset work. */
-	enable_work(&ptdev->reset.work);
-
 	/* ~3 frames */
 	pm_runtime_set_autosuspend_delay(ptdev->base.dev, 50);
 	pm_runtime_use_autosuspend(ptdev->base.dev);
@@ -316,6 +313,14 @@ int panthor_device_init(struct panthor_device *ptdev)
 	if (ret)
 		goto err_disable_autosuspend;
 
+	/* Now that everything is initialized, we can enable the reset work.
+	 * If there was a reset pending, clear and reschedule, otherwise the
+	 * reset.pending bit is stuck.
+	 */
+	enable_work(&ptdev->reset.work);
+	if (atomic_read(&ptdev->reset.pending))
+		queue_work(ptdev->reset.wq, &ptdev->reset.work);
+
 	pm_runtime_put_autosuspend(ptdev->base.dev);
 	return 0;
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 03/17] drm/panthor: Make sure reset requests in the resume path are not lost
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
  2026-08-13 10:56 ` [PATCH v3 01/17] drm/panthor: Disable reset work before unplug Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 02/17] drm/panthor: Further delay reset work enablement Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:09   ` sashiko-bot
  2026-08-13 10:57 ` [PATCH v3 04/17] drm/panthor: Make sure reset requests in the post reset " Boris Brezillon
                   ` (14 subsequent siblings)
  17 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon,
	sashiko-bot

A reset might have been queued while we were resuming. Make sure it's
not lost by rescheduling it.

Fixes: 5fe909cae118 ("drm/panthor: Add the device logical block")
Reported-by: sashiko-bot@kernel.org
Closes: https://sashiko.dev/#/patchset/20260804-panthor-unplug-fixes-v1-0-abbbd2d41b13@collabora.com?part=2
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index 47dc7faf9728..1a8f5ac24399 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -569,7 +569,24 @@ int panthor_device_resume(struct device *dev)
 	unmap_mapping_range(ptdev->base.anon_inode->i_mapping,
 			    DRM_PANTHOR_USER_MMIO_OFFSET, 0, 1);
 	atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_ACTIVE);
+
+	/* The smp_mb__after_atomic() is here to make sure the pm.state update
+	 * is flushed before we check the reset.pending bit, otherwise,
+	 * according to Sashiko, there's a with the checks that exist in
+	 * panthor_device_schedule_reset().
+	 * Not sure how real this is, because the try_cmpxchg_release() in
+	 * the mutex_unlock() path also acts as a barrier, but it's not a
+	 * fast-path anyway, so better safe than sorry.
+	 */
+	smp_mb__after_atomic();
 	mutex_unlock(&ptdev->pm.mmio_lock);
+
+	/* A reset might have been queued while we were resuming. Make sure
+	 * it's not lost by rescheduling it.
+	 */
+	if (atomic_read(&ptdev->reset.pending))
+		queue_work(ptdev->reset.wq, &ptdev->reset.work);
+
 	return 0;
 
 err_suspend_devfreq:

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 04/17] drm/panthor: Make sure reset requests in the post reset path are not lost
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (2 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 03/17] drm/panthor: Make sure reset requests in the resume path are not lost Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 05/17] drm/panthor: Flush the cleanup_wq in the unplug path Boris Brezillon
                   ` (13 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon,
	sashiko-bot

In theory, there might be MMU/FW faults happening after the FW has
successfully started, and since we clear the reset.pending bit after
panthor_fw_post_reset() has returned, there's a short window during
which a reset request can be ignored.

The other case is a reset condition in other subcomponents that would
not prevent the FW to boot, but given what's currently done in the
post_reset() helpers, I don't see how this can happen. Anyway, it's
probably safer to reset the pending bit just before the SOFT_RESET is
issued, so there's absolutely no timeframe during which a reset event
can be lost. The risk is an infinite reset loop if the reset condition
doesn't prevent the FW to boot, and keeps happening in subsequent resets.

Fixes: 5fe909cae118 ("drm/panthor: Add the device logical block")
Reported-by: sashiko-bot@kernel.org
Closes: https://sashiko.dev/#/patchset/20260804-panthor-unplug-fixes-v1-0-abbbd2d41b13@collabora.com?part=2
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index 1a8f5ac24399..ffaff8c7088e 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -156,11 +156,18 @@ static void panthor_device_reset_work(struct work_struct *work)
 	panthor_sched_pre_reset(ptdev);
 	panthor_fw_pre_reset(ptdev, true);
 	panthor_mmu_pre_reset(ptdev);
+
+	/* Reset the pending bit just before the SOFT_RESET to catch any reset
+	 * condition happening in the post reset path. If we're in such a bad
+	 * state we can't even resume the FW, we will bail out and unplug
+	 * anyway, at which point the reset work is disabled, which should
+	 * prevent an infinite reset loop.
+	 */
+	atomic_set(&ptdev->reset.pending, 0);
 	panthor_hw_soft_reset(ptdev);
 	panthor_hw_l2_power_on(ptdev);
 	panthor_mmu_post_reset(ptdev);
 	ret = panthor_fw_post_reset(ptdev);
-	atomic_set(&ptdev->reset.pending, 0);
 	panthor_sched_post_reset(ptdev, ret != 0);
 	drm_dev_exit(cookie);
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 05/17] drm/panthor: Flush the cleanup_wq in the unplug path
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (3 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 04/17] drm/panthor: Make sure reset requests in the post reset " Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:18   ` sashiko-bot
  2026-08-13 10:57 ` [PATCH v3 06/17] drm/panthor: Drop unused vm argument passed to panthor_vm_prepare_sync_only_op_ctx() Boris Brezillon
                   ` (12 subsequent siblings)
  17 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon,
	sashiko-bot

If we don't do that, we might face various UAFs, because the resource
referenced by these work items might be gone by the time they get
executed.

In each subcomponent making use of the panthor_cleanup_wq, we add a
flush_workqueue() at the end of the _unplug() function. Note that this
assumes no more work items from this subcomponent gets queued after
that point, which is not yet guaranteed, but this will be fixed in
upcoming changes.

Fixes: de8548813824 ("drm/panthor: Add the scheduler logical block")
Fixes: 647810ec2476 ("drm/panthor: Add the MMU/VM logical block")
Reported-by: sashiko-bot@kernel.org
Closes: https://sashiko.dev/#/patchset/20260625-panthor-signal-from-irq-v5-0-8836a74e0ef9@collabora.com?part=2
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_mmu.c   | 6 ++++++
 drivers/gpu/drm/panthor/panthor_sched.c | 6 ++++++
 2 files changed, 12 insertions(+)

diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 0182b72f1932..0b862d3c3605 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -3365,6 +3365,12 @@ void panthor_mmu_unplug(struct panthor_device *ptdev)
 		}
 	}
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
+
+	/* Ensure any pending job cleanup work are executed before returning,
+	 * otherwise those might access objects that are gone if the work is
+	 * executed after other components are unplugged.
+	 */
+	flush_workqueue(panthor_cleanup_wq);
 }
 
 static void panthor_mmu_release_wq(struct drm_device *ddev, void *res)
diff --git a/drivers/gpu/drm/panthor/panthor_sched.c b/drivers/gpu/drm/panthor/panthor_sched.c
index 5832dccfc093..f18b2e03f2fd 100644
--- a/drivers/gpu/drm/panthor/panthor_sched.c
+++ b/drivers/gpu/drm/panthor/panthor_sched.c
@@ -4074,6 +4074,12 @@ void panthor_sched_unplug(struct panthor_device *ptdev)
 		sched->pm.has_ref = false;
 	}
 	mutex_unlock(&sched->lock);
+
+	/* Ensure any pending group release work are executed before returning,
+	 * otherwise those might access objects that are gone if the work is
+	 * executed after other components are unplugged.
+	 */
+	flush_workqueue(panthor_cleanup_wq);
 }
 
 static void panthor_sched_fini(struct drm_device *ddev, void *res)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 06/17] drm/panthor: Drop unused vm argument passed to panthor_vm_prepare_sync_only_op_ctx()
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (4 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 05/17] drm/panthor: Flush the cleanup_wq in the unplug path Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 07/17] drm/panthor: Move the debugfs initialization to panthor_device.c Boris Brezillon
                   ` (11 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

No need to pass a panthor_vm around if it's unused.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_mmu.c | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 0b862d3c3605..6c48e88ad17f 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -1463,8 +1463,8 @@ static int panthor_vm_prepare_unmap_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	return ret;
 }
 
-static void panthor_vm_prepare_sync_only_op_ctx(struct panthor_vm_op_ctx *op_ctx,
-						struct panthor_vm *vm)
+static void
+panthor_vm_prepare_sync_only_op_ctx(struct panthor_vm_op_ctx *op_ctx)
 {
 	memset(op_ctx, 0, sizeof(*op_ctx));
 	op_ctx->flags = DRM_PANTHOR_VM_BIND_OP_TYPE_SYNC_ONLY;
@@ -3026,7 +3026,7 @@ panthor_vm_bind_prepare_op_ctx(struct drm_file *file,
 		if (!op->syncs.count)
 			return -EINVAL;
 
-		panthor_vm_prepare_sync_only_op_ctx(op_ctx, vm);
+		panthor_vm_prepare_sync_only_op_ctx(op_ctx);
 		return 0;
 
 	default:

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 07/17] drm/panthor: Move the debugfs initialization to panthor_device.c
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (5 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 06/17] drm/panthor: Drop unused vm argument passed to panthor_vm_prepare_sync_only_op_ctx() Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 08/17] drm/panthor: Split panthor_vm Boris Brezillon
                   ` (10 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

Those are per-device debugfs-files, so it makes sense to have the
initialization logic in panthor_device.c.

Reviewed-by: Liviu Dudau <liviu.dudau@arm.com>
Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c |  9 +++++++++
 drivers/gpu/drm/panthor/panthor_device.h |  4 ++++
 drivers/gpu/drm/panthor/panthor_drv.c    | 10 +---------
 3 files changed, 14 insertions(+), 9 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index ffaff8c7088e..d4c75ff206bb 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -13,6 +13,7 @@
 #include <linux/reset.h>
 
 #include <drm/drm_drv.h>
+#include <drm/drm_file.h>
 #include <drm/drm_managed.h>
 #include <drm/drm_print.h>
 
@@ -656,3 +657,11 @@ int panthor_device_suspend(struct device *dev)
 	atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_SUSPENDED);
 	return 0;
 }
+
+#ifdef CONFIG_DEBUG_FS
+void panthor_device_debugfs_init(struct drm_minor *minor)
+{
+	panthor_mmu_debugfs_init(minor);
+	panthor_gem_debugfs_init(minor);
+}
+#endif
diff --git a/drivers/gpu/drm/panthor/panthor_device.h b/drivers/gpu/drm/panthor/panthor_device.h
index ea23dde90fea..10c96abf9cff 100644
--- a/drivers/gpu/drm/panthor/panthor_device.h
+++ b/drivers/gpu/drm/panthor/panthor_device.h
@@ -406,6 +406,10 @@ int panthor_device_mmap_io(struct panthor_device *ptdev,
 int panthor_device_resume(struct device *dev);
 int panthor_device_suspend(struct device *dev);
 
+#ifdef CONFIG_DEBUG_FS
+void panthor_device_debugfs_init(struct drm_minor *minor);
+#endif
+
 static inline int panthor_device_resume_and_get(struct panthor_device *ptdev)
 {
 	int ret = pm_runtime_resume_and_get(ptdev->base.dev);
diff --git a/drivers/gpu/drm/panthor/panthor_drv.c b/drivers/gpu/drm/panthor/panthor_drv.c
index 46a3080b0b20..924a7ecd3733 100644
--- a/drivers/gpu/drm/panthor/panthor_drv.c
+++ b/drivers/gpu/drm/panthor/panthor_drv.c
@@ -1764,14 +1764,6 @@ static const struct file_operations panthor_drm_driver_fops = {
 	.fop_flags = FOP_UNSIGNED_OFFSET,
 };
 
-#ifdef CONFIG_DEBUG_FS
-static void panthor_debugfs_init(struct drm_minor *minor)
-{
-	panthor_mmu_debugfs_init(minor);
-	panthor_gem_debugfs_init(minor);
-}
-#endif
-
 /*
  * PanCSF driver version:
  * - 1.0 - initial interface
@@ -1807,7 +1799,7 @@ static const struct drm_driver panthor_drm_driver = {
 	.gem_prime_import_sg_table = panthor_gem_prime_import_sg_table,
 	.gem_prime_import = panthor_gem_prime_import,
 #ifdef CONFIG_DEBUG_FS
-	.debugfs_init = panthor_debugfs_init,
+	.debugfs_init = panthor_device_debugfs_init,
 #endif
 };
 

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 08/17] drm/panthor: Split panthor_vm
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (6 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 07/17] drm/panthor: Move the debugfs initialization to panthor_device.c Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:37   ` sashiko-bot
  2026-08-13 10:57 ` [PATCH v3 09/17] drm/panthor: Add fine-grained restrictions on VMs Boris Brezillon
                   ` (9 subsequent siblings)
  17 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

The way things are currently defined makes the cleanup procedure harder
because the panthor_vm object cleanup happens after drm_gpuvm_fini() has
been called, and sometimes we need a drm_gpuvm to undo things.
This has been worked around by things like the panthor_vm_unmap_range()
call in panthor_vm_destroy(), but there are still situations where this
is problematic, like the show_each_vm() where we walk a list of VM and
call drm_debugfs_gpuva_info() on each, with the risk of hitting an object
that had drm_gpuvm_fini() called on it already.

There's more of these tricky situations to come when we get to making
the unplug logic more robust, so let's address the problem ahead of it
and split the panthor_vm object in two:

- panthor_as: this is the object embedding drm_gpuvm and more
  generally dealing with page table updates/residency
- panthor_vm: this is the user-visible object wrapping around
  panthor_as. Among other things, it contains the scheduler for
  the bind queue and the drm_mm tree for kernel BO allocation.
  This object owns a drm_gpuvm ref.

With this in place, we can do the cleanup steps that need a valid
drm_gpuvm object in panthor_vm_release(), and the rest is cleaned up
in panthor_as_free().

Note that there's a bunch of s/as[_nr]/slot/ variable/argument renames
to clear the confusion between the AS slot number and the newly
introduced panthor_as object.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_mmu.c | 1051 +++++++++++++++++----------------
 1 file changed, 551 insertions(+), 500 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 6c48e88ad17f..041836552953 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -41,14 +41,14 @@
 
 #define MAX_AS_SLOTS			32
 
-struct panthor_vm;
+struct panthor_as;
 
 /**
  * struct panthor_as_slot - Address space slot
  */
 struct panthor_as_slot {
-	/** @vm: VM bound to this slot. NULL is no VM is bound. */
-	struct panthor_vm *vm;
+	/** @as: AS bound to this slot. NULL if no AS is bound. */
+	struct panthor_as *as;
 };
 
 /**
@@ -77,19 +77,19 @@ struct panthor_mmu {
 		/** @as.faulty_mask: Bitmask encoding the faulty slots. */
 		unsigned long faulty_mask;
 
-		/** @as.slots: VMs currently bound to the AS slots. */
+		/** @as.slots: AS currently bound to the AS slots. */
 		struct panthor_as_slot slots[MAX_AS_SLOTS];
 
 		/**
-		 * @as.lru_list: List of least recently used VMs.
+		 * @as.lru_list: List of least recently used AS.
 		 *
-		 * We use this list to pick a VM to evict when all slots are
+		 * We use this list to pick an AS to evict when all slots are
 		 * used.
 		 *
-		 * There should be no more active VMs than there are AS slots,
-		 * so this LRU is just here to keep VMs bound until there's
-		 * a need to release a slot, thus avoid unnecessary TLB/cache
-		 * flushes.
+		 * There should be no more active AS than there are AS slots,
+		 * so this LRU is just here to keep page tables bound until
+		 * there's a need to release a slot, thus avoiding unnecessary
+		 * TLB/cache flushes.
 		 */
 		struct list_head lru_list;
 	} as;
@@ -153,9 +153,9 @@ struct panthor_vma {
 };
 
 /**
- * struct panthor_vm_op_ctx - VM operation context
+ * struct panthor_as_op_ctx - AS operation context
  *
- * With VM operations potentially taking place in a dma-signaling path, we
+ * With AS operations potentially taking place in a dma-signaling path, we
  * need to make sure everything that might require resource allocation is
  * pre-allocated upfront. This is what this operation context is far.
  *
@@ -163,7 +163,7 @@ struct panthor_vma {
  * asynchronously, and let the VM_BIND scheduler process the next VM_BIND
  * request.
  */
-struct panthor_vm_op_ctx {
+struct panthor_as_op_ctx {
 	/** @rsvd_page_tables: Pages reserved for the MMU page table update. */
 	struct {
 		/** @rsvd_page_tables.count: Number of pages reserved. */
@@ -215,13 +215,120 @@ struct panthor_vm_op_ctx {
 	} map;
 };
 
+/**
+ * struct panthor_as - Used to managed a GPU address space.
+ */
+struct panthor_as {
+	/**
+	 * @base: Inherit from drm_gpuvm.
+	 *
+	 * We delegate all the VA management to the common drm_gpuvm framework
+	 * and only implement hooks to update the MMU page table.
+	 */
+	struct drm_gpuvm base;
+
+	/** @memattr: Value to program to the AS_MEMATTR register. */
+	u64 memattr;
+
+	/** @pt: Page table fields. */
+	struct {
+		/** @pt.ops: Page table ops. */
+		struct io_pgtable_ops *ops;
+
+		/** @pt.root: Page table root. */
+		void *root;
+	} pt;
+
+	/**
+	 * @op_lock: Lock used to serialize operations on the AS.
+	 *
+	 * The serialization of jobs queued to the VM_BIND queue is already
+	 * taken care of by drm_sched, but we need to serialize synchronous
+	 * and asynchronous VM_BIND request. This is what this lock is for.
+	 */
+	struct mutex op_lock;
+
+	/**
+	 * @op_ctx: The context attached to the currently executing operation.
+	 *
+	 * NULL when no operation is in progress.
+	 */
+	struct panthor_as_op_ctx *op_ctx;
+
+	/** @active_cnt: Number of active users of this address space. */
+	refcount_t active_cnt;
+
+	/** @hw_slot: Hardware slot related fields. */
+	struct {
+		/**
+		 * @hw_slot.id: ID of the slot this AS is bound to.
+		 *
+		 * A value of -1 means the AS is inactive/not bound.
+		 */
+		int id;
+
+		/**
+		 * @hw_slot.lru_node: Used to insert the AS in panthor_mmu::as::lru_list.
+		 *
+		 * Active ASs should not be inserted in the LRU list.
+		 */
+		struct list_head lru_node;
+	} hw_slot;
+
+	/**
+	 * @unusable: True if the AS has turned unusable because something
+	 * bad happened during an asynchronous request.
+	 *
+	 * We don't try to recover from such failures, because this implies
+	 * informing userspace about the specific operation that failed, and
+	 * hoping the userspace driver can replay things from there. This all
+	 * sounds very complicated for little gain.
+	 *
+	 * Instead, we should just flag the AS as unusable, and fail any
+	 * further request targeting this AS.
+	 *
+	 * We also provide a way to query an AS state, so userspace can
+	 * destroy it and create a new one.
+	 *
+	 * As an analogy, this would be mapped to a VK_ERROR_DEVICE_LOST
+	 * situation, where the logical device needs to be re-created.
+	 */
+	bool unusable;
+
+	/**
+	 * @unhandled_fault: Unhandled fault happened.
+	 *
+	 * This should be reported to the scheduler, and the queue/group be
+	 * flagged as faulty as a result.
+	 */
+	bool unhandled_fault;
+
+	/** @locked_region: Information about the currently locked region currently. */
+	struct {
+		/** @locked_region.start: Start of the locked region. */
+		u64 start;
+
+		/** @locked_region.size: Size of the locked region. */
+		u64 size;
+	} locked_region;
+
+	/** @reclaim: Fields related to BO reclaim. */
+	struct {
+		/** @reclaim.lru: LRU of BOs that are only mapped to this AS. */
+		struct drm_gem_lru lru;
+
+		/**
+		 * @reclaim.lru_node: Node used to insert the AS in
+		 * panthor_device::reclaim::vms.
+		 */
+		struct list_head lru_node;
+	} reclaim;
+};
+
 /**
  * struct panthor_vm - VM object
  *
  * A VM is an object representing a GPU (or MCU) virtual address space.
- * It embeds the MMU page table for this address space, a tree containing
- * all the virtual mappings of GEM objects, and other things needed to manage
- * the VM.
  *
  * Except for the MCU VM, which is managed by the kernel, all other VMs are
  * created by userspace and mostly managed by userspace, using the
@@ -233,13 +340,11 @@ struct panthor_vm_op_ctx {
  * by default).
  */
 struct panthor_vm {
-	/**
-	 * @base: Inherit from drm_gpuvm.
-	 *
-	 * We delegate all the VA management to the common drm_gpuvm framework
-	 * and only implement hooks to update the MMU page table.
-	 */
-	struct drm_gpuvm base;
+	/** @refcount: VM refcount. */
+	struct kref refcount;
+
+	/** @as: VM address space. */
+	struct panthor_as *as;
 
 	/**
 	 * @sched: Scheduler used for asynchronous VM_BIND request.
@@ -256,34 +361,6 @@ struct panthor_vm {
 	 */
 	struct drm_sched_entity entity;
 
-	/** @ptdev: Device. */
-	struct panthor_device *ptdev;
-
-	/** @memattr: Value to program to the AS_MEMATTR register. */
-	u64 memattr;
-
-	/** @pgtbl_ops: Page table operations. */
-	struct io_pgtable_ops *pgtbl_ops;
-
-	/** @root_page_table: Stores the root page table pointer. */
-	void *root_page_table;
-
-	/**
-	 * @op_lock: Lock used to serialize operations on a VM.
-	 *
-	 * The serialization of jobs queued to the VM_BIND queue is already
-	 * taken care of by drm_sched, but we need to serialize synchronous
-	 * and asynchronous VM_BIND request. This is what this lock is for.
-	 */
-	struct mutex op_lock;
-
-	/**
-	 * @op_ctx: The context attached to the currently executing VM operation.
-	 *
-	 * NULL when no operation is in progress.
-	 */
-	struct panthor_vm_op_ctx *op_ctx;
-
 	/**
 	 * @mm: Memory management object representing the auto-VA/kernel-VA.
 	 *
@@ -313,26 +390,6 @@ struct panthor_vm {
 	/** @user_va_range: Upper boundary of VAs VM users can map objects against. */
 	u64 user_va_range;
 
-	/** @as: Address space related fields. */
-	struct {
-		/**
-		 * @as.id: ID of the address space this VM is bound to.
-		 *
-		 * A value of -1 means the VM is inactive/not bound.
-		 */
-		int id;
-
-		/** @as.active_cnt: Number of active users of this VM. */
-		refcount_t active_cnt;
-
-		/**
-		 * @as.lru_node: Used to instead the VM in the panthor_mmu::as::lru_list.
-		 *
-		 * Active VMs should not be inserted in the LRU list.
-		 */
-		struct list_head lru_node;
-	} as;
-
 	/**
 	 * @heaps: Tiler heap related fields.
 	 */
@@ -361,55 +418,6 @@ struct panthor_vm {
 	 */
 	bool destroyed;
 
-	/**
-	 * @unusable: True if the VM has turned unusable because something
-	 * bad happened during an asynchronous request.
-	 *
-	 * We don't try to recover from such failures, because this implies
-	 * informing userspace about the specific operation that failed, and
-	 * hoping the userspace driver can replay things from there. This all
-	 * sounds very complicated for little gain.
-	 *
-	 * Instead, we should just flag the VM as unusable, and fail any
-	 * further request targeting this VM.
-	 *
-	 * We also provide a way to query a VM state, so userspace can destroy
-	 * it and create a new one.
-	 *
-	 * As an analogy, this would be mapped to a VK_ERROR_DEVICE_LOST
-	 * situation, where the logical device needs to be re-created.
-	 */
-	bool unusable;
-
-	/**
-	 * @unhandled_fault: Unhandled fault happened.
-	 *
-	 * This should be reported to the scheduler, and the queue/group be
-	 * flagged as faulty as a result.
-	 */
-	bool unhandled_fault;
-
-	/** @locked_region: Information about the currently locked region currently. */
-	struct {
-		/** @locked_region.start: Start of the locked region. */
-		u64 start;
-
-		/** @locked_region.size: Size of the locked region. */
-		u64 size;
-	} locked_region;
-
-	/** @reclaim: Fields related to BO reclaim. */
-	struct {
-		/** @reclaim.lru: LRU of BOs that are only mapped to this VM. */
-		struct drm_gem_lru lru;
-
-		/**
-		 * @reclaim.lru_node: Node used to insert the VM in
-		 * panthor_device::reclaim::vms.
-		 */
-		struct list_head lru_node;
-	} reclaim;
-
 	/**
 	 * @dummy: Dummy object used for sparse mappings.
 	 *
@@ -437,7 +445,7 @@ struct panthor_vm_bind_job {
 	struct panthor_vm *vm;
 
 	/** @ctx: Operation context. */
-	struct panthor_vm_op_ctx ctx;
+	struct panthor_as_op_ctx ctx;
 };
 
 /*
@@ -466,36 +474,37 @@ static struct kmem_cache *pt_cache;
  */
 static void *alloc_pt(void *cookie, size_t size, gfp_t gfp)
 {
-	struct panthor_vm *vm = cookie;
+	struct panthor_as *as = cookie;
+	struct panthor_as_op_ctx *op_ctx = as->op_ctx;
+	struct drm_device *ddev = as->base.drm;
 	void *page;
 
 	/* Allocation of the root page table happening during init. */
-	if (unlikely(!vm->root_page_table)) {
+	if (unlikely(!as->pt.root)) {
+		struct device *dev = drm_dev_dma_dev(ddev);
 		struct page *p;
 
-		drm_WARN_ON(&vm->ptdev->base, vm->op_ctx);
-		p = alloc_pages_node(dev_to_node(vm->ptdev->base.dev),
-				     gfp | __GFP_ZERO, get_order(size));
+		drm_WARN_ON(ddev, op_ctx);
+		p = alloc_pages_node(dev_to_node(dev), gfp | __GFP_ZERO, get_order(size));
 		page = p ? page_address(p) : NULL;
-		vm->root_page_table = page;
+		as->pt.root = page;
 		return page;
 	}
 
 	/* We're not supposed to have anything bigger than 4k here, because we picked a
 	 * 4k granule size at init time.
 	 */
-	if (drm_WARN_ON(&vm->ptdev->base, size != SZ_4K))
+	if (drm_WARN_ON(ddev, size != SZ_4K))
 		return NULL;
 
 	/* We must have some op_ctx attached to the VM and it must have at least one
 	 * free page.
 	 */
-	if (drm_WARN_ON(&vm->ptdev->base, !vm->op_ctx) ||
-	    drm_WARN_ON(&vm->ptdev->base,
-			vm->op_ctx->rsvd_page_tables.ptr >= vm->op_ctx->rsvd_page_tables.count))
+	if (drm_WARN_ON(ddev, !op_ctx) ||
+	    drm_WARN_ON(ddev, op_ctx->rsvd_page_tables.ptr >= op_ctx->rsvd_page_tables.count))
 		return NULL;
 
-	page = vm->op_ctx->rsvd_page_tables.pages[vm->op_ctx->rsvd_page_tables.ptr++];
+	page = op_ctx->rsvd_page_tables.pages[op_ctx->rsvd_page_tables.ptr++];
 	memset(page, 0, SZ_4K);
 
 	/* Page table entries don't use virtual addresses, which trips out
@@ -518,22 +527,23 @@ static void *alloc_pt(void *cookie, size_t size, gfp_t gfp)
  */
 static void free_pt(void *cookie, void *data, size_t size)
 {
-	struct panthor_vm *vm = cookie;
+	struct panthor_as *as = cookie;
+	struct drm_device *ddev = as->base.drm;
 
-	if (unlikely(vm->root_page_table == data)) {
+	if (unlikely(as->pt.root == data)) {
 		free_pages((unsigned long)data, get_order(size));
-		vm->root_page_table = NULL;
+		as->pt.root = NULL;
 		return;
 	}
 
-	if (drm_WARN_ON(&vm->ptdev->base, size != SZ_4K))
+	if (drm_WARN_ON(ddev, size != SZ_4K))
 		return;
 
 	/* Return the page to the pt_cache. */
 	kmem_cache_free(pt_cache, data);
 }
 
-static int wait_ready(struct panthor_device *ptdev, u32 as_nr)
+static int wait_ready(struct panthor_device *ptdev, u32 slot)
 {
 	struct panthor_mmu *mmu = ptdev->mmu;
 	int ret;
@@ -542,7 +552,7 @@ static int wait_ready(struct panthor_device *ptdev, u32 as_nr)
 	/* Wait for the MMU status to indicate there is no active command, in
 	 * case one is pending.
 	 */
-	ret = gpu_read_relaxed_poll_timeout_atomic(mmu->iomem, AS_STATUS(as_nr), val,
+	ret = gpu_read_relaxed_poll_timeout_atomic(mmu->iomem, AS_STATUS(slot), val,
 						   !(val & AS_STATUS_AS_ACTIVE), 10, 100000);
 
 	if (ret) {
@@ -553,15 +563,15 @@ static int wait_ready(struct panthor_device *ptdev, u32 as_nr)
 	return ret;
 }
 
-static int as_send_cmd_and_wait(struct panthor_device *ptdev, u32 as_nr, u32 cmd)
+static int as_send_cmd_and_wait(struct panthor_device *ptdev, u32 slot, u32 cmd)
 {
 	int status;
 
 	/* write AS_COMMAND when MMU is ready to accept another command */
-	status = wait_ready(ptdev, as_nr);
+	status = wait_ready(ptdev, slot);
 	if (!status) {
-		gpu_write(ptdev->mmu->iomem, AS_COMMAND(as_nr), cmd);
-		status = wait_ready(ptdev, as_nr);
+		gpu_write(ptdev->mmu->iomem, AS_COMMAND(slot), cmd);
+		status = wait_ready(ptdev, slot);
 	}
 
 	return status;
@@ -596,41 +606,41 @@ static u64 pack_region_range(struct panthor_device *ptdev, u64 *region_start, u6
 	return region_width | *region_start;
 }
 
-static u32 panthor_mmu_as_fault_mask(struct panthor_device *ptdev, u32 as)
+static u32 panthor_mmu_as_fault_mask(struct panthor_device *ptdev, u32 slot)
 {
-	return BIT(as);
+	return BIT(slot);
 }
 
 /* Forward declaration to call helpers within as_enable/disable */
 static void panthor_mmu_irq_handler(struct panthor_device *ptdev, u32 status);
 PANTHOR_IRQ_HANDLER(mmu, panthor_mmu_irq_handler);
 
-static int panthor_mmu_as_enable(struct panthor_device *ptdev, u32 as_nr,
+static int panthor_mmu_as_enable(struct panthor_device *ptdev, u32 slot,
 				 u64 transtab, u64 transcfg, u64 memattr)
 {
 	struct panthor_mmu *mmu = ptdev->mmu;
 
 	panthor_mmu_irq_enable_events(&ptdev->mmu->irq,
-				      panthor_mmu_as_fault_mask(ptdev, as_nr));
+				      panthor_mmu_as_fault_mask(ptdev, slot));
 
-	gpu_write64(mmu->iomem, AS_TRANSTAB(as_nr), transtab);
-	gpu_write64(mmu->iomem, AS_MEMATTR(as_nr), memattr);
-	gpu_write64(mmu->iomem, AS_TRANSCFG(as_nr), transcfg);
+	gpu_write64(mmu->iomem, AS_TRANSTAB(slot), transtab);
+	gpu_write64(mmu->iomem, AS_MEMATTR(slot), memattr);
+	gpu_write64(mmu->iomem, AS_TRANSCFG(slot), transcfg);
 
-	return as_send_cmd_and_wait(ptdev, as_nr, AS_COMMAND_UPDATE);
+	return as_send_cmd_and_wait(ptdev, slot, AS_COMMAND_UPDATE);
 }
 
-static int panthor_mmu_as_disable(struct panthor_device *ptdev, u32 as_nr,
+static int panthor_mmu_as_disable(struct panthor_device *ptdev, u32 slot,
 				  bool recycle_slot)
 {
 	struct panthor_mmu *mmu = ptdev->mmu;
-	struct panthor_vm *vm = ptdev->mmu->as.slots[as_nr].vm;
+	struct panthor_as *as = ptdev->mmu->as.slots[slot].as;
 	int ret;
 
 	lockdep_assert_held(&ptdev->mmu->as.slots_lock);
 
 	panthor_mmu_irq_disable_events(&ptdev->mmu->irq,
-				       panthor_mmu_as_fault_mask(ptdev, as_nr));
+				       panthor_mmu_as_fault_mask(ptdev, slot));
 
 	/* Flush+invalidate RW caches, invalidate RO ones. */
 	ret = panthor_gpu_flush_caches(ptdev, CACHE_CLEAN | CACHE_INV,
@@ -638,9 +648,9 @@ static int panthor_mmu_as_disable(struct panthor_device *ptdev, u32 as_nr,
 	if (ret)
 		return ret;
 
-	if (vm && vm->locked_region.size) {
+	if (as && as->locked_region.size) {
 		/* Unlock the region if there's a lock pending. */
-		ret = as_send_cmd_and_wait(ptdev, vm->as.id, AS_COMMAND_UNLOCK);
+		ret = as_send_cmd_and_wait(ptdev, slot, AS_COMMAND_UNLOCK);
 		if (ret)
 			return ret;
 	}
@@ -651,11 +661,11 @@ static int panthor_mmu_as_disable(struct panthor_device *ptdev, u32 as_nr,
 	if (recycle_slot)
 		return 0;
 
-	gpu_write64(mmu->iomem, AS_TRANSTAB(as_nr), 0);
-	gpu_write64(mmu->iomem, AS_MEMATTR(as_nr), 0);
-	gpu_write64(mmu->iomem, AS_TRANSCFG(as_nr), AS_TRANSCFG_ADRMODE_UNMAPPED);
+	gpu_write64(mmu->iomem, AS_TRANSTAB(slot), 0);
+	gpu_write64(mmu->iomem, AS_MEMATTR(slot), 0);
+	gpu_write64(mmu->iomem, AS_TRANSCFG(slot), AS_TRANSCFG_ADRMODE_UNMAPPED);
 
-	return as_send_cmd_and_wait(ptdev, as_nr, AS_COMMAND_UPDATE);
+	return as_send_cmd_and_wait(ptdev, slot, AS_COMMAND_UPDATE);
 }
 
 static u32 panthor_mmu_fault_mask(struct panthor_device *ptdev, u32 value)
@@ -672,7 +682,7 @@ static u32 panthor_mmu_fault_mask(struct panthor_device *ptdev, u32 value)
  */
 bool panthor_vm_has_unhandled_faults(struct panthor_vm *vm)
 {
-	return vm->unhandled_fault;
+	return vm->as->unhandled_fault;
 }
 
 /**
@@ -683,23 +693,23 @@ bool panthor_vm_has_unhandled_faults(struct panthor_vm *vm)
  */
 bool panthor_vm_is_unusable(struct panthor_vm *vm)
 {
-	return vm->unusable;
+	return vm->as->unusable;
 }
 
-static void panthor_vm_release_as_locked(struct panthor_vm *vm)
+static void panthor_as_release_hw_slot_locked(struct panthor_as *as)
 {
-	struct panthor_device *ptdev = vm->ptdev;
+	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 
 	lockdep_assert_held(&ptdev->mmu->as.slots_lock);
 
-	if (drm_WARN_ON(&ptdev->base, vm->as.id < 0))
+	if (drm_WARN_ON(&ptdev->base, as->hw_slot.id < 0))
 		return;
 
-	ptdev->mmu->as.slots[vm->as.id].vm = NULL;
-	clear_bit(vm->as.id, &ptdev->mmu->as.alloc_mask);
-	refcount_set(&vm->as.active_cnt, 0);
-	list_del_init(&vm->as.lru_node);
-	vm->as.id = -1;
+	ptdev->mmu->as.slots[as->hw_slot.id].as = NULL;
+	clear_bit(as->hw_slot.id, &ptdev->mmu->as.alloc_mask);
+	refcount_set(&as->active_cnt, 0);
+	list_del_init(&as->hw_slot.lru_node);
+	as->hw_slot.id = -1;
 }
 
 /**
@@ -712,17 +722,18 @@ static void panthor_vm_release_as_locked(struct panthor_vm *vm)
  */
 int panthor_vm_active(struct panthor_vm *vm)
 {
-	struct panthor_device *ptdev = vm->ptdev;
+	struct panthor_as *as = vm->as;
+	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 	u32 va_bits = GPU_MMU_FEATURES_VA_BITS(ptdev->gpu_info.mmu_features);
-	struct io_pgtable_cfg *cfg = &io_pgtable_ops_to_pgtable(vm->pgtbl_ops)->cfg;
-	int ret = 0, as, cookie;
+	struct io_pgtable_cfg *cfg = &io_pgtable_ops_to_pgtable(as->pt.ops)->cfg;
+	int ret = 0, slot, cookie;
 	u64 transtab, transcfg;
 	u32 fault_mask;
 
 	if (!drm_dev_enter(&ptdev->base, &cookie))
 		return -ENODEV;
 
-	if (refcount_inc_not_zero(&vm->as.active_cnt))
+	if (refcount_inc_not_zero(&as->active_cnt))
 		goto out_dev_exit;
 
 	/* As soon as active is called, we place the VM at the end of the VM LRU.
@@ -731,25 +742,25 @@ int panthor_vm_active(struct panthor_vm *vm)
 	 * that's an acceptable trade-off.
 	 */
 	mutex_lock(&ptdev->base.gem_lru_mutex);
-	if (vm->reclaim.lru.count)
-		list_move_tail(&vm->reclaim.lru_node, &ptdev->reclaim.vms);
+	if (as->reclaim.lru.count)
+		list_move_tail(&as->reclaim.lru_node, &ptdev->reclaim.vms);
 	mutex_unlock(&ptdev->base.gem_lru_mutex);
 
 	/* Make sure we don't race with lock/unlock_region() calls
 	 * happening around VM bind operations.
 	 */
-	mutex_lock(&vm->op_lock);
+	mutex_lock(&as->op_lock);
 	mutex_lock(&ptdev->mmu->as.slots_lock);
 
-	if (refcount_inc_not_zero(&vm->as.active_cnt))
+	if (refcount_inc_not_zero(&as->active_cnt))
 		goto out_unlock;
 
-	as = vm->as.id;
-	if (as >= 0) {
+	slot = as->hw_slot.id;
+	if (slot >= 0) {
 		/* Unhandled pagefault on this AS, the MMU was disabled. We need to
 		 * re-enable the MMU after clearing+unmasking the AS interrupts.
 		 */
-		if (ptdev->mmu->as.faulty_mask & panthor_mmu_as_fault_mask(ptdev, as))
+		if (ptdev->mmu->as.faulty_mask & panthor_mmu_as_fault_mask(ptdev, slot))
 			goto out_enable_as;
 
 		goto out_make_active;
@@ -758,36 +769,36 @@ int panthor_vm_active(struct panthor_vm *vm)
 	/* Check for a free AS */
 	if (vm->for_mcu) {
 		drm_WARN_ON(&ptdev->base, ptdev->mmu->as.alloc_mask & BIT(0));
-		as = 0;
+		slot = 0;
 	} else {
-		as = ffz(ptdev->mmu->as.alloc_mask | BIT(0));
+		slot = ffz(ptdev->mmu->as.alloc_mask | BIT(0));
 	}
 
-	if (!(BIT(as) & ptdev->gpu_info.as_present)) {
-		struct panthor_vm *lru_vm;
+	if (!(BIT(slot) & ptdev->gpu_info.as_present)) {
+		struct panthor_as *lru_as;
 
-		lru_vm = list_first_entry_or_null(&ptdev->mmu->as.lru_list,
-						  struct panthor_vm,
-						  as.lru_node);
-		if (drm_WARN_ON(&ptdev->base, !lru_vm)) {
+		lru_as = list_first_entry_or_null(&ptdev->mmu->as.lru_list,
+						  struct panthor_as,
+						  hw_slot.lru_node);
+		if (drm_WARN_ON(&ptdev->base, !lru_as)) {
 			ret = -EBUSY;
 			goto out_unlock;
 		}
 
-		drm_WARN_ON(&ptdev->base, refcount_read(&lru_vm->as.active_cnt));
-		as = lru_vm->as.id;
+		drm_WARN_ON(&ptdev->base, refcount_read(&lru_as->active_cnt));
+		slot = lru_as->hw_slot.id;
 
-		ret = panthor_mmu_as_disable(ptdev, as, true);
+		ret = panthor_mmu_as_disable(ptdev, slot, true);
 		if (ret)
 			goto out_unlock;
 
-		panthor_vm_release_as_locked(lru_vm);
+		panthor_as_release_hw_slot_locked(lru_as);
 	}
 
 	/* Assign the free or reclaimed AS to the FD */
-	vm->as.id = as;
-	set_bit(as, &ptdev->mmu->as.alloc_mask);
-	ptdev->mmu->as.slots[as].vm = vm;
+	as->hw_slot.id = slot;
+	set_bit(slot, &ptdev->mmu->as.alloc_mask);
+	ptdev->mmu->as.slots[slot].as = as;
 
 out_enable_as:
 	transtab = cfg->arm_lpae_s1_cfg.ttbr;
@@ -799,12 +810,12 @@ int panthor_vm_active(struct panthor_vm *vm)
 		transcfg |= AS_TRANSCFG_PTW_SH_OS;
 
 	/* If the VM is re-activated, we clear the fault. */
-	vm->unhandled_fault = false;
+	as->unhandled_fault = false;
 
 	/* Unhandled pagefault on this AS, clear the fault and enable the AS,
 	 * which re-enables interrupts.
 	 */
-	fault_mask = panthor_mmu_as_fault_mask(ptdev, as);
+	fault_mask = panthor_mmu_as_fault_mask(ptdev, slot);
 	if (ptdev->mmu->as.faulty_mask & fault_mask) {
 		gpu_write(ptdev->mmu->irq.iomem, INT_CLEAR, fault_mask);
 		ptdev->mmu->as.faulty_mask &= ~fault_mask;
@@ -813,18 +824,18 @@ int panthor_vm_active(struct panthor_vm *vm)
 	/* The VM update is guarded by ::op_lock, which we take at the beginning
 	 * of this function, so we don't expect any locked region here.
 	 */
-	drm_WARN_ON(&vm->ptdev->base, vm->locked_region.size > 0);
-	ret = panthor_mmu_as_enable(vm->ptdev, vm->as.id, transtab, transcfg, vm->memattr);
+	drm_WARN_ON(&ptdev->base, as->locked_region.size > 0);
+	ret = panthor_mmu_as_enable(ptdev, as->hw_slot.id, transtab, transcfg, as->memattr);
 
 out_make_active:
 	if (!ret) {
-		refcount_set(&vm->as.active_cnt, 1);
-		list_del_init(&vm->as.lru_node);
+		refcount_set(&as->active_cnt, 1);
+		list_del_init(&as->hw_slot.lru_node);
 	}
 
 out_unlock:
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
-	mutex_unlock(&vm->op_lock);
+	mutex_unlock(&as->op_lock);
 
 out_dev_exit:
 	drm_dev_exit(cookie);
@@ -846,21 +857,22 @@ int panthor_vm_active(struct panthor_vm *vm)
  */
 void panthor_vm_idle(struct panthor_vm *vm)
 {
-	struct panthor_device *ptdev = vm->ptdev;
+	struct panthor_as *as = vm->as;
+	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 
-	if (!refcount_dec_and_mutex_lock(&vm->as.active_cnt, &ptdev->mmu->as.slots_lock))
+	if (!refcount_dec_and_mutex_lock(&as->active_cnt, &ptdev->mmu->as.slots_lock))
 		return;
 
-	if (!drm_WARN_ON(&ptdev->base, vm->as.id == -1 || !list_empty(&vm->as.lru_node)))
-		list_add_tail(&vm->as.lru_node, &ptdev->mmu->as.lru_list);
+	if (!drm_WARN_ON(&ptdev->base, as->hw_slot.id == -1 || !list_empty(&as->hw_slot.lru_node)))
+		list_add_tail(&as->hw_slot.lru_node, &ptdev->mmu->as.lru_list);
 
-	refcount_set(&vm->as.active_cnt, 0);
+	refcount_set(&as->active_cnt, 0);
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
 }
 
 u32 panthor_vm_page_size(struct panthor_vm *vm)
 {
-	const struct io_pgtable *pgt = io_pgtable_ops_to_pgtable(vm->pgtbl_ops);
+	const struct io_pgtable *pgt = io_pgtable_ops_to_pgtable(vm->as->pt.ops);
 	u32 pg_shift = ffs(pgt->cfg.pgsize_bitmap) - 1;
 
 	return 1u << pg_shift;
@@ -884,7 +896,7 @@ static void panthor_vm_start(struct panthor_vm *vm)
  */
 int panthor_vm_as(struct panthor_vm *vm)
 {
-	return vm->as.id;
+	return vm->as->hw_slot.id;
 }
 
 static size_t get_pgsize(u64 addr, size_t size, size_t *count)
@@ -908,43 +920,43 @@ static size_t get_pgsize(u64 addr, size_t size, size_t *count)
 	return SZ_2M;
 }
 
-static void panthor_vm_declare_unusable(struct panthor_vm *vm)
+static void panthor_as_declare_unusable(struct panthor_as *as)
 {
-	struct panthor_device *ptdev = vm->ptdev;
+	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 	int cookie;
 
-	if (vm->unusable)
+	if (as->unusable)
 		return;
 
-	vm->unusable = true;
+	as->unusable = true;
 	mutex_lock(&ptdev->mmu->as.slots_lock);
-	if (vm->as.id >= 0 && drm_dev_enter(&ptdev->base, &cookie)) {
-		panthor_mmu_as_disable(ptdev, vm->as.id, false);
+	if (as->hw_slot.id >= 0 && drm_dev_enter(&ptdev->base, &cookie)) {
+		panthor_mmu_as_disable(ptdev, as->hw_slot.id, false);
 		drm_dev_exit(cookie);
 	}
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
 }
 
-static void panthor_vm_unmap_pages(struct panthor_vm *vm, u64 iova, u64 size)
+static void panthor_as_unmap_pages(struct panthor_as *as, u64 iova, u64 size)
 {
-	struct panthor_device *ptdev = vm->ptdev;
-	struct io_pgtable_ops *ops = vm->pgtbl_ops;
+	struct drm_device *ddev = as->base.drm;
+	struct io_pgtable_ops *ops = as->pt.ops;
 	u64 start_iova = iova;
 	u64 offset = 0;
 
 	if (!size)
 		return;
 
-	drm_WARN_ON(&ptdev->base,
-		    (iova < vm->locked_region.start) ||
-		    (iova + size > vm->locked_region.start + vm->locked_region.size));
+	drm_WARN_ON(ddev,
+		    (iova < as->locked_region.start) ||
+		    (iova + size > as->locked_region.start + as->locked_region.size));
 
 	while (offset < size) {
 		size_t unmapped_sz = 0, pgcount;
 		size_t pgsize = get_pgsize(iova + offset, size - offset, &pgcount);
 
 		unmapped_sz = ops->unmap_pages(ops, iova + offset, pgsize, pgcount, NULL);
-		if (drm_WARN_ON_ONCE(&ptdev->base, unmapped_sz != pgsize * pgcount)) {
+		if (drm_WARN_ON_ONCE(ddev, unmapped_sz != pgsize * pgcount)) {
 			/* Gracefully handle sparsely unmapped regions to avoid leaving
 			 * page table pages behind when the drm_gpuvm and VM page table
 			 * are out-of-sync. This is not supposed to happen, hence the
@@ -958,33 +970,32 @@ static void panthor_vm_unmap_pages(struct panthor_vm *vm, u64 iova, u64 size)
 			 * so flag the VM unusable to make sure it's not going
 			 * to be used anymore.
 			 */
-			panthor_vm_declare_unusable(vm);
+			panthor_as_declare_unusable(as);
 
 			/* If we don't make progress, we're screwed. That also means
 			 * something else prevents us from unmapping the region, but
 			 * there's not much we can do here: time for debugging.
 			 */
-			if (drm_WARN_ON_ONCE(&ptdev->base, !unmapped_sz))
+			if (drm_WARN_ON_ONCE(ddev, !unmapped_sz))
 				return;
 		}
 
-		drm_dbg(&ptdev->base,
-			"unmap: as=%d, iova=0x%llx, sz=%llu, va=0x%llx, pgcnt=%zu, pgsz=%zu",
-			vm->as.id, start_iova, size, iova + offset,
-			unmapped_sz / pgsize, pgsize);
+		drm_dbg(ddev,
+			"unmap: iova=0x%llx, sz=%llu, va=0x%llx, pgcnt=%zu, pgsz=%zu",
+			start_iova, size, iova + offset, unmapped_sz / pgsize, pgsize);
 
 		offset += unmapped_sz;
 	}
 }
 
 static int
-panthor_vm_map_pages(struct panthor_vm *vm, u64 iova, int prot,
+panthor_as_map_pages(struct panthor_as *as, u64 iova, int prot,
 		     struct sg_table *sgt, u64 offset, u64 size)
 {
-	struct panthor_device *ptdev = vm->ptdev;
+	struct drm_device *ddev = as->base.drm;
 	unsigned int count;
 	struct scatterlist *sgl;
-	struct io_pgtable_ops *ops = vm->pgtbl_ops;
+	struct io_pgtable_ops *ops = as->pt.ops;
 	u64 start_iova = iova;
 	u64 start_size = size;
 	int ret;
@@ -992,9 +1003,9 @@ panthor_vm_map_pages(struct panthor_vm *vm, u64 iova, int prot,
 	if (!size)
 		return 0;
 
-	drm_WARN_ON(&ptdev->base,
-		    (iova < vm->locked_region.start) ||
-		    (iova + size > vm->locked_region.start + vm->locked_region.size));
+	drm_WARN_ON(ddev,
+		    (iova < as->locked_region.start) ||
+		    (iova + size > as->locked_region.start + as->locked_region.size));
 
 	for_each_sgtable_dma_sg(sgt, sgl, count) {
 		dma_addr_t paddr = sg_dma_address(sgl);
@@ -1017,10 +1028,9 @@ panthor_vm_map_pages(struct panthor_vm *vm, u64 iova, int prot,
 			ret = ops->map_pages(ops, iova, paddr, pgsize, pgcount, prot,
 					     GFP_KERNEL, &mapped);
 
-			drm_dbg(&ptdev->base,
-				"map: as=%d, iova=0x%llx, sz=%llu, va=0x%llx, pa=%pad, pgcnt=%zu, pgsz=%zu",
-				vm->as.id, start_iova, start_size, iova, &paddr,
-				mapped / pgsize, pgsize);
+			drm_dbg(ddev,
+				"map: iova=0x%llx, sz=%llu, va=0x%llx, pa=%pad, pgcnt=%zu, pgsz=%zu",
+				start_iova, start_size, iova, &paddr, mapped / pgsize, pgsize);
 
 			iova += mapped;
 			paddr += mapped;
@@ -1031,12 +1041,12 @@ panthor_vm_map_pages(struct panthor_vm *vm, u64 iova, int prot,
 				ret = -ENOMEM;
 
 			/* If something fails, we stop there, and flag the VM unusable. */
-			if (drm_WARN_ON_ONCE(&ptdev->base, ret)) {
+			if (drm_WARN_ON_ONCE(ddev, ret)) {
 				/* Unmap what we've already mapped to avoid leaving page
 				 * table pages behind.
 				 */
-				panthor_vm_unmap_pages(vm, start_iova, iova - start_iova);
-				panthor_vm_declare_unusable(vm);
+				panthor_as_unmap_pages(as, start_iova, iova - start_iova);
+				panthor_as_declare_unusable(as);
 				return ret;
 			}
 		}
@@ -1051,8 +1061,8 @@ panthor_vm_map_pages(struct panthor_vm *vm, u64 iova, int prot,
 }
 
 static int
-panthor_vm_map_sparse(struct panthor_vm *vm, u64 iova, int prot,
-		      struct sg_table *sgt, u64 size)
+panthor_as_map_sparse(struct panthor_as *as, u64 iova,
+		      int prot, struct sg_table *sgt, u64 size)
 {
 	u64 mapped = 0;
 	int ret;
@@ -1061,10 +1071,10 @@ panthor_vm_map_sparse(struct panthor_vm *vm, u64 iova, int prot,
 		u64 addr = iova + mapped;
 		u32 chunk_size = min(size - mapped, SZ_2M - (addr & (SZ_2M - 1)));
 
-		ret = panthor_vm_map_pages(vm, addr, prot, sgt,
+		ret = panthor_as_map_pages(as, addr, prot, sgt,
 					   addr % SZ_2M, chunk_size);
 		if (ret) {
-			panthor_vm_unmap_pages(vm, iova, mapped);
+			panthor_as_unmap_pages(as, iova, mapped);
 			return ret;
 		}
 
@@ -1166,8 +1176,8 @@ static void panthor_vm_bo_free(struct drm_gpuvm_bo *vm_bo)
 	kfree(vm_bo);
 }
 
-static void panthor_vm_cleanup_op_ctx(struct panthor_vm_op_ctx *op_ctx,
-				      struct panthor_vm *vm)
+static void panthor_as_cleanup_op_ctx(struct panthor_as_op_ctx *op_ctx,
+				      struct panthor_as *as)
 {
 	u32 remaining_pt_count = op_ctx->rsvd_page_tables.count -
 				 op_ctx->rsvd_page_tables.ptr;
@@ -1202,11 +1212,11 @@ static void panthor_vm_cleanup_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 		kfree(op_ctx->preallocated_vmas[i]);
 
 	if (!skip_deferred_cleanup)
-		drm_gpuvm_bo_deferred_cleanup(&vm->base);
+		drm_gpuvm_bo_deferred_cleanup(&as->base);
 }
 
 static void
-panthor_vm_op_ctx_return_vma(struct panthor_vm_op_ctx *op_ctx,
+panthor_as_op_ctx_return_vma(struct panthor_as_op_ctx *op_ctx,
 			     struct panthor_vma *vma)
 {
 	for (u32 i = 0; i < ARRAY_SIZE(op_ctx->preallocated_vmas); i++) {
@@ -1220,7 +1230,7 @@ panthor_vm_op_ctx_return_vma(struct panthor_vm_op_ctx *op_ctx,
 }
 
 static struct panthor_vma *
-panthor_vm_op_ctx_get_vma(struct panthor_vm_op_ctx *op_ctx)
+panthor_as_op_ctx_get_vma(struct panthor_as_op_ctx *op_ctx)
 {
 	for (u32 i = 0; i < ARRAY_SIZE(op_ctx->preallocated_vmas); i++) {
 		struct panthor_vma *vma = op_ctx->preallocated_vmas[i];
@@ -1235,7 +1245,7 @@ panthor_vm_op_ctx_get_vma(struct panthor_vm_op_ctx *op_ctx)
 }
 
 static int
-panthor_vm_op_ctx_prealloc_vmas(struct panthor_vm_op_ctx *op_ctx)
+panthor_as_op_ctx_prealloc_vmas(struct panthor_as_op_ctx *op_ctx)
 {
 	u32 vma_count;
 
@@ -1274,7 +1284,7 @@ panthor_vm_op_ctx_prealloc_vmas(struct panthor_vm_op_ctx *op_ctx)
 	return 0;
 }
 
-static void panthor_vm_init_op_ctx(struct panthor_vm_op_ctx *op_ctx,
+static void panthor_vm_init_op_ctx(struct panthor_as_op_ctx *op_ctx,
 				   u64 size, u64 va, u32 flags)
 {
 	memset(op_ctx, 0, sizeof(*op_ctx));
@@ -1283,7 +1293,7 @@ static void panthor_vm_init_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	op_ctx->va.addr = va;
 }
 
-static int panthor_vm_op_ctx_prealloc_pts(struct panthor_vm_op_ctx *op_ctx)
+static int panthor_as_op_ctx_prealloc_pts(struct panthor_as_op_ctx *op_ctx)
 {
 	u64 size = op_ctx->va.range;
 	u64 va = op_ctx->va.addr;
@@ -1319,8 +1329,8 @@ static int panthor_vm_op_ctx_prealloc_pts(struct panthor_vm_op_ctx *op_ctx)
 	 DRM_PANTHOR_VM_BIND_OP_MAP_SPARSE | \
 	 DRM_PANTHOR_VM_BIND_OP_TYPE_MASK)
 
-static int panthor_vm_prepare_map_op_ctx(struct panthor_vm_op_ctx *op_ctx,
-					 struct panthor_vm *vm,
+static int panthor_as_prepare_map_op_ctx(struct panthor_as_op_ctx *op_ctx,
+					 struct panthor_as *as,
 					 struct panthor_gem_object *bo,
 					 const struct drm_panthor_vm_bind_op *op)
 {
@@ -1355,12 +1365,12 @@ static int panthor_vm_prepare_map_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 
 	/* If the BO has an exclusive VM attached, it can't be mapped to other VMs. */
 	if (bo->exclusive_vm_root_gem &&
-	    bo->exclusive_vm_root_gem != panthor_vm_root_gem(vm))
+	    bo->exclusive_vm_root_gem != as->base.r_obj)
 		return -EINVAL;
 
 	panthor_vm_init_op_ctx(op_ctx, op->size, op->va, op->flags);
 
-	ret = panthor_vm_op_ctx_prealloc_vmas(op_ctx);
+	ret = panthor_as_op_ctx_prealloc_vmas(op_ctx);
 	if (ret)
 		goto err_cleanup;
 
@@ -1380,7 +1390,7 @@ static int panthor_vm_prepare_map_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 		goto err_cleanup;
 	}
 
-	preallocated_vm_bo = drm_gpuvm_bo_create(&vm->base, &bo->base);
+	preallocated_vm_bo = drm_gpuvm_bo_create(&as->base, &bo->base);
 	if (!preallocated_vm_bo) {
 		ret = -ENOMEM;
 		goto err_cleanup;
@@ -1389,15 +1399,15 @@ static int panthor_vm_prepare_map_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	op_ctx->map.vm_bo = drm_gpuvm_bo_obtain_prealloc(preallocated_vm_bo);
 	op_ctx->map.bo_offset = op->bo_offset;
 
-	ret = panthor_vm_op_ctx_prealloc_pts(op_ctx);
+	ret = panthor_as_op_ctx_prealloc_pts(op_ctx);
 	if (ret)
 		goto err_cleanup;
 
 	/* Insert BO into the extobj list last, when we know nothing can fail. */
-	if (bo->base.resv != panthor_vm_resv(vm)) {
-		dma_resv_lock(panthor_vm_resv(vm), NULL);
+	if (bo->base.resv != drm_gpuvm_resv(&as->base)) {
+		dma_resv_lock(drm_gpuvm_resv(&as->base), NULL);
 		drm_gpuvm_bo_extobj_add(op_ctx->map.vm_bo);
-		dma_resv_unlock(panthor_vm_resv(vm));
+		dma_resv_unlock(drm_gpuvm_resv(&as->base));
 	}
 
 	/* And finally update the BO state. */
@@ -1410,12 +1420,12 @@ static int panthor_vm_prepare_map_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	return 0;
 
 err_cleanup:
-	panthor_vm_cleanup_op_ctx(op_ctx, vm);
+	panthor_as_cleanup_op_ctx(op_ctx, as);
 	return ret;
 }
 
-static int panthor_vm_prepare_unmap_op_ctx(struct panthor_vm_op_ctx *op_ctx,
-					   struct panthor_vm *vm,
+static int panthor_as_prepare_unmap_op_ctx(struct panthor_as_op_ctx *op_ctx,
+					   struct panthor_as *as,
 					   u64 va, u64 size)
 {
 	u32 pt_count = 0;
@@ -1436,7 +1446,7 @@ static int panthor_vm_prepare_unmap_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	    ALIGN(va + size, SZ_2M) != ALIGN(va, SZ_2M))
 		pt_count++;
 
-	ret = panthor_vm_op_ctx_prealloc_vmas(op_ctx);
+	ret = panthor_as_op_ctx_prealloc_vmas(op_ctx);
 	if (ret)
 		goto err_cleanup;
 
@@ -1459,12 +1469,12 @@ static int panthor_vm_prepare_unmap_op_ctx(struct panthor_vm_op_ctx *op_ctx,
 	return 0;
 
 err_cleanup:
-	panthor_vm_cleanup_op_ctx(op_ctx, vm);
+	panthor_as_cleanup_op_ctx(op_ctx, as);
 	return ret;
 }
 
 static void
-panthor_vm_prepare_sync_only_op_ctx(struct panthor_vm_op_ctx *op_ctx)
+panthor_as_prepare_sync_only_op_ctx(struct panthor_as_op_ctx *op_ctx)
 {
 	memset(op_ctx, 0, sizeof(*op_ctx));
 	op_ctx->flags = DRM_PANTHOR_VM_BIND_OP_TYPE_SYNC_ONLY;
@@ -1492,8 +1502,8 @@ panthor_vm_get_bo_for_va(struct panthor_vm *vm, u64 va, u64 *bo_offset)
 	struct panthor_vma *vma;
 
 	/* Take the VM lock to prevent concurrent map/unmap operations. */
-	mutex_lock(&vm->op_lock);
-	gpuva = drm_gpuva_find_first(&vm->base, va, 1);
+	mutex_lock(&vm->as->op_lock);
+	gpuva = drm_gpuva_find_first(&vm->as->base, va, 1);
 	vma = gpuva ? container_of(gpuva, struct panthor_vma, base) : NULL;
 	if (vma && vma->base.gem.obj) {
 		drm_gem_object_get(vma->base.gem.obj);
@@ -1502,7 +1512,7 @@ panthor_vm_get_bo_for_va(struct panthor_vm *vm, u64 va, u64 *bo_offset)
 			vma->base.gem.offset + (va - vma->base.va.addr) :
 			va & (SZ_2M - 1);
 	}
-	mutex_unlock(&vm->op_lock);
+	mutex_unlock(&vm->as->op_lock);
 
 	return bo;
 }
@@ -1622,22 +1632,24 @@ int panthor_vm_pool_create_vm(struct panthor_device *ptdev,
 
 static void panthor_vm_destroy(struct panthor_vm *vm)
 {
+	struct panthor_as *as;
+	struct panthor_device *ptdev;
+
 	if (!vm)
 		return;
 
+	as = vm->as;
+	ptdev = container_of(as->base.drm, struct panthor_device, base);
 	vm->destroyed = true;
 
 	/* Tell scheduler to stop all GPU work related to this VM */
-	if (refcount_read(&vm->as.active_cnt) > 0)
-		panthor_sched_prepare_for_vm_destruction(vm->ptdev);
+	if (refcount_read(&as->active_cnt) > 0)
+		panthor_sched_prepare_for_vm_destruction(ptdev);
 
 	mutex_lock(&vm->heaps.lock);
 	panthor_heap_pool_destroy(vm->heaps.pool);
 	vm->heaps.pool = NULL;
 	mutex_unlock(&vm->heaps.lock);
-
-	drm_WARN_ON(&vm->ptdev->base,
-		    panthor_vm_unmap_range(vm, vm->base.mm_start, vm->base.mm_range));
 	panthor_vm_put(vm);
 }
 
@@ -1777,17 +1789,18 @@ static const char *access_type_name(struct panthor_device *ptdev,
 	}
 }
 
-static int panthor_vm_lock_region(struct panthor_vm *vm, u64 start, u64 size)
+static int panthor_as_lock_region(struct panthor_as *as, u64 start, u64 size)
 {
-	struct panthor_device *ptdev = vm->ptdev;
+	struct drm_device *ddev = as->base.drm;
+	struct panthor_device *ptdev = container_of(ddev, struct panthor_device, base);
 	int ret = 0;
 
-	/* sm_step_remap() can call panthor_vm_lock_region() to account for
+	/* sm_step_remap() can call panthor_as_lock_region() to account for
 	 * the wider unmap needed when doing a partial huge page unamp. We
 	 * need to ignore the lock if it's already part of the locked region.
 	 */
-	if (start >= vm->locked_region.start &&
-	    start + size <= vm->locked_region.start + vm->locked_region.size)
+	if (start >= as->locked_region.start &&
+	    start + size <= as->locked_region.start + as->locked_region.size)
 		return 0;
 
 	/* sm_step_remap() may need a locked region that isn't a strict superset
@@ -1798,42 +1811,42 @@ static int panthor_vm_lock_region(struct panthor_vm *vm, u64 start, u64 size)
 	 * boundaries in a remap operation can only shift up or down respectively,
 	 * but never otherwise.
 	 */
-	if (vm->locked_region.size) {
-		u64 end = max(vm->locked_region.start + vm->locked_region.size,
+	if (as->locked_region.size) {
+		u64 end = max(as->locked_region.start + as->locked_region.size,
 			      start + size);
 
-		drm_WARN_ON_ONCE(&vm->ptdev->base, (start + size <= vm->locked_region.start) ||
-				 (start >= vm->locked_region.start + vm->locked_region.size));
+		drm_WARN_ON_ONCE(ddev, (start + size <= as->locked_region.start) ||
+				 (start >= as->locked_region.start + as->locked_region.size));
 
-		start = min(start, vm->locked_region.start);
+		start = min(start, as->locked_region.start);
 		size = end - start;
 	}
 
 	mutex_lock(&ptdev->mmu->as.slots_lock);
-	if (vm->as.id >= 0 && size) {
+	if (as->hw_slot.id >= 0 && size) {
 		/* Lock the region that needs to be updated */
-		gpu_write64(ptdev->mmu->iomem, AS_LOCKADDR(vm->as.id),
+		gpu_write64(ptdev->mmu->iomem, AS_LOCKADDR(as->hw_slot.id),
 			    pack_region_range(ptdev, &start, &size));
 
 		/* If the lock succeeded, update the locked_region info. */
-		ret = as_send_cmd_and_wait(ptdev, vm->as.id, AS_COMMAND_LOCK);
+		ret = as_send_cmd_and_wait(ptdev, as->hw_slot.id, AS_COMMAND_LOCK);
 	}
 
 	if (!ret) {
-		vm->locked_region.start = start;
-		vm->locked_region.size = size;
+		as->locked_region.start = start;
+		as->locked_region.size = size;
 	}
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
 
 	return ret;
 }
 
-static void panthor_vm_unlock_region(struct panthor_vm *vm)
+static void panthor_as_unlock_region(struct panthor_as *as)
 {
-	struct panthor_device *ptdev = vm->ptdev;
+	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 
 	mutex_lock(&ptdev->mmu->as.slots_lock);
-	if (vm->as.id >= 0) {
+	if (as->hw_slot.id >= 0) {
 		int ret;
 
 		/* flush+invalidate RW caches and invalidate RO ones.
@@ -1846,7 +1859,7 @@ static void panthor_vm_unlock_region(struct panthor_vm *vm)
 
 		/* Unlock the region if the flush is effective. */
 		if (!ret)
-			ret = as_send_cmd_and_wait(ptdev, vm->as.id, AS_COMMAND_UNLOCK);
+			ret = as_send_cmd_and_wait(ptdev, as->hw_slot.id, AS_COMMAND_UNLOCK);
 
 		/* If we fail to flush or unlock the region, schedule a GPU reset
 		 * to unblock the situation.
@@ -1854,8 +1867,8 @@ static void panthor_vm_unlock_region(struct panthor_vm *vm)
 		if (ret)
 			panthor_device_schedule_reset(ptdev);
 	}
-	vm->locked_region.start = 0;
-	vm->locked_region.size = 0;
+	as->locked_region.start = 0;
+	as->locked_region.size = 0;
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
 }
 
@@ -1908,8 +1921,8 @@ static void panthor_mmu_irq_handler(struct panthor_device *ptdev, u32 status)
 		 */
 		gpu_write(mmu->irq.iomem, INT_CLEAR, mask);
 
-		if (ptdev->mmu->as.slots[as].vm)
-			ptdev->mmu->as.slots[as].vm->unhandled_fault = true;
+		if (ptdev->mmu->as.slots[as].as)
+			ptdev->mmu->as.slots[as].as->unhandled_fault = true;
 
 		/* Disable the MMU to kill jobs on this AS. */
 		panthor_mmu_as_disable(ptdev, as, false);
@@ -1937,12 +1950,12 @@ void panthor_mmu_suspend(struct panthor_device *ptdev)
 {
 	mutex_lock(&ptdev->mmu->as.slots_lock);
 	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
-		struct panthor_vm *vm = ptdev->mmu->as.slots[i].vm;
+		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
 
-		if (vm) {
+		if (as) {
 			drm_WARN_ON(&ptdev->base,
 				    panthor_mmu_as_disable(ptdev, i, false));
-			panthor_vm_release_as_locked(vm);
+			panthor_as_release_hw_slot_locked(as);
 		}
 	}
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
@@ -2012,10 +2025,10 @@ void panthor_mmu_post_reset(struct panthor_device *ptdev)
 	ptdev->mmu->as.faulty_mask = 0;
 
 	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
-		struct panthor_vm *vm = ptdev->mmu->as.slots[i].vm;
+		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
 
-		if (vm)
-			panthor_vm_release_as_locked(vm);
+		if (as)
+			panthor_as_release_hw_slot_locked(as);
 	}
 
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
@@ -2031,15 +2044,23 @@ void panthor_mmu_post_reset(struct panthor_device *ptdev)
 	mutex_unlock(&ptdev->mmu->vm.lock);
 }
 
-static void panthor_vm_free(struct drm_gpuvm *gpuvm)
+static void panthor_vm_release(struct kref *kref)
 {
-	struct panthor_vm *vm = container_of(gpuvm, struct panthor_vm, base);
-	struct panthor_device *ptdev = vm->ptdev;
+	struct panthor_vm *vm = container_of(kref, struct panthor_vm, refcount);
+	struct panthor_as *as = vm->as;
+	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 
+	/* Make sure the page table behind this VM doesn't participate in reclaim
+	 * after that point, since we're about to release everything anyway.
+	 */
 	mutex_lock(&ptdev->base.gem_lru_mutex);
-	list_del_init(&vm->reclaim.lru_node);
+	list_del_init(&as->reclaim.lru_node);
 	mutex_unlock(&ptdev->base.gem_lru_mutex);
 
+	/* Unmap everything in case some BOs were still mapped. */
+	drm_WARN_ON(&ptdev->base,
+		    panthor_vm_unmap_range(vm, as->base.mm_start, as->base.mm_range));
+
 	mutex_lock(&vm->heaps.lock);
 	if (drm_WARN_ON(&ptdev->base, vm->heaps.pool))
 		panthor_heap_pool_destroy(vm->heaps.pool);
@@ -2060,29 +2081,26 @@ static void panthor_vm_free(struct drm_gpuvm *gpuvm)
 	drm_sched_entity_destroy(&vm->entity);
 	drm_sched_fini(&vm->sched);
 
-	mutex_lock(&vm->op_lock);
+	mutex_lock(&vm->as->op_lock);
 	mutex_lock(&ptdev->mmu->as.slots_lock);
-	if (vm->as.id >= 0) {
+	if (as->hw_slot.id >= 0) {
 		int cookie;
 
 		if (drm_dev_enter(&ptdev->base, &cookie)) {
-			panthor_mmu_as_disable(ptdev, vm->as.id, false);
+			panthor_mmu_as_disable(ptdev, as->hw_slot.id, false);
 			drm_dev_exit(cookie);
 		}
 
-		ptdev->mmu->as.slots[vm->as.id].vm = NULL;
-		clear_bit(vm->as.id, &ptdev->mmu->as.alloc_mask);
-		list_del(&vm->as.lru_node);
+		panthor_as_release_hw_slot_locked(as);
 	}
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
-	mutex_unlock(&vm->op_lock);
-
-	free_io_pgtable_ops(vm->pgtbl_ops);
+	mutex_unlock(&vm->as->op_lock);
 
 	if (vm->dummy)
 		drm_gem_object_put(&vm->dummy->base);
 
 	drm_mm_takedown(&vm->mm);
+	drm_gpuvm_put(&as->base);
 	kfree(vm);
 }
 
@@ -2092,7 +2110,8 @@ static void panthor_vm_free(struct drm_gpuvm *gpuvm)
  */
 void panthor_vm_put(struct panthor_vm *vm)
 {
-	drm_gpuvm_put(vm ? &vm->base : NULL);
+	if (vm)
+		kref_put(&vm->refcount, panthor_vm_release);
 }
 
 /**
@@ -2104,7 +2123,7 @@ void panthor_vm_put(struct panthor_vm *vm)
 struct panthor_vm *panthor_vm_get(struct panthor_vm *vm)
 {
 	if (vm)
-		drm_gpuvm_get(&vm->base);
+		kref_get(&vm->refcount);
 
 	return vm;
 }
@@ -2125,6 +2144,8 @@ struct panthor_vm *panthor_vm_get(struct panthor_vm *vm)
  */
 struct panthor_heap_pool *panthor_vm_get_heap_pool(struct panthor_vm *vm, bool create)
 {
+	struct panthor_device *ptdev = container_of(vm->as->base.drm,
+						    struct panthor_device, base);
 	struct panthor_heap_pool *pool;
 
 	mutex_lock(&vm->heaps.lock);
@@ -2132,7 +2153,7 @@ struct panthor_heap_pool *panthor_vm_get_heap_pool(struct panthor_vm *vm, bool c
 		if (vm->destroyed)
 			pool = ERR_PTR(-EINVAL);
 		else
-			pool = panthor_heap_pool_create(vm->ptdev, vm);
+			pool = panthor_heap_pool_create(ptdev, vm);
 
 		if (!IS_ERR(pool))
 			vm->heaps.pool = panthor_heap_pool_get(pool);
@@ -2167,7 +2188,7 @@ void panthor_vm_heaps_sizes(struct panthor_file *pfile, struct drm_memory_stats
 	xa_for_each(&pfile->vms->xa, i, vm) {
 		size_t size = panthor_heap_pool_size(vm->heaps.pool);
 		stats->resident += size;
-		if (vm->as.id >= 0)
+		if (vm->as->hw_slot.id >= 0)
 			stats->active += size;
 	}
 	xa_unlock(&pfile->vms->xa);
@@ -2215,8 +2236,7 @@ static u64 mair_to_memattr(u64 mair, bool coherent)
 	return memattr;
 }
 
-static void panthor_vma_link(struct panthor_vm *vm,
-			     struct panthor_vma *vma,
+static void panthor_vma_link(struct panthor_vma *vma,
 			     struct drm_gpuvm_bo *vm_bo)
 {
 	struct panthor_gem_object *bo = to_panthor_bo(vma->base.gem.obj);
@@ -2252,25 +2272,25 @@ panthor_fix_sparse_map_offset(struct drm_gpuva_op_map *op, u32 flags)
 }
 
 static int
-panthor_vm_exec_map_op(struct panthor_vm *vm, u32 flags,
+panthor_as_exec_map_op(struct panthor_as *as, u32 flags,
 		       const struct drm_gpuva_op_map *op)
 {
 	struct panthor_gem_object *bo = to_panthor_bo(op->gem.obj);
 	int prot = flags_to_prot(flags);
 
 	if (flags & DRM_PANTHOR_VM_BIND_OP_MAP_SPARSE)
-		return panthor_vm_map_sparse(vm, op->va.addr, prot,
+		return panthor_as_map_sparse(as, op->va.addr, prot,
 					     bo->dmap.sgt, op->va.range);
 
-	return panthor_vm_map_pages(vm, op->va.addr, prot, bo->dmap.sgt,
+	return panthor_as_map_pages(as, op->va.addr, prot, bo->dmap.sgt,
 				    op->gem.offset, op->va.range);
 }
 
 static int panthor_gpuva_sm_step_map(struct drm_gpuva_op *op, void *priv)
 {
-	struct panthor_vm *vm = priv;
-	struct panthor_vm_op_ctx *op_ctx = vm->op_ctx;
-	struct panthor_vma *vma = panthor_vm_op_ctx_get_vma(op_ctx);
+	struct panthor_as *as = priv;
+	struct panthor_as_op_ctx *op_ctx = as->op_ctx;
+	struct panthor_vma *vma = panthor_as_op_ctx_get_vma(op_ctx);
 	int ret;
 
 	if (!vma)
@@ -2279,14 +2299,14 @@ static int panthor_gpuva_sm_step_map(struct drm_gpuva_op *op, void *priv)
 	panthor_vma_init(vma, op_ctx->flags & PANTHOR_VM_MAP_FLAGS);
 	panthor_fix_sparse_map_offset(&op->map, vma->flags);
 
-	ret = panthor_vm_exec_map_op(vm, vma->flags, &op->map);
+	ret = panthor_as_exec_map_op(as, vma->flags, &op->map);
 	if (ret) {
-		panthor_vm_op_ctx_return_vma(op_ctx, vma);
+		panthor_as_op_ctx_return_vma(op_ctx, vma);
 		return ret;
 	}
 
-	drm_gpuva_map(&vm->base, &vma->base, &op->map);
-	panthor_vma_link(vm, vma, op_ctx->map.vm_bo);
+	drm_gpuva_map(&as->base, &vma->base, &op->map);
+	panthor_vma_link(vma, op_ctx->map.vm_bo);
 
 	drm_gpuvm_bo_put_deferred(op_ctx->map.vm_bo);
 	op_ctx->map.vm_bo = NULL;
@@ -2347,8 +2367,8 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 				       void *priv)
 {
 	struct panthor_vma *unmap_vma = container_of(op->remap.unmap->va, struct panthor_vma, base);
-	struct panthor_vm *vm = priv;
-	struct panthor_vm_op_ctx *op_ctx = vm->op_ctx;
+	struct panthor_as *as = priv;
+	struct panthor_as_op_ctx *op_ctx = as->op_ctx;
 	struct panthor_vma *prev_vma = NULL, *next_vma = NULL;
 	u64 unmap_start, unmap_range;
 	int ret;
@@ -2374,8 +2394,8 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 		 * atomicity. panthor_vm_lock_region() bails out early if the new region
 		 * is already part of the locked region, so no need to do this check here.
 		 */
-		panthor_vm_lock_region(vm, unmap_start, unmap_range);
-		panthor_vm_unmap_pages(vm, unmap_start, unmap_range);
+		panthor_as_lock_region(as, unmap_start, unmap_range);
+		panthor_as_unmap_pages(as, unmap_start, unmap_range);
 	}
 
 	if (op->remap.prev) {
@@ -2391,12 +2411,12 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 			};
 			panthor_fix_sparse_map_offset(&map_op, unmap_vma->flags);
 
-			ret = panthor_vm_exec_map_op(vm, unmap_vma->flags, &map_op);
+			ret = panthor_as_exec_map_op(as, unmap_vma->flags, &map_op);
 			if (ret)
 				return ret;
 		}
 
-		prev_vma = panthor_vm_op_ctx_get_vma(op_ctx);
+		prev_vma = panthor_as_op_ctx_get_vma(op_ctx);
 		panthor_vma_init(prev_vma, unmap_vma->flags);
 		prev_vma->evicted = unmap_vma->evicted;
 	}
@@ -2414,12 +2434,12 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 			};
 			panthor_fix_sparse_map_offset(&map_op, unmap_vma->flags);
 
-			ret = panthor_vm_exec_map_op(vm, unmap_vma->flags, &map_op);
+			ret = panthor_as_exec_map_op(as, unmap_vma->flags, &map_op);
 			if (ret)
 				return ret;
 		}
 
-		next_vma = panthor_vm_op_ctx_get_vma(op_ctx);
+		next_vma = panthor_as_op_ctx_get_vma(op_ctx);
 		panthor_vma_init(next_vma, unmap_vma->flags);
 		next_vma->evicted = unmap_vma->evicted;
 	}
@@ -2434,11 +2454,11 @@ static int panthor_gpuva_sm_step_remap(struct drm_gpuva_op *op,
 		 * owned by the old mapping which will be released when this
 		 * mapping is destroyed, we need to grab a ref here.
 		 */
-		panthor_vma_link(vm, prev_vma, op->remap.unmap->va->vm_bo);
+		panthor_vma_link(prev_vma, op->remap.unmap->va->vm_bo);
 	}
 
 	if (next_vma) {
-		panthor_vma_link(vm, next_vma, op->remap.unmap->va->vm_bo);
+		panthor_vma_link(next_vma, op->remap.unmap->va->vm_bo);
 	}
 
 	panthor_vma_unlink(unmap_vma);
@@ -2449,10 +2469,10 @@ static int panthor_gpuva_sm_step_unmap(struct drm_gpuva_op *op,
 				       void *priv)
 {
 	struct panthor_vma *unmap_vma = container_of(op->unmap.va, struct panthor_vma, base);
-	struct panthor_vm *vm = priv;
+	struct panthor_as *as = priv;
 
 	if (!unmap_vma->evicted) {
-		panthor_vm_unmap_pages(vm, unmap_vma->base.va.addr,
+		panthor_as_unmap_pages(as, unmap_vma->base.va.addr,
 				       unmap_vma->base.va.range);
 	}
 
@@ -2464,7 +2484,7 @@ static int panthor_gpuva_sm_step_unmap(struct drm_gpuva_op *op,
 void panthor_vm_update_bo_reclaim_lru_locked(struct panthor_gem_object *bo)
 {
 	struct panthor_device *ptdev = container_of(bo->base.dev, struct panthor_device, base);
-	struct panthor_vm *vm = NULL;
+	struct panthor_as *as = NULL;
 	struct drm_gpuvm_bo *vm_bo;
 
 	dma_resv_assert_held(bo->base.resv);
@@ -2477,13 +2497,13 @@ void panthor_vm_update_bo_reclaim_lru_locked(struct panthor_gem_object *bo)
 		/* We're only supposed to have one non-evicted vm_bo in the list if we get
 		 * there.
 		 */
-		drm_WARN_ON(&ptdev->base, vm);
-		vm = container_of(vm_bo->vm, struct panthor_vm, base);
+		drm_WARN_ON(&ptdev->base, as);
+		as = container_of(vm_bo->vm, struct panthor_as, base);
 
 		mutex_lock(&ptdev->base.gem_lru_mutex);
-		drm_gem_lru_move_tail_locked(&vm->reclaim.lru, &bo->base);
-		if (list_empty(&vm->reclaim.lru_node))
-			list_move(&vm->reclaim.lru_node, &ptdev->reclaim.vms);
+		drm_gem_lru_move_tail_locked(&as->reclaim.lru, &bo->base);
+		if (list_empty(&as->reclaim.lru_node))
+			list_move(&as->reclaim.lru_node, &ptdev->reclaim.vms);
 		mutex_unlock(&ptdev->base.gem_lru_mutex);
 	}
 }
@@ -2494,10 +2514,10 @@ int panthor_vm_evict_bo_mappings_locked(struct panthor_gem_object *bo)
 	int ret = 0;
 
 	drm_gem_for_each_gpuvm_bo(vm_bo, &bo->base) {
-		struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+		struct panthor_as *as = container_of(vm_bo->vm, struct panthor_as, base);
 		struct drm_gpuva *va;
 
-		if (!mutex_trylock(&vm->op_lock))
+		if (!mutex_trylock(&as->op_lock))
 			return -EDEADLK;
 
 		/* It can be that the vm_bo was already evicted but a new
@@ -2526,16 +2546,16 @@ int panthor_vm_evict_bo_mappings_locked(struct panthor_gem_object *bo)
 			 * will be validated, causing all its evicted VMAs to be repopulated
 			 * before the job runs. So no GPU fault expected.
 			 */
-			ret = panthor_vm_lock_region(vm, va->va.addr, va->va.range);
+			ret = panthor_as_lock_region(as, va->va.addr, va->va.range);
 			if (ret)
 				break;
 
-			panthor_vm_unmap_pages(vm, va->va.addr, va->va.range);
-			panthor_vm_unlock_region(vm);
+			panthor_as_unmap_pages(as, va->va.addr, va->va.range);
+			panthor_as_unlock_region(as);
 			vma->evicted = true;
 		}
 
-		mutex_unlock(&vm->op_lock);
+		mutex_unlock(&as->op_lock);
 
 		if (ret)
 			break;
@@ -2545,14 +2565,14 @@ int panthor_vm_evict_bo_mappings_locked(struct panthor_gem_object *bo)
 }
 
 static struct panthor_vma *select_evicted_vma(struct drm_gpuvm_bo *vm_bo,
-					      struct panthor_vm_op_ctx *op_ctx)
+					      struct panthor_as_op_ctx *op_ctx)
 {
-	struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+	struct panthor_as *as = container_of(vm_bo->vm, struct panthor_as, base);
 	struct panthor_vma *first_evicted_vma = NULL;
 	struct drm_gpuva *va;
 
 	/* Take op_lock to protect against va insertion/removal. */
-	mutex_lock(&vm->op_lock);
+	mutex_lock(&as->op_lock);
 	drm_gpuvm_bo_for_each_va(va, vm_bo) {
 		struct panthor_vma *vma = container_of(va, struct panthor_vma, base);
 
@@ -2563,22 +2583,22 @@ static struct panthor_vma *select_evicted_vma(struct drm_gpuvm_bo *vm_bo,
 			break;
 		}
 	}
-	mutex_unlock(&vm->op_lock);
+	mutex_unlock(&as->op_lock);
 
 	return first_evicted_vma;
 }
 
 static int remap_evicted_vma(struct drm_gpuvm_bo *vm_bo,
 			     struct panthor_vma *evicted_vma,
-			     struct panthor_vm_op_ctx *op_ctx)
+			     struct panthor_as_op_ctx *op_ctx)
 {
-	struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+	struct panthor_as *as = container_of(vm_bo->vm, struct panthor_as, base);
 	struct panthor_gem_object *bo = to_panthor_bo(vm_bo->obj);
 	struct drm_gpuva *va;
 	bool found = false;
 	int ret;
 
-	ret = panthor_vm_op_ctx_prealloc_pts(op_ctx);
+	ret = panthor_as_op_ctx_prealloc_pts(op_ctx);
 	if (ret)
 		goto out_cleanup;
 
@@ -2587,7 +2607,7 @@ static int remap_evicted_vma(struct drm_gpuvm_bo *vm_bo,
 	 * to release it so we can allocate PTs, because this very same lock
 	 * is taken in a DMA-signalling path.
 	 */
-	mutex_lock(&vm->op_lock);
+	mutex_lock(&as->op_lock);
 	drm_gpuvm_bo_for_each_va(va, vm_bo) {
 		struct panthor_vma *vma = container_of(va, struct panthor_vma, base);
 
@@ -2607,8 +2627,8 @@ static int remap_evicted_vma(struct drm_gpuvm_bo *vm_bo,
 	}
 
 	if (found) {
-		vm->op_ctx = op_ctx;
-		ret = panthor_vm_lock_region(vm, evicted_vma->base.va.addr,
+		as->op_ctx = op_ctx;
+		ret = panthor_as_lock_region(as, evicted_vma->base.va.addr,
 					     evicted_vma->base.va.range);
 		if (!ret) {
 			struct drm_gpuva_op_map map_op = {
@@ -2617,34 +2637,37 @@ static int remap_evicted_vma(struct drm_gpuvm_bo *vm_bo,
 				.gem.obj = &bo->base,
 				.gem.offset = evicted_vma->base.gem.offset,
 			};
-			if (evicted_vma->flags & DRM_PANTHOR_VM_BIND_OP_MAP_SPARSE)
-				drm_WARN_ON_ONCE(&vm->ptdev->base, map_op.gem.offset !=
-						 (map_op.va.addr & (SZ_2M - 1)));
+			if (evicted_vma->flags & DRM_PANTHOR_VM_BIND_OP_MAP_SPARSE) {
+				u64 expected_offset = map_op.va.addr & (SZ_2M - 1);
 
-			ret = panthor_vm_exec_map_op(vm, evicted_vma->flags, &map_op);
+				drm_WARN_ON_ONCE(as->base.drm,
+						 map_op.gem.offset != expected_offset);
+			}
+
+			ret = panthor_as_exec_map_op(as, evicted_vma->flags, &map_op);
 			if (!ret)
 				evicted_vma->evicted = false;
 
-			panthor_vm_unlock_region(vm);
+			panthor_as_unlock_region(as);
 		}
 
-		vm->op_ctx = NULL;
+		as->op_ctx = NULL;
 	}
 
-	mutex_unlock(&vm->op_lock);
+	mutex_unlock(&as->op_lock);
 
 out_cleanup:
-	panthor_vm_cleanup_op_ctx(op_ctx, vm);
+	panthor_as_cleanup_op_ctx(op_ctx, as);
 	return ret;
 }
 
 static int panthor_vm_restore_vmas(struct drm_gpuvm_bo *vm_bo)
 {
-	struct panthor_vm *vm = container_of(vm_bo->vm, struct panthor_vm, base);
+	struct panthor_as *as = container_of(vm_bo->vm, struct panthor_as, base);
 	struct panthor_gem_object *bo = to_panthor_bo(vm_bo->obj);
-	struct panthor_vm_op_ctx op_ctx;
+	struct panthor_as_op_ctx op_ctx;
 
-	if (drm_WARN_ON_ONCE(&vm->ptdev->base, !bo->dmap.sgt))
+	if (drm_WARN_ON_ONCE(as->base.drm, !bo->dmap.sgt))
 		return -EINVAL;
 
 	for (struct panthor_vma *vma = select_evicted_vma(vm_bo, &op_ctx);
@@ -2680,8 +2703,19 @@ static int panthor_vm_bo_validate(struct drm_gpuvm_bo *vm_bo,
 	return 0;
 }
 
+static void panthor_as_free(struct drm_gpuvm *gpuvm)
+{
+	struct panthor_as *as = container_of(gpuvm, struct panthor_as, base);
+
+	if (as->pt.ops)
+		free_io_pgtable_ops(as->pt.ops);
+
+	mutex_destroy(&as->op_lock);
+	kfree(as);
+}
+
 static const struct drm_gpuvm_ops panthor_gpuvm_ops = {
-	.vm_free = panthor_vm_free,
+	.vm_free = panthor_as_free,
 	.vm_bo_free = panthor_vm_bo_free,
 	.sm_step_map = panthor_gpuva_sm_step_map,
 	.sm_step_remap = panthor_gpuva_sm_step_remap,
@@ -2697,7 +2731,7 @@ static const struct drm_gpuvm_ops panthor_gpuvm_ops = {
  */
 struct dma_resv *panthor_vm_resv(struct panthor_vm *vm)
 {
-	return drm_gpuvm_resv(&vm->base);
+	return drm_gpuvm_resv(&vm->as->base);
 }
 
 struct drm_gem_object *panthor_vm_root_gem(struct panthor_vm *vm)
@@ -2705,12 +2739,12 @@ struct drm_gem_object *panthor_vm_root_gem(struct panthor_vm *vm)
 	if (!vm)
 		return NULL;
 
-	return vm->base.r_obj;
+	return vm->as->base.r_obj;
 }
 
-static int
-panthor_vm_exec_op(struct panthor_vm *vm, struct panthor_vm_op_ctx *op,
-		   bool flag_vm_unusable_on_failure)
+static int panthor_as_exec_op(struct panthor_as *as,
+			      struct panthor_as_op_ctx *op,
+			      bool flag_vm_unusable_on_failure)
 {
 	u32 op_type = op->flags & DRM_PANTHOR_VM_BIND_OP_TYPE_MASK;
 	int ret;
@@ -2718,10 +2752,10 @@ panthor_vm_exec_op(struct panthor_vm *vm, struct panthor_vm_op_ctx *op,
 	if (op_type == DRM_PANTHOR_VM_BIND_OP_TYPE_SYNC_ONLY)
 		return 0;
 
-	mutex_lock(&vm->op_lock);
-	vm->op_ctx = op;
+	mutex_lock(&as->op_lock);
+	as->op_ctx = op;
 
-	ret = panthor_vm_lock_region(vm, op->va.addr, op->va.range);
+	ret = panthor_as_lock_region(as, op->va.addr, op->va.range);
 	if (ret)
 		goto out;
 
@@ -2734,17 +2768,17 @@ panthor_vm_exec_op(struct panthor_vm *vm, struct panthor_vm_op_ctx *op,
 			.map.gem.offset = op->map.bo_offset,
 		};
 
-		if (vm->unusable) {
+		if (as->unusable) {
 			ret = -EINVAL;
 			break;
 		}
 
-		ret = drm_gpuvm_sm_map(&vm->base, vm, &map_req);
+		ret = drm_gpuvm_sm_map(&as->base, as, &map_req);
 		break;
 	}
 
 	case DRM_PANTHOR_VM_BIND_OP_TYPE_UNMAP:
-		ret = drm_gpuvm_sm_unmap(&vm->base, vm, op->va.addr, op->va.range);
+		ret = drm_gpuvm_sm_unmap(&as->base, as, op->va.addr, op->va.range);
 		break;
 
 	default:
@@ -2752,14 +2786,14 @@ panthor_vm_exec_op(struct panthor_vm *vm, struct panthor_vm_op_ctx *op,
 		break;
 	}
 
-	panthor_vm_unlock_region(vm);
+	panthor_as_unlock_region(as);
 
 out:
 	if (ret && flag_vm_unusable_on_failure)
-		panthor_vm_declare_unusable(vm);
+		panthor_as_declare_unusable(as);
 
-	vm->op_ctx = NULL;
-	mutex_unlock(&vm->op_lock);
+	as->op_ctx = NULL;
+	mutex_unlock(&as->op_lock);
 
 	return ret;
 }
@@ -2777,7 +2811,7 @@ panthor_vm_bind_run_job(struct drm_sched_job *sched_job)
 	 * to be destroyed and recreated.
 	 */
 	cookie = dma_fence_begin_signalling();
-	ret = panthor_vm_exec_op(job->vm, &job->ctx, true);
+	ret = panthor_as_exec_op(job->vm->as, &job->ctx, true);
 	dma_fence_end_signalling(cookie);
 
 	return ret ? ERR_PTR(ret) : NULL;
@@ -2790,7 +2824,7 @@ static void panthor_vm_bind_job_release(struct kref *kref)
 	if (job->base.s_fence)
 		drm_sched_job_cleanup(&job->base);
 
-	panthor_vm_cleanup_op_ctx(&job->ctx, job->vm);
+	panthor_as_cleanup_op_ctx(&job->ctx, job->vm->as);
 	panthor_vm_put(job->vm);
 	kfree(job);
 }
@@ -2835,6 +2869,62 @@ static const struct drm_sched_backend_ops panthor_vm_bind_ops = {
 	.timedout_job = panthor_vm_bind_timedout_job,
 };
 
+static struct panthor_as *
+panthor_as_create(struct panthor_device *ptdev, const char *name,
+		  u64 min_va, u64 va_range)
+{
+	struct io_pgtable_cfg as_cfg = {
+		.pgsize_bitmap	= ptdev->mmu_info.page_size_bitmap,
+		.ias		= GPU_MMU_FEATURES_VA_BITS(ptdev->gpu_info.mmu_features),
+		.oas		= GPU_MMU_FEATURES_PA_BITS(ptdev->gpu_info.mmu_features),
+		.coherent_walk	= ptdev->coherent,
+		.tlb		= &mmu_tlb_ops,
+		.iommu_dev	= drm_dev_dma_dev(&ptdev->base),
+		.alloc		= alloc_pt,
+		.free		= free_pt,
+	};
+	struct drm_gem_object *dummy_gem;
+	struct panthor_as *as;
+	u64 mair;
+
+	/* We allocate a dummy GEM for the VM. */
+	dummy_gem = drm_gpuvm_resv_object_alloc(&ptdev->base);
+	if (!dummy_gem)
+		return ERR_PTR(-ENOMEM);
+
+	as = kzalloc_obj(*as);
+	if (!as) {
+		drm_gem_object_put(dummy_gem);
+		return ERR_PTR(-ENOMEM);
+	}
+
+	mutex_init(&as->op_lock);
+	drm_gem_lru_init(&as->reclaim.lru);
+	INIT_LIST_HEAD(&as->reclaim.lru_node);
+	INIT_LIST_HEAD(&as->hw_slot.lru_node);
+	as->hw_slot.id = -1;
+	refcount_set(&as->active_cnt, 0);
+
+	/* We intentionally leave the reserved range to zero, because we want kernel VMAs
+	 * to be handled the same way user VMAs are.
+	 */
+	drm_gpuvm_init(&as->base, name,
+		       DRM_GPUVM_RESV_PROTECTED | DRM_GPUVM_IMMEDIATE_MODE,
+		       &ptdev->base, dummy_gem, min_va, va_range, 0, 0,
+		       &panthor_gpuvm_ops);
+	drm_gem_object_put(dummy_gem);
+
+	as->pt.ops = alloc_io_pgtable_ops(ARM_64_LPAE_S1, &as_cfg, as);
+	if (!as->pt.ops) {
+		drm_gpuvm_put(&as->base);
+		return ERR_PTR(-EINVAL);
+	}
+
+	mair = io_pgtable_ops_to_pgtable(as->pt.ops)->cfg.arm_lpae_s1_cfg.mair;
+	as->memattr = mair_to_memattr(mair, ptdev->coherent);
+	return as;
+}
+
 /**
  * panthor_vm_create() - Create a VM
  * @ptdev: Device.
@@ -2852,9 +2942,8 @@ panthor_vm_create(struct panthor_device *ptdev, bool for_mcu,
 		  u64 auto_kernel_va_start, u64 auto_kernel_va_size)
 {
 	u32 va_bits = GPU_MMU_FEATURES_VA_BITS(ptdev->gpu_info.mmu_features);
-	u32 pa_bits = GPU_MMU_FEATURES_PA_BITS(ptdev->gpu_info.mmu_features);
+	const char *name = for_mcu ? "panthor-MCU-VM" : "panthor-GPU-VM";
 	u64 full_va_range = 1ull << va_bits;
-	struct drm_gem_object *dummy_gem;
 	struct drm_gpu_scheduler *sched;
 	const struct drm_sched_init_args sched_args = {
 		.ops = &panthor_vm_bind_ops,
@@ -2865,27 +2954,11 @@ panthor_vm_create(struct panthor_device *ptdev, bool for_mcu,
 		.name = "panthor-vm-bind",
 		.dev = ptdev->base.dev,
 	};
-	struct io_pgtable_cfg pgtbl_cfg;
-	u64 mair, min_va, va_range;
+	struct panthor_as *as;
 	struct panthor_vm *vm;
+	u64 min_va, va_range;
 	int ret;
 
-	vm = kzalloc_obj(*vm);
-	if (!vm)
-		return ERR_PTR(-ENOMEM);
-
-	/* We allocate a dummy GEM for the VM. */
-	dummy_gem = drm_gpuvm_resv_object_alloc(&ptdev->base);
-	if (!dummy_gem) {
-		ret = -ENOMEM;
-		goto err_free_vm;
-	}
-
-	mutex_init(&vm->heaps.lock);
-	vm->for_mcu = for_mcu;
-	vm->ptdev = ptdev;
-	mutex_init(&vm->op_lock);
-
 	if (for_mcu) {
 		/* CSF MCU is a cortex M7, and can only address 4G */
 		min_va = 0;
@@ -2895,49 +2968,35 @@ panthor_vm_create(struct panthor_device *ptdev, bool for_mcu,
 		va_range = full_va_range;
 	}
 
+	as = panthor_as_create(ptdev, name, min_va, va_range);
+	if (IS_ERR(as))
+		return ERR_CAST(as);
+
+	vm = kzalloc_obj(*vm);
+	if (!vm) {
+		ret = -ENOMEM;
+		goto err_put_as;
+	}
+
 	vm->user_va_range = kernel_va_start;
+	vm->as = as;
+	mutex_init(&vm->heaps.lock);
+	vm->for_mcu = for_mcu;
 
 	mutex_init(&vm->mm_lock);
 	drm_mm_init(&vm->mm, kernel_va_start, kernel_va_size);
 	vm->kernel_auto_va.start = auto_kernel_va_start;
 	vm->kernel_auto_va.end = vm->kernel_auto_va.start + auto_kernel_va_size - 1;
 
-	drm_gem_lru_init(&vm->reclaim.lru);
-	INIT_LIST_HEAD(&vm->reclaim.lru_node);
-	INIT_LIST_HEAD(&vm->node);
-	INIT_LIST_HEAD(&vm->as.lru_node);
-	vm->as.id = -1;
-	refcount_set(&vm->as.active_cnt, 0);
-
-	pgtbl_cfg = (struct io_pgtable_cfg) {
-		.pgsize_bitmap	= ptdev->mmu_info.page_size_bitmap,
-		.ias		= va_bits,
-		.oas		= pa_bits,
-		.coherent_walk	= ptdev->coherent,
-		.tlb		= &mmu_tlb_ops,
-		.iommu_dev	= ptdev->base.dev,
-		.alloc		= alloc_pt,
-		.free		= free_pt,
-	};
-
-	vm->pgtbl_ops = alloc_io_pgtable_ops(ARM_64_LPAE_S1, &pgtbl_cfg, vm);
-	if (!vm->pgtbl_ops) {
-		ret = -EINVAL;
-		goto err_mm_takedown;
-	}
-
 	ret = drm_sched_init(&vm->sched, &sched_args);
 	if (ret)
-		goto err_free_io_pgtable;
+		goto err_free_vm;
 
 	sched = &vm->sched;
 	ret = drm_sched_entity_init(&vm->entity, 0, &sched, 1, NULL);
 	if (ret)
 		goto err_sched_fini;
 
-	mair = io_pgtable_ops_to_pgtable(vm->pgtbl_ops)->cfg.arm_lpae_s1_cfg.mair;
-	vm->memattr = mair_to_memattr(mair, ptdev->coherent);
-
 	mutex_lock(&ptdev->mmu->vm.lock);
 	list_add_tail(&vm->node, &ptdev->mmu->vm.list);
 
@@ -2946,28 +3005,20 @@ panthor_vm_create(struct panthor_device *ptdev, bool for_mcu,
 		panthor_vm_stop(vm);
 	mutex_unlock(&ptdev->mmu->vm.lock);
 
-	/* We intentionally leave the reserved range to zero, because we want kernel VMAs
-	 * to be handled the same way user VMAs are.
-	 */
-	drm_gpuvm_init(&vm->base, for_mcu ? "panthor-MCU-VM" : "panthor-GPU-VM",
-		       DRM_GPUVM_RESV_PROTECTED | DRM_GPUVM_IMMEDIATE_MODE,
-		       &ptdev->base, dummy_gem, min_va, va_range, 0, 0,
-		       &panthor_gpuvm_ops);
-	drm_gem_object_put(dummy_gem);
+	kref_init(&vm->refcount);
 	return vm;
 
 err_sched_fini:
 	drm_sched_fini(&vm->sched);
 
-err_free_io_pgtable:
-	free_io_pgtable_ops(vm->pgtbl_ops);
-
-err_mm_takedown:
-	drm_mm_takedown(&vm->mm);
-	drm_gem_object_put(dummy_gem);
-
 err_free_vm:
+	drm_mm_takedown(&vm->mm);
+	mutex_destroy(&vm->mm_lock);
+	mutex_destroy(&vm->heaps.lock);
 	kfree(vm);
+
+err_put_as:
+	drm_gpuvm_put(&as->base);
 	return ERR_PTR(ret);
 }
 
@@ -2975,7 +3026,7 @@ static int
 panthor_vm_bind_prepare_op_ctx(struct drm_file *file,
 			       struct panthor_vm *vm,
 			       const struct drm_panthor_vm_bind_op *op,
-			       struct panthor_vm_op_ctx *op_ctx)
+			       struct panthor_as_op_ctx *op_ctx)
 {
 	ssize_t vm_pgsz = panthor_vm_page_size(vm);
 	struct drm_gem_object *gem;
@@ -2998,7 +3049,7 @@ panthor_vm_bind_prepare_op_ctx(struct drm_file *file,
 			drm_gem_object_get(&vm->dummy->base);
 		}
 
-		ret = panthor_vm_prepare_map_op_ctx(op_ctx, vm,
+		ret = panthor_as_prepare_map_op_ctx(op_ctx, vm->as,
 						    gem ? to_panthor_bo(gem) : NULL,
 						    op);
 		drm_gem_object_put(gem);
@@ -3011,7 +3062,7 @@ panthor_vm_bind_prepare_op_ctx(struct drm_file *file,
 		if (op->bo_handle || op->bo_offset)
 			return -EINVAL;
 
-		return panthor_vm_prepare_unmap_op_ctx(op_ctx, vm, op->va, op->size);
+		return panthor_as_prepare_unmap_op_ctx(op_ctx, vm->as, op->va, op->size);
 
 	case DRM_PANTHOR_VM_BIND_OP_TYPE_SYNC_ONLY:
 		if (op->flags & ~DRM_PANTHOR_VM_BIND_OP_TYPE_MASK)
@@ -3026,7 +3077,7 @@ panthor_vm_bind_prepare_op_ctx(struct drm_file *file,
 		if (!op->syncs.count)
 			return -EINVAL;
 
-		panthor_vm_prepare_sync_only_op_ctx(op_ctx);
+		panthor_as_prepare_sync_only_op_ctx(op_ctx);
 		return 0;
 
 	default:
@@ -3061,7 +3112,7 @@ panthor_vm_bind_job_create(struct drm_file *file,
 	if (!vm)
 		return ERR_PTR(-EINVAL);
 
-	if (vm->destroyed || vm->unusable)
+	if (vm->destroyed || vm->as->unusable)
 		return ERR_PTR(-EINVAL);
 
 	job = kzalloc_obj(*job);
@@ -3107,7 +3158,7 @@ int panthor_vm_bind_job_prepare_resvs(struct drm_exec *exec,
 	int ret;
 
 	/* Acquire the VM lock an reserve a slot for this VM bind job. */
-	ret = drm_gpuvm_prepare_vm(&job->vm->base, exec, 1);
+	ret = drm_gpuvm_prepare_vm(&job->vm->as->base, exec, 1);
 	if (ret)
 		return ret;
 
@@ -3132,7 +3183,7 @@ void panthor_vm_bind_job_update_resvs(struct drm_exec *exec,
 	struct panthor_vm_bind_job *job = container_of(sched_job, struct panthor_vm_bind_job, base);
 
 	/* Explicit sync => we just register our job finished fence as bookkeep. */
-	drm_gpuvm_resv_add_fence(&job->vm->base, exec,
+	drm_gpuvm_resv_add_fence(&job->vm->as->base, exec,
 				 &sched_job->s_fence->finished,
 				 DMA_RESV_USAGE_BOOKKEEP,
 				 DMA_RESV_USAGE_BOOKKEEP);
@@ -3143,7 +3194,7 @@ void panthor_vm_update_resvs(struct panthor_vm *vm, struct drm_exec *exec,
 			     enum dma_resv_usage private_usage,
 			     enum dma_resv_usage extobj_usage)
 {
-	drm_gpuvm_resv_add_fence(&vm->base, exec, fence, private_usage, extobj_usage);
+	drm_gpuvm_resv_add_fence(&vm->as->base, exec, fence, private_usage, extobj_usage);
 }
 
 /**
@@ -3158,7 +3209,7 @@ int panthor_vm_bind_exec_sync_op(struct drm_file *file,
 				 struct panthor_vm *vm,
 				 struct drm_panthor_vm_bind_op *op)
 {
-	struct panthor_vm_op_ctx op_ctx;
+	struct panthor_as_op_ctx op_ctx;
 	int ret;
 
 	/* No sync objects allowed on synchronous operations. */
@@ -3172,8 +3223,8 @@ int panthor_vm_bind_exec_sync_op(struct drm_file *file,
 	if (ret)
 		return ret;
 
-	ret = panthor_vm_exec_op(vm, &op_ctx, false);
-	panthor_vm_cleanup_op_ctx(&op_ctx, vm);
+	ret = panthor_as_exec_op(vm->as, &op_ctx, false);
+	panthor_as_cleanup_op_ctx(&op_ctx, vm->as);
 
 	return ret;
 }
@@ -3202,18 +3253,18 @@ int panthor_vm_map_bo_range(struct panthor_vm *vm, struct panthor_gem_object *bo
 		.va = va,
 		.flags = flags,
 	};
-	struct panthor_vm_op_ctx op_ctx;
+	struct panthor_as_op_ctx op_ctx;
 	int ret;
 
-	if (drm_WARN_ON(&vm->ptdev->base, flags & DRM_PANTHOR_VM_BIND_OP_MAP_SPARSE))
+	if (drm_WARN_ON(vm->as->base.drm, flags & DRM_PANTHOR_VM_BIND_OP_MAP_SPARSE))
 		return -EINVAL;
 
-	ret = panthor_vm_prepare_map_op_ctx(&op_ctx, vm, bo, &op);
+	ret = panthor_as_prepare_map_op_ctx(&op_ctx, vm->as, bo, &op);
 	if (ret)
 		return ret;
 
-	ret = panthor_vm_exec_op(vm, &op_ctx, false);
-	panthor_vm_cleanup_op_ctx(&op_ctx, vm);
+	ret = panthor_as_exec_op(vm->as, &op_ctx, false);
+	panthor_as_cleanup_op_ctx(&op_ctx, vm->as);
 
 	return ret;
 }
@@ -3231,15 +3282,15 @@ int panthor_vm_map_bo_range(struct panthor_vm *vm, struct panthor_gem_object *bo
  */
 int panthor_vm_unmap_range(struct panthor_vm *vm, u64 va, u64 size)
 {
-	struct panthor_vm_op_ctx op_ctx;
+	struct panthor_as_op_ctx op_ctx;
 	int ret;
 
-	ret = panthor_vm_prepare_unmap_op_ctx(&op_ctx, vm, va, size);
+	ret = panthor_as_prepare_unmap_op_ctx(&op_ctx, vm->as, va, size);
 	if (ret)
 		return ret;
 
-	ret = panthor_vm_exec_op(vm, &op_ctx, false);
-	panthor_vm_cleanup_op_ctx(&op_ctx, vm);
+	ret = panthor_as_exec_op(vm->as, &op_ctx, false);
+	panthor_as_cleanup_op_ctx(&op_ctx, vm->as);
 
 	return ret;
 }
@@ -3263,15 +3314,15 @@ int panthor_vm_prepare_mapped_bos_resvs(struct drm_exec *exec, struct panthor_vm
 	int ret;
 
 	/* Acquire the VM lock and reserve a slot for this GPU job. */
-	ret = drm_gpuvm_prepare_vm(&vm->base, exec, slot_count);
+	ret = drm_gpuvm_prepare_vm(&vm->as->base, exec, slot_count);
 	if (ret)
 		return ret;
 
-	ret = drm_gpuvm_prepare_objects(&vm->base, exec, slot_count);
+	ret = drm_gpuvm_prepare_objects(&vm->as->base, exec, slot_count);
 	if (ret)
 		return ret;
 
-	return drm_gpuvm_validate(&vm->base, exec);
+	return drm_gpuvm_validate(&vm->as->base, exec);
 }
 
 unsigned long
@@ -3288,21 +3339,21 @@ panthor_mmu_reclaim_priv_bos(struct panthor_device *ptdev,
 	list_splice_init(&ptdev->reclaim.vms, &vms);
 
 	while (freed < nr_to_scan) {
-		struct panthor_vm *vm;
+		struct panthor_as *as;
 
-		vm = list_first_entry_or_null(&vms, typeof(*vm),
+		as = list_first_entry_or_null(&vms, typeof(*as),
 					      reclaim.lru_node);
-		if (!vm)
+		if (!as)
 			break;
 
-		if (!kref_get_unless_zero(&vm->base.kref)) {
-			list_del_init(&vm->reclaim.lru_node);
+		if (!kref_get_unless_zero(&as->base.kref)) {
+			list_del_init(&as->reclaim.lru_node);
 			continue;
 		}
 
 		mutex_unlock(&ptdev->base.gem_lru_mutex);
 
-		freed += drm_gem_lru_scan(&ptdev->base, &vm->reclaim.lru,
+		freed += drm_gem_lru_scan(&ptdev->base, &as->reclaim.lru,
 					  nr_to_scan - freed,
 					  remaining, shrink, NULL);
 
@@ -3311,20 +3362,20 @@ panthor_mmu_reclaim_priv_bos(struct panthor_device *ptdev,
 		/* If the VM is still in the temporary list, remove it so we
 		 * can proceed with the next VM.
 		 */
-		if (vm == list_first_entry_or_null(&vms, typeof(*vm), reclaim.lru_node)) {
-			list_del_init(&vm->reclaim.lru_node);
+		if (as == list_first_entry_or_null(&vms, typeof(*as), reclaim.lru_node)) {
+			list_del_init(&as->reclaim.lru_node);
 
 			/* Keep the VM around if there are still things to
 			 * reclaim, so we can preserve the LRU order when
 			 * re-inserting in ptdev->reclaim.vms at the end.
 			 */
-			if (vm->reclaim.lru.count > 0)
-				list_add_tail(&vm->reclaim.lru_node, &remaining_vms);
+			if (as->reclaim.lru.count > 0)
+				list_add_tail(&as->reclaim.lru_node, &remaining_vms);
 		}
 
 		mutex_unlock(&ptdev->base.gem_lru_mutex);
 
-		panthor_vm_put(vm);
+		drm_gpuvm_put(&as->base);
 
 		mutex_lock(&ptdev->base.gem_lru_mutex);
 	}
@@ -3356,12 +3407,12 @@ void panthor_mmu_unplug(struct panthor_device *ptdev)
 
 	mutex_lock(&ptdev->mmu->as.slots_lock);
 	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
-		struct panthor_vm *vm = ptdev->mmu->as.slots[i].vm;
+		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
 
-		if (vm) {
+		if (as) {
 			drm_WARN_ON(&ptdev->base,
 				    panthor_mmu_as_disable(ptdev, i, false));
-			panthor_vm_release_as_locked(vm);
+			panthor_as_release_hw_slot_locked(as);
 		}
 	}
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
@@ -3451,9 +3502,9 @@ static int show_vm_gpuvas(struct panthor_vm *vm, struct seq_file *m)
 {
 	int ret;
 
-	mutex_lock(&vm->op_lock);
-	ret = drm_debugfs_gpuva_info(m, &vm->base);
-	mutex_unlock(&vm->op_lock);
+	mutex_lock(&vm->as->op_lock);
+	ret = drm_debugfs_gpuva_info(m, &vm->as->base);
+	mutex_unlock(&vm->as->op_lock);
 
 	return ret;
 }

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 09/17] drm/panthor: Add fine-grained restrictions on VMs
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (7 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 08/17] drm/panthor: Split panthor_vm Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 10/17] drm/panthor: Check AS state before disabling Boris Brezillon
                   ` (8 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

We currently restrict what a VM is allowed to do based on two states:
panthor_vm::destroyed and panthor_vm_pgtable::unusable, but we'll soon
need a no-unmap restriction to fix the unplug logic.

Instead of adding a third boolean that would reflect this new limitation,
let's overhaul the current restriction logic by adding separate
restriction flags representing the operations we want to prevent (map,
unmap and use).

Map and use restrictions are set everywhere we were previously
calling panthor_vm_pgtable_declare_unusable() or setting ::destroyed
to true, since that's what those two flags were preventing.

We also add restriction checks in
panthor_vm_pgtable_prepare_[un]map_op_ctx() and
panthor_vm_pgtable_exec_op() and drop the ones we had in
panthor_vm_bind_job_create() since they are redundant.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_mmu.c | 134 ++++++++++++++++++++++------------
 1 file changed, 88 insertions(+), 46 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 041836552953..1b401fd0d2d7 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -215,6 +215,25 @@ struct panthor_as_op_ctx {
 	} map;
 };
 
+/**
+ * enum panthor_as_restriction - List of restrictions that can apply to an AS.
+ *
+ * An AS always starts unrestricted, but based on the faults or device state
+ * changes, restrictions can be added over time. Restrictions can't be removed
+ * though. Once a VM is restricted, a new one must be created to lift the
+ * restrictions.
+ */
+enum panthor_as_restriction {
+	/** @PANTHOR_AS_FORBID_MAP: The AS can't map new buffers. */
+	PANTHOR_AS_FORBID_MAP = BIT(0),
+
+	/** @PANTHOR_AS_FORBID_UNMAP: The AS can't remove existing mappings. */
+	PANTHOR_AS_FORBID_UNMAP = BIT(1),
+
+	/** @PANTHOR_AS_FORBID_USE: The AS can't become active again. */
+	PANTHOR_AS_FORBID_USE = BIT(2),
+};
+
 /**
  * struct panthor_as - Used to managed a GPU address space.
  */
@@ -275,25 +294,8 @@ struct panthor_as {
 		struct list_head lru_node;
 	} hw_slot;
 
-	/**
-	 * @unusable: True if the AS has turned unusable because something
-	 * bad happened during an asynchronous request.
-	 *
-	 * We don't try to recover from such failures, because this implies
-	 * informing userspace about the specific operation that failed, and
-	 * hoping the userspace driver can replay things from there. This all
-	 * sounds very complicated for little gain.
-	 *
-	 * Instead, we should just flag the AS as unusable, and fail any
-	 * further request targeting this AS.
-	 *
-	 * We also provide a way to query an AS state, so userspace can
-	 * destroy it and create a new one.
-	 *
-	 * As an analogy, this would be mapped to a VK_ERROR_DEVICE_LOST
-	 * situation, where the logical device needs to be re-created.
-	 */
-	bool unusable;
+	/** @restrictions: Bitmask of panthor_as_restriction flags. */
+	atomic_t restrictions;
 
 	/**
 	 * @unhandled_fault: Unhandled fault happened.
@@ -411,13 +413,6 @@ struct panthor_vm {
 	/** @for_mcu: True if this is the MCU VM. */
 	bool for_mcu;
 
-	/**
-	 * @destroyed: True if the VM was destroyed.
-	 *
-	 * No further bind requests should be queued to a destroyed VM.
-	 */
-	bool destroyed;
-
 	/**
 	 * @dummy: Dummy object used for sparse mappings.
 	 *
@@ -693,7 +688,9 @@ bool panthor_vm_has_unhandled_faults(struct panthor_vm *vm)
  */
 bool panthor_vm_is_unusable(struct panthor_vm *vm)
 {
-	return vm->as->unusable;
+	return (atomic_read(&vm->as->restrictions) &
+		(PANTHOR_AS_FORBID_USE | PANTHOR_AS_FORBID_MAP |
+		 PANTHOR_AS_FORBID_UNMAP));
 }
 
 static void panthor_as_release_hw_slot_locked(struct panthor_as *as)
@@ -752,6 +749,11 @@ int panthor_vm_active(struct panthor_vm *vm)
 	mutex_lock(&as->op_lock);
 	mutex_lock(&ptdev->mmu->as.slots_lock);
 
+	if (atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_USE) {
+		ret = -EINVAL;
+		goto out_unlock;
+	}
+
 	if (refcount_inc_not_zero(&as->active_cnt))
 		goto out_unlock;
 
@@ -920,21 +922,29 @@ static size_t get_pgsize(u64 addr, size_t size, size_t *count)
 	return SZ_2M;
 }
 
-static void panthor_as_declare_unusable(struct panthor_as *as)
+static void panthor_as_restrict_usage_locked(struct panthor_as *as,
+					     u32 new_restrictions)
 {
 	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 	int cookie;
 
-	if (as->unusable)
-		return;
+	lockdep_assert_held(&as->op_lock);
 
-	as->unusable = true;
-	mutex_lock(&ptdev->mmu->as.slots_lock);
-	if (as->hw_slot.id >= 0 && drm_dev_enter(&ptdev->base, &cookie)) {
-		panthor_mmu_as_disable(ptdev, as->hw_slot.id, false);
-		drm_dev_exit(cookie);
+	if (new_restrictions & PANTHOR_AS_FORBID_USE) {
+		guard(mutex)(&ptdev->mmu->as.slots_lock);
+		if (as->hw_slot.id >= 0 && drm_dev_enter(&ptdev->base, &cookie)) {
+			/* Try to disable the AS. If as_disable() passed, this should cause
+			 * a fault on the next memory access. If it failed, a reset is
+			 * scheduled to recover from the GPU hang.
+			 * We intentionally don't call release_as_locked() here, because
+			 * this would mess up with the active_cnt refcount.
+			 */
+			panthor_mmu_as_disable(ptdev, as->hw_slot.id, false);
+			drm_dev_exit(cookie);
+		}
 	}
-	mutex_unlock(&ptdev->mmu->as.slots_lock);
+
+	atomic_or(new_restrictions, &as->restrictions);
 }
 
 static void panthor_as_unmap_pages(struct panthor_as *as, u64 iova, u64 size)
@@ -970,7 +980,9 @@ static void panthor_as_unmap_pages(struct panthor_as *as, u64 iova, u64 size)
 			 * so flag the VM unusable to make sure it's not going
 			 * to be used anymore.
 			 */
-			panthor_as_declare_unusable(as);
+			panthor_as_restrict_usage_locked(as,
+							 PANTHOR_AS_FORBID_USE |
+							 PANTHOR_AS_FORBID_MAP);
 
 			/* If we don't make progress, we're screwed. That also means
 			 * something else prevents us from unmapping the region, but
@@ -1046,7 +1058,9 @@ panthor_as_map_pages(struct panthor_as *as, u64 iova, int prot,
 				 * table pages behind.
 				 */
 				panthor_as_unmap_pages(as, start_iova, iova - start_iova);
-				panthor_as_declare_unusable(as);
+				panthor_as_restrict_usage_locked(as,
+								 PANTHOR_AS_FORBID_USE |
+								 PANTHOR_AS_FORBID_MAP);
 				return ret;
 			}
 		}
@@ -1339,6 +1353,9 @@ static int panthor_as_prepare_map_op_ctx(struct panthor_as_op_ctx *op_ctx,
 	struct sg_table *sgt = NULL;
 	int ret;
 
+	if (atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_MAP)
+		return -EINVAL;
+
 	if (!bo)
 		return -EINVAL;
 
@@ -1431,6 +1448,9 @@ static int panthor_as_prepare_unmap_op_ctx(struct panthor_as_op_ctx *op_ctx,
 	u32 pt_count = 0;
 	int ret;
 
+	if (atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_UNMAP)
+		return -EINVAL;
+
 	memset(op_ctx, 0, sizeof(*op_ctx));
 	op_ctx->va.range = size;
 	op_ctx->va.addr = va;
@@ -1640,7 +1660,12 @@ static void panthor_vm_destroy(struct panthor_vm *vm)
 
 	as = vm->as;
 	ptdev = container_of(as->base.drm, struct panthor_device, base);
-	vm->destroyed = true;
+
+	scoped_guard(mutex, &as->op_lock) {
+		panthor_as_restrict_usage_locked(as,
+						 PANTHOR_AS_FORBID_USE |
+						 PANTHOR_AS_FORBID_MAP);
+	}
 
 	/* Tell scheduler to stop all GPU work related to this VM */
 	if (refcount_read(&as->active_cnt) > 0)
@@ -2150,7 +2175,7 @@ struct panthor_heap_pool *panthor_vm_get_heap_pool(struct panthor_vm *vm, bool c
 
 	mutex_lock(&vm->heaps.lock);
 	if (!vm->heaps.pool && create) {
-		if (vm->destroyed)
+		if (panthor_vm_is_unusable(vm))
 			pool = ERR_PTR(-EINVAL);
 		else
 			pool = panthor_heap_pool_create(ptdev, vm);
@@ -2520,6 +2545,17 @@ int panthor_vm_evict_bo_mappings_locked(struct panthor_gem_object *bo)
 		if (!mutex_trylock(&as->op_lock))
 			return -EDEADLK;
 
+		/* Unmaps are forbidden when we failed to communicate with the GPU,
+		 * meaning we can't guarantee that the GPU will see our page table
+		 * updates which might lead to UAF situations. In that case, we
+		 * just skip eviction on this VM. Things should go back to normal
+		 * after a GPU reset.
+		 */
+		if (atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_UNMAP) {
+			ret = -EBUSY;
+			goto unlock_op;
+		}
+
 		/* It can be that the vm_bo was already evicted but a new
 		 * mapping pointing to this BO got created in the meantime,
 		 * thus turning the vm_bo in partially evicted state. In that case
@@ -2555,6 +2591,7 @@ int panthor_vm_evict_bo_mappings_locked(struct panthor_gem_object *bo)
 			vma->evicted = true;
 		}
 
+unlock_op:
 		mutex_unlock(&as->op_lock);
 
 		if (ret)
@@ -2768,7 +2805,7 @@ static int panthor_as_exec_op(struct panthor_as *as,
 			.map.gem.offset = op->map.bo_offset,
 		};
 
-		if (as->unusable) {
+		if (atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_MAP) {
 			ret = -EINVAL;
 			break;
 		}
@@ -2778,6 +2815,11 @@ static int panthor_as_exec_op(struct panthor_as *as,
 	}
 
 	case DRM_PANTHOR_VM_BIND_OP_TYPE_UNMAP:
+		if (atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_UNMAP) {
+			ret = -EINVAL;
+			break;
+		}
+
 		ret = drm_gpuvm_sm_unmap(&as->base, as, op->va.addr, op->va.range);
 		break;
 
@@ -2789,8 +2831,11 @@ static int panthor_as_exec_op(struct panthor_as *as,
 	panthor_as_unlock_region(as);
 
 out:
-	if (ret && flag_vm_unusable_on_failure)
-		panthor_as_declare_unusable(as);
+	if (ret && flag_vm_unusable_on_failure) {
+		panthor_as_restrict_usage_locked(as,
+						 PANTHOR_AS_FORBID_USE |
+						 PANTHOR_AS_FORBID_MAP);
+	}
 
 	as->op_ctx = NULL;
 	mutex_unlock(&as->op_lock);
@@ -3112,9 +3157,6 @@ panthor_vm_bind_job_create(struct drm_file *file,
 	if (!vm)
 		return ERR_PTR(-EINVAL);
 
-	if (vm->destroyed || vm->as->unusable)
-		return ERR_PTR(-EINVAL);
-
 	job = kzalloc_obj(*job);
 	if (!job)
 		return ERR_PTR(-ENOMEM);

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 10/17] drm/panthor: Check AS state before disabling
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (8 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 09/17] drm/panthor: Add fine-grained restrictions on VMs Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 11/17] drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap Boris Brezillon
                   ` (7 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

Use TRANSTAB == 0 as a way to detect if an AS slot is idle. This
allows us to make panthor_mmu_as_disable() a NOP when it's called
after a SOFT_RESET, which will be needed for our unplug rework.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_mmu.c | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 1b401fd0d2d7..a48466788f4d 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -634,6 +634,10 @@ static int panthor_mmu_as_disable(struct panthor_device *ptdev, u32 slot,
 
 	lockdep_assert_held(&ptdev->mmu->as.slots_lock);
 
+	/* The AS was disabled already, nothing to do. */
+	if (!gpu_read64(mmu->iomem, AS_TRANSTAB(slot)))
+		return 0;
+
 	panthor_mmu_irq_disable_events(&ptdev->mmu->irq,
 				       panthor_mmu_as_fault_mask(ptdev, slot));
 
@@ -656,11 +660,17 @@ static int panthor_mmu_as_disable(struct panthor_device *ptdev, u32 slot,
 	if (recycle_slot)
 		return 0;
 
-	gpu_write64(mmu->iomem, AS_TRANSTAB(slot), 0);
-	gpu_write64(mmu->iomem, AS_MEMATTR(slot), 0);
 	gpu_write64(mmu->iomem, AS_TRANSCFG(slot), AS_TRANSCFG_ADRMODE_UNMAPPED);
+	ret = as_send_cmd_and_wait(ptdev, slot, AS_COMMAND_UPDATE);
+	if (ret)
+		return ret;
 
-	return as_send_cmd_and_wait(ptdev, slot, AS_COMMAND_UPDATE);
+	/* We reset the other fields late to ensure that, if something fails,
+	 * the page table is considered active (TRANSTAB != NULL).
+	 */
+	gpu_write64(mmu->iomem, AS_MEMATTR(slot), 0);
+	gpu_write64(mmu->iomem, AS_TRANSTAB(slot), 0);
+	return 0;
 }
 
 static u32 panthor_mmu_fault_mask(struct panthor_device *ptdev, u32 value)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 11/17] drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (9 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 10/17] drm/panthor: Check AS state before disabling Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:12   ` sashiko-bot
  2026-08-13 10:57 ` [PATCH v3 12/17] drm/panthor: Make the VM cleanup path more robust against UAF Boris Brezillon
                   ` (6 subsequent siblings)
  17 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

In the cleanup path, we unmap the full VA range to make sure things are
clean before the VM is released. I'd rather not fail on memory
allocation in that path, so let's make sure
panthor_vm_pgtable_prepare_unmap_op_ctx() doesn't allocate VMAs or
page tables when the unmap range matches the VM virtual address range.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_mmu.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index a48466788f4d..acdc0dd04f8b 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -1466,6 +1466,10 @@ static int panthor_as_prepare_unmap_op_ctx(struct panthor_as_op_ctx *op_ctx,
 	op_ctx->va.addr = va;
 	op_ctx->flags = DRM_PANTHOR_VM_BIND_OP_TYPE_UNMAP;
 
+	/* Unmap on the whole VM range don't need new VMAs or page tables. */
+	if (va == as->base.mm_start && size == as->base.mm_range)
+		return 0;
+
 	/* Pre-allocate L3 page tables to account for the split-2M-block
 	 * situation on unmap.
 	 */

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 12/17] drm/panthor: Make the VM cleanup path more robust against UAF
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (10 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 11/17] drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:20   ` sashiko-bot
  2026-08-13 10:57 ` [PATCH v3 13/17] drm/panthor: Track user owned VMs Boris Brezillon
                   ` (5 subsequent siblings)
  17 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

The VM cleanup logic tries to gracefully evict the page table from its
AS slot to make sure the HW doesn't have access to the memory anymore.
But it might happen that the eviction fails because the HW hung, and
in that case, we have no guarantee that the HW won't access the memory
until we've properly reset the GPU.

Defer the cleanup of VMs after the reset is effective when this situation
happens.

Note that this introduces a leak if the VM is released while being
assigned an AS slot and the device was unplugged, because we don't
clear the FORBID_UNMAP restriction in that case. It's just one more
issue in an ocean of unplug bugs. These unplug issues will be addressed
in an upcoming commit.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c |   6 +-
 drivers/gpu/drm/panthor/panthor_mmu.c    | 307 +++++++++++++++++++++++--------
 drivers/gpu/drm/panthor/panthor_mmu.h    |   3 +-
 3 files changed, 238 insertions(+), 78 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index d4c75ff206bb..0ccdf392d194 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -512,9 +512,10 @@ static int panthor_device_resume_hw_components(struct panthor_device *ptdev)
 	if (!ret)
 		return 0;
 
-	panthor_mmu_suspend(ptdev);
+	panthor_mmu_pre_suspend(ptdev);
 	panthor_gpu_suspend(ptdev);
 	panthor_pwr_suspend(ptdev);
+	panthor_mmu_post_suspend(ptdev);
 	return ret;
 }
 
@@ -643,9 +644,10 @@ int panthor_device_suspend(struct device *dev)
 		 */
 		panthor_sched_suspend(ptdev);
 		panthor_fw_suspend(ptdev);
-		panthor_mmu_suspend(ptdev);
+		panthor_mmu_pre_suspend(ptdev);
 		panthor_gpu_suspend(ptdev);
 		panthor_pwr_suspend(ptdev);
+		panthor_mmu_post_suspend(ptdev);
 		drm_dev_exit(cookie);
 	}
 
diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index acdc0dd04f8b..5f4aab5813d5 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -92,6 +92,17 @@ struct panthor_mmu {
 		 * TLB/cache flushes.
 		 */
 		struct list_head lru_list;
+
+		/**
+		 * @cleanup_list: List containing VMs waiting for cleanup.
+		 *
+		 * This list is used to keep track of VMs that got released but
+		 * couldn't be evicted from their AS slot because the HW hanged.
+		 * In that case, we add the VM to the list, and wait for the next
+		 * post_reset, at which point we're sure the HW is idle and the
+		 * VM resources can go away.
+		 */
+		struct list_head cleanup_list;
 	} as;
 
 	/** @vm: VMs management fields */
@@ -107,6 +118,12 @@ struct panthor_mmu {
 
 		/** @vm.wq: Workqueue used for the VM_BIND queues. */
 		struct workqueue_struct *wq;
+
+		/**
+		 * @vm.cleanup_work: Used to cleanup the VMs that are in
+		 * panthor_mmu::as::cleanup_list.
+		 */
+		struct work_struct cleanup_work;
 	} vm;
 };
 
@@ -1976,7 +1993,7 @@ static void panthor_mmu_irq_handler(struct panthor_device *ptdev, u32 status)
 }
 
 /**
- * panthor_mmu_suspend() - Suspend the MMU logic
+ * panthor_mmu_pre_suspend() - Prepare the MMU block for a suspend
  * @ptdev: Device.
  *
  * All we do here is de-assign the AS slots on all active VMs, so things
@@ -1985,23 +2002,67 @@ static void panthor_mmu_irq_handler(struct panthor_device *ptdev, u32 status)
  *
  * We also suspend the MMU IRQ.
  */
-void panthor_mmu_suspend(struct panthor_device *ptdev)
+void panthor_mmu_pre_suspend(struct panthor_device *ptdev)
 {
 	mutex_lock(&ptdev->mmu->as.slots_lock);
 	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
 		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
+		int ret;
 
-		if (as) {
-			drm_WARN_ON(&ptdev->base,
-				    panthor_mmu_as_disable(ptdev, i, false));
+		if (!as)
+			continue;
+
+		/* If the disable fails, leave the AS on its slot so we can
+		 * properly evict it when we're sure the GPU is off.
+		 */
+		ret = panthor_mmu_as_disable(ptdev, i, false);
+		if (!ret)
 			panthor_as_release_hw_slot_locked(as);
-		}
+		else
+			panthor_as_restrict_usage_locked(as,
+							 PANTHOR_AS_FORBID_USE |
+							 PANTHOR_AS_FORBID_MAP |
+							 PANTHOR_AS_FORBID_UNMAP);
 	}
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
 
 	panthor_mmu_irq_suspend(&ptdev->mmu->irq);
 }
 
+static void mmu_post_reset_cleanup(struct panthor_device *ptdev)
+{
+	guard(mutex)(&ptdev->mmu->as.slots_lock);
+
+	/* Now that the reset is effective, we can assume that none of the
+	 * AS slots are setup, and clear the faulty flags too.
+	 */
+	ptdev->mmu->as.alloc_mask = 0;
+	ptdev->mmu->as.faulty_mask = 0;
+
+	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
+		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
+
+		if (!as)
+			continue;
+
+		panthor_as_release_hw_slot_locked(as);
+
+		/* FIXME: We shouldn't drop the no-unmap restriction if
+		 * we're in the unplug path and the device wasn't properly
+		 * stopped with a SOFT_RESET.
+		 */
+		atomic_and(~PANTHOR_AS_FORBID_UNMAP, &as->restrictions);
+	}
+
+	if (!list_empty(&ptdev->mmu->as.cleanup_list))
+		queue_work(panthor_cleanup_wq, &ptdev->mmu->vm.cleanup_work);
+}
+
+void panthor_mmu_post_suspend(struct panthor_device *ptdev)
+{
+	mmu_post_reset_cleanup(ptdev);
+}
+
 /**
  * panthor_mmu_resume() - Resume the MMU logic
  * @ptdev: Device.
@@ -2013,11 +2074,7 @@ void panthor_mmu_suspend(struct panthor_device *ptdev)
  */
 void panthor_mmu_resume(struct panthor_device *ptdev)
 {
-	mutex_lock(&ptdev->mmu->as.slots_lock);
-	ptdev->mmu->as.alloc_mask = 0;
-	ptdev->mmu->as.faulty_mask = 0;
-	mutex_unlock(&ptdev->mmu->as.slots_lock);
-
+	mmu_post_reset_cleanup(ptdev);
 	panthor_mmu_irq_resume(&ptdev->mmu->irq);
 }
 
@@ -2055,22 +2112,7 @@ void panthor_mmu_post_reset(struct panthor_device *ptdev)
 {
 	struct panthor_vm *vm;
 
-	mutex_lock(&ptdev->mmu->as.slots_lock);
-
-	/* Now that the reset is effective, we can assume that none of the
-	 * AS slots are setup, and clear the faulty flags too.
-	 */
-	ptdev->mmu->as.alloc_mask = 0;
-	ptdev->mmu->as.faulty_mask = 0;
-
-	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
-		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
-
-		if (as)
-			panthor_as_release_hw_slot_locked(as);
-	}
-
-	mutex_unlock(&ptdev->mmu->as.slots_lock);
+	mmu_post_reset_cleanup(ptdev);
 
 	panthor_mmu_irq_resume(&ptdev->mmu->irq);
 
@@ -2083,58 +2125,41 @@ void panthor_mmu_post_reset(struct panthor_device *ptdev)
 	mutex_unlock(&ptdev->mmu->vm.lock);
 }
 
-static void panthor_vm_release(struct kref *kref)
+static void vm_cleanup(struct panthor_vm *vm)
 {
-	struct panthor_vm *vm = container_of(kref, struct panthor_vm, refcount);
 	struct panthor_as *as = vm->as;
 	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
 
-	/* Make sure the page table behind this VM doesn't participate in reclaim
-	 * after that point, since we're about to release everything anyway.
+	if (!(atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_UNMAP)) {
+		/* Unmap everything in case some BOs were still mapped. */
+		drm_WARN_ON(&ptdev->base,
+			    panthor_vm_unmap_range(vm, as->base.mm_start, as->base.mm_range));
+	}
+
+	/* It's safe to destroy the heaps and return the dummy BO even if
+	 * the FORBID_UNMAP restriction stands because this BO will be retained
+	 * by the drm_gpuvm_bo held by the active drm_gpuva entries, which
+	 * were preserved because the panthor_vm_unmap_range() was skipped.
+	 * The drm_gpuvm object itself is retained by the live drm_gpuvas
+	 * living there. So this is one of the rare occasions where we actually
+	 * want the circular referencing to silently leak objects.
+	 * Another side effect of this is that the drm_device is retained too,
+	 * because drm_gpuvm holds a ref on this object. This is okay, because
+	 * drm_dev_unregister() makes sure this device is no longer exposed
+	 * to userspace, so it's basically a zombie drm_device.
+	 *
+	 * TLDR; the memory a rogue GPU might have its hands on is leaked,
+	 * even though we explicitly destroy the objects owning these resources.
 	 */
-	mutex_lock(&ptdev->base.gem_lru_mutex);
-	list_del_init(&as->reclaim.lru_node);
-	mutex_unlock(&ptdev->base.gem_lru_mutex);
-
-	/* Unmap everything in case some BOs were still mapped. */
-	drm_WARN_ON(&ptdev->base,
-		    panthor_vm_unmap_range(vm, as->base.mm_start, as->base.mm_range));
-
-	mutex_lock(&vm->heaps.lock);
-	if (drm_WARN_ON(&ptdev->base, vm->heaps.pool))
-		panthor_heap_pool_destroy(vm->heaps.pool);
-	mutex_unlock(&vm->heaps.lock);
+	scoped_guard(mutex, &vm->heaps.lock) {
+		if (drm_WARN_ON(&ptdev->base, vm->heaps.pool))
+			panthor_heap_pool_destroy(vm->heaps.pool);
+	}
 	mutex_destroy(&vm->heaps.lock);
 
-	mutex_lock(&ptdev->mmu->vm.lock);
-	list_del(&vm->node);
-	/* Restore the scheduler state so we can call drm_sched_entity_destroy()
-	 * and drm_sched_fini(). If get there, that means we have no job left
-	 * and no new jobs can be queued, so we can start the scheduler without
-	 * risking interfering with the reset.
-	 */
-	if (ptdev->mmu->vm.reset_in_progress)
-		panthor_vm_start(vm);
-	mutex_unlock(&ptdev->mmu->vm.lock);
-
 	drm_sched_entity_destroy(&vm->entity);
 	drm_sched_fini(&vm->sched);
 
-	mutex_lock(&vm->as->op_lock);
-	mutex_lock(&ptdev->mmu->as.slots_lock);
-	if (as->hw_slot.id >= 0) {
-		int cookie;
-
-		if (drm_dev_enter(&ptdev->base, &cookie)) {
-			panthor_mmu_as_disable(ptdev, as->hw_slot.id, false);
-			drm_dev_exit(cookie);
-		}
-
-		panthor_as_release_hw_slot_locked(as);
-	}
-	mutex_unlock(&ptdev->mmu->as.slots_lock);
-	mutex_unlock(&vm->as->op_lock);
-
 	if (vm->dummy)
 		drm_gem_object_put(&vm->dummy->base);
 
@@ -2143,6 +2168,92 @@ static void panthor_vm_release(struct kref *kref)
 	kfree(vm);
 }
 
+static bool vm_prep_for_cleanup(struct panthor_vm *vm)
+{
+	struct panthor_as *as = vm->as;
+	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
+	bool ready_for_cleanup;
+	int cookie, ret;
+
+	/* First we forbid any kind of use on the VM that's about to be
+	 * released. UNMAP will be restored later if we manage to evict
+	 * the page table from its AS slot.
+	 */
+	atomic_or(PANTHOR_AS_FORBID_USE |
+		  PANTHOR_AS_FORBID_MAP |
+		  PANTHOR_AS_FORBID_UNMAP,
+		  &vm->as->restrictions);
+
+	/* Make sure the page table behind this VM doesn't participate in reclaim
+	 * after that point, since we're about to release everything anyway.
+	 */
+	scoped_guard(mutex, &ptdev->base.gem_lru_mutex)
+		list_del_init(&as->reclaim.lru_node);
+
+	scoped_guard(mutex, &ptdev->mmu->vm.lock) {
+		/* Remove the VM from the list early, so it can't be seen by the VM list
+		 * walkers after that point.
+		 */
+		list_del(&vm->node);
+
+		/* Restore the scheduler state so we can call drm_sched_entity_destroy()
+		 * and drm_sched_fini(). If get there, that means we have no job left
+		 * and no new jobs can be queued, so we can start the scheduler without
+		 * risking interfering with the reset.
+		 */
+		if (ptdev->mmu->vm.reset_in_progress)
+			panthor_vm_start(vm);
+	}
+
+	if (!drm_dev_enter(&ptdev->base, &cookie)) {
+		guard(mutex)(&ptdev->mmu->as.slots_lock);
+
+		/* If we're still on slot after an unplug, it means
+		 * drm_dev_unplug() has returned but the part in
+		 * panthor_mmu_unplug() that evicts all resident AS has
+		 * not been executed yet. In that case, we simply queue
+		 * the VM to the cleanup list and wait for
+		 * panthor_mmu_unplug() to do its job.
+		 */
+		if (as->hw_slot.id >= 0) {
+			list_add_tail(&vm->node, &ptdev->mmu->as.cleanup_list);
+			return false;
+		}
+
+		return true;
+	}
+
+	scoped_guard(mutex, &ptdev->mmu->as.slots_lock) {
+		if (as->hw_slot.id >= 0) {
+			ret = panthor_mmu_as_disable(ptdev, as->hw_slot.id, false);
+			if (!ret) {
+				panthor_as_release_hw_slot_locked(as);
+			} else {
+				list_add_tail(&vm->node, &ptdev->mmu->as.cleanup_list);
+				panthor_device_schedule_reset(ptdev);
+			}
+		}
+
+		/* Page table is no longer resident, we can relax the no-unmap
+		 * restriction.
+		 */
+		ready_for_cleanup = as->hw_slot.id < 0;
+		if (ready_for_cleanup)
+			atomic_and(~PANTHOR_AS_FORBID_UNMAP, &as->restrictions);
+	}
+
+	drm_dev_exit(cookie);
+	return ready_for_cleanup;
+}
+
+static void panthor_vm_release(struct kref *kref)
+{
+	struct panthor_vm *vm = container_of(kref, struct panthor_vm, refcount);
+
+	if (vm_prep_for_cleanup(vm))
+		vm_cleanup(vm);
+}
+
 /**
  * panthor_vm_put() - Release a reference on a VM
  * @vm: VM to release the reference on. Can be NULL.
@@ -2758,7 +2869,11 @@ static void panthor_as_free(struct drm_gpuvm *gpuvm)
 {
 	struct panthor_as *as = container_of(gpuvm, struct panthor_as, base);
 
-	if (as->pt.ops)
+	/* If we get to that point and we're still not allowed to unmap,
+	 * this means the HW is still running and has a access to the page
+	 * table, so we just leak it to avoid UAF.
+	 */
+	if (as->pt.ops && !(atomic_read(&as->restrictions) & PANTHOR_AS_FORBID_UNMAP))
 		free_io_pgtable_ops(as->pt.ops);
 
 	mutex_destroy(&as->op_lock);
@@ -3464,15 +3579,34 @@ void panthor_mmu_unplug(struct panthor_device *ptdev)
 	mutex_lock(&ptdev->mmu->as.slots_lock);
 	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
 		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
+		int ret;
 
-		if (as) {
-			drm_WARN_ON(&ptdev->base,
-				    panthor_mmu_as_disable(ptdev, i, false));
-			panthor_as_release_hw_slot_locked(as);
-		}
+		if (!as)
+			continue;
+
+		ret = panthor_mmu_as_disable(ptdev, i, false);
+		drm_WARN_ON(&ptdev->base, ret);
+
+		/* Drop the unmap restriction if the disabled worked, so we
+		 * don't leak resources in the normal situation.
+		 */
+		if (!ret)
+			atomic_and(~PANTHOR_AS_FORBID_UNMAP, &as->restrictions);
+
+		panthor_as_release_hw_slot_locked(as);
 	}
+
+	if (!list_empty(&ptdev->mmu->as.cleanup_list))
+		queue_work(panthor_cleanup_wq, &ptdev->mmu->vm.cleanup_work);
 	mutex_unlock(&ptdev->mmu->as.slots_lock);
 
+	/* Make sure pending VM cleanups are processed before leaving. Those
+	 * cleanups might schedule vm_bind_job cleanups, so keep this
+	 * flush_work() before the final flush_workqueue(panthor_cleanup_wq).
+	 */
+	flush_work(&ptdev->mmu->vm.cleanup_work);
+	drm_WARN_ON(&ptdev->base, !list_empty(&ptdev->mmu->as.cleanup_list));
+
 	/* Ensure any pending job cleanup work are executed before returning,
 	 * otherwise those might access objects that are gone if the work is
 	 * executed after other components are unplugged.
@@ -3490,6 +3624,27 @@ static void panthor_mmu_info_init(struct panthor_device *ptdev)
 	ptdev->mmu_info.page_size_bitmap = SZ_4K | SZ_2M;
 }
 
+static void mmu_cleanup_vms_work(struct work_struct *work)
+{
+	struct panthor_mmu *mmu =
+		container_of(work, struct panthor_mmu, vm.cleanup_work);
+	struct panthor_vm *vm, *tmp;
+	LIST_HEAD(cleanup_list);
+
+	/* Collect the VMs to cleanup first. */
+	scoped_guard(mutex, &mmu->as.slots_lock) {
+		list_for_each_entry_safe(vm, tmp, &mmu->as.cleanup_list, node) {
+			if (vm->as->hw_slot.id < 0)
+				list_move_tail(&vm->node, &cleanup_list);
+		}
+	}
+
+	list_for_each_entry_safe(vm, tmp, &cleanup_list, node) {
+		list_del(&vm->node);
+		vm_cleanup(vm);
+	}
+}
+
 /**
  * panthor_mmu_init() - Initialize the MMU logic.
  * @ptdev: Device.
@@ -3508,7 +3663,9 @@ int panthor_mmu_init(struct panthor_device *ptdev)
 	if (!mmu)
 		return -ENOMEM;
 
+	INIT_WORK(&mmu->vm.cleanup_work, mmu_cleanup_vms_work);
 	INIT_LIST_HEAD(&mmu->as.lru_list);
+	INIT_LIST_HEAD(&mmu->as.cleanup_list);
 
 	ret = drmm_mutex_init(&ptdev->base, &mmu->as.slots_lock);
 	if (ret)
diff --git a/drivers/gpu/drm/panthor/panthor_mmu.h b/drivers/gpu/drm/panthor/panthor_mmu.h
index 3522fbbce369..a02875fd5d44 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.h
+++ b/drivers/gpu/drm/panthor/panthor_mmu.h
@@ -21,7 +21,8 @@ int panthor_mmu_init(struct panthor_device *ptdev);
 void panthor_mmu_unplug(struct panthor_device *ptdev);
 void panthor_mmu_pre_reset(struct panthor_device *ptdev);
 void panthor_mmu_post_reset(struct panthor_device *ptdev);
-void panthor_mmu_suspend(struct panthor_device *ptdev);
+void panthor_mmu_pre_suspend(struct panthor_device *ptdev);
+void panthor_mmu_post_suspend(struct panthor_device *ptdev);
 void panthor_mmu_resume(struct panthor_device *ptdev);
 
 int panthor_vm_map_bo_range(struct panthor_vm *vm, struct panthor_gem_object *bo,

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 13/17] drm/panthor: Track user owned VMs
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (11 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 12/17] drm/panthor: Make the VM cleanup path more robust against UAF Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:18   ` sashiko-bot
  2026-08-13 10:57 ` [PATCH v3 14/17] drm/panthor: Track user owned groups Boris Brezillon
                   ` (4 subsequent siblings)
  17 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

We will soon need this to fix the unplug logic and make sure panthor_vm
objects are not left behind after an unplug.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_mmu.c | 31 +++++++++++++++++++++++++++----
 1 file changed, 27 insertions(+), 4 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 5f4aab5813d5..4c01b0e4288b 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -107,12 +107,15 @@ struct panthor_mmu {
 
 	/** @vm: VMs management fields */
 	struct {
-		/** @vm.lock: Lock protecting access to list. */
+		/** @vm.lock: Lock protecting access to list and user_owned. */
 		struct mutex lock;
 
 		/** @vm.list: List containing all VMs. */
 		struct list_head list;
 
+		/** @vm.list: List containing VMs with a valid handle. */
+		struct list_head user_owned;
+
 		/** @vm.reset_in_progress: True if a reset is in progress. */
 		bool reset_in_progress;
 
@@ -427,6 +430,9 @@ struct panthor_vm {
 	/** @node: Used to insert the VM in the panthor_mmu::vm::list. */
 	struct list_head node;
 
+	/* @user_node: Used to insert the VM in the panthor_mmu::vm::user_owned list. */
+	struct list_head user_node;
+
 	/** @for_mcu: True if this is the MCU VM. */
 	bool for_mcu;
 
@@ -1669,10 +1675,19 @@ int panthor_vm_pool_create_vm(struct panthor_device *ptdev,
 	drm_gem_object_get(&pool->dummy->base);
 	vm->dummy = pool->dummy;
 
+	/* Insert in the list before xa_alloc() so we can't race with
+	 * panthor_vm_pool_destroy_vm() have the VM inserted in the
+	 * user_owned list after it's been destroyed.
+	 */
+	scoped_guard(mutex, &ptdev->mmu->vm.lock)
+		list_add_tail(&vm->user_node, &ptdev->mmu->vm.user_owned);
+
 	ret = xa_alloc(&pool->xa, &id, vm,
 		       XA_LIMIT(1, PANTHOR_MAX_VMS_PER_FILE), GFP_KERNEL);
 
 	if (ret) {
+		scoped_guard(mutex, &ptdev->mmu->vm.lock)
+			list_del_init(&vm->user_node);
 		panthor_vm_put(vm);
 		return ret;
 	}
@@ -1727,13 +1742,19 @@ static void panthor_vm_destroy(struct panthor_vm *vm)
  */
 int panthor_vm_pool_destroy_vm(struct panthor_vm_pool *pool, u32 handle)
 {
+	struct panthor_device *ptdev;
 	struct panthor_vm *vm;
 
 	vm = xa_erase(&pool->xa, handle);
+	if (!vm)
+		return -EINVAL;
+
+	ptdev = container_of(vm->as->base.drm, struct panthor_device, base);
+	scoped_guard(mutex, &ptdev->mmu->vm.lock)
+		list_del_init(&vm->user_node);
 
 	panthor_vm_destroy(vm);
-
-	return vm ? 0 : -EINVAL;
+	return 0;
 }
 
 /**
@@ -1773,7 +1794,7 @@ void panthor_vm_pool_destroy(struct panthor_file *pfile)
 		return;
 
 	xa_for_each(&pfile->vms->xa, i, vm)
-		panthor_vm_destroy(vm);
+		panthor_vm_pool_destroy_vm(pfile->vms, i);
 
 	if (pfile->vms->dummy)
 		drm_gem_object_put(&pfile->vms->dummy->base);
@@ -3152,6 +3173,7 @@ panthor_vm_create(struct panthor_device *ptdev, bool for_mcu,
 		goto err_put_as;
 	}
 
+	INIT_LIST_HEAD(&vm->user_node);
 	vm->user_va_range = kernel_va_start;
 	vm->as = as;
 	mutex_init(&vm->heaps.lock);
@@ -3672,6 +3694,7 @@ int panthor_mmu_init(struct panthor_device *ptdev)
 		return ret;
 
 	INIT_LIST_HEAD(&mmu->vm.list);
+	INIT_LIST_HEAD(&mmu->vm.user_owned);
 	ret = drmm_mutex_init(&ptdev->base, &mmu->vm.lock);
 	if (ret)
 		return ret;

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 14/17] drm/panthor: Track user owned groups
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (12 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 13/17] drm/panthor: Track user owned VMs Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 15/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (3 subsequent siblings)
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

A group can outlive its user handle because of internal refs. In order
to fix the unplug logic, we need to keep track of groups that have a
valid user handle so we can release the references that were owned by
the user processes in the unplug path.

This is the prep work to keep track of user owned groups. Note that
the destroyed attribute is dropped because it's equivalent to checking
whether the group is inserted in the user_owned list now.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_sched.c | 43 ++++++++++++++++++++++++---------
 1 file changed, 31 insertions(+), 12 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_sched.c b/drivers/gpu/drm/panthor/panthor_sched.c
index f18b2e03f2fd..026f3105c646 100644
--- a/drivers/gpu/drm/panthor/panthor_sched.c
+++ b/drivers/gpu/drm/panthor/panthor_sched.c
@@ -252,6 +252,15 @@ struct panthor_scheduler {
 		 * This list is evaluated in the @sync_upd_work work.
 		 */
 		struct list_head waiting;
+
+		/**
+		 * @user_owned: List of groups that have a valid user handle.
+		 *
+		 * All groups are inserted in this list at creation time through their
+		 * panthor_group;:user_node, and evicted from this list when
+		 * panthor_group_destroy() is called.
+		 */
+		struct list_head user_owned;
 	} groups;
 
 	/**
@@ -587,15 +596,6 @@ struct panthor_group {
 	 */
 	int csg_id;
 
-	/**
-	 * @destroyed: True when the group has been destroyed.
-	 *
-	 * If a group is destroyed it becomes useless: no further jobs can be submitted
-	 * to its queues. We simply wait for all references to be dropped so we can
-	 * release the group object.
-	 */
-	bool destroyed;
-
 	/**
 	 * @timedout: True when a timeout occurred on any of the queues owned by
 	 * this group.
@@ -705,6 +705,17 @@ struct panthor_group {
 	 * panthor_group::groups::waiting list.
 	 */
 	struct list_head wait_node;
+
+	/**
+	 * @user_node: Used to insert the group in the panthor_scheduler::groups::user_owned list.
+	 *
+	 * When the group is created, it's inserted in panthor_scheduler::groups::user_owned,
+	 * and when panthor_group_destroy, the group is remove from this list.
+	 *
+	 * When the device is unplugged, all groups that remain in this list must have an extra
+	 * put_group() called on them to release the reference owned by the per-file group pool.
+	 */
+	struct list_head user_node;
 };
 
 struct panthor_job_profiling_data {
@@ -967,6 +978,7 @@ static void group_release(struct kref *kref)
 	struct panthor_device *ptdev = group->ptdev;
 
 	drm_WARN_ON(&ptdev->base, group->csg_id >= 0);
+	drm_WARN_ON(&ptdev->base, !list_empty(&group->user_node));
 	drm_WARN_ON(&ptdev->base, !list_empty(&group->run_node));
 	drm_WARN_ON(&ptdev->base, !list_empty(&group->wait_node));
 
@@ -1082,7 +1094,7 @@ group_can_run(struct panthor_group *group)
 {
 	return group->state != PANTHOR_CS_GROUP_TERMINATED &&
 	       group->state != PANTHOR_CS_GROUP_UNKNOWN_STATE &&
-	       !group->destroyed && group->fatal_queues == 0 &&
+	       !list_empty(&group->user_node) && group->fatal_queues == 0 &&
 	       !group->timedout;
 }
 
@@ -2403,7 +2415,7 @@ tick_ctx_apply(struct panthor_scheduler *sched, struct panthor_sched_tick_ctx *c
 			 * re-evaluate as soon as possible and get rid of
 			 * this dangling group.
 			 */
-			if (group->destroyed)
+			if (list_empty(&group->user_node))
 				ctx->immediate_tick = true;
 			group_put(group);
 		}
@@ -3691,6 +3703,7 @@ int panthor_group_create(struct panthor_file *pfile,
 	group->tiler_core_mask = group_args->tiler_core_mask;
 	group->priority = group_args->priority;
 
+	INIT_LIST_HEAD(&group->user_node);
 	INIT_LIST_HEAD(&group->wait_node);
 	INIT_LIST_HEAD(&group->run_node);
 	INIT_WORK(&group->term_work, group_term_work);
@@ -3760,8 +3773,13 @@ int panthor_group_create(struct panthor_file *pfile,
 	mutex_lock(&sched->reset.lock);
 	if (atomic_read(&sched->reset.in_progress)) {
 		panthor_group_stop(group);
+
+		mutex_lock(&sched->lock);
+		list_add_tail(&group->user_node, &sched->groups.user_owned);
+		mutex_unlock(&sched->lock);
 	} else {
 		mutex_lock(&sched->lock);
+		list_add_tail(&group->user_node, &sched->groups.user_owned);
 		list_add_tail(&group->run_node,
 			      &sched->groups.idle[group->priority]);
 		mutex_unlock(&sched->lock);
@@ -3801,7 +3819,7 @@ int panthor_group_destroy(struct panthor_file *pfile, u32 group_handle)
 
 	mutex_lock(&sched->reset.lock);
 	mutex_lock(&sched->lock);
-	group->destroyed = true;
+	list_del_init(&group->user_node);
 	if (group->csg_id >= 0) {
 		sched_queue_delayed_work(sched, tick, 0);
 	} else if (!atomic_read(&sched->reset.in_progress)) {
@@ -4167,6 +4185,7 @@ int panthor_sched_init(struct panthor_device *ptdev)
 		INIT_LIST_HEAD(&sched->groups.idle[prio]);
 	}
 	INIT_LIST_HEAD(&sched->groups.waiting);
+	INIT_LIST_HEAD(&sched->groups.user_owned);
 
 	ret = drmm_mutex_init(&ptdev->base, &sched->reset.lock);
 	if (ret)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 15/17] drm/panthor: Fix the unplug logic
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (13 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 14/17] drm/panthor: Track user owned groups Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:25   ` sashiko-bot
  2026-08-13 10:57 ` [PATCH v3 16/17] drm/panthor: Add a debugfs knob to simulate unplug failures Boris Brezillon
                   ` (2 subsequent siblings)
  17 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

The current unplug logic is broken in multiple subtle ways:

1. it assumes that the HW is still accessible in multiple places,
   which goes against the very concept of hot-unplug
2. it doesn't take into account the fact the stop is a failible
   operation, and that we theoretically have no guarantee that the HW
   is actually stopped after we've released the resources

Those issues are hard to reason about because Mali GPUs are on a
platform bus, which is not hot-pluggable, so they are in practice
always accessible as long as we can enable their dependencies (clocks,
power-domain, ...). The problem is, if the GPU is in such a bad state
it can't properly reset/resume, there are various operations that can't
be done properly, and the unplug logic is clearly not ready for that.
And more importantly, if we can't guarantee the reset was effective,
we have to assume the HW still has access to the resource we passed to
it, meaning we can't return these resources to the system without
risking a UAF.

This patch does several things:

- it resets the GPU before calling the <component>_unplug() functions
- it drops the pm_get/put that around the sub-component unplug calls
  (no longer needed if we assume the HW is gone and can't be accessed
  anymore)
- it changes the _unplug() implementations to not touch the HW anymore
- it let's each component know whether it should leak resources the HW
  might have its hands on at the time the unplug happens
- it releases all resources at unplug time even if open FDs exist. This
  is needed otherwise we could have deferred cleanup work accessing
  objects that have been freed

Unfortunately, I couldn't find a way to break things into multiple
commits while preserving bisectability.

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c |  43 +++++++++--
 drivers/gpu/drm/panthor/panthor_device.h |  15 ++++
 drivers/gpu/drm/panthor/panthor_drv.c    | 122 ++++++++++++++++++++++++-------
 drivers/gpu/drm/panthor/panthor_fw.c     |   9 +--
 drivers/gpu/drm/panthor/panthor_mmu.c    | 108 ++++++++++++++++++---------
 drivers/gpu/drm/panthor/panthor_mmu.h    |   1 +
 drivers/gpu/drm/panthor/panthor_sched.c  |  83 ++++++++++++++++++++-
 7 files changed, 304 insertions(+), 77 deletions(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index 0ccdf392d194..db7bbe503d53 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -62,8 +62,34 @@ static int panthor_init_power(struct device *dev)
 	return devm_pm_domain_attach_list(dev, NULL, &pd_list);
 }
 
+static int panthor_device_stop_before_unplug(struct panthor_device *ptdev)
+{
+	int ret;
+
+	/* Make sure any further modification to the existing VMs are blocked
+	 * before proceeding with the SOFT_RESET.
+	 */
+	panthor_mmu_freeze_before_unplug(ptdev);
+
+	/* Core clock should be enough to issue a reset. */
+	ret = clk_prepare_enable(ptdev->clks.core);
+	if (ret)
+		return ret;
+
+	/* A successful soft-reset should guarantee that all components of the
+	 * HW are off, meaning we can proceed with the rest of the unplug
+	 * procedure.
+	 */
+	ret = panthor_hw_soft_reset(ptdev);
+
+	clk_disable_unprepare(ptdev->clks.core);
+	return ret;
+}
+
 void panthor_device_unplug(struct panthor_device *ptdev)
 {
+	int ret;
+
 	/* This function can be called from two different path: the reset work
 	 * and the platform device remove callback. drm_dev_unplug() doesn't
 	 * deal with concurrent callers, so we have to protect drm_dev_unplug()
@@ -90,6 +116,16 @@ void panthor_device_unplug(struct panthor_device *ptdev)
 	 */
 	drm_dev_unplug(&ptdev->base);
 
+	/* Do anything we can to stop the HW. If we can't guarantee that the HW
+	 * is fully stopped, we also can't guarantee the resources it had access
+	 * to won't be touched after the device is gone (clocks and regulators
+	 * can be shared, and the HW might still be running behind our back).
+	 */
+	ret = panthor_device_stop_before_unplug(ptdev);
+	if (drm_WARN(&ptdev->base, ret,
+		     "Couldn't stop the device, this might lead to resource leaks"))
+		ptdev->unplug.leak_active_resources = true;
+
 	/* We do the rest of the unplug with the unplug lock released,
 	 * future callers will wait on ptdev->unplug.done anyway.
 	 */
@@ -112,13 +148,6 @@ void panthor_device_unplug(struct panthor_device *ptdev)
 	panthor_gpu_unplug(ptdev);
 	panthor_pwr_unplug(ptdev);
 
-	pm_runtime_dont_use_autosuspend(ptdev->base.dev);
-	pm_runtime_put_sync_suspend(ptdev->base.dev);
-
-	/* If PM is disabled, we need to call the suspend handler manually. */
-	if (!IS_ENABLED(CONFIG_PM))
-		panthor_device_suspend(ptdev->base.dev);
-
 	/* Report the unplug operation as done to unblock concurrent
 	 * panthor_device_unplug() callers.
 	 */
diff --git a/drivers/gpu/drm/panthor/panthor_device.h b/drivers/gpu/drm/panthor/panthor_device.h
index 10c96abf9cff..8c9177cf5da2 100644
--- a/drivers/gpu/drm/panthor/panthor_device.h
+++ b/drivers/gpu/drm/panthor/panthor_device.h
@@ -267,6 +267,21 @@ struct panthor_device {
 
 		/** @work: Unplug work. */
 		struct work_struct work;
+
+		/**
+		 * @leak_active_resources: Sub-components should leak resources HW has
+		 * access to.
+		 *
+		 * This is set to true when we can guarantee the HW has been fully stopped
+		 * in the unplug path. In that case, we'd rather leak resource than return
+		 * them to the system with the risk that they might be accessed by the
+		 * HW behind our back.
+		 *
+		 * This is particularly important for any piece of memory used by the GPU
+		 * (MMU page tables, FW sections, group resources shared with the FW,
+		 * any BO attached to an active VM, ...).
+		 */
+		bool leak_active_resources;
 	} unplug;
 
 	/** @reset: Reset related fields. */
diff --git a/drivers/gpu/drm/panthor/panthor_drv.c b/drivers/gpu/drm/panthor/panthor_drv.c
index 924a7ecd3733..6798b07d9dc7 100644
--- a/drivers/gpu/drm/panthor/panthor_drv.c
+++ b/drivers/gpu/drm/panthor/panthor_drv.c
@@ -1025,11 +1025,21 @@ static int panthor_ioctl_vm_destroy(struct drm_device *ddev, void *data,
 {
 	struct panthor_file *pfile = file->driver_priv;
 	struct drm_panthor_vm_destroy *args = data;
+	int cookie, ret;
 
-	if (args->pad)
-		return -EINVAL;
+	if (!drm_dev_enter(ddev, &cookie))
+		return -ENODEV;
 
-	return panthor_vm_pool_destroy_vm(pfile->vms, args->id);
+	if (args->pad) {
+		ret = -EINVAL;
+		goto out_dev_exit;
+	}
+
+	ret = panthor_vm_pool_destroy_vm(pfile->vms, args->id);
+
+out_dev_exit:
+	drm_dev_exit(cookie);
+	return ret;
 }
 
 #define PANTHOR_BO_FLAGS		(DRM_PANTHOR_BO_NO_MMAP | \
@@ -1219,11 +1229,21 @@ static int panthor_ioctl_group_destroy(struct drm_device *ddev, void *data,
 {
 	struct panthor_file *pfile = file->driver_priv;
 	struct drm_panthor_group_destroy *args = data;
+	int cookie, ret;
 
-	if (args->pad)
-		return -EINVAL;
+	if (!drm_dev_enter(ddev, &cookie))
+		return -ENODEV;
 
-	return panthor_group_destroy(pfile, args->group_handle);
+	if (args->pad) {
+		ret = -EINVAL;
+		goto out_dev_exit;
+	}
+
+	ret = panthor_group_destroy(pfile, args->group_handle);
+
+out_dev_exit:
+	drm_dev_exit(cookie);
+	return ret;
 }
 
 static int panthor_ioctl_group_create(struct drm_device *ddev, void *data,
@@ -1232,27 +1252,36 @@ static int panthor_ioctl_group_create(struct drm_device *ddev, void *data,
 	struct panthor_file *pfile = file->driver_priv;
 	struct drm_panthor_group_create *args = data;
 	struct drm_panthor_queue_create *queue_args;
-	int ret;
+	int cookie, ret;
 
-	if (!args->queues.count || args->queues.count > MAX_CS_PER_CSG)
-		return -EINVAL;
+	if (!drm_dev_enter(ddev, &cookie))
+		return -ENODEV;
+
+	if (!args->queues.count || args->queues.count > MAX_CS_PER_CSG) {
+		ret = -EINVAL;
+		goto out_dev_exit;
+	}
 
 	ret = PANTHOR_UOBJ_GET_ARRAY(queue_args, &args->queues);
 	if (ret)
-		return ret;
+		goto out_dev_exit;
 
 	ret = group_priority_permit(file, args->priority);
 	if (ret)
-		goto out;
+		goto out_free_args;
 
 	ret = panthor_group_create(pfile, args, queue_args, file->client_id);
 	if (ret < 0)
-		goto out;
+		goto out_free_args;
+
 	args->group_handle = ret;
 	ret = 0;
 
-out:
+out_free_args:
 	kvfree(queue_args);
+
+out_dev_exit:
+	drm_dev_exit(cookie);
 	return ret;
 }
 
@@ -1261,8 +1290,15 @@ static int panthor_ioctl_group_get_state(struct drm_device *ddev, void *data,
 {
 	struct panthor_file *pfile = file->driver_priv;
 	struct drm_panthor_group_get_state *args = data;
+	int cookie, ret;
 
-	return panthor_group_get_state(pfile, args);
+	if (!drm_dev_enter(ddev, &cookie))
+		return -ENODEV;
+
+	ret = panthor_group_get_state(pfile, args);
+
+	drm_dev_exit(cookie);
+	return ret;
 }
 
 static int panthor_ioctl_tiler_heap_create(struct drm_device *ddev, void *data,
@@ -1272,11 +1308,16 @@ static int panthor_ioctl_tiler_heap_create(struct drm_device *ddev, void *data,
 	struct drm_panthor_tiler_heap_create *args = data;
 	struct panthor_heap_pool *pool;
 	struct panthor_vm *vm;
-	int ret;
+	int cookie, ret;
+
+	if (!drm_dev_enter(ddev, &cookie))
+		return -ENODEV;
 
 	vm = panthor_vm_pool_get_vm(pfile->vms, args->vm_id);
-	if (!vm)
-		return -EINVAL;
+	if (!vm) {
+		ret = -EINVAL;
+		goto out_dev_exit;
+	}
 
 	pool = panthor_vm_get_heap_pool(vm, true);
 	if (IS_ERR(pool)) {
@@ -1305,6 +1346,9 @@ static int panthor_ioctl_tiler_heap_create(struct drm_device *ddev, void *data,
 
 out_put_vm:
 	panthor_vm_put(vm);
+
+out_dev_exit:
+	drm_dev_exit(cookie);
 	return ret;
 }
 
@@ -1315,14 +1359,21 @@ static int panthor_ioctl_tiler_heap_destroy(struct drm_device *ddev, void *data,
 	struct drm_panthor_tiler_heap_destroy *args = data;
 	struct panthor_heap_pool *pool;
 	struct panthor_vm *vm;
-	int ret;
+	int cookie, ret;
 
-	if (args->pad)
-		return -EINVAL;
+	if (!drm_dev_enter(ddev, &cookie))
+		return -ENODEV;
+
+	if (args->pad) {
+		ret = -EINVAL;
+		goto out_dev_exit;
+	}
 
 	vm = panthor_vm_pool_get_vm(pfile->vms, args->handle >> 16);
-	if (!vm)
-		return -EINVAL;
+	if (!vm) {
+		ret = -EINVAL;
+		goto out_dev_exit;
+	}
 
 	pool = panthor_vm_get_heap_pool(vm, false);
 	if (IS_ERR(pool)) {
@@ -1335,6 +1386,9 @@ static int panthor_ioctl_tiler_heap_destroy(struct drm_device *ddev, void *data,
 
 out_put_vm:
 	panthor_vm_put(vm);
+
+out_dev_exit:
+	drm_dev_exit(cookie);
 	return ret;
 }
 
@@ -1466,10 +1520,16 @@ static int panthor_ioctl_vm_get_state(struct drm_device *ddev, void *data,
 	struct panthor_file *pfile = file->driver_priv;
 	struct drm_panthor_vm_get_state *args = data;
 	struct panthor_vm *vm;
+	int cookie, ret;
+
+	if (!drm_dev_enter(ddev, &cookie))
+		return -ENODEV;
 
 	vm = panthor_vm_pool_get_vm(pfile->vms, args->vm_id);
-	if (!vm)
-		return -EINVAL;
+	if (!vm) {
+		ret = -EINVAL;
+		goto out_dev_exit;
+	}
 
 	if (panthor_vm_is_unusable(vm))
 		args->state = DRM_PANTHOR_VM_STATE_UNUSABLE;
@@ -1477,7 +1537,11 @@ static int panthor_ioctl_vm_get_state(struct drm_device *ddev, void *data,
 		args->state = DRM_PANTHOR_VM_STATE_USABLE;
 
 	panthor_vm_put(vm);
-	return 0;
+	ret = 0;
+
+out_dev_exit:
+	drm_dev_exit(cookie);
+	return ret;
 }
 
 static int panthor_ioctl_bo_set_label(struct drm_device *ddev, void *data,
@@ -1730,9 +1794,13 @@ static void panthor_show_internal_memory_stats(struct drm_printer *p, struct drm
 	char *drv_name = file->minor->dev->driver->name;
 	struct panthor_file *pfile = file->driver_priv;
 	struct drm_memory_stats stats = {0};
+	int cookie;
 
-	panthor_fdinfo_gather_group_mem_info(pfile, &stats);
-	panthor_vm_heaps_sizes(pfile, &stats);
+	if (drm_dev_enter(&pfile->ptdev->base, &cookie)) {
+		panthor_fdinfo_gather_group_mem_info(pfile, &stats);
+		panthor_vm_heaps_sizes(pfile, &stats);
+		drm_dev_exit(cookie);
+	}
 
 	drm_fdinfo_print_size(p, drv_name, "resident", "memory", stats.resident);
 	drm_fdinfo_print_size(p, drv_name, "active", "memory", stats.active);
diff --git a/drivers/gpu/drm/panthor/panthor_fw.c b/drivers/gpu/drm/panthor/panthor_fw.c
index fc1a423e48a8..8d9fdc3202a1 100644
--- a/drivers/gpu/drm/panthor/panthor_fw.c
+++ b/drivers/gpu/drm/panthor/panthor_fw.c
@@ -1285,11 +1285,9 @@ void panthor_fw_unplug(struct panthor_device *ptdev)
 
 	disable_delayed_work_sync(&ptdev->fw->watchdog.ping_work);
 
-	if (!IS_ENABLED(CONFIG_PM) || pm_runtime_active(ptdev->base.dev)) {
-		/* Make sure the IRQ handler cannot be called after that point. */
+	/* Make sure the IRQ handler cannot be called after that point. */
+	if (!IS_ENABLED(CONFIG_PM) || pm_runtime_active(ptdev->base.dev))
 		panthor_job_irq_suspend(&ptdev->fw->irq);
-		panthor_fw_stop(ptdev);
-	}
 
 	list_for_each_entry(section, &ptdev->fw->sections, node)
 		panthor_kernel_bo_destroy(section->mem);
@@ -1301,9 +1299,6 @@ void panthor_fw_unplug(struct panthor_device *ptdev)
 	 */
 	panthor_vm_put(ptdev->fw->vm);
 	ptdev->fw->vm = NULL;
-
-	if (!IS_ENABLED(CONFIG_PM) || pm_runtime_active(ptdev->base.dev))
-		panthor_hw_l2_power_off(ptdev);
 }
 
 /**
diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
index 4c01b0e4288b..0335cd2bebc6 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.c
+++ b/drivers/gpu/drm/panthor/panthor_mmu.c
@@ -1787,17 +1787,27 @@ panthor_vm_pool_get_vm(struct panthor_vm_pool *pool, u32 handle)
  */
 void panthor_vm_pool_destroy(struct panthor_file *pfile)
 {
+	struct panthor_device *ptdev = pfile->ptdev;
 	struct panthor_vm *vm;
 	unsigned long i;
+	int cookie;
 
 	if (!pfile->vms)
 		return;
 
-	xa_for_each(&pfile->vms->xa, i, vm)
-		panthor_vm_pool_destroy_vm(pfile->vms, i);
+	/* If device is gone VMs have been destroyed already, and the XArray
+	 * contains pointers to objects that have been freed.
+	 */
+	if (drm_dev_enter(&ptdev->base, &cookie)) {
+		xa_for_each(&pfile->vms->xa, i, vm)
+			panthor_vm_pool_destroy_vm(pfile->vms, i);
+
+		drm_dev_exit(cookie);
+	}
 
 	if (pfile->vms->dummy)
 		drm_gem_object_put(&pfile->vms->dummy->base);
+
 	xa_destroy(&pfile->vms->xa);
 	kfree(pfile->vms);
 }
@@ -2050,7 +2060,7 @@ void panthor_mmu_pre_suspend(struct panthor_device *ptdev)
 	panthor_mmu_irq_suspend(&ptdev->mmu->irq);
 }
 
-static void mmu_post_reset_cleanup(struct panthor_device *ptdev)
+static void mmu_post_reset_cleanup(struct panthor_device *ptdev, bool on_unplug)
 {
 	guard(mutex)(&ptdev->mmu->as.slots_lock);
 
@@ -2068,11 +2078,12 @@ static void mmu_post_reset_cleanup(struct panthor_device *ptdev)
 
 		panthor_as_release_hw_slot_locked(as);
 
-		/* FIXME: We shouldn't drop the no-unmap restriction if
-		 * we're in the unplug path and the device wasn't properly
-		 * stopped with a SOFT_RESET.
+		/* If this is an unplug situation and leak_active_resources is
+		 * true, we have to keep the no-unmap restriction to force a
+		 * resource leak.
 		 */
-		atomic_and(~PANTHOR_AS_FORBID_UNMAP, &as->restrictions);
+		if (!on_unplug || !ptdev->unplug.leak_active_resources)
+			atomic_and(~PANTHOR_AS_FORBID_UNMAP, &as->restrictions);
 	}
 
 	if (!list_empty(&ptdev->mmu->as.cleanup_list))
@@ -2081,7 +2092,7 @@ static void mmu_post_reset_cleanup(struct panthor_device *ptdev)
 
 void panthor_mmu_post_suspend(struct panthor_device *ptdev)
 {
-	mmu_post_reset_cleanup(ptdev);
+	mmu_post_reset_cleanup(ptdev, false);
 }
 
 /**
@@ -2095,7 +2106,7 @@ void panthor_mmu_post_suspend(struct panthor_device *ptdev)
  */
 void panthor_mmu_resume(struct panthor_device *ptdev)
 {
-	mmu_post_reset_cleanup(ptdev);
+	mmu_post_reset_cleanup(ptdev, false);
 	panthor_mmu_irq_resume(&ptdev->mmu->irq);
 }
 
@@ -2133,7 +2144,7 @@ void panthor_mmu_post_reset(struct panthor_device *ptdev)
 {
 	struct panthor_vm *vm;
 
-	mmu_post_reset_cleanup(ptdev);
+	mmu_post_reset_cleanup(ptdev, false);
 
 	panthor_mmu_irq_resume(&ptdev->mmu->irq);
 
@@ -2227,6 +2238,11 @@ static bool vm_prep_for_cleanup(struct panthor_vm *vm)
 	}
 
 	if (!drm_dev_enter(&ptdev->base, &cookie)) {
+		/* Device is gone, take the unplug lock to make sure
+		 * panthor_device_stop_before_unplug() has run and
+		 * ::leak_active_resources is valid.
+		 */
+		guard(mutex)(&ptdev->unplug.lock);
 		guard(mutex)(&ptdev->mmu->as.slots_lock);
 
 		/* If we're still on slot after an unplug, it means
@@ -3586,6 +3602,41 @@ panthor_mmu_reclaim_priv_bos(struct panthor_device *ptdev,
 	return freed;
 }
 
+void panthor_mmu_freeze_before_unplug(struct panthor_device *ptdev)
+{
+	struct panthor_vm *vm;
+
+	guard(mutex)(&ptdev->mmu->vm.lock);
+	guard(mutex)(&ptdev->mmu->as.slots_lock);
+	list_for_each_entry(vm, &ptdev->mmu->vm.list, node) {
+		/* We intentionally don't use panthor_vm_restrict_usage_locked() here
+		 * because we don't want the AS eviction to happen, otherwise we
+		 * won't be able to know which VMs were active at the time the
+		 * unplug happened. Unmap is forbidden to make sure any modification
+		 * to the VM is blocked after that point. This way, if the reset
+		 * fails, we're able to flag VMs that need to leak their resources.
+		 */
+		atomic_or(PANTHOR_AS_FORBID_USE |
+			  PANTHOR_AS_FORBID_MAP |
+			  PANTHOR_AS_FORBID_UNMAP,
+			  &vm->as->restrictions);
+	}
+}
+
+static struct panthor_vm *
+pop_user_owned_vm(struct panthor_device *ptdev)
+{
+	struct panthor_vm *vm;
+
+	guard(mutex)(&ptdev->mmu->vm.lock);
+	vm = list_first_entry_or_null(&ptdev->mmu->vm.user_owned,
+				      struct panthor_vm, user_node);
+	if (vm)
+		list_del_init(&vm->user_node);
+
+	return vm;
+}
+
 /**
  * panthor_mmu_unplug() - Unplug the MMU logic
  * @ptdev: Device.
@@ -3595,32 +3646,21 @@ panthor_mmu_reclaim_priv_bos(struct panthor_device *ptdev,
  */
 void panthor_mmu_unplug(struct panthor_device *ptdev)
 {
+	/* Collect non-destroyed user VMs so we can return the ref owned by the
+	 * XArray. If we don't do that, we leak all user VMs that were still
+	 * alive at the point drm_dev_unplug() was called, because
+	 * panthor_ioctl_vm_destroy() bails out early if the device is
+	 * unplugged.
+	 */
+	for (struct panthor_vm *vm = pop_user_owned_vm(ptdev); vm;
+	     vm = pop_user_owned_vm(ptdev)) {
+		panthor_vm_destroy(vm);
+	}
+
 	if (!IS_ENABLED(CONFIG_PM) || pm_runtime_active(ptdev->base.dev))
 		panthor_mmu_irq_suspend(&ptdev->mmu->irq);
 
-	mutex_lock(&ptdev->mmu->as.slots_lock);
-	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
-		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
-		int ret;
-
-		if (!as)
-			continue;
-
-		ret = panthor_mmu_as_disable(ptdev, i, false);
-		drm_WARN_ON(&ptdev->base, ret);
-
-		/* Drop the unmap restriction if the disabled worked, so we
-		 * don't leak resources in the normal situation.
-		 */
-		if (!ret)
-			atomic_and(~PANTHOR_AS_FORBID_UNMAP, &as->restrictions);
-
-		panthor_as_release_hw_slot_locked(as);
-	}
-
-	if (!list_empty(&ptdev->mmu->as.cleanup_list))
-		queue_work(panthor_cleanup_wq, &ptdev->mmu->vm.cleanup_work);
-	mutex_unlock(&ptdev->mmu->as.slots_lock);
+	mmu_post_reset_cleanup(ptdev, true);
 
 	/* Make sure pending VM cleanups are processed before leaving. Those
 	 * cleanups might schedule vm_bind_job cleanups, so keep this
@@ -3628,6 +3668,8 @@ void panthor_mmu_unplug(struct panthor_device *ptdev)
 	 */
 	flush_work(&ptdev->mmu->vm.cleanup_work);
 	drm_WARN_ON(&ptdev->base, !list_empty(&ptdev->mmu->as.cleanup_list));
+	drm_WARN_ON(&ptdev->base, !list_empty(&ptdev->mmu->vm.list));
+	drm_WARN_ON(&ptdev->base, !list_empty(&ptdev->mmu->vm.user_owned));
 
 	/* Ensure any pending job cleanup work are executed before returning,
 	 * otherwise those might access objects that are gone if the work is
diff --git a/drivers/gpu/drm/panthor/panthor_mmu.h b/drivers/gpu/drm/panthor/panthor_mmu.h
index a02875fd5d44..abd7a0f6338a 100644
--- a/drivers/gpu/drm/panthor/panthor_mmu.h
+++ b/drivers/gpu/drm/panthor/panthor_mmu.h
@@ -18,6 +18,7 @@ struct panthor_vma;
 struct panthor_mmu;
 
 int panthor_mmu_init(struct panthor_device *ptdev);
+void panthor_mmu_freeze_before_unplug(struct panthor_device *ptdev);
 void panthor_mmu_unplug(struct panthor_device *ptdev);
 void panthor_mmu_pre_reset(struct panthor_device *ptdev);
 void panthor_mmu_post_reset(struct panthor_device *ptdev);
diff --git a/drivers/gpu/drm/panthor/panthor_sched.c b/drivers/gpu/drm/panthor/panthor_sched.c
index 026f3105c646..2206dd44c2f9 100644
--- a/drivers/gpu/drm/panthor/panthor_sched.c
+++ b/drivers/gpu/drm/panthor/panthor_sched.c
@@ -3049,12 +3049,17 @@ static void update_fdinfo_stats(struct panthor_job *job)
 void panthor_fdinfo_gather_group_samples(struct panthor_file *pfile)
 {
 	struct panthor_group_pool *gpool = pfile->groups;
+	struct panthor_device *ptdev = pfile->ptdev;
 	struct panthor_group *group;
 	unsigned long i;
+	int cookie;
 
 	if (IS_ERR_OR_NULL(gpool))
 		return;
 
+	if (!drm_dev_enter(&ptdev->base, &cookie))
+		return;
+
 	xa_lock(&gpool->xa);
 	xa_for_each_marked(&gpool->xa, i, group, GROUP_REGISTERED) {
 		guard(spinlock)(&group->fdinfo.lock);
@@ -3064,6 +3069,8 @@ void panthor_fdinfo_gather_group_samples(struct panthor_file *pfile)
 		group->fdinfo.data.time = 0;
 	}
 	xa_unlock(&gpool->xa);
+
+	drm_dev_exit(cookie);
 }
 
 static bool queue_check_job_completion(struct panthor_queue *queue)
@@ -3897,14 +3904,23 @@ int panthor_group_pool_create(struct panthor_file *pfile)
 void panthor_group_pool_destroy(struct panthor_file *pfile)
 {
 	struct panthor_group_pool *gpool = pfile->groups;
+	struct panthor_device *ptdev = pfile->ptdev;
 	struct panthor_group *group;
 	unsigned long i;
+	int cookie;
 
 	if (IS_ERR_OR_NULL(gpool))
 		return;
 
-	xa_for_each(&gpool->xa, i, group)
-		panthor_group_destroy(pfile, i);
+	/* If device is gone groups have been destroyed already, and the XArray
+	 * contains pointers to objects that have been freed.
+	 */
+	if (drm_dev_enter(&ptdev->base, &cookie)) {
+		xa_for_each(&gpool->xa, i, group)
+			panthor_group_destroy(pfile, i);
+
+		drm_dev_exit(cookie);
+	}
 
 	xa_destroy(&gpool->xa);
 	kfree(gpool);
@@ -3923,11 +3939,16 @@ panthor_fdinfo_gather_group_mem_info(struct panthor_file *pfile,
 				     struct drm_memory_stats *stats)
 {
 	struct panthor_group_pool *gpool = pfile->groups;
+	struct panthor_device *ptdev = pfile->ptdev;
 	struct panthor_group *group;
 	unsigned long i;
+	int cookie;
+
+	if (!drm_dev_enter(&ptdev->base, &cookie))
+		return;
 
 	if (IS_ERR_OR_NULL(gpool))
-		return;
+		goto out_dev_exit;
 
 	xa_lock(&gpool->xa);
 	xa_for_each_marked(&gpool->xa, i, group, GROUP_REGISTERED) {
@@ -3936,6 +3957,9 @@ panthor_fdinfo_gather_group_mem_info(struct panthor_file *pfile,
 			stats->active += group->fdinfo.kbo_sizes;
 	}
 	xa_unlock(&gpool->xa);
+
+out_dev_exit:
+	drm_dev_exit(cookie);
 }
 
 static void job_release(struct kref *ref)
@@ -4081,23 +4105,76 @@ void panthor_job_update_resvs(struct drm_exec *exec, struct drm_sched_job *sched
 void panthor_sched_unplug(struct panthor_device *ptdev)
 {
 	struct panthor_scheduler *sched = ptdev->scheduler;
+	struct panthor_group *group, *tmp_group;
+	LIST_HEAD(groups);
 
 	disable_delayed_work_sync(&sched->tick_work);
 	disable_work_sync(&sched->fw_events_work);
 	disable_work_sync(&sched->sync_upd_work);
 
 	mutex_lock(&sched->lock);
+
+	/* Do a pass on the on-slot groups, and schedule termination. */
+	for (u32 i = 0; i < sched->csg_slot_count; i++) {
+		struct panthor_csg_slot *csg_slot = &sched->csg_slots[i];
+		struct panthor_group *group = csg_slot->group;
+
+		if (!group)
+			continue;
+
+		group_get(group);
+		group->state = PANTHOR_CS_GROUP_TERMINATED;
+		group_unbind_locked(group);
+		list_del_init(&group->wait_node);
+		group_queue_work(group, term);
+
+		group_put(group);
+	}
+
+	/* Now take care of the non-resident groups. */
+	for (u32 i = 0; i < ARRAY_SIZE(sched->groups.runnable); i++)
+		list_splice_init(&sched->groups.runnable[i], &groups);
+
+	for (u32 i = 0; i < ARRAY_SIZE(sched->groups.idle); i++)
+		list_splice_init(&sched->groups.idle[i], &groups);
+
+	list_for_each_entry_safe(group, tmp_group, &groups, run_node) {
+		list_del_init(&group->run_node);
+		list_del_init(&group->wait_node);
+		group_queue_work(group, term);
+	}
+
+	/* All groups that still have a user handle need a group_put()
+	 * because after drm_dev_unplug() has been called those handles
+	 * can't be released through the GROUP_DESTROY IOCTL anymore.
+	 */
+	list_for_each_entry_safe(group, tmp_group, &sched->groups.user_owned, user_node) {
+		list_del_init(&group->user_node);
+		group_put(group);
+	}
+
 	if (sched->pm.has_ref) {
 		pm_runtime_put(ptdev->base.dev);
 		sched->pm.has_ref = false;
 	}
 	mutex_unlock(&sched->lock);
 
+	/* Ensure all term work are done. */
+	flush_workqueue(sched->wq);
+
 	/* Ensure any pending group release work are executed before returning,
 	 * otherwise those might access objects that are gone if the work is
 	 * executed after other components are unplugged.
 	 */
 	flush_workqueue(panthor_cleanup_wq);
+
+	/* After we've flushed the workqueues, all lists should be empty. */
+	drm_WARN_ON(&ptdev->base, !list_empty(&sched->groups.user_owned));
+	for (u32 i = 0; i < ARRAY_SIZE(sched->groups.runnable); i++)
+		drm_WARN_ON(&ptdev->base, !list_empty(&sched->groups.runnable[i]));
+
+	for (u32 i = 0; i < ARRAY_SIZE(sched->groups.idle); i++)
+		drm_WARN_ON(&ptdev->base, !list_empty(&sched->groups.idle[i]));
 }
 
 static void panthor_sched_fini(struct drm_device *ddev, void *res)

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 16/17] drm/panthor: Add a debugfs knob to simulate unplug failures
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (14 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 15/17] drm/panthor: Fix the unplug logic Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 10:57 ` [PATCH v3 17/17] drm/panthor: Add a debugfs knobs to simulate reset failures Boris Brezillon
  2026-08-13 11:23 ` [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

Unplug failures are almost impossible to reproduce in practice, so let's
add a debugfs knob to simulate those.

With this new knob, we can check this error case with the following
sequence:

 # echo 1 > /sys/kernel/debug/dri/128/fake_unplug_failure
 # <start-some-GPU-workload>
 # echo fb000000.gpu > /sys/module/panthor/drivers/platform\:panthor/unbind
 # <stop-the-GPU-workload>

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c | 28 ++++++++++++++++++++++++++++
 drivers/gpu/drm/panthor/panthor_device.h |  8 ++++++++
 2 files changed, 36 insertions(+)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index db7bbe503d53..c6d4a97d60f6 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -4,6 +4,7 @@
 /* Copyright 2023 Collabora ltd. */
 /* Copyright 2025 ARM Limited. All rights reserved. */
 
+#include <linux/debugfs.h>
 #include <linux/clk.h>
 #include <linux/mm.h>
 #include <linux/platform_device.h>
@@ -81,6 +82,8 @@ static int panthor_device_stop_before_unplug(struct panthor_device *ptdev)
 	 * procedure.
 	 */
 	ret = panthor_hw_soft_reset(ptdev);
+	if (!ret && ptdev->unplug.fake_failure)
+		ret = -EIO;
 
 	clk_disable_unprepare(ptdev->clks.core);
 	return ret;
@@ -690,8 +693,33 @@ int panthor_device_suspend(struct device *dev)
 }
 
 #ifdef CONFIG_DEBUG_FS
+static int panthor_device_fake_unplug_failure_get(void *data, u64 *val)
+{
+	struct panthor_device *ptdev = data;
+
+	*val = ptdev->unplug.fake_failure ? 1 : 0;
+	return 0;
+}
+
+static int panthor_device_fake_unplug_failure_set(void *data, u64 val)
+{
+	struct panthor_device *ptdev = data;
+
+	ptdev->unplug.fake_failure = val ? true : false;
+	return 0;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(panthor_device_fake_unplug_failure_fops,
+			 panthor_device_fake_unplug_failure_get,
+			 panthor_device_fake_unplug_failure_set, "%llu\n");
+
 void panthor_device_debugfs_init(struct drm_minor *minor)
 {
+	struct panthor_device *ptdev = container_of(minor->dev, struct panthor_device, base);
+
+	debugfs_create_file("fake_unplug_failure", 0644,
+			    minor->debugfs_root, ptdev,
+			    &panthor_device_fake_unplug_failure_fops);
 	panthor_mmu_debugfs_init(minor);
 	panthor_gem_debugfs_init(minor);
 }
diff --git a/drivers/gpu/drm/panthor/panthor_device.h b/drivers/gpu/drm/panthor/panthor_device.h
index 8c9177cf5da2..b2788373bfa9 100644
--- a/drivers/gpu/drm/panthor/panthor_device.h
+++ b/drivers/gpu/drm/panthor/panthor_device.h
@@ -282,6 +282,14 @@ struct panthor_device {
 		 * any BO attached to an active VM, ...).
 		 */
 		bool leak_active_resources;
+
+		/**
+		 * @fake_failure: When true, pretend the SOFT_RESET in the unplug path failed.
+		 *
+		 * This is important to check that we're doing the right thing in this very
+		 * unlikely case.
+		 */
+		bool fake_failure;
 	} unplug;
 
 	/** @reset: Reset related fields. */

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* [PATCH v3 17/17] drm/panthor: Add a debugfs knobs to simulate reset failures
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (15 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 16/17] drm/panthor: Add a debugfs knob to simulate unplug failures Boris Brezillon
@ 2026-08-13 10:57 ` Boris Brezillon
  2026-08-13 11:23 ` [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
  17 siblings, 0 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 10:57 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, Boris Brezillon

It's almost impossible to trigger a situation where the reset
doesn't work now that the driver is more mature, so let's add two
knobs to exercise this error path:

- a knob to trigger a reset
- a knob to fake an error in the reset path

Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
---
 drivers/gpu/drm/panthor/panthor_device.c | 45 +++++++++++++++++++++++++++++++-
 drivers/gpu/drm/panthor/panthor_device.h |  8 ++++++
 2 files changed, 52 insertions(+), 1 deletion(-)

diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
index c6d4a97d60f6..242bf7e538df 100644
--- a/drivers/gpu/drm/panthor/panthor_device.c
+++ b/drivers/gpu/drm/panthor/panthor_device.c
@@ -200,7 +200,10 @@ static void panthor_device_reset_work(struct work_struct *work)
 	panthor_hw_soft_reset(ptdev);
 	panthor_hw_l2_power_on(ptdev);
 	panthor_mmu_post_reset(ptdev);
-	ret = panthor_fw_post_reset(ptdev);
+	if (ptdev->reset.fake_failure)
+		ret = -EIO;
+	else
+		ret = panthor_fw_post_reset(ptdev);
 	panthor_sched_post_reset(ptdev, ret != 0);
 	drm_dev_exit(cookie);
 
@@ -713,6 +716,40 @@ DEFINE_DEBUGFS_ATTRIBUTE(panthor_device_fake_unplug_failure_fops,
 			 panthor_device_fake_unplug_failure_get,
 			 panthor_device_fake_unplug_failure_set, "%llu\n");
 
+static int panthor_device_fake_fw_reset_failure_get(void *data, u64 *val)
+{
+	struct panthor_device *ptdev = data;
+
+	*val = ptdev->reset.fake_failure ? 1 : 0;
+	return 0;
+}
+
+static int panthor_device_fake_fw_reset_failure_set(void *data, u64 val)
+{
+	struct panthor_device *ptdev = data;
+
+	ptdev->reset.fake_failure = val ? true : false;
+	return 0;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(panthor_device_fake_fw_reset_failure_fops,
+			 panthor_device_fake_fw_reset_failure_get,
+			 panthor_device_fake_fw_reset_failure_set, "%llu\n");
+
+static ssize_t panthor_device_reset_file_write(struct file *file,
+					       const char __user *, size_t size,
+					       loff_t *)
+{
+	struct panthor_device *ptdev = file_inode(file)->i_private;
+
+	panthor_device_schedule_reset(ptdev);
+	return size;
+}
+
+static const struct debugfs_short_fops panthor_device_reset_fops = {
+	.write = panthor_device_reset_file_write,
+};
+
 void panthor_device_debugfs_init(struct drm_minor *minor)
 {
 	struct panthor_device *ptdev = container_of(minor->dev, struct panthor_device, base);
@@ -720,6 +757,12 @@ void panthor_device_debugfs_init(struct drm_minor *minor)
 	debugfs_create_file("fake_unplug_failure", 0644,
 			    minor->debugfs_root, ptdev,
 			    &panthor_device_fake_unplug_failure_fops);
+	debugfs_create_file("fake_fw_reset_failure", 0644,
+			    minor->debugfs_root, ptdev,
+			    &panthor_device_fake_fw_reset_failure_fops);
+	debugfs_create_file("reset", 0200,
+			    minor->debugfs_root, ptdev,
+			    &panthor_device_reset_fops);
 	panthor_mmu_debugfs_init(minor);
 	panthor_gem_debugfs_init(minor);
 }
diff --git a/drivers/gpu/drm/panthor/panthor_device.h b/drivers/gpu/drm/panthor/panthor_device.h
index b2788373bfa9..160593824ef2 100644
--- a/drivers/gpu/drm/panthor/panthor_device.h
+++ b/drivers/gpu/drm/panthor/panthor_device.h
@@ -313,6 +313,14 @@ struct panthor_device {
 		 * all FW sections to make sure we start from a fresh state.
 		 */
 		bool fast;
+
+		/**
+		 * @fake_failure: When true, pretend the FW boot in the reset path failed.
+		 *
+		 * This is important to check that we're doing the right thing in this very
+		 * unlikely case.
+		 */
+		bool fake_failure;
 	} reset;
 
 	/** @pm: Power management related data. */

-- 
2.55.0


^ permalink raw reply related	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 03/17] drm/panthor: Make sure reset requests in the resume path are not lost
  2026-08-13 10:57 ` [PATCH v3 03/17] drm/panthor: Make sure reset requests in the resume path are not lost Boris Brezillon
@ 2026-08-13 11:09   ` sashiko-bot
  0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-13 11:09 UTC (permalink / raw)
  To: Boris Brezillon; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Incorrect use of `smp_mb__after_atomic()` after `atomic_set()` fails to provide a guaranteed full memory barrier, potentially leading to a lost reset due to Store-Load reordering.
- [Low] Missing word in the newly added inline comment makes it grammatically incomplete and confusing.
--

--- Patch [3]: [PATCH v3 03/17] drm/panthor: Make sure reset requests in the resume path are not lost ---
commit df28c58d554a7a21bebf387cbb4ce59c9aa88cf4
Author: Boris Brezillon <boris.brezillon@collabora.com>

drm/panthor: Make sure reset requests in the resume path are not lost

A reset might have been queued while we were resuming. Make sure it's
not lost by rescheduling it.

Closes: https://sashiko.dev/#/patchset/20260804-panthor-unplug-fixes-v1-0-abbbd2d41b13@collabora.com?part=2

> diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
> index 47dc7faf9728c..1a8f5ac24399a 100644
> --- a/drivers/gpu/drm/panthor/panthor_device.c
> +++ b/drivers/gpu/drm/panthor/panthor_device.c
> @@ -569,7 +569,24 @@ int panthor_device_resume(struct device *dev)
>  	unmap_mapping_range(ptdev->base.anon_inode->i_mapping,
>  			    DRM_PANTHOR_USER_MMIO_OFFSET, 0, 1);
>  	atomic_set(&ptdev->pm.state, PANTHOR_DEVICE_PM_STATE_ACTIVE);
> +
> +	/* The smp_mb__after_atomic() is here to make sure the pm.state update
> +	 * is flushed before we check the reset.pending bit, otherwise,
> +	 * according to Sashiko, there's a with the checks that exist in
                                          ^^^^^^
[Severity: Low]
This isn't a bug, but it looks like a word is missing here. Did you mean
"there's a race with the checks"?

> +	 * panthor_device_schedule_reset().
> +	 * Not sure how real this is, because the try_cmpxchg_release() in
> +	 * the mutex_unlock() path also acts as a barrier, but it's not a
> +	 * fast-path anyway, so better safe than sorry.
> +	 */
> +	smp_mb__after_atomic();

[Severity: High]
Does this actually provide the intended memory barrier?

Since atomic_set() is not a read-modify-write (RMW) operation,
smp_mb__after_atomic() doesn't guarantee a full memory barrier. On some
architectures, it is defined merely as a compiler barrier.

This means the CPU could reorder the subsequent load of ptdev->reset.pending
before the store to pm.state becomes globally visible (Store-Load reordering).

If this reordering happens concurrently with panthor_device_schedule_reset()
setting reset.pending = 1 and checking pm.state, could this regression lead
to both threads failing to queue the reset work?

Also, can the try_cmpxchg_release() in the mutex_unlock() path prevent this?

Since release semantics only prevent prior operations from moving past the
release, they don't stop subsequent loads (like reading reset.pending) from
being hoisted before the release.

Should this use a full smp_mb() instead?

>  	mutex_unlock(&ptdev->pm.mmio_lock);
> +
> +	/* A reset might have been queued while we were resuming. Make sure
> +	 * it's not lost by rescheduling it.
> +	 */
> +	if (atomic_read(&ptdev->reset.pending))
> +		queue_work(ptdev->reset.wq, &ptdev->reset.work);
> +
>  	return 0;
>  
>  err_suspend_devfreq:

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260813-panthor-unplug-fixes-v3-0-3ed4e961bbe7@collabora.com?part=3

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 11/17] drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap
  2026-08-13 10:57 ` [PATCH v3 11/17] drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap Boris Brezillon
@ 2026-08-13 11:12   ` sashiko-bot
  0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-13 11:12 UTC (permalink / raw)
  To: Boris Brezillon; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [Critical] panthor_kernel_bo_destroy ignores the return value of panthor_vm_unmap_range, which can fail with -ENOMEM, leaving physical memory mapped in the GPU after it is freed.
--

--- Patch [11]: [PATCH v3 11/17] drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap ---
commit e1c4ff0ccf43825a86dfb74945a18fbc59b6d1ca
Author: Boris Brezillon <boris.brezillon@collabora.com>

drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap

In the cleanup path, we unmap the full VA range to make sure things are
clean before the VM is released. I'd rather not fail on memory
allocation in that path, so let's make sure
panthor_vm_pgtable_prepare_unmap_op_ctx() doesn't allocate VMAs or
page tables when the unmap range matches the VM virtual address range.

> diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
> index a48466788f4db..acdc0dd04f8be 100644
> --- a/drivers/gpu/drm/panthor/panthor_mmu.c
> +++ b/drivers/gpu/drm/panthor/panthor_mmu.c
> @@ -1466,6 +1466,10 @@ static int panthor_as_prepare_unmap_op_ctx(struct panthor_as_op_ctx *op_ctx,
>  	op_ctx->va.addr = va;
>  	op_ctx->flags = DRM_PANTHOR_VM_BIND_OP_TYPE_UNMAP;
>  
> +	/* Unmap on the whole VM range don't need new VMAs or page tables. */
> +	if (va == as->base.mm_start && size == as->base.mm_range)
> +		return 0;
> +

[Severity: Critical]
This is a pre-existing issue, but I noticed that memory allocation failures 
during teardown of a kernel BO can lead to a use-after-free condition.

Because this new early return only applies to full VM unmaps, non-full unmaps 
like those in panthor_kernel_bo_destroy() will still attempt allocations via
panthor_as_op_ctx_prealloc_vmas() which can fail with -ENOMEM.

If memory pressure causes this allocation to fail, panthor_vm_unmap_range() 
will abort and return -ENOMEM. However, panthor_kernel_bo_destroy() ignores 
this return value:

drivers/gpu/drm/panthor/panthor_gem.c:panthor_kernel_bo_destroy() {
	...
	panthor_vm_unmap_range(vm, bo->va_node.start, bo->va_node.size);
	panthor_vm_free_va(vm, &bo->va_node);
	if (vm == panthor_fw_vm(ptdev))
		panthor_gem_unpin(to_panthor_bo(bo->obj));
	drm_gem_object_put(bo->obj);
}

Does this leave physical memory mapped in the GPU after the underlying GEM 
object is destroyed and freed back to the system? 

If a local user can trigger this by creating and destroying scheduling groups 
or heap pools under memory pressure, could they exploit the stale GPU mapping 
to access newly allocated kernel or user memory?

>  	/* Pre-allocate L3 page tables to account for the split-2M-block
>  	 * situation on unmap.
>  	 */

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260813-panthor-unplug-fixes-v3-0-3ed4e961bbe7@collabora.com?part=11

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 05/17] drm/panthor: Flush the cleanup_wq in the unplug path
  2026-08-13 10:57 ` [PATCH v3 05/17] drm/panthor: Flush the cleanup_wq in the unplug path Boris Brezillon
@ 2026-08-13 11:18   ` sashiko-bot
  0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-13 11:18 UTC (permalink / raw)
  To: Boris Brezillon; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

New issues:
- [Medium] Flushing a global, module-wide workqueue inside a device-specific unplug path stalls the unplug process for all active devices.

Pre-existing issues:
- [High] The patch adds `flush_workqueue(panthor_cleanup_wq)` to prevent Use-After-Free (UAF), but it does not prevent new work items from being queued after the flush completes, leaving a race window that still triggers UAFs.
--

--- Patch [5]: [PATCH v3 05/17] drm/panthor: Flush the cleanup_wq in the unplug path ---
commit ab033ca0df2b4b2d5ba70d4e669044aa6bb08bda
Author: Boris Brezillon <boris.brezillon@collabora.com>

drm/panthor: Flush the cleanup_wq in the unplug path

If we don't do that, we might face various UAFs, because the resource
referenced by these work items might be gone by the time they get
executed.

Closes: https://sashiko.dev/#/patchset/20260625-panthor-signal-from-irq-v5-0-8836a74e0ef9@collabora.com?part=2

> diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
> index 0182b72f19320..0b862d3c36058 100644
> --- a/drivers/gpu/drm/panthor/panthor_mmu.c
> +++ b/drivers/gpu/drm/panthor/panthor_mmu.c
> @@ -3365,6 +3365,12 @@ void panthor_mmu_unplug(struct panthor_device *ptdev)
>  		}
>  	}
>  	mutex_unlock(&ptdev->mmu->as.slots_lock);
> +
> +	/* Ensure any pending job cleanup work are executed before returning,
> +	 * otherwise those might access objects that are gone if the work is
> +	 * executed after other components are unplugged.
> +	 */
> +	flush_workqueue(panthor_cleanup_wq);
>  }

[Severity: Medium]
Since panthor_cleanup_wq is allocated globally in panthor_drv_init() as a
module-wide workqueue, will flushing it here stall the teardown for all
active devices?

If the system has multiple Panthor GPUs and another device has a backlog of
jobs or a stuck work item, it seems this could block the current device's
unplug thread until all unrelated work items complete.

> diff --git a/drivers/gpu/drm/panthor/panthor_sched.c b/drivers/gpu/drm/panthor/panthor_sched.c
> index 5832dccfc0932..f18b2e03f2fd8 100644
> --- a/drivers/gpu/drm/panthor/panthor_sched.c
> +++ b/drivers/gpu/drm/panthor/panthor_sched.c
[ ... ]
> @@ -4074,6 +4074,12 @@ void panthor_sched_unplug(struct panthor_device *ptdev)
>  		sched->pm.has_ref = false;
>  	}
>  	mutex_unlock(&sched->lock);
> +
> +	/* Ensure any pending group release work are executed before returning,
> +	 * otherwise those might access objects that are gone if the work is
> +	 * executed after other components are unplugged.
> +	 */
> +	flush_workqueue(panthor_cleanup_wq);
>  }

[Severity: High]
This isn't a bug introduced by this patch, but does this flush actually
prevent the UAF if userspace still holds an open DRM file descriptor?

When userspace closes the file descriptor after the unplug completes, it
triggers the teardown path:
drm_file_free() -> panthor_postclose() -> panthor_group_pool_destroy()

This drops group references, calling group_release(), which will call:
queue_work(panthor_cleanup_wq, &group->release_work);

Once drm_file_free() completes, the last reference drops and drm_dev_release()
synchronously frees the panthor_device memory. Then group->release_work executes
asynchronously on the workqueue, accessing the freed group->ptdev and its
resources.

A similar race seems to exist in panthor_vm_bind_job_release() in
panthor_mmu.c, where dropped VM bind jobs queue cleanup_op_ctx_work after
the MMU flush. Can an unprivileged user trigger a GPU reset, wait for the
unplug, and then close the file descriptor to reliably hit this UAF?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260813-panthor-unplug-fixes-v3-0-3ed4e961bbe7@collabora.com?part=5

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 13/17] drm/panthor: Track user owned VMs
  2026-08-13 10:57 ` [PATCH v3 13/17] drm/panthor: Track user owned VMs Boris Brezillon
@ 2026-08-13 11:18   ` sashiko-bot
  0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-13 11:18 UTC (permalink / raw)
  To: Boris Brezillon; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] The kernel-doc comment for the new `user_owned` list incorrectly references `@vm.list` instead of `@vm.user_owned` and contains inaccurate behavioral claims.
--

--- Patch [13]: [PATCH v3 13/17] drm/panthor: Track user owned VMs ---
commit ac0f4e912e13271ec8810a284d5bae2a86d22478
Author: Boris Brezillon <boris.brezillon@collabora.com>

drm/panthor: Track user owned VMs

This commit introduces tracking for user-owned VMs in the panthor driver by
adding a new user_owned list to the panthor_mmu structure. This will be
needed to fix unplug logic and prevent panthor_vm objects from being left
behind.

> diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
> index 5f4aab5813d52..4c01b0e4288be 100644
> --- a/drivers/gpu/drm/panthor/panthor_mmu.c
> +++ b/drivers/gpu/drm/panthor/panthor_mmu.c
> @@ -107,12 +107,15 @@ struct panthor_mmu {
>  
>  	/** @vm: VMs management fields */
>  	struct {
> -		/** @vm.lock: Lock protecting access to list. */
> +		/** @vm.lock: Lock protecting access to list and user_owned. */
>  		struct mutex lock;
>  
>  		/** @vm.list: List containing all VMs. */
>  		struct list_head list;
>  
> +		/** @vm.list: List containing VMs with a valid handle. */
> +		struct list_head user_owned;

[Severity: Low]
This isn't a bug, but is this kernel-doc comment accurate? It references
@vm.list instead of @vm.user_owned due to what looks like a copy-paste
error.

Also, the comment mentions it contains "VMs with a valid handle", but
looking at panthor_vm_pool_create_vm(), the VM is added to this list
before xa_alloc() assigns a handle:

	scoped_guard(mutex, &ptdev->mmu->vm.lock)
		list_add_tail(&vm->user_node, &ptdev->mmu->vm.user_owned);

	ret = xa_alloc(&pool->xa, &id, vm, ...

Should the description be updated to reflect that it might briefly contain
VMs that have not yet been assigned a handle?

[ ... ]

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260813-panthor-unplug-fixes-v3-0-3ed4e961bbe7@collabora.com?part=13

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 12/17] drm/panthor: Make the VM cleanup path more robust against UAF
  2026-08-13 10:57 ` [PATCH v3 12/17] drm/panthor: Make the VM cleanup path more robust against UAF Boris Brezillon
@ 2026-08-13 11:20   ` sashiko-bot
  0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-13 11:20 UTC (permalink / raw)
  To: Boris Brezillon; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [High] Self-deadlock and lockdep violation in panthor_mmu_pre_suspend when AS disable fails.
- [High] Unconditional removal of PANTHOR_AS_FORBID_UNMAP leads to Use-After-Free if soft reset fails.
--

--- Patch [12]: [PATCH v3 12/17] drm/panthor: Make the VM cleanup path more robust against UAF ---
commit 5812e6e60c1cdbc7bb20a9803d7bf5a58663d7d2
Author: Boris Brezillon <boris.brezillon@collabora.com>
Subject: drm/panthor: Make the VM cleanup path more robust against UAF

The commit defers the cleanup of VMs when a hardware hang prevents eviction
of a page table from its AS slot, waiting for the next post_reset. It
introduces a cleanup list for VMs and schedules a workqueue to process it
once the hardware is confirmed idle.

> diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
> index acdc0dd04f8be..5f4aab5813d52 100644
> --- a/drivers/gpu/drm/panthor/panthor_mmu.c
> +++ b/drivers/gpu/drm/panthor/panthor_mmu.c
[ ... ]
> @@ -1985,23 +2002,67 @@ static void panthor_mmu_irq_handler(struct panthor_device *ptdev, u32 status)
>   *
>   * We also suspend the MMU IRQ.
>   */
> -void panthor_mmu_suspend(struct panthor_device *ptdev)
> +void panthor_mmu_pre_suspend(struct panthor_device *ptdev)
>  {
>  	mutex_lock(&ptdev->mmu->as.slots_lock);
>  	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
>  		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
> +		int ret;
>  
> -		if (as) {
> -			drm_WARN_ON(&ptdev->base,
> -				    panthor_mmu_as_disable(ptdev, i, false));
> +		if (!as)
> +			continue;
> +
> +		/* If the disable fails, leave the AS on its slot so we can
> +		 * properly evict it when we're sure the GPU is off.
> +		 */
> +		ret = panthor_mmu_as_disable(ptdev, i, false);
> +		if (!ret)
>  			panthor_as_release_hw_slot_locked(as);
> -		}
> +		else
> +			panthor_as_restrict_usage_locked(as,
> +							 PANTHOR_AS_FORBID_USE |
> +							 PANTHOR_AS_FORBID_MAP |
> +							 PANTHOR_AS_FORBID_UNMAP);
>  	}
>  	mutex_unlock(&ptdev->mmu->as.slots_lock);

[Severity: High]
Does this code cause a lockdep violation and self-deadlock if the hardware
hangs and panthor_mmu_as_disable() fails?

The panthor_mmu_pre_suspend() function acquires ptdev->mmu->as.slots_lock
before the loop. When calling panthor_as_restrict_usage_locked(), that
function asserts lockdep_assert_held(&as->op_lock) which doesn't appear
to be held here.

Furthermore, panthor_as_restrict_usage_locked() immediately tries to
acquire the slots_lock again:

static void panthor_as_restrict_usage_locked(...)
{
	...
	if (new_restrictions & PANTHOR_AS_FORBID_USE) {
		guard(mutex)(&ptdev->mmu->as.slots_lock);

Since standard mutexes are non-recursive, won't this self-deadlock the
thread performing the suspend?

>  
>  	panthor_mmu_irq_suspend(&ptdev->mmu->irq);
>  }
>  
> +static void mmu_post_reset_cleanup(struct panthor_device *ptdev)
> +{
> +	guard(mutex)(&ptdev->mmu->as.slots_lock);
> +
> +	/* Now that the reset is effective, we can assume that none of the
> +	 * AS slots are setup, and clear the faulty flags too.
> +	 */
> +	ptdev->mmu->as.alloc_mask = 0;
> +	ptdev->mmu->as.faulty_mask = 0;
> +
> +	for (u32 i = 0; i < ARRAY_SIZE(ptdev->mmu->as.slots); i++) {
> +		struct panthor_as *as = ptdev->mmu->as.slots[i].as;
> +
> +		if (!as)
> +			continue;
> +
> +		panthor_as_release_hw_slot_locked(as);
> +
> +		/* FIXME: We shouldn't drop the no-unmap restriction if
> +		 * we're in the unplug path and the device wasn't properly
> +		 * stopped with a SOFT_RESET.
> +		 */
> +		atomic_and(~PANTHOR_AS_FORBID_UNMAP, &as->restrictions);

[Severity: High]
Can this unconditional clearing lead to a use-after-free if a soft reset
fails to halt the GPU?

As noted in the FIXME comment, dropping this restriction might be dangerous.
If the device wasn't properly stopped, the hardware could still be actively
performing DMA. Dropping PANTHOR_AS_FORBID_UNMAP here will allow
vm_cleanup() to execute panthor_vm_unmap_range() and
panthor_heap_pool_destroy(), potentially freeing memory that the active
GPU can still access.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260813-panthor-unplug-fixes-v3-0-3ed4e961bbe7@collabora.com?part=12

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 00/17] drm/panthor: Fix the unplug logic
  2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
                   ` (16 preceding siblings ...)
  2026-08-13 10:57 ` [PATCH v3 17/17] drm/panthor: Add a debugfs knobs to simulate reset failures Boris Brezillon
@ 2026-08-13 11:23 ` Boris Brezillon
  2026-08-13 15:11   ` Liviu Dudau
  2026-08-13 18:50   ` Danilo Krummrich
  17 siblings, 2 replies; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 11:23 UTC (permalink / raw)
  To: Steven Price, Liviu Dudau
  Cc: Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann, David Airlie,
	Simona Vetter, dri-devel, linux-kernel, sashiko-bot,
	Danilo Krummrich

+Danilo, since you worked on the 'bound lifetime stuff in rust, and I
feel this is related to the problem I'm trying to fix here.

On Thu, 13 Aug 2026 12:56:58 +0200
Boris Brezillon <boris.brezillon@collabora.com> wrote:

> The current unplug logic is broken in multiple ways. This is an attempt
> at addressing the various problems found along the way (some were
> reported by Sashiko, others have been found while trying to address
> Sashiko's concerns).
> 
> Sending a new version even though v2 didn't receive any human review
> just to try and address the new stuff pointed out by Sashiko.

Just a note I forgot to add to my cover letter. I've already spent way
more time than I wanted on this, not just because Sashiko keeps finding
new issues at each of my attempt, but also because the whole idea of
pretending a device on a platform bus is unplugged and can't harm us is
doomed. This is not an hot-pluggable bus, and the device is still there,
so, unless we can be absolutely sure it's inactive (which a RESET can
provide, but RESETs are fallible) we just have two options:

1. prevent the device from going away until we managed to properly
   shutdown the GPU

2. make sure all resources the HW might have its hands on at the time
   the failure of RESET in the unplug path happened are leaked

Option 1 is no longer possible since platform_driver::remove() can't
return an error. That leaves options 2, which is basically what this
patchset is doing, but the whole idea of leaking resources when the
final RESET in the unplug path fails has various nasty implications,
like the fact we end up with dangling drm_device (drm_gpuvm retains a
ref, and each GPU mapping we kept alive in the gpuvm is what keeps the
gpuvm and the BOs alive). In practice, there should be no one
triggering operations on this drm_device, because all the user-facing
interfaces have been shutdown by drm_dev_unregister() (which is called
by drm_dev_unplug()), but as things stand now, this drm_device still
has access to module-specific vtables, and there's nothing retaining
the module either.

TLDR; this is all super fragile stuff, on the other hand the current
situation is probably even worse. so if anyone has any idea how to
handle this properly (or at least a bit better than we do), please let
me know. I know a lot of this stuff is currently being considered as
part of the drm-rust abstractions, so hopefully we have a long-term
solution for rust drivers, but I'd really like a short-term solution
for panthor that doesn't involve nasty tricks or overly complex
refactoring.

> 
> Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> ---
> Changes in v3:
> - Fix a race in the reset reschedule logic we added to
>   panthor_device_resume() (missing smp_mb__after_atomic())
> - Fix a VM leak when reset and suspend are racing with each other
> - Add missing drm_dev_enter/exit() sections
> - Insert the groups in the user_owned list even if the group creation
>   happens during a reset
> - Try to document why some of the issues pointed out by Sashiko are
>   either not real issues, or are expected (either fixed in a later
>   commits, or just expected behavior)
> - Fix a race between panthor_device_unplug() and vm_prep_for_cleanup()
>   (introduced in v2)
> - Link to v2: https://patch.msgid.link/20260811-panthor-unplug-fixes-v2-0-6b583e37f9ae@collabora.com
> 
> Changes in v2:
> - Fix UAFs caused by deferred cleanup works
> - Fix UAFs caused by open FDs closed after unplug
> - Fix deadlock when device_unplug() is called from the reset work
> - Make sure reset requests are not lost in the resume and post_reset
>   paths
> - Fix a deadlock in the suspend path
> - Fix a clk prepare_enable leak in the unplug path
> - Don't use a drmm_action to flush the cleanup queue (this could cause
>   UAFs)
> - Drop the now unused panthor_vm::unusable field
> - Keep track of user owned resources to prevent leaks and/or UAFs
> - Link to v1: https://patch.msgid.link/20260804-panthor-unplug-fixes-v1-0-abbbd2d41b13@collabora.com
> 
> ---
> Boris Brezillon (17):
>       drm/panthor: Disable reset work before unplug
>       drm/panthor: Further delay reset work enablement
>       drm/panthor: Make sure reset requests in the resume path are not lost
>       drm/panthor: Make sure reset requests in the post reset path are not lost
>       drm/panthor: Flush the cleanup_wq in the unplug path
>       drm/panthor: Drop unused vm argument passed to panthor_vm_prepare_sync_only_op_ctx()
>       drm/panthor: Move the debugfs initialization to panthor_device.c
>       drm/panthor: Split panthor_vm
>       drm/panthor: Add fine-grained restrictions on VMs
>       drm/panthor: Check AS state before disabling
>       drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap
>       drm/panthor: Make the VM cleanup path more robust against UAF
>       drm/panthor: Track user owned VMs
>       drm/panthor: Track user owned groups
>       drm/panthor: Fix the unplug logic
>       drm/panthor: Add a debugfs knob to simulate unplug failures
>       drm/panthor: Add a debugfs knobs to simulate reset failures
> 
>  drivers/gpu/drm/panthor/panthor_device.c |  189 +++-
>  drivers/gpu/drm/panthor/panthor_device.h |   38 +
>  drivers/gpu/drm/panthor/panthor_drv.c    |  132 ++-
>  drivers/gpu/drm/panthor/panthor_fw.c     |    9 +-
>  drivers/gpu/drm/panthor/panthor_mmu.c    | 1493 ++++++++++++++++++------------
>  drivers/gpu/drm/panthor/panthor_mmu.h    |    4 +-
>  drivers/gpu/drm/panthor/panthor_sched.c  |  132 ++-
>  7 files changed, 1344 insertions(+), 653 deletions(-)
> ---
> base-commit: 44e9eb5a762142a4aa46c0b5da7c39bfeb78910e
> change-id: 20260804-panthor-unplug-fixes-7927b3ddc2f9
> 
> Best regards,
> --  
> Boris Brezillon <boris.brezillon@collabora.com>
> 


^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 15/17] drm/panthor: Fix the unplug logic
  2026-08-13 10:57 ` [PATCH v3 15/17] drm/panthor: Fix the unplug logic Boris Brezillon
@ 2026-08-13 11:25   ` sashiko-bot
  0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-13 11:25 UTC (permalink / raw)
  To: Boris Brezillon; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Unconditional memory leak of non-resident VMs (including page tables and mapped BOs) during device unplug.
- [High] Groups in the `waiting` list are skipped during scheduler unplug, leading to leaked jobs and list corruption.
- [High] The device's PM runtime usage counter is permanently elevated because the `put` operation was removed while the `get` was retained.
- [High] Kernel BOs (firmware sections) are unconditionally freed during unplug, bypassing the leak_active_resources mitigation and leading to hardware Use-After-Free.
--

--- Patch [15]: [PATCH v3 15/17] drm/panthor: Fix the unplug logic ---
commit ca8eb24c765a734575adb5e02e03ca868df2f058
Author: Boris Brezillon <boris.brezillon@collabora.com>

drm/panthor: Fix the unplug logic

This commit resets the GPU before calling the component unplug functions,
drops unnecessary pm_get/put calls around sub-component unplugs, and attempts
to safely release resources at unplug time even if open FDs exist. It also
introduces a leak_active_resources flag to purposefully leak resources if the
hardware cannot be successfully stopped.

> diff --git a/drivers/gpu/drm/panthor/panthor_device.c b/drivers/gpu/drm/panthor/panthor_device.c
> --- a/drivers/gpu/drm/panthor/panthor_device.c
> +++ b/drivers/gpu/drm/panthor/panthor_device.c
> [ ... ]
> @@ -112,13 +148,6 @@ void panthor_device_unplug(struct panthor_device *ptdev)
>  	panthor_gpu_unplug(ptdev);
>  	panthor_pwr_unplug(ptdev);
>  
> -	pm_runtime_dont_use_autosuspend(ptdev->base.dev);
> -	pm_runtime_put_sync_suspend(ptdev->base.dev);
> -
> -	/* If PM is disabled, we need to call the suspend handler manually. */
> -	if (!IS_ENABLED(CONFIG_PM))
> -		panthor_device_suspend(ptdev->base.dev);
> -
>  	/* Report the unplug operation as done to unblock concurrent
>  	 * panthor_device_unplug() callers.
>  	 */

[Severity: High]
Does this leave the PM runtime usage counter unbalanced?

The panthor_device_unplug() function retains the call to
pm_runtime_get_sync(ptdev->base.dev) earlier in this path, but this patch
removes the matching pm_runtime_put_sync_suspend() call here.

Could this permanent elevation of the usage counter prevent the device and
its parent power domains from entering low-power states?

> diff --git a/drivers/gpu/drm/panthor/panthor_fw.c b/drivers/gpu/drm/panthor/panthor_fw.c
> --- a/drivers/gpu/drm/panthor/panthor_fw.c
> +++ b/drivers/gpu/drm/panthor/panthor_fw.c
> [ ... ]
> @@ -1285,11 +1285,9 @@ void panthor_fw_unplug(struct panthor_device *ptdev)
>  
>  	disable_delayed_work_sync(&ptdev->fw->watchdog.ping_work);
>  
> -	if (!IS_ENABLED(CONFIG_PM) || pm_runtime_active(ptdev->base.dev)) {
> -		/* Make sure the IRQ handler cannot be called after that point. */
> +	/* Make sure the IRQ handler cannot be called after that point. */
> +	if (!IS_ENABLED(CONFIG_PM) || pm_runtime_active(ptdev->base.dev))
>  		panthor_job_irq_suspend(&ptdev->fw->irq);
> -		panthor_fw_stop(ptdev);
> -	}
>  
>  	list_for_each_entry(section, &ptdev->fw->sections, node)
>  		panthor_kernel_bo_destroy(section->mem);

[Severity: High]
Should this check ptdev->unplug.leak_active_resources before destroying
the kernel buffer objects?

The commit message mentions leaking resources if the hardware cannot be
stopped, but panthor_fw_unplug() unconditionally destroys the firmware
sections here.

If the hardware soft-reset failed, destroying these buffer objects could
return physical memory to the OS allocator while the active GPU still
retains access, potentially causing memory corruption.

> diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
> --- a/drivers/gpu/drm/panthor/panthor_mmu.c
> +++ b/drivers/gpu/drm/panthor/panthor_mmu.c
> [ ... ]
> @@ -2227,6 +2238,11 @@ static bool vm_prep_for_cleanup(struct panthor_vm *vm)
>  	}
>  
>  	if (!drm_dev_enter(&ptdev->base, &cookie)) {
> +		/* Device is gone, take the unplug lock to make sure
> +		 * panthor_device_stop_before_unplug() has run and
> +		 * ::leak_active_resources is valid.
> +		 */
> +		guard(mutex)(&ptdev->unplug.lock);
>  		guard(mutex)(&ptdev->mmu->as.slots_lock);
>  
>  		/* If we're still on slot after an unplug, it means
>  		 * drm_dev_unplug() has returned but the part in
>  		 * panthor_mmu_unplug() that evicts all resident AS has
>  		 * not been executed yet. In that case, we simply queue
>  		 * the VM to the cleanup list and wait for
>  		 * panthor_mmu_unplug() to do its job.
>  		 */
>  		if (as->hw_slot.id >= 0) {
>  			list_add_tail(&vm->node, &ptdev->mmu->as.cleanup_list);
>  			return false;
>  		}
>  
>  		return true;
>  	}

[Severity: High]
When drm_dev_enter() fails during an unplug, this path returns true for
non-resident VMs without clearing the PANTHOR_AS_FORBID_UNMAP restriction.

Since panthor_mmu_freeze_before_unplug() applies this restriction to all
VMs earlier in the unplug sequence, leaving it set forces unmap operations
to fail or skip.

Could this cause an unconditional memory leak of the non-resident VMs,
their page tables, and mapped buffer objects?

> diff --git a/drivers/gpu/drm/panthor/panthor_sched.c b/drivers/gpu/drm/panthor/panthor_sched.c
> --- a/drivers/gpu/drm/panthor/panthor_sched.c
> +++ b/drivers/gpu/drm/panthor/panthor_sched.c
> [ ... ]
> @@ -4081,23 +4105,76 @@ void panthor_job_update_resvs(struct drm_exec *exec, struct drm_sched_job *sched
>  void panthor_sched_unplug(struct panthor_device *ptdev)
>  {
>  	struct panthor_scheduler *sched = ptdev->scheduler;
> +	struct panthor_group *group, *tmp_group;
> +	LIST_HEAD(groups);
>  
>  	disable_delayed_work_sync(&sched->tick_work);
>  	disable_work_sync(&sched->fw_events_work);
>  	disable_work_sync(&sched->sync_upd_work);
>  
>  	mutex_lock(&sched->lock);
> [ ... ]
> +	/* Now take care of the non-resident groups. */
> +	for (u32 i = 0; i < ARRAY_SIZE(sched->groups.runnable); i++)
> +		list_splice_init(&sched->groups.runnable[i], &groups);
> +
> +	for (u32 i = 0; i < ARRAY_SIZE(sched->groups.idle); i++)
> +		list_splice_init(&sched->groups.idle[i], &groups);
> +
> +	list_for_each_entry_safe(group, tmp_group, &groups, run_node) {
> +		list_del_init(&group->run_node);
> +		list_del_init(&group->wait_node);
> +		group_queue_work(group, term);
> +	}

[Severity: High]
Does this skip the groups currently waiting on synchronization objects?

The logic splices the sched->groups.runnable and sched->groups.idle
lists to terminate them, but it appears to entirely omit the
sched->groups.waiting list.

It seems any groups in the waiting list at the time of unplug will be
stranded here, leading to a memory leak and a later WARN_ON during
scheduler teardown.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260813-panthor-unplug-fixes-v3-0-3ed4e961bbe7@collabora.com?part=15

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 08/17] drm/panthor: Split panthor_vm
  2026-08-13 10:57 ` [PATCH v3 08/17] drm/panthor: Split panthor_vm Boris Brezillon
@ 2026-08-13 11:37   ` sashiko-bot
  0 siblings, 0 replies; 30+ messages in thread
From: sashiko-bot @ 2026-08-13 11:37 UTC (permalink / raw)
  To: Boris Brezillon; +Cc: dri-devel

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

New issues:
- [High] Premature removal of the AS from the reclaim list allows it to be re-added concurrently, leading to a Use-After-Free when the AS is subsequently freed.

Pre-existing issues:
- [High] If a VM is released concurrently with device unplug, the hardware MMU slot is left enabled and pointing to freed page tables.
--

--- Patch [8]: [PATCH v3 08/17] drm/panthor: Split panthor_vm ---
commit 7c73a792e45cba201c66a596be6469687f3cb78d
Author: Boris Brezillon <boris.brezillon@collabora.com>
Subject: drm/panthor: Split panthor_vm

This commit splits the panthor_vm object into panthor_vm and panthor_as
to manage address spaces and page table residency separately. This prepares
the cleanup logic for future unplug robustness improvements.

> diff --git a/drivers/gpu/drm/panthor/panthor_mmu.c b/drivers/gpu/drm/panthor/panthor_mmu.c
> index 6c48e88ad17fb..0418365529538 100644
> --- a/drivers/gpu/drm/panthor/panthor_mmu.c
> +++ b/drivers/gpu/drm/panthor/panthor_mmu.c

[ ... ]

> @@ -2031,15 +2044,23 @@ void panthor_mmu_post_reset(struct panthor_device *ptdev)
>  	mutex_unlock(&ptdev->mmu->vm.lock);
>  }
>  
> -static void panthor_vm_free(struct drm_gpuvm *gpuvm)
> +static void panthor_vm_release(struct kref *kref)
>  {
> -	struct panthor_vm *vm = container_of(gpuvm, struct panthor_vm, base);
> -	struct panthor_device *ptdev = vm->ptdev;
> +	struct panthor_vm *vm = container_of(kref, struct panthor_vm, refcount);
> +	struct panthor_as *as = vm->as;
> +	struct panthor_device *ptdev = container_of(as->base.drm, struct panthor_device, base);
>  
> +	/* Make sure the page table behind this VM doesn't participate in reclaim
> +	 * after that point, since we're about to release everything anyway.
> +	 */
>  	mutex_lock(&ptdev->base.gem_lru_mutex);
> -	list_del_init(&vm->reclaim.lru_node);
> +	list_del_init(&as->reclaim.lru_node);
>  	mutex_unlock(&ptdev->base.gem_lru_mutex);
>  
> +	/* Unmap everything in case some BOs were still mapped. */
> +	drm_WARN_ON(&ptdev->base,
> +		    panthor_vm_unmap_range(vm, as->base.mm_start, as->base.mm_range));
> +

[Severity: High]
Can this cause a use-after-free if another thread modifies a shared BO's
reclaim state? 

Because the AS is removed from the reclaim list before its BO mappings are
unmapped, couldn't a concurrent call to
panthor_vm_update_bo_reclaim_lru_locked() see the empty list node and re-add
the AS to ptdev->reclaim.vms? 

Once panthor_as_free() executes later, the AS would be freed while it is
still linked in the global reclaim list.

>  	mutex_lock(&vm->heaps.lock);
>  	if (drm_WARN_ON(&ptdev->base, vm->heaps.pool))
>  		panthor_heap_pool_destroy(vm->heaps.pool);
> @@ -2060,29 +2081,26 @@ static void panthor_vm_free(struct drm_gpuvm *gpuvm)
>  	drm_sched_entity_destroy(&vm->entity);
>  	drm_sched_fini(&vm->sched);
>  
> -	mutex_lock(&vm->op_lock);
> +	mutex_lock(&vm->as->op_lock);
>  	mutex_lock(&ptdev->mmu->as.slots_lock);
> -	if (vm->as.id >= 0) {
> +	if (as->hw_slot.id >= 0) {
>  		int cookie;
>  
>  		if (drm_dev_enter(&ptdev->base, &cookie)) {
> -			panthor_mmu_as_disable(ptdev, vm->as.id, false);
> +			panthor_mmu_as_disable(ptdev, as->hw_slot.id, false);
>  			drm_dev_exit(cookie);
>  		}
>  
> -		ptdev->mmu->as.slots[vm->as.id].vm = NULL;
> -		clear_bit(vm->as.id, &ptdev->mmu->as.alloc_mask);
> -		list_del(&vm->as.lru_node);
> +		panthor_as_release_hw_slot_locked(as);
>  	}
>  	mutex_unlock(&ptdev->mmu->as.slots_lock);

[Severity: High]
This is a pre-existing issue, but what happens to the hardware MMU slot if
drm_dev_enter() fails during an unplug?

It looks like the code skips panthor_mmu_as_disable() but still calls
panthor_as_release_hw_slot_locked(as) directly below it.

If the slot tracking is cleared here, won't panthor_mmu_unplug() also skip
disabling the slot because it appears unused, leaving the GPU MMU active on
freed page tables?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260813-panthor-unplug-fixes-v3-0-3ed4e961bbe7@collabora.com?part=8

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 00/17] drm/panthor: Fix the unplug logic
  2026-08-13 11:23 ` [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
@ 2026-08-13 15:11   ` Liviu Dudau
  2026-08-13 15:55     ` Boris Brezillon
  2026-08-13 18:50   ` Danilo Krummrich
  1 sibling, 1 reply; 30+ messages in thread
From: Liviu Dudau @ 2026-08-13 15:11 UTC (permalink / raw)
  To: Boris Brezillon
  Cc: Steven Price, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, dri-devel, linux-kernel, sashiko-bot,
	Danilo Krummrich

On Thu, Aug 13, 2026 at 01:23:20PM +0200, Boris Brezillon wrote:
> +Danilo, since you worked on the 'bound lifetime stuff in rust, and I
> feel this is related to the problem I'm trying to fix here.
> 
> On Thu, 13 Aug 2026 12:56:58 +0200
> Boris Brezillon <boris.brezillon@collabora.com> wrote:
> 
> > The current unplug logic is broken in multiple ways. This is an attempt
> > at addressing the various problems found along the way (some were
> > reported by Sashiko, others have been found while trying to address
> > Sashiko's concerns).
> > 
> > Sending a new version even though v2 didn't receive any human review
> > just to try and address the new stuff pointed out by Sashiko.

Sorry, I was on holiday at the beginning of the week, back today.

> 
> Just a note I forgot to add to my cover letter. I've already spent way
> more time than I wanted on this, not just because Sashiko keeps finding
> new issues at each of my attempt, but also because the whole idea of
> pretending a device on a platform bus is unplugged and can't harm us is
> doomed. This is not an hot-pluggable bus, and the device is still there,
> so, unless we can be absolutely sure it's inactive (which a RESET can
> provide, but RESETs are fallible) we just have two options:
> 
> 1. prevent the device from going away until we managed to properly
>    shutdown the GPU

That's going to be event harder with the upcoming HW where the GPU
slice can be made inaccessible by an arbiter.

> 
> 2. make sure all resources the HW might have its hands on at the time
>    the failure of RESET in the unplug path happened are leaked

There is another option which is to make sure that the HW can only
access the dummy pages. We're still in control of the MMU and the page
tables, once we update those and flush them we should be safe in the
knowledge that the HW cannot access live resources.


> 
> Option 1 is no longer possible since platform_driver::remove() can't
> return an error. That leaves options 2, which is basically what this
> patchset is doing, but the whole idea of leaking resources when the
> final RESET in the unplug path fails has various nasty implications,
> like the fact we end up with dangling drm_device (drm_gpuvm retains a
> ref, and each GPU mapping we kept alive in the gpuvm is what keeps the
> gpuvm and the BOs alive). In practice, there should be no one
> triggering operations on this drm_device, because all the user-facing
> interfaces have been shutdown by drm_dev_unregister() (which is called
> by drm_dev_unplug()), but as things stand now, this drm_device still
> has access to module-specific vtables, and there's nothing retaining
> the module either.
> 
> TLDR; this is all super fragile stuff, on the other hand the current
> situation is probably even worse. so if anyone has any idea how to
> handle this properly (or at least a bit better than we do), please let
> me know. I know a lot of this stuff is currently being considered as
> part of the drm-rust abstractions, so hopefully we have a long-term
> solution for rust drivers, but I'd really like a short-term solution
> for panthor that doesn't involve nasty tricks or overly complex
> refactoring.

I think some of the pain we're suffering comes from the overlap (that
you've tried to address in this series) between the resources that
are visible to the HW and the ones that are visible to user space. The
split of AS and VM is the right thing to do.

My proposal for handling the unplugging would be to have race as quick
as possible to the MMU unplug and then free up all BOs and VMs that
were allocated at the request of user space, then go back and free
the kernel BOs. Then hopefully we should be in a position where there
are no GPU mappings and we can unplug the drm_gpuvm.

Best regards,
Liviu

> 
> > 
> > Signed-off-by: Boris Brezillon <boris.brezillon@collabora.com>
> > ---
> > Changes in v3:
> > - Fix a race in the reset reschedule logic we added to
> >   panthor_device_resume() (missing smp_mb__after_atomic())
> > - Fix a VM leak when reset and suspend are racing with each other
> > - Add missing drm_dev_enter/exit() sections
> > - Insert the groups in the user_owned list even if the group creation
> >   happens during a reset
> > - Try to document why some of the issues pointed out by Sashiko are
> >   either not real issues, or are expected (either fixed in a later
> >   commits, or just expected behavior)
> > - Fix a race between panthor_device_unplug() and vm_prep_for_cleanup()
> >   (introduced in v2)
> > - Link to v2: https://patch.msgid.link/20260811-panthor-unplug-fixes-v2-0-6b583e37f9ae@collabora.com
> > 
> > Changes in v2:
> > - Fix UAFs caused by deferred cleanup works
> > - Fix UAFs caused by open FDs closed after unplug
> > - Fix deadlock when device_unplug() is called from the reset work
> > - Make sure reset requests are not lost in the resume and post_reset
> >   paths
> > - Fix a deadlock in the suspend path
> > - Fix a clk prepare_enable leak in the unplug path
> > - Don't use a drmm_action to flush the cleanup queue (this could cause
> >   UAFs)
> > - Drop the now unused panthor_vm::unusable field
> > - Keep track of user owned resources to prevent leaks and/or UAFs
> > - Link to v1: https://patch.msgid.link/20260804-panthor-unplug-fixes-v1-0-abbbd2d41b13@collabora.com
> > 
> > ---
> > Boris Brezillon (17):
> >       drm/panthor: Disable reset work before unplug
> >       drm/panthor: Further delay reset work enablement
> >       drm/panthor: Make sure reset requests in the resume path are not lost
> >       drm/panthor: Make sure reset requests in the post reset path are not lost
> >       drm/panthor: Flush the cleanup_wq in the unplug path
> >       drm/panthor: Drop unused vm argument passed to panthor_vm_prepare_sync_only_op_ctx()
> >       drm/panthor: Move the debugfs initialization to panthor_device.c
> >       drm/panthor: Split panthor_vm
> >       drm/panthor: Add fine-grained restrictions on VMs
> >       drm/panthor: Check AS state before disabling
> >       drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap
> >       drm/panthor: Make the VM cleanup path more robust against UAF
> >       drm/panthor: Track user owned VMs
> >       drm/panthor: Track user owned groups
> >       drm/panthor: Fix the unplug logic
> >       drm/panthor: Add a debugfs knob to simulate unplug failures
> >       drm/panthor: Add a debugfs knobs to simulate reset failures
> > 
> >  drivers/gpu/drm/panthor/panthor_device.c |  189 +++-
> >  drivers/gpu/drm/panthor/panthor_device.h |   38 +
> >  drivers/gpu/drm/panthor/panthor_drv.c    |  132 ++-
> >  drivers/gpu/drm/panthor/panthor_fw.c     |    9 +-
> >  drivers/gpu/drm/panthor/panthor_mmu.c    | 1493 ++++++++++++++++++------------
> >  drivers/gpu/drm/panthor/panthor_mmu.h    |    4 +-
> >  drivers/gpu/drm/panthor/panthor_sched.c  |  132 ++-
> >  7 files changed, 1344 insertions(+), 653 deletions(-)
> > ---
> > base-commit: 44e9eb5a762142a4aa46c0b5da7c39bfeb78910e
> > change-id: 20260804-panthor-unplug-fixes-7927b3ddc2f9
> > 
> > Best regards,
> > --  
> > Boris Brezillon <boris.brezillon@collabora.com>
> > 
> 

-- 
====================
| I would like to |
| fix the world,  |
| but they're not |
| giving me the   |
 \ source code!  /
  ---------------
    ¯\_(ツ)_/¯

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 00/17] drm/panthor: Fix the unplug logic
  2026-08-13 15:11   ` Liviu Dudau
@ 2026-08-13 15:55     ` Boris Brezillon
  2026-08-13 17:06       ` Liviu Dudau
  0 siblings, 1 reply; 30+ messages in thread
From: Boris Brezillon @ 2026-08-13 15:55 UTC (permalink / raw)
  To: Liviu Dudau
  Cc: Steven Price, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, dri-devel, linux-kernel, sashiko-bot,
	Danilo Krummrich

On Thu, 13 Aug 2026 16:11:37 +0100
Liviu Dudau <liviu.dudau@arm.com> wrote:

> On Thu, Aug 13, 2026 at 01:23:20PM +0200, Boris Brezillon wrote:
> > +Danilo, since you worked on the 'bound lifetime stuff in rust, and I
> > feel this is related to the problem I'm trying to fix here.
> > 
> > On Thu, 13 Aug 2026 12:56:58 +0200
> > Boris Brezillon <boris.brezillon@collabora.com> wrote:
> >   
> > > The current unplug logic is broken in multiple ways. This is an attempt
> > > at addressing the various problems found along the way (some were
> > > reported by Sashiko, others have been found while trying to address
> > > Sashiko's concerns).
> > > 
> > > Sending a new version even though v2 didn't receive any human review
> > > just to try and address the new stuff pointed out by Sashiko.  
> 
> Sorry, I was on holiday at the beginning of the week, back today.
> 
> > 
> > Just a note I forgot to add to my cover letter. I've already spent way
> > more time than I wanted on this, not just because Sashiko keeps finding
> > new issues at each of my attempt, but also because the whole idea of
> > pretending a device on a platform bus is unplugged and can't harm us is
> > doomed. This is not an hot-pluggable bus, and the device is still there,
> > so, unless we can be absolutely sure it's inactive (which a RESET can
> > provide, but RESETs are fallible) we just have two options:
> > 
> > 1. prevent the device from going away until we managed to properly
> >    shutdown the GPU  
> 
> That's going to be event harder with the upcoming HW where the GPU
> slice can be made inaccessible by an arbiter.
> 
> > 
> > 2. make sure all resources the HW might have its hands on at the time
> >    the failure of RESET in the unplug path happened are leaked  
> 
> There is another option which is to make sure that the HW can only
> access the dummy pages. We're still in control of the MMU and the page
> tables, once we update those and flush them we should be safe in the
> knowledge that the HW cannot access live resources.

That's more for an "active device" situation though. Active as in,
device is probed and ready to accept user requests, even if it might be
temporarily inaccessible because of RESETs (or access-window loss
on new gens).

The thing I'm trying to fix here is the unplug logic: device is going
away, we just need to make sure it's either

- off

or

- the resources it had access to are leaked

or

- we prevent the removal until we're sure it's off (retry the SOFT_RESET
  indefinitely?)

> 
> 
> > 
> > Option 1 is no longer possible since platform_driver::remove() can't
> > return an error. That leaves options 2, which is basically what this
> > patchset is doing, but the whole idea of leaking resources when the
> > final RESET in the unplug path fails has various nasty implications,
> > like the fact we end up with dangling drm_device (drm_gpuvm retains a
> > ref, and each GPU mapping we kept alive in the gpuvm is what keeps the
> > gpuvm and the BOs alive). In practice, there should be no one
> > triggering operations on this drm_device, because all the user-facing
> > interfaces have been shutdown by drm_dev_unregister() (which is called
> > by drm_dev_unplug()), but as things stand now, this drm_device still
> > has access to module-specific vtables, and there's nothing retaining
> > the module either.
> > 
> > TLDR; this is all super fragile stuff, on the other hand the current
> > situation is probably even worse. so if anyone has any idea how to
> > handle this properly (or at least a bit better than we do), please let
> > me know. I know a lot of this stuff is currently being considered as
> > part of the drm-rust abstractions, so hopefully we have a long-term
> > solution for rust drivers, but I'd really like a short-term solution
> > for panthor that doesn't involve nasty tricks or overly complex
> > refactoring.  
> 
> I think some of the pain we're suffering comes from the overlap (that
> you've tried to address in this series) between the resources that
> are visible to the HW and the ones that are visible to user space. The
> split of AS and VM is the right thing to do.

Yeah, that definitely makes things harder to disconnect when the device
goes away. But even with this split, there's still the problem that the
"unplug" we have is not HW based (unlike a PCI bus), so the HW still has
access to the memory we shared with it (for its MMU page table, and the
pages those point to).

> 
> My proposal for handling the unplugging would be to have race as quick
> as possible to the MMU unplug and then free up all BOs and VMs that
> were allocated at the request of user space, then go back and free
> the kernel BOs. Then hopefully we should be in a position where there
> are no GPU mappings and we can unplug the drm_gpuvm.

I mean, that's basically what this patchset is doing. To be accurate,
what the unplug logic does at the end of this patchset is:

1. RESET the GPU, so the HW is inactive => basically faking a real
   unplug on an hot-pluggable bus
2. unplug each component, and make sure the unplug logic doesn't
   interact with the HW. It just acts as a janitor releasing all the
   objects that were left behind at the moment the unplug happens. The
   only thing left are the user-facing objects (panthor_file) so that
   DRM FDs can be closed after the unplug, but all other operations
   IOCTLs fail with ENODEV. panthor_device also stays around a bit
   longer, but it's mostly here to keep the drm_device around until the
   last ref is dropped
3. if and only if the RESET failed in step 1, the MMU unplug logic leaks
   the GPU mappings of the resident AS instead of releasing them. This
   leak retains the gpuvm which retains the drm_device/panthor_device

Step 3 is only here to cover for failures in step 1 (in a normal
situation, there's no leak and everything is released as expected),
and that's the problematic part. I don't mind refactor the code to
isolate objects containing HW resource from the user-facing objects,
but that won't solve the fact that, on a RESET failure, we either leak
memory, or we expose ourselves to HW UAFs. If you tell me HARD_RESET is
not fallible and is safe, I can go for that. But last I looked, I've
read that it could leave the memory bus in a bad state, with the risk of
impacting the rest of the system.

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 00/17] drm/panthor: Fix the unplug logic
  2026-08-13 15:55     ` Boris Brezillon
@ 2026-08-13 17:06       ` Liviu Dudau
  0 siblings, 0 replies; 30+ messages in thread
From: Liviu Dudau @ 2026-08-13 17:06 UTC (permalink / raw)
  To: Boris Brezillon
  Cc: Steven Price, Maarten Lankhorst, Maxime Ripard, Thomas Zimmermann,
	David Airlie, Simona Vetter, dri-devel, linux-kernel, sashiko-bot,
	Danilo Krummrich

On Thu, Aug 13, 2026 at 05:55:22PM +0200, Boris Brezillon wrote:
> On Thu, 13 Aug 2026 16:11:37 +0100
> Liviu Dudau <liviu.dudau@arm.com> wrote:
> 
> > On Thu, Aug 13, 2026 at 01:23:20PM +0200, Boris Brezillon wrote:
> > > +Danilo, since you worked on the 'bound lifetime stuff in rust, and I
> > > feel this is related to the problem I'm trying to fix here.
> > > 
> > > On Thu, 13 Aug 2026 12:56:58 +0200
> > > Boris Brezillon <boris.brezillon@collabora.com> wrote:
> > >   
> > > > The current unplug logic is broken in multiple ways. This is an attempt
> > > > at addressing the various problems found along the way (some were
> > > > reported by Sashiko, others have been found while trying to address
> > > > Sashiko's concerns).
> > > > 
> > > > Sending a new version even though v2 didn't receive any human review
> > > > just to try and address the new stuff pointed out by Sashiko.  
> > 
> > Sorry, I was on holiday at the beginning of the week, back today.
> > 
> > > 
> > > Just a note I forgot to add to my cover letter. I've already spent way
> > > more time than I wanted on this, not just because Sashiko keeps finding
> > > new issues at each of my attempt, but also because the whole idea of
> > > pretending a device on a platform bus is unplugged and can't harm us is
> > > doomed. This is not an hot-pluggable bus, and the device is still there,
> > > so, unless we can be absolutely sure it's inactive (which a RESET can
> > > provide, but RESETs are fallible) we just have two options:
> > > 
> > > 1. prevent the device from going away until we managed to properly
> > >    shutdown the GPU  
> > 
> > That's going to be event harder with the upcoming HW where the GPU
> > slice can be made inaccessible by an arbiter.
> > 
> > > 
> > > 2. make sure all resources the HW might have its hands on at the time
> > >    the failure of RESET in the unplug path happened are leaked  
> > 
> > There is another option which is to make sure that the HW can only
> > access the dummy pages. We're still in control of the MMU and the page
> > tables, once we update those and flush them we should be safe in the
> > knowledge that the HW cannot access live resources.
> 
> That's more for an "active device" situation though. Active as in,
> device is probed and ready to accept user requests, even if it might be
> temporarily inaccessible because of RESETs (or access-window loss
> on new gens).
> 
> The thing I'm trying to fix here is the unplug logic: device is going
> away, we just need to make sure it's either
> 
> - off
> 
> or
> 
> - the resources it had access to are leaked
> 
> or
> 
> - we prevent the removal until we're sure it's off (retry the SOFT_RESET
>   indefinitely?)
> 
> > 
> > 
> > > 
> > > Option 1 is no longer possible since platform_driver::remove() can't
> > > return an error. That leaves options 2, which is basically what this
> > > patchset is doing, but the whole idea of leaking resources when the
> > > final RESET in the unplug path fails has various nasty implications,
> > > like the fact we end up with dangling drm_device (drm_gpuvm retains a
> > > ref, and each GPU mapping we kept alive in the gpuvm is what keeps the
> > > gpuvm and the BOs alive). In practice, there should be no one
> > > triggering operations on this drm_device, because all the user-facing
> > > interfaces have been shutdown by drm_dev_unregister() (which is called
> > > by drm_dev_unplug()), but as things stand now, this drm_device still
> > > has access to module-specific vtables, and there's nothing retaining
> > > the module either.
> > > 
> > > TLDR; this is all super fragile stuff, on the other hand the current
> > > situation is probably even worse. so if anyone has any idea how to
> > > handle this properly (or at least a bit better than we do), please let
> > > me know. I know a lot of this stuff is currently being considered as
> > > part of the drm-rust abstractions, so hopefully we have a long-term
> > > solution for rust drivers, but I'd really like a short-term solution
> > > for panthor that doesn't involve nasty tricks or overly complex
> > > refactoring.  
> > 
> > I think some of the pain we're suffering comes from the overlap (that
> > you've tried to address in this series) between the resources that
> > are visible to the HW and the ones that are visible to user space. The
> > split of AS and VM is the right thing to do.
> 
> Yeah, that definitely makes things harder to disconnect when the device
> goes away. But even with this split, there's still the problem that the
> "unplug" we have is not HW based (unlike a PCI bus), so the HW still has
> access to the memory we shared with it (for its MMU page table, and the
> pages those point to).

There is no copy of the MMU page tables that the FW or the hardware own.
Panthor is in charge of the page tables and it can force change them if it
wants to be sure that HW doesn't access memory we don't want to. If it
does, HW will get a bus access violation and halt.

> 
> > 
> > My proposal for handling the unplugging would be to have race as quick
> > as possible to the MMU unplug and then free up all BOs and VMs that
> > were allocated at the request of user space, then go back and free
> > the kernel BOs. Then hopefully we should be in a position where there
> > are no GPU mappings and we can unplug the drm_gpuvm.
> 
> I mean, that's basically what this patchset is doing. To be accurate,
> what the unplug logic does at the end of this patchset is:
> 
> 1. RESET the GPU, so the HW is inactive => basically faking a real
>    unplug on an hot-pluggable bus
> 2. unplug each component, and make sure the unplug logic doesn't
>    interact with the HW. It just acts as a janitor releasing all the
>    objects that were left behind at the moment the unplug happens. The
>    only thing left are the user-facing objects (panthor_file) so that
>    DRM FDs can be closed after the unplug, but all other operations
>    IOCTLs fail with ENODEV. panthor_device also stays around a bit
>    longer, but it's mostly here to keep the drm_device around until the
>    last ref is dropped
> 3. if and only if the RESET failed in step 1, the MMU unplug logic leaks
>    the GPU mappings of the resident AS instead of releasing them. This
>    leak retains the gpuvm which retains the drm_device/panthor_device
> 
> Step 3 is only here to cover for failures in step 1 (in a normal
> situation, there's no leak and everything is released as expected),
> and that's the problematic part. I don't mind refactor the code to
> isolate objects containing HW resource from the user-facing objects,
> but that won't solve the fact that, on a RESET failure, we either leak
> memory, or we expose ourselves to HW UAFs. If you tell me HARD_RESET is
> not fallible and is safe, I can go for that. But last I looked, I've
> read that it could leave the memory bus in a bad state, with the risk of
> impacting the rest of the system.

My suggestion would be to do step 1, 3 and then 2. But on step 3 I would
not leak the GPU mappings, but replace them with the dummy pages and release
the resident AS.

Best regards,
Liviu

-- 
====================
| I would like to |
| fix the world,  |
| but they're not |
| giving me the   |
 \ source code!  /
  ---------------
    ¯\_(ツ)_/¯

^ permalink raw reply	[flat|nested] 30+ messages in thread

* Re: [PATCH v3 00/17] drm/panthor: Fix the unplug logic
  2026-08-13 11:23 ` [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
  2026-08-13 15:11   ` Liviu Dudau
@ 2026-08-13 18:50   ` Danilo Krummrich
  1 sibling, 0 replies; 30+ messages in thread
From: Danilo Krummrich @ 2026-08-13 18:50 UTC (permalink / raw)
  To: Boris Brezillon
  Cc: Steven Price, Liviu Dudau, Maarten Lankhorst, Maxime Ripard,
	Thomas Zimmermann, David Airlie, Simona Vetter, dri-devel,
	linux-kernel, sashiko-bot, Danilo Krummrich

On Thu Aug 13, 2026 at 1:23 PM CEST, Boris Brezillon wrote:
> Just a note I forgot to add to my cover letter. I've already spent way
> more time than I wanted on this, not just because Sashiko keeps finding
> new issues at each of my attempt, but also because the whole idea of
> pretending a device on a platform bus is unplugged and can't harm us is
> doomed. This is not an hot-pluggable bus, and the device is still there,
> so, unless we can be absolutely sure it's inactive (which a RESET can
> provide, but RESETs are fallible) we just have two options:

I probably need a bit more context about which exact problem(s) you are trying
to solve.

> 1. prevent the device from going away until we managed to properly
>    shutdown the GPU

I'm not exactly sure what you mean with "device going away". If you mean
"prevent the device from being unbound from the driver" this is essentially what
you do by waiting for the completion of some HW teardown operation in remove().

In general, the implementation of remove() should ensure that on the one hand
the device it torn down (or reset), so it does not mess with system resources
anymore (e.g. attempt to do any DMA transfers) and behaves correctly on a
subsequent probe of this or another driver.

And on the other hand, the driver must release all device assoicated resources,
such as DMA mappings, IRQs, I/O memory mappings, etc. and it should also ensure
that no more driver code is reachable from any asynchronous paths, such as
workqueues, IOCTLs, timers, etc.

The latter obviously also depends on the subsystem and whether the lifetime of
userspace structurs and their associated driver private data is cleanly
decoupled (e.g. struct drm_file and ->driver_priv).

Since you also mention hot-unplug; those rules are universial regardless of
whether remove is triggered by a hot-unplug event or because the driver is
unbound for a different reason. The DRM API is a bit misleading about this,
because with drm_dev_unregister() there is no way to prevent DRM IOCTLs from
running after remove(), which wrongly suggests that this is not a potential
issue.

Not summarizing this because I think you are not aware already, but it may
provide a good entry point for you to point out where exactly things are getting
tricky.

> 2. make sure all resources the HW might have its hands on at the time
>    the failure of RESET in the unplug path happened are leaked

I'm not sure what you mean by this. But it suggests that the problem you try to
deal with is a misbehaving device that fails to reset?

Also, what do you mean with leaking the device resources?

^ permalink raw reply	[flat|nested] 30+ messages in thread

end of thread, other threads:[~2026-08-13 18:50 UTC | newest]

Thread overview: 30+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-13 10:56 [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
2026-08-13 10:56 ` [PATCH v3 01/17] drm/panthor: Disable reset work before unplug Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 02/17] drm/panthor: Further delay reset work enablement Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 03/17] drm/panthor: Make sure reset requests in the resume path are not lost Boris Brezillon
2026-08-13 11:09   ` sashiko-bot
2026-08-13 10:57 ` [PATCH v3 04/17] drm/panthor: Make sure reset requests in the post reset " Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 05/17] drm/panthor: Flush the cleanup_wq in the unplug path Boris Brezillon
2026-08-13 11:18   ` sashiko-bot
2026-08-13 10:57 ` [PATCH v3 06/17] drm/panthor: Drop unused vm argument passed to panthor_vm_prepare_sync_only_op_ctx() Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 07/17] drm/panthor: Move the debugfs initialization to panthor_device.c Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 08/17] drm/panthor: Split panthor_vm Boris Brezillon
2026-08-13 11:37   ` sashiko-bot
2026-08-13 10:57 ` [PATCH v3 09/17] drm/panthor: Add fine-grained restrictions on VMs Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 10/17] drm/panthor: Check AS state before disabling Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 11/17] drm/panthor: Don't pre-allocate VMAs or page tables when preparing a full VM unmap Boris Brezillon
2026-08-13 11:12   ` sashiko-bot
2026-08-13 10:57 ` [PATCH v3 12/17] drm/panthor: Make the VM cleanup path more robust against UAF Boris Brezillon
2026-08-13 11:20   ` sashiko-bot
2026-08-13 10:57 ` [PATCH v3 13/17] drm/panthor: Track user owned VMs Boris Brezillon
2026-08-13 11:18   ` sashiko-bot
2026-08-13 10:57 ` [PATCH v3 14/17] drm/panthor: Track user owned groups Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 15/17] drm/panthor: Fix the unplug logic Boris Brezillon
2026-08-13 11:25   ` sashiko-bot
2026-08-13 10:57 ` [PATCH v3 16/17] drm/panthor: Add a debugfs knob to simulate unplug failures Boris Brezillon
2026-08-13 10:57 ` [PATCH v3 17/17] drm/panthor: Add a debugfs knobs to simulate reset failures Boris Brezillon
2026-08-13 11:23 ` [PATCH v3 00/17] drm/panthor: Fix the unplug logic Boris Brezillon
2026-08-13 15:11   ` Liviu Dudau
2026-08-13 15:55     ` Boris Brezillon
2026-08-13 17:06       ` Liviu Dudau
2026-08-13 18:50   ` Danilo Krummrich

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.