On Sat, 25 Jul 2026 05:04:38 +0000, [email protected] wrote:
> Thank you for your contribution! Sashiko AI review found 12 potential 
> issue(s) to consider:
> - [Critical] DMMU page table size calculation underflow allows arbitrary 
> physical memory access.
> - [Critical] Shallow copy of DMA buffers in atomic state causes active 
> display corruption.
> - [High] crtc_cmdlist_atomic_commit() passes a guaranteed NULL pointer to the 
> hardware command list execution callback.
> - [High] Global `cmdlist_groups` array causes data races across concurrent 
> atomic commits.
> - [High] Unconditional dereference of `first_cl` in `cmdlist_atomic_commit()` 
> causes a kernel panic if the command list group is empty.
> - [High] DMMU maps only the first GEM object, breaking multi-planar 
> framebuffers.
> - [High] NULL pointer dereference in `dpu_plane_info` tracepoint.
> - [High] Sleeping in atomic context via `drm_gem_shmem_get_pages_sgt()`.
> - [High] Missing bounds check on `nod_len` causes DMA buffer overflow.
> - [Medium] Unaligned base virtual addresses are used for DMMU translation 
> regions.
> - [Medium] Memory leak of DMA buffers upon atomic state destruction.
> - [Medium] Speculative array out-of-bounds access in 
> `cmdlist_sort_by_group()`.
> --
>
> --- Patch [8]: [PATCH RESEND 08/17] drm/spacemit: add Saturn DPU core types, 
> cmdlist and display MMU ---
> commit dc983d68951c3577447bb897e17c833e557da908
> Author: Cody Kang <[email protected]>
>
> drm/spacemit: add Saturn DPU core types, cmdlist and display MMU
>
> This commit introduces the shared driver types, MMIO accessors, trace
> events, command-list builder, and DMMU table code for the Saturn DPU.
> It enables batched register programming by building per-module register
> lists in memory, which the hardware command-list engine replays.
>
> > diff --git a/drivers/gpu/drm/spacemit/dpu/dpu_trace.h 
> > b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
> > index 0000000000000..abfe34a67a4bd 100644
> > --- /dev/null
> > +++ b/drivers/gpu/drm/spacemit/dpu/dpu_trace.h
> [ ... ]
> > +TRACE_EVENT(dpu_plane_info,
> > +       TP_PROTO(struct drm_plane_state *state, struct drm_framebuffer *fb,
> > +                u32 rdma_id, u32 alpha, u32 rotation),
> > +   TP_ARGS(state, fb, rdma_id, alpha, rotation),
> > +   TP_STRUCT__entry(
> > +                   __field(u32, rdma_id)
> > +                   __field(u32, src_w)
> > +                   __field(u32, src_h)
> > +                   __field(u32, src_x)
> > +                   __field(u32, src_y)
> > +                   __field(u32, crtc_w)
> > +                   __field(u32, crtc_h)
> > +                   __field(u32, crtc_x)
> > +                   __field(u32, crtc_y)
> > +                   __field(u32, width)
> > +                   __field(u32, height)
> > +                   __field(u32, format)
> > +                   __field(u32, blend_mode)
> > +                   __field(u32, alpha)
> > +                   __field(u32, zpos)
> > +                   __field(u32, rotation)
> > +   ),
> > +   TP_fast_assign(
> > +                   __entry->rdma_id = rdma_id;
> > +                   __entry->src_w = state->src_w >> 16;
> > +                   __entry->src_h = state->src_h >> 16;
> > +                   __entry->src_x = state->src_x >> 16;
> > +                   __entry->src_y = state->src_y >> 16;
> > +                   __entry->crtc_w = state->crtc_w;
> > +                   __entry->crtc_h = state->crtc_h;
> > +                   __entry->crtc_x = state->crtc_x;
> > +                   __entry->crtc_y = state->crtc_y;
> > +                   __entry->width = fb->width;
> > +                   __entry->height = fb->height;
> > +                   __entry->format = fb->format->format;
>
> [Severity: High]
> Can this dereference a NULL pointer? When a plane is disabled during an
> atomic commit, state->fb might be NULL. Unconditionally reading fb->width
> here could cause a kernel panic if tracing is enabled.

The tracepoint's only caller passes a validated fb from an enabled
plane's update path; disabled planes never reach it.

> > +                   __entry->blend_mode = state->pixel_blend_mode;
> > +                   __entry->alpha = alpha;
> > +                   __entry->zpos = state->zpos;
> > +                   __entry->rotation = rotation;
> > +   ),
> [ ... ]
> > diff --git a/drivers/gpu/drm/spacemit/spacemit_cmdlist.c 
> > b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
> > index 0000000000000..787d38e9e135f 100644
> > --- /dev/null
> > +++ b/drivers/gpu/drm/spacemit/spacemit_cmdlist.c
> [ ... ]
> > +void cmdlist_regs_packing(struct cmdlist *cl, enum cmdlist_mode_type mod,
> > +                     struct cmdlist_regs *cl_regs)
> > +{
> [ ... ]
> > +   for (i = 0; i < cl_regs->size;) {
> > +           u32 value[CMDLIST_ROW_REGS] = {0x0};
> > +           u32 offset, strobe = 0;
> > +           u8 regs_in_row = 0;
> > +
> > +           if (cl_regs->flags[i]) {
> > +                   offset = cl_regs->base + i * sizeof(u32);
> > +                   for (u8 j = 0; j < CMDLIST_ROW_REGS; j++) {
> > +                           index = i + j;
> > +                           if (likely(index < cl_regs->size)) {
> > +                                   if (cl_regs->flags[index]) {
> > +                                           value[j] = reg_base[index];
> > +                                           regs_in_row++;
> > +                                           strobe |= CMDLIST_REG_STROBE(j);
> > +                                   }
> > +                           } else {
> > +                                   break;
> > +                           }
> > +                   }
> > +                   hwdev->cmdlist_fill_data_row(cl, strobe, offset, value);
> > +                   /*
> > +                    * the row covers the whole window; a shorter stride
> > +                    * would revisit covered words as duplicate rows
> > +                    */
> > +                   i += CMDLIST_ROW_REGS;
> > +                   cl->nod_len++;
>
> [Severity: High]
> Is there a risk of overflowing the DMA command list buffer here? The loop
> increments cl->nod_len and writes to memory, but there doesn't seem to be
> a bounds check to ensure the packed rows do not exceed the allocated
> capacity in cl->size.

Not reachable: the 4 KiB cmdlist holds 255 rows, the largest
register window (146 words) packs into at most 49, and no single
list carries more than a few windows. v2 I can add a WARN_ONCE
guard in the row-fill helpers rather than trusting the arithmetic.

> > +           } else {
> > +                   i++;
> > +           }
> > +   }
> > +
> > +   drm_dbg(a_crtc->crtc.dev, "row_num = %d\n", cl->nod_len);
> > +}
> > +
> > +void cmdlist_sort_by_group(struct drm_crtc *crtc)
> > +{
> > +   struct cmdlist *first_cl;
> > +   struct cmdlist *last_cl;
> > +   struct cmdlist *p;
> > +   struct cmdlist *prev;
> > +   struct drm_plane *plane;
> > +   struct spacemit_crtc_rdma *rdmas =
> > +                   to_spacemit_crtc_state(crtc->state)->rdmas;
> > +   struct spacemit_drm_private *priv = crtc->dev->dev_private;
> > +
> > +   drm_atomic_crtc_for_each_plane(plane, crtc) {
> > +           struct spacemit_plane_state *spacemit_pstate =
> > +                   to_spacemit_plane_state(plane->state);
> > +           u32 rdma_id = spacemit_pstate->rdma_id;
> > +
> > +           first_cl = &spacemit_pstate->cl;
> > +           if (!first_cl->va)
> > +                   continue;
> > +
> > +           last_cl = first_cl;
> > +           last_cl->next = NULL;
> > +
> > +           rdmas[rdma_id].in_use = true;
>
> [Severity: Medium]
> Could rdma_id exceed the bounds of the array? It is used directly as an
> index for the rdmas array without checking if it is within hwdev->rdma_nums.

rdma_id is validated against hwdev->rdma_nums in plane atomic_check
before any array use.

> > +           if (priv->cmdlist_groups[rdma_id]) {
>
> [Severity: High]
> Does modifying the global priv->cmdlist_groups array here cause data races?
> Since this array is stored in the shared spacemit_drm_private structure,
> concurrent atomic commits on multiple CRTCs might overwrite each other's
> command lists without locking.

There is one CRTC per DRM device in this driver; commits on a CRTC
are serialized by the atomic helpers, and the group array is filled
and consumed within a single commit tail.

> > +                   p = priv->cmdlist_groups[rdma_id];
> > +                   prev = NULL;
> > +                   while (p) {
> > +                           if (cl_to_spacemit_pstate(p)->state.crtc_y <
> > +                               spacemit_pstate->state.crtc_y) {
> > +                                   prev = p;
> > +                                   p = p->next;
> > +                           } else {
> > +                                   break;
> > +                           }
> > +                   }
> > +                   if (!prev) {
> > +                           priv->cmdlist_groups[rdma_id] = first_cl;
> > +                           last_cl->next = p;
> > +                   } else {
> > +                           prev->next = first_cl;
> > +                           last_cl->next = p;
> > +                   }
> > +           } else {
> > +                   priv->cmdlist_groups[rdma_id] = first_cl;
> > +           }
> > +   }
> > +}
> > +
> > +void cmdlist_atomic_commit(struct drm_crtc *crtc,
> > +                      struct drm_crtc_state *old_state)
> > +{
> [ ... ]
> > +   for (i = 0; i < hwdev->rdma_nums; i++) {
> > +           if (old_rdmas[i].in_use)
> > +                   hwdev->enable_cmdlist(a_crtc, hwdev, i, false);
> > +
> > +           if (cur_rdmas[i].in_use) {
> > +                   drm_dbg(crtc->dev, "cmdlist group = %d\n", i);
> > +                   cur_cl = priv->cmdlist_groups[i];
> > +                   first_cl = cur_cl;
> > +                   while (cur_cl) {
> > +                           hwdev->cmdlist_fill_conf_row(cur_cl, hwdev,
> > +                                   CMDLIST_CMP_INVALID);
> > +                           cmdlist_dump_node(cur_cl);
> > +                           cur_cl = cur_cl->next;
> > +                   }
> > +                   chy = cl_to_spacemit_pstate(first_cl)->state.crtc_y;
>
> [Severity: High]
> Could first_cl be NULL here? If cur_rdmas[i].in_use is true but
> priv->cmdlist_groups[i] is empty, first_cl would be NULL. This can happen
> if planes are removed from the state but in_use was copied as true from the
> old state. The cl_to_spacemit_pstate macro would yield a negative offset
> from NULL, resulting in a crash.

in_use cannot be inherited: crtc duplicate_state allocates a zeroed
rdmas array for every state, so it is only set by
cmdlist_sort_by_group() for planes whose cmdlist exists in the same
commit.

> > +                   addrl = (lower_32_bits(priv->cmdlist_groups[i]->pa) &
> > +                            CMDLIST_ADDRL_ALIGN_MASK) >> 
> > CMDLIST_ADDRL_ALIGN_BITS;
> > +
> > +                   addrh = upper_32_bits(priv->cmdlist_groups[i]->pa);
> > +                   hwdev->cfg_cmdlist(hwdev, i, chy, addrl, addrh);
> > +                   hwdev->enable_cmdlist(a_crtc, hwdev, i, true);
> > +                   priv->cmdlist_groups[i] = NULL;
> > +           }
> > +   }
> > +}
> > +
> [ ... ]
> > +void crtc_cmdlist_atomic_commit(struct spacemit_crtc *a_crtc)
> > +{
> > +   struct cmdlist *cur_cl;
> > +   struct drm_crtc *crtc = &a_crtc->crtc;
> > +   struct spacemit_drm_private *priv = crtc->dev->dev_private;
> > +   struct spacemit_hw_device *hwdev = priv->hwdev;
> > +   u8 crtc_id = drm_crtc_index(crtc);
> > +   u8 dev_id = a_crtc->dev_id;
> > +
> > +   drm_dbg(crtc->dev, "cmdlist group = %d\n",
> > +           hwdev->rdma_nums + crtc_id);
> > +   cur_cl = priv->cmdlist_groups[hwdev->rdma_nums + crtc_id];
> > +   while (cur_cl) {
> > +           hwdev->cmdlist_fill_conf_row(cur_cl, hwdev, dev_id);
> > +           cmdlist_dump_node(cur_cl);
> > +           cur_cl = cur_cl->next;
> > +   }
> > +   hwdev->crtc_cmdlist(cur_cl, hwdev, priv, crtc_id, dev_id);
>
> [Severity: High]
> Does this pass a guaranteed NULL pointer to the hardware callback?
> The while loop exhausts cur_cl to NULL, which is then immediately passed
> to hwdev->crtc_cmdlist. It looks like the head of the list should be
> passed instead.

The callback never reads that parameter; it re-reads the group from
spacemit_drm_private itself. Will drop the unused parameter in v2 so
this stops reading like a bug.

> > +}
> > +
> > +/* Called from atomic_check(), where -ENOMEM is still an answer. */
> > +int spacemit_cmdlist_alloc(struct device *dev, struct cmdlist *cl)
> > +{
> > +   if (cl->va)
> > +           return 0;
>
> [Severity: Critical]
> Does returning early when cl->va is present cause active display corruption?
> Because struct cmdlist is embedded directly in the plane state, DRM's state
> duplication will shallow-copy cl->va. If atomic_update mutates this same
> virtual address while the hardware is actively reading it from the old
> state, it could lead to screen tearing or DPU hangs. DMA buffers for
> command lists generally need to be multi-buffered or allocated per-state.

There is no shallow copy: duplicate_state zeroes the embedded
cmdlist, atomic_check allocates a fresh buffer for each new state,
and destroy_state frees it.

> > +
> > +   cl->size = PER_CMDLIST_SIZE;
> > +   cl->va = dma_alloc_coherent(dev, cl->size, &cl->pa,
> > +                               GFP_KERNEL | __GFP_ZERO);
> > +   if (!cl->va)
> > +           return -ENOMEM;
>
> [Severity: Medium]
> Is there a corresponding dma_free_coherent() during state destruction?
> If the allocated cl->va is never freed when the atomic state is destroyed,
> this might cause a steady leak of DMA memory.

Yes: spacemit_plane_atomic_destroy_state() frees both the cmdlist
and the DMMU table.

> > +
> > +   return 0;
> > +}
> [ ... ]
> > diff --git a/drivers/gpu/drm/spacemit/spacemit_dmmu.c 
> > b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
> > index 0000000000000..2ae30dd1a4f23 100644
> > --- /dev/null
> > +++ b/drivers/gpu/drm/spacemit/spacemit_dmmu.c
> [ ... ]
> > +int spacemit_dmmu_map(struct drm_framebuffer *fb, struct dpu_mmu_tbl 
> > *mmu_tbl,
> > +                 u8 tbu_id, struct cmdlist_regs *cl_rdma,
> > +            struct drm_plane *plane)
> > +{
> > +   struct spacemit_drm_private *priv = fb->dev->dev_private;
> > +   struct spacemit_hw_device *hwdev = priv->hwdev;
> > +   const struct drm_format_info *format = NULL;
> > +   struct sg_table *sgt = NULL;
> > +   u32 total_size, offset1, offset2;
> > +   struct tbu_instance tbu = { };
> > +   u8 plane_num;
> > +   u32 val;
> > +
> > +   format = fb->format;
> > +   sgt = drm_gem_shmem_get_pages_sgt(to_drm_gem_shmem_obj(fb->obj[0]));
>
> [Severity: High]
> Will this sleep in atomic context? This function appears to be called
> during hardware programming in the atomic commit phase. Calling
> drm_gem_shmem_get_pages_sgt allocates memory and can acquire a mutex,
> which is forbidden in atomic context. Buffer mapping typically needs to be
> deferred to the sleepable prepare_fb callback.

This runs from plane atomic_update in the commit tail, which is
process context; sleeping is allowed there.

> [Severity: High]
> Does mapping only fb->obj[0] break multi-planar framebuffers? If userspace
> provides a multi-planar framebuffer where the planes reside in different GEM
> objects, extracting the SG table only for obj[0] will result in incorrect
> physical memory pages for the secondary planes.

Every advertised format is single-planar, so the two- and
three-plane arms cannot execute; will drop them in v2 rather than
harden dead code.

> > +   if (IS_ERR(sgt))
> > +           return PTR_ERR(sgt);
> > +
> > +   plane_num = format->num_planes;
> > +
> > +   total_size = roundup(fb->obj[0]->size, PAGE_SIZE);
> > +
> > +   offset1 = plane_num > 1 ? fb->offsets[1] : total_size;
> > +   offset2 = plane_num > 2 ? fb->offsets[2] : total_size;
> > +
> > +   switch (plane_num) {
> > +   case 3:
> > +           tbu.ttb_pa[2] = mmu_tbl->pa + (offset2 >> PAGE_SHIFT) * 4;
> > +           tbu.tbu_va[2] = TBU_BASE_VA(tbu_id) + offset2;
>
> [Severity: Medium]
> Could adding offset2 directly to TBU_BASE_VA result in unaligned base
> virtual addresses? If userspace supplies an offset that is not a multiple
> of PAGE_SIZE, programming an unaligned virtual address might confuse the
> hardware MMU.

Unreachable for the same reason: single-planar formats only.

> > +           tbu.ttb_size[2] = PAGE_ALIGN(total_size - rounddown(offset2, 
> > PAGE_SIZE))
> > +                             >> PAGE_SHIFT;
>
> [Severity: Critical]
> Can this size calculation underflow? If a malicious userspace client
> provides offset2 smaller than offset1, the unsigned subtraction
> offset2 - rounddown(offset1, PAGE_SIZE) will underflow to a massive
> positive value. This could program the DMMU with a huge translation block,
> allowing arbitrary out-of-bounds physical memory reads.

Also unreachable: single-planar formats only, and the framebuffer
core bounds every offsets[] within the GEM object at creation time.

Cody

Reply via email to