On 2026-06-29 12:51, Geoffrey McRae wrote:
> DM IRQ teardown can race with interrupt handling and low-context work.
> The IRQ handler can still walk the DM IRQ handler tables while the
> teardown path removes and frees entries. Low-context work can also
> remain queued after its handler has been removed, leading to a possible
> use-after-free when the work item later runs.
> 
> Add an irq_fini_in_progress flag and set it before the IRQ tables are
> torn down. Check the flag in the ISR and work scheduling paths so they
> do not access the handler tables or queue new work once teardown has
> started.
> 
> Rework amdgpu_dm_irq_fini() to detach all low and high context handlers
> from the IRQ tables under the table lock, then cancel pending
> low-context work outside the lock before freeing the handlers. Also
> cancel low-context work in remove_irq_handler() before freeing an
> individual handler.
> 
> Fix the suspend path by disabling HPD and HPD RX hardware interrupts
> under the IRQ table lock before flushing pending low-context work,
> avoiding a TOCTOU window where new work could be queued after the list
> check.
> 
> Finally, call amdgpu_dm_irq_fini() from amdgpu_dm_fini() before DC is
> destroyed, so IRQ teardown happens while the display core state is still
> valid.
> 
> Signed-off-by: Geoffrey McRae <[email protected]>
> Cc: Harry Wentland <[email protected]>
> Cc: Leo Li <[email protected]>
> Cc: Alex Deucher <[email protected]>
> Cc: Christian König <[email protected]>
> ---
>  .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c |   8 +-
>  .../gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h |   3 +
>  .../drm/amd/display/amdgpu_dm/amdgpu_dm_irq.c | 164 ++++++++++--------
>  3 files changed, 96 insertions(+), 79 deletions(-)
> 
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c 
> b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
> index b97ceabe6173..9c5e963337cc 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.c
> @@ -1010,14 +1010,11 @@ static void amdgpu_dm_fini(struct amdgpu_device *adev)
>               adev->dm.hpd_rx_offload_wq = NULL;
>       }
>  
> +     amdgpu_dm_irq_fini(adev);
> +
>       /* DC Destroy TODO: Replace destroy DAL */
>       if (adev->dm.dc)
>               dc_destroy(&adev->dm.dc);
> -     /*
> -      * TODO: pageflip, vlank interrupt
> -      *
> -      * amdgpu_dm_irq_fini(adev);
> -      */
>  
>       if (adev->dm.cgs_device) {
>               amdgpu_cgs_destroy_device(adev->dm.cgs_device);
> @@ -1523,7 +1520,6 @@ static int dm_hw_fini(struct amdgpu_ip_block *ip_block)
>  
>       amdgpu_dm_hpd_fini(adev);
>  
> -     amdgpu_dm_irq_fini(adev);
>       amdgpu_dm_fini(adev);
>       return 0;
>  }
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h 
> b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
> index 909ee71d6d59..88687a7e01a5 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm.h
> @@ -340,6 +340,8 @@ struct hpd_rx_irq_offload_work {
>   * @dmcub_trace_event_en: enable dmcub trace events
>   * @dmub_outbox_params: DMUB Outbox parameters
>   * @num_of_edps: number of backlight eDPs
> + * @irq_fini_in_progress: Set during IRQ teardown to prevent interrupt 
> handlers
> + *                     from accessing the IRQ tables during cleanup
>   * @disable_hpd_irq: disables all HPD and HPD RX interrupt handling in the
>   *                driver when true
>   * @dmub_aux_transfer_done: struct completion used to indicate when DMUB
> @@ -634,6 +636,7 @@ struct amdgpu_display_manager {
>        */
>       struct amdgpu_encoder mst_encoders[AMDGPU_DM_MAX_CRTC];
>       bool force_timing_sync;
> +     bool irq_fini_in_progress;
>       bool disable_hpd_irq;
>       bool dmcub_trace_event_en;
>       /**
> diff --git a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_irq.c 
> b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_irq.c
> index c5467f34c51f..3a5de9364ed1 100644
> --- a/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_irq.c
> +++ b/drivers/gpu/drm/amd/display/amdgpu_dm/amdgpu_dm_irq.c
> @@ -195,6 +195,9 @@ static struct list_head *remove_irq_handler(struct 
> amdgpu_device *adev,
>               return NULL;
>       }
>  
> +     if (int_params->int_context == INTERRUPT_LOW_IRQ_CONTEXT)
> +             cancel_work_sync(&handler->work);
> +
>       kfree(handler);
>  
>       DRM_DEBUG_KMS(
> @@ -204,55 +207,6 @@ static struct list_head *remove_irq_handler(struct 
> amdgpu_device *adev,
>       return hnd_list;
>  }
>  
> -/**
> - * unregister_all_irq_handlers() - Cleans up handlers from the DM IRQ table
> - * @adev: The base driver device containing the DM device
> - *
> - * Go through low and high context IRQ tables and deallocate handlers.
> - */
> -static void unregister_all_irq_handlers(struct amdgpu_device *adev)
> -{
> -     struct list_head *hnd_list_low;
> -     struct list_head *hnd_list_high;
> -     struct list_head *entry, *tmp;
> -     struct amdgpu_dm_irq_handler_data *handler;
> -     unsigned long irq_table_flags;
> -     int i;
> -
> -     DM_IRQ_TABLE_LOCK(adev, irq_table_flags);
> -
> -     for (i = 0; i < DAL_IRQ_SOURCES_NUMBER; i++) {
> -             hnd_list_low = &adev->dm.irq_handler_list_low_tab[i];
> -             hnd_list_high = &adev->dm.irq_handler_list_high_tab[i];
> -
> -             list_for_each_safe(entry, tmp, hnd_list_low) {
> -
> -                     handler = list_entry(entry, struct 
> amdgpu_dm_irq_handler_data,
> -                                          list);
> -
> -                     if (handler == NULL || handler->handler == NULL)
> -                             continue;
> -
> -                     list_del(&handler->list);
> -                     kfree(handler);
> -             }
> -
> -             list_for_each_safe(entry, tmp, hnd_list_high) {
> -
> -                     handler = list_entry(entry, struct 
> amdgpu_dm_irq_handler_data,
> -                                          list);
> -
> -                     if (handler == NULL || handler->handler == NULL)
> -                             continue;
> -
> -                     list_del(&handler->list);
> -                     kfree(handler);
> -             }
> -     }
> -
> -     DM_IRQ_TABLE_UNLOCK(adev, irq_table_flags);
> -}
> -
>  static bool
>  validate_irq_registration_params(struct dc_interrupt_params *int_params,
>                                void (*ih)(void *))
> @@ -459,38 +413,84 @@ EXPORT_IF_KUNIT(amdgpu_dm_irq_init);
>   * amdgpu_dm_irq_fini() - Tear down DM IRQ management
>   * @adev: The base driver device containing the DM device
>   *
> - * Flush all work within the low context IRQ table.
> + * Prevents any new interrupt handler scheduling, removes all handlers from
> + * the IRQ tables, cancels pending work items, and deallocates all handler
> + * data. The irq_fini_in_progress flag ensures the ISR and work scheduler
> + * do not access the handler lists during teardown.
>   */
>  void amdgpu_dm_irq_fini(struct amdgpu_device *adev)
>  {
>       int src;
> -     struct list_head *lh;
> +     LIST_HEAD(low_handlers);
> +     LIST_HEAD(high_handlers);
>       struct list_head *entry, *tmp;
>       struct amdgpu_dm_irq_handler_data *handler;
>       unsigned long irq_table_flags;
>  
>       DRM_DEBUG_KMS("DM_IRQ: releasing resources.\n");
> +
> +     /*
> +      * Set the fini flag before tearing down the IRQ tables. This ensures
> +      * that any concurrent ISR (amdgpu_dm_irq_handler()) or work scheduler
> +      * (amdgpu_dm_irq_schedule_work()) will bail out early rather than
> +      * accessing handler data that is about to be freed.
> +      *
> +      * smp_store_release() pairs with the READ_ONCE() in the ISR and work
> +      * scheduler paths to guarantee visibility across CPUs.
> +      */
> +     smp_store_release(&adev->dm.irq_fini_in_progress, true);

Hi Geoffrey, thanks for the patches.

Do we need irq_fini_in_progress if we're clearing the
irq_hander_list_low/high_tab under the DM_IRQ_TABLE_LOCK?

It looks to me that any concurrent amdgpu_dm_irq_immediate_work/schedule_work
will -- because of DM_IRQ_TABLE_LOCK maintaining serialization -- either:

A) run before list_splice and handle the IRQ, which is OK. In the
   case of schedule_work(), where queued work can be pending, the
   cance_work_sync() ensures they flush before continuing. Or,  
B) run after list clear and early return since the list is empty, which
   prevents any use after free.

Generally I'm a little hesitant of using mbs unless absolutely necessary, since
it's difficult to understand their correctness. If we have spinlocks handling
acquire/releases already, I'd prefer to just use those.

Thanks,
Leo

> +
>       for (src = 0; src < DAL_IRQ_SOURCES_NUMBER; src++) {
>               DM_IRQ_TABLE_LOCK(adev, irq_table_flags);
> -             /* The handler was removed from the table,
> -              * it means it is safe to flush all the 'work'
> -              * (because no code can schedule a new one).
> +
> +             /*
> +              * Move all handlers from the low and high context tables to
> +              * temporary lists under the lock. This prevents the ISR from
> +              * finding them while we process them outside the lock.
>                */
> -             lh = &adev->dm.irq_handler_list_low_tab[src];
> +             list_splice_init(&adev->dm.irq_handler_list_low_tab[src],
> +                              &low_handlers);
> +             list_splice_init(&adev->dm.irq_handler_list_high_tab[src],
> +                              &high_handlers);
> +
>               DM_IRQ_TABLE_UNLOCK(adev, irq_table_flags);
>  
> -             if (!list_empty(lh)) {
> -                     list_for_each_safe(entry, tmp, lh) {
> -                             handler = list_entry(
> -                                     entry,
> -                                     struct amdgpu_dm_irq_handler_data,
> -                                     list);
> -                             flush_work(&handler->work);
> -                     }
> +             /*
> +              * Cancel all pending work for the low-context handlers
> +              * outside the lock. cancel_work_sync() may sleep and waits
> +              * until any running work completes, preventing UAF.
> +              */
> +             list_for_each_safe(entry, tmp, &low_handlers) {
> +                     handler = list_entry(entry,
> +                                         struct amdgpu_dm_irq_handler_data,
> +                                         list);
> +                     cancel_work_sync(&handler->work);
>               }
> +
> +             /*
> +              * High-context handlers are executed synchronously within ISR
> +              * context (see amdgpu_dm_irq_immediate_work()) and have no
> +              * work_struct, so there is no pending work to cancel here.
> +              * They will be freed along with low_handlers after the loop.
> +              */
> +     }
> +
> +     /* Deallocate all handlers. */
> +     list_for_each_safe(entry, tmp, &low_handlers) {
> +             handler = list_entry(entry,
> +                                  struct amdgpu_dm_irq_handler_data,
> +                                  list);
> +             list_del(&handler->list);
> +             kfree(handler);
> +     }
> +
> +     list_for_each_safe(entry, tmp, &high_handlers) {
> +             handler = list_entry(entry,
> +                                  struct amdgpu_dm_irq_handler_data,
> +                                  list);
> +             list_del(&handler->list);
> +             kfree(handler);
>       }
> -     /* Deallocate handlers from the table. */
> -     unregister_all_irq_handlers(adev);
>  }
>  EXPORT_IF_KUNIT(amdgpu_dm_irq_fini);
>  
> @@ -498,7 +498,6 @@ void amdgpu_dm_irq_suspend(struct amdgpu_device *adev)
>  {
>       struct drm_device *dev = adev_to_drm(adev);
>       int src;
> -     struct list_head *hnd_list_h;
>       struct list_head *hnd_list_l;
>       unsigned long irq_table_flags;
>       struct list_head *entry, *tmp;
> @@ -511,12 +510,15 @@ void amdgpu_dm_irq_suspend(struct amdgpu_device *adev)
>       /**
>        * Disable HW interrupt  for HPD and HPDRX only since FLIP and VBLANK
>        * will be disabled from manage_dm_interrupts on disable CRTC.
> +      *
> +      * Disable the HW interrupt first, then flush any pending work. Since
> +      * the HW interrupt is disabled under the lock, no new IRQ can be
> +      * generated after the disable completes. Any work already queued by an
> +      * in-flight ISR will be flushed below.
>        */
>       for (src = DC_IRQ_SOURCE_HPD1; src <= DC_IRQ_SOURCE_HPD6RX; src++) {
>               hnd_list_l = &adev->dm.irq_handler_list_low_tab[src];
> -             hnd_list_h = &adev->dm.irq_handler_list_high_tab[src];
> -             if (!list_empty(hnd_list_l) || !list_empty(hnd_list_h))
> -                     dc_interrupt_set(adev->dm.dc, src, false);
> +             dc_interrupt_set(adev->dm.dc, src, false);
>  
>               DM_IRQ_TABLE_UNLOCK(adev, irq_table_flags);
>  
> @@ -597,10 +599,20 @@ static void amdgpu_dm_irq_schedule_work(struct 
> amdgpu_device *adev,
>       struct  list_head *handler_list = 
> &adev->dm.irq_handler_list_low_tab[irq_source];
>       struct  amdgpu_dm_irq_handler_data *handler_data;
>       bool    work_queued = false;
> +     unsigned long irq_table_flags;
>  
> -     if (list_empty(handler_list))
> +     /*perform a lockless check first*/
> +     if (READ_ONCE(adev->dm.irq_fini_in_progress))
>               return;
>  
> +     DM_IRQ_TABLE_LOCK(adev, irq_table_flags);
> +
> +     if (READ_ONCE(adev->dm.irq_fini_in_progress))
> +             goto out_unlock;
> +
> +     if (list_empty(handler_list))
> +             goto out_unlock;
> +
>       list_for_each_entry(handler_data, handler_list, list) {
>               if (queue_work(system_highpri_wq, &handler_data->work)) {
>                       work_queued = true;
> @@ -617,7 +629,7 @@ static void amdgpu_dm_irq_schedule_work(struct 
> amdgpu_device *adev,
>               handler_data_add = kzalloc(sizeof(*handler_data), GFP_ATOMIC);
>               if (!handler_data_add) {
>                       DRM_ERROR("DM_IRQ: failed to allocate irq handler!\n");
> -                     return;
> +                     goto out_unlock;
>               }
>  
>               /*copy new amdgpu_dm_irq_handler_data members from 
> handler_data*/
> @@ -639,6 +651,9 @@ static void amdgpu_dm_irq_schedule_work(struct 
> amdgpu_device *adev,
>                                 "from display for IRQ source %d\n",
>                                 irq_source);
>       }
> +
> +out_unlock:
> +     DM_IRQ_TABLE_UNLOCK(adev, irq_table_flags);
>  }
>  
>  /*
> @@ -678,9 +693,12 @@ static int amdgpu_dm_irq_handler(struct amdgpu_device 
> *adev,
>                                struct amdgpu_irq_src *source,
>                                struct amdgpu_iv_entry *entry)
>  {
> +     enum dc_irq_source src;
> +
> +     if (READ_ONCE(adev->dm.irq_fini_in_progress))
> +             return 0;
>  
> -     enum dc_irq_source src =
> -             dc_interrupt_to_irq_source(
> +     src = dc_interrupt_to_irq_source(
>                       adev->dm.dc,
>                       entry->src_id,
>                       entry->src_data[0]);

Reply via email to