On Thu Jul 16, 2026 at 5:14 PM CST, sashiko-bot wrote:
> Thank you for your contribution! Sashiko AI review found 6 potential issue(s) 
> to consider:
>
> New issues:
> - [High] Missing framebuffer validation in atomic paths leads to an 
> unconditional NULL pointer dereference when a plane is configured without a 
> framebuffer.
> - [High] `imx_lcdc_plane_helper_atomic_check` fetches a potentially NULL CRTC 
> state, causing a kernel panic in core helpers.
> - [High] Redundant clock enable calls cause a persistent reference count leak 
> for `clk_per` on every display modeset.
>
> Pre-existing issues:
> - [Critical] The driver accesses peripheral MMIO registers and leaves 
> interrupts unmasked while the AHB and IPG bus clocks are disabled.
> - [High] The CRTC atomic check unconditionally overwrites the `mode_changed` 
> flag, potentially discarding modeset requests initiated by the DRM core.
> - [High] VBLANK completion events are signaled prematurely during display 
> disable/modeset sequences.
> --
>
> --- Patch [1]: [PATCH v2] drm/imx: replace struct drm_simple_display_pipe 
> with regular atomic helpers ---
> commit 78bda5593353310f7405860b2b4d7ea3060e0afa
> Author: Ze Huang <[email protected]>
> Subject: drm/imx: replace struct drm_simple_display_pipe with regular atomic 
> helpers
>
> This commit converts the i.MX LCDC driver to explicit primary plane, CRTC, 
> and encoder objects, removing the dependency on the simple-KMS framework. 
> It preserves most of the previous callbacks by rewiring them into the 
> standard atomic helper paths.
>
> Link: 
> https://patch.msgid.link/20260705-drm-simple-kms-removal-v1-0-b4e1ca053...@oss.qualcomm.com
>
>> diff --git a/drivers/gpu/drm/imx/lcdc/imx-lcdc.c 
>> b/drivers/gpu/drm/imx/lcdc/imx-lcdc.c
>> index c67fe80b8115e..5c858fc81a78c 100644
>> --- a/drivers/gpu/drm/imx/lcdc/imx-lcdc.c
>> +++ b/drivers/gpu/drm/imx/lcdc/imx-lcdc.c
>
> [ ... ]
>
>> -static void imx_lcdc_pipe_enable(struct drm_simple_display_pipe *pipe,
>> -                             struct drm_crtc_state *crtc_state,
>> -                             struct drm_plane_state *plane_state)
>> +static void imx_lcdc_crtc_helper_atomic_enable(struct drm_crtc *crtc,
>> +                                           struct drm_atomic_commit *commit)
>>  {
>>      int ret;
>>      int clk_div;
>>      int bpp;
>> -    struct imx_lcdc *lcdc = imx_lcdc_from_drmdev(pipe->crtc.dev);
>> -    struct drm_display_mode *mode = &pipe->crtc.mode;
>> +    struct imx_lcdc *lcdc = imx_lcdc_from_drmdev(crtc->dev);
>> +    struct drm_crtc_state *cstate = drm_atomic_get_new_crtc_state(commit, 
>> crtc);
>> +    struct drm_plane_state *plane_state = lcdc->plane.state;
>
> [Severity: High]
> Does this code result in a NULL pointer dereference when a primary plane is
> configured without a framebuffer?
>
> If userspace submits an atomic commit that enables the CRTC but sets the
> primary plane's FB_ID to 0, plane_state->fb will be NULL.
>

Will access plane_state with drm_atomic_get_new_plane_state()

>
> Since drm_atomic_helper_check_crtc_primary_plane only verifies that the
> primary plane is present in the plane mask, it does not prevent a NULL
> framebuffer. This causes an unconditional dereference shortly after this line
> when extracting the format via imx_lcdc_get_format(), as well as inside 
> imx_lcdc_update_hw_registers() when calling drm_fb_dma_get_gem_addr().
>
>> +    struct drm_display_mode *mode = &cstate->mode;
>>      struct drm_display_info *disp_info = &lcdc->connector->display_info;
>>      const int hsync_pol = (mode->flags & DRM_MODE_FLAG_PHSYNC) ? 0 : 1;
>>      const int vsync_pol = (mode->flags & DRM_MODE_FLAG_PVSYNC) ? 0 : 1;
>
> [ ... ]
>
>> @@ -230,40 +232,46 @@ static void imx_lcdc_pipe_enable(struct 
>> drm_simple_display_pipe *pipe,
>>  
>>      ret = clk_prepare_enable(lcdc->clk_ipg);
>>      if (ret) {
>> -            dev_err(pipe->crtc.dev->dev, "Cannot enable ipg clock: %pe\n", 
>> ERR_PTR(ret));
>> +            dev_err(crtc->dev->dev, "Cannot enable ipg clock: %pe\n", 
>> ERR_PTR(ret));
>>              return;
>>      }
>
> [Severity: Critical]
> This is a pre-existing issue, but does this perform memory-mapped writes to
> peripheral registers before the IPG and AHB bus clocks are enabled?
>
> Just prior to enabling the IPG clock here, the driver performs multiple
> writel() configuration writes to the LPCR and LCPR registers. Accessing
> registers of an unclocked IP block on i.MX platforms can hang the bus and
> trigger a synchronous external abort. Could we ensure the clocks are
> prepared and enabled before doing configuration writes?
>
>>      ret = clk_prepare_enable(lcdc->clk_ahb);
>>      if (ret) {
>> -            dev_err(pipe->crtc.dev->dev, "Cannot enable ahb clock: %pe\n", 
>> ERR_PTR(ret));
>> +            dev_err(crtc->dev->dev, "Cannot enable ahb clock: %pe\n", 
>> ERR_PTR(ret));
>>  
>>              clk_disable_unprepare(lcdc->clk_ipg);
>>  
>>              return;
>>      }
>>  
>> -    imx_lcdc_update_hw_registers(pipe, NULL, true);
>> +    imx_lcdc_update_hw_registers(crtc, NULL, true);
>>  
>>      /* Enable VBLANK Interrupt */
>>      writel(INTR_EOF, lcdc->base + IMX21LCDC_LIER);
>> +
>> +    drm_crtc_vblank_on(crtc);
>>  }
>>  
>> -static void imx_lcdc_pipe_disable(struct drm_simple_display_pipe *pipe)
>> +static void imx_lcdc_crtc_helper_atomic_disable(struct drm_crtc *crtc,
>> +                                            struct drm_atomic_commit 
>> *commit)
>>  {
>> -    struct imx_lcdc *lcdc = imx_lcdc_from_drmdev(pipe->crtc.dev);
>> -    struct drm_crtc *crtc = &lcdc->pipe.crtc;
>> +    struct drm_crtc_state *old_crtc_state = 
>> drm_atomic_get_old_crtc_state(commit, crtc);
>> +    struct drm_crtc_state *new_crtc_state = 
>> drm_atomic_get_new_crtc_state(commit, crtc);
>> +    struct imx_lcdc *lcdc = imx_lcdc_from_drmdev(crtc->dev);
>>      struct drm_pending_vblank_event *event;
>>  
>> +    drm_crtc_vblank_off(crtc);
>> +
>>      clk_disable_unprepare(lcdc->clk_ahb);
>>      clk_disable_unprepare(lcdc->clk_ipg);
>>  
>> -    if (pipe->crtc.enabled)
>> +    if (old_crtc_state->enable)
>>              clk_disable_unprepare(lcdc->clk_per);
>>  
>>      spin_lock_irq(&lcdc->drm.event_lock);
>> -    event = crtc->state->event;
>> +    event = new_crtc_state->event;
>>      if (event) {
>> -            crtc->state->event = NULL;
>> +            new_crtc_state->event = NULL;
>>              drm_crtc_send_vblank_event(crtc, event);
>>      }
>
> [Severity: High]
> This isn't a bug introduced by this patch, but does consuming the event
> during the disable phase signal completion to userspace prematurely?
>
> Sending the event here signals to the compositor that the modeset is complete
> before the new mode is programmed and flushed, which could lead to frame
> submission races or compositor stalls.
>
>>      spin_unlock_irq(&lcdc->drm.event_lock);
>> @@ -272,17 +280,20 @@ static void imx_lcdc_pipe_disable(struct 
>> drm_simple_display_pipe *pipe)
>>      writel(0, lcdc->base + IMX21LCDC_LIER);
>
> [Severity: Critical]
> This is a pre-existing issue, but are the bus clocks disabled before clearing
> the VBLANK interrupt?
>
> Similar to the enable path, writing to the LIER register while the IPG and
> AHB clocks are disabled can trigger a bus fault. Furthermore, since the
> interrupt is unmasked before the clocks are disabled, an incoming IRQ could
> cause the handler to read the LISR register while clocks are off.
>
>>  }
>>  
>> -static int imx_lcdc_pipe_check(struct drm_simple_display_pipe *pipe,
>> -                           struct drm_plane_state *plane_state,
>> -                           struct drm_crtc_state *crtc_state)
>> +static int imx_lcdc_crtc_helper_atomic_check(struct drm_crtc *crtc,
>> +                                         struct drm_atomic_commit *commit)
>>  {
>> +    struct drm_crtc_state *crtc_state = 
>> drm_atomic_get_new_crtc_state(commit, crtc);
>> +    struct drm_crtc_state *old_crtc_state = 
>> drm_atomic_get_old_crtc_state(commit, crtc);
>>      const struct drm_display_mode *mode = &crtc_state->mode;
>> -    const struct drm_display_mode *old_mode = &pipe->crtc.state->mode;
>> +    const struct drm_display_mode *old_mode = &old_crtc_state->mode;
>> +    int ret;
>>  
>> -    if (mode->hdisplay < LCDC_MIN_XRES || mode->hdisplay > LCDC_MAX_XRES ||
>> -        mode->vdisplay < LCDC_MIN_YRES || mode->vdisplay > LCDC_MAX_YRES ||
>> -        mode->hdisplay % 0x10) { /* must be multiple of 16 */
>> -            drm_err(pipe->crtc.dev, "unsupported display mode (%u x %u)\n",
>> +    if (crtc_state->enable &&
>> +        (mode->hdisplay < LCDC_MIN_XRES || mode->hdisplay > LCDC_MAX_XRES ||
>> +         mode->vdisplay < LCDC_MIN_YRES || mode->vdisplay > LCDC_MAX_YRES ||
>> +         mode->hdisplay % 0x10)) { /* must be multiple of 16 */
>> +            drm_err(crtc->dev, "unsupported display mode (%u x %u)\n",
>>                      mode->hdisplay, mode->vdisplay);
>>              return -EINVAL;
>>      }
>> @@ -291,46 +302,116 @@ static int imx_lcdc_pipe_check(struct 
>> drm_simple_display_pipe *pipe,
>>              old_mode->hdisplay != mode->hdisplay ||
>>              old_mode->vdisplay != mode->vdisplay;
>
> [Severity: High]
> This is a pre-existing issue, but does this direct assignment to
> mode_changed discard modeset requests initiated by the DRM core?
>
> The core drm_atomic_helper_check_modeset() function may have already set
> mode_changed to true due to connector routing changes or userspace forcing a
> modeset. Using a direct assignment instead of a bitwise OR (|=) silently
> clears that flag if the spatial dimensions haven't changed.
>
>> -    return 0;
>> +    if (crtc_state->enable) {
>> +            ret = drm_atomic_helper_check_crtc_primary_plane(crtc_state);
>> +            if (ret)
>> +                    return ret;
>> +    }
>> +
>> +    return drm_atomic_add_affected_planes(commit, crtc);
>>  }
>>  
>> -static void imx_lcdc_pipe_update(struct drm_simple_display_pipe *pipe,
>> -                             struct drm_plane_state *old_state)
>> +static void imx_lcdc_plane_helper_atomic_update(struct drm_plane *plane,
>> +                                            struct drm_atomic_commit 
>> *commit)
>>  {
>> -    struct drm_crtc *crtc = &pipe->crtc;
>> -    struct drm_pending_vblank_event *event = crtc->state->event;
>> -    struct drm_plane_state *new_state = pipe->plane.state;
>> +    struct drm_plane_state *old_state = 
>> drm_atomic_get_old_plane_state(commit, plane);
>> +    struct drm_plane_state *new_state = 
>> drm_atomic_get_new_plane_state(commit, plane);
>> +    struct drm_crtc *crtc = new_state->crtc ?: old_state->crtc;
>>      struct drm_framebuffer *fb = new_state->fb;
>>      struct drm_framebuffer *old_fb = old_state->fb;
>>      struct drm_crtc *old_crtc = old_state->crtc;
>>      bool mode_changed = false;
>>  
>> +    if (!fb)
>> +            return;
>> +
>>      if (old_fb && old_fb->format != fb->format)
>>              mode_changed = true;
>>      else if (old_crtc != crtc)
>>              mode_changed = true;
>>  
>> -    imx_lcdc_update_hw_registers(pipe, old_state, mode_changed);
>> +    imx_lcdc_update_hw_registers(crtc, old_state, mode_changed);
>> +}
>
> [Severity: High]
> Does calling imx_lcdc_update_hw_registers() here cause a persistent reference
> count leak for clk_per?
>
> During a modeset, imx_lcdc_crtc_helper_atomic_enable() already calls
> imx_lcdc_update_hw_registers() which increments the clock reference count
> when the CRTC is enabled.
>
> When the plane helper subsequently calls it again here, the clock prepare
> enable block is hit a second time. However, during display disable, the
> plane update returns early due to a NULL fb, meaning the clock is only
> disabled once in the CRTC disable path.
>
>>  
>> -    if (event) {
>> -            crtc->state->event = NULL;
>> +static int imx_lcdc_plane_helper_atomic_check(struct drm_plane *plane,
>> +                                          struct drm_atomic_commit *commit)
>> +{
>> +    struct drm_plane_state *plane_state = 
>> drm_atomic_get_new_plane_state(commit, plane);
>> +    struct drm_crtc *crtc = plane_state->crtc;
>> +    struct drm_crtc_state *crtc_state = NULL;
>>  
>> -            spin_lock_irq(&crtc->dev->event_lock);
>> +    if (crtc)
>> +            crtc_state = drm_atomic_get_new_crtc_state(commit, crtc);
>>  
>> -            if (crtc->state->active && drm_crtc_vblank_get(crtc) == 0)
>> -                    drm_crtc_arm_vblank_event(crtc, event);
>> -            else
>> -                    drm_crtc_send_vblank_event(crtc, event);
>> +    return drm_atomic_helper_check_plane_state(plane_state, crtc_state,
>
> [Severity: High]
> Can fetching the CRTC state this way cause a kernel panic in the core atomic
> helpers?
>
> If userspace submits a commit that modifies only plane properties without
> affecting the CRTC, the CRTC is not implicitly added to the atomic commit
> state.
>
> In this scenario, drm_atomic_get_new_crtc_state() returns NULL. Passing this
> NULL pointer directly to drm_atomic_helper_check_plane_state() causes a crash
> when it dereferences crtc_state->enable.
>
> Could this use drm_atomic_get_crtc_state() instead to ensure the state is
> brought into the commit if it is missing?

I think it is fine here; I'll just copy the pattern from [1].

[1] 
https://elixir.bootlin.com/linux/v7.1.2/source/drivers/gpu/drm/mgag200/mgag200_mode.c#L487

Reply via email to