This isn't correct

On Sun, 2026-08-16 at 15:09 +0200, Marek Czernohous wrote:
> From: Marek Czernohous <[email protected]>
> 
> nv50_sor_atomic_disable() unconditionally computes
> nv50_head(nv_encoder->crtc) and dereferences the result a few lines
> later.  nv_encoder->crtc is nouveau's own shadow pointer, set in
> .atomic_enable and cleared at the end of .atomic_disable.
> Commit f575f2bdb6c3 ("drm/nouveau/kms/nv50-: Remove (nv_encoder-
> >crtc)
> checks in ->disable callbacks") removed the NULL check here,
> reasoning that the atomic helpers never call ->disable without a
> crtc.  On NVAC (MCP79) under Wayland sessions (observed with Weston's
> DRM backend and with labwc/wlroots) we have hit the NULL case in
> practice during session teardown and VT switches: disable runs
> without
> (or after) its matching enable, and because nv50_head() is
> container_of(), the NULL does not stay NULL but becomes a bogus
> non-NULL pointer, so the subsequent head dereferences fault and the
> kernel oopses.

In the future you should probably put a backtrace if you've seen this
in the wild

> 
> Restore the guard, as drm_WARN_ON_ONCE() instead of a silent return:
> a NULL crtc here still indicates a state-tracking inconsistency that
> should stay visible.  Return without touching the output; in this
> path
> either enable never ran (nothing to tear down) or an earlier disable
> already did the teardown, and the encoder release is handled by the
> commit_tail release loop in both cases.  (That loop then rejects the
> release of a never-acquired output with -EINVAL in the nvif layer,
> which is harmless; the vanilla code oopsed before ever reaching it.)
> 
> The same inconsistent-state path can also leave the encoder without
> an
> old connector state, in which case nv50_outp_get_old_connector()
> returns NULL while the backlight teardown dereferenced it
> unconditionally, so the oops would only have moved there.  Hoist the
> guard above all of that and look at the old connector only after
> checking it.
> 
> Fixes: f575f2bdb6c3 ("drm/nouveau/kms/nv50-: Remove (nv_encoder-
> >crtc) checks in ->disable callbacks")
> Cc: <[email protected]>
> Tested-by: Fab Stz <[email protected]>
> Assisted-by: Claude:claude-opus-4-7
> Assisted-by: Claude:claude-opus-4-8
> Assisted-by: Claude:claude-opus-5
> Signed-off-by: Marek Czernohous <[email protected]>
> ---
>  drivers/gpu/drm/nouveau/dispnv50/disp.c | 30 ++++++++++++++++++++---
> --
>  1 file changed, 25 insertions(+), 5 deletions(-)
> 
> diff --git a/drivers/gpu/drm/nouveau/dispnv50/disp.c
> b/drivers/gpu/drm/nouveau/dispnv50/disp.c
> index 364227f5456f..f532b0ed8880 100644
> --- a/drivers/gpu/drm/nouveau/dispnv50/disp.c
> +++ b/drivers/gpu/drm/nouveau/dispnv50/disp.c
> @@ -1565,16 +1565,36 @@ static void
>  nv50_sor_atomic_disable(struct drm_encoder *encoder, struct
> drm_atomic_commit *state)
>  {
>       struct nouveau_encoder *nv_encoder =
> nouveau_encoder(encoder);
> -     struct nv50_head *head = nv50_head(nv_encoder->crtc);
> +     struct nv50_head *head;
>  #ifdef CONFIG_DRM_NOUVEAU_BACKLIGHT
> -     struct nouveau_connector *nv_connector =
> nv50_outp_get_old_connector(state, nv_encoder);
> +     struct nouveau_connector *nv_connector;
>       struct nouveau_drm *drm = nouveau_drm(nv_encoder-
> >base.base.dev);
> -     struct nouveau_backlight *backlight = nv_connector-
> >backlight;
> -     struct drm_dp_aux *aux = &nv_connector->aux;
> +     struct nouveau_backlight *backlight;
>       int ret;
> +#endif
>  
> +     /* nv_encoder->crtc is the driver's shadow pointer, set in

It's actually an outdated variable from the pre-atomic days

> +      * .atomic_enable (and by the boot-time hardware readback)
> and

We don't set nv_encoder->crtc at the boot-time hardware state readback.

> +      * cleared at the end of this function.  NULL here
> +      * means disable-without-enable or a double disable; bail
> before
> +      * container_of() turns it into a bogus head pointer
> (checking the
> +      * result would not work, container_of(NULL) is never
> NULL).  The
> +      * encoder release is handled by the commit_tail release
> loop, so
> +      * there is nothing to clean up here.
> +      */

this is way too verbose

> +     if (drm_WARN_ON_ONCE(encoder->dev, !nv_encoder->crtc))
> +             return;
> +     head = nv50_head(nv_encoder->crtc);

this isn't the bug, the bug is that we're reading nv_encoder->crtc at
all here.

> +
> +#ifdef CONFIG_DRM_NOUVEAU_BACKLIGHT
> +     /* The same inconsistent-state path can leave us without an
> old
> +      * connector state, so check before touching it.
> +      */
> +     nv_connector = nv50_outp_get_old_connector(state,
> nv_encoder);
> +     backlight = nv_connector ? nv_connector->backlight : NULL;
>       if (backlight && backlight->uses_dpcd) {
> -             ret = drm_edp_backlight_disable(aux, &backlight-
> >edp_info);
> +             ret = drm_edp_backlight_disable(&nv_connector->aux,
> +                                             &backlight-
> >edp_info);

This doesn't need changing either if we just fix where the encoder came
from.

I'll send out proper fixes for this in a moment

>               if (ret < 0)
>                       NV_ERROR(drm, "Failed to disable backlight
> on [CONNECTOR:%d:%s]: %d\n",
>                                nv_connector->base.base.id,
> nv_connector->base.name, ret);

Reply via email to