All of lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Loic Poulain" <loic.poulain@oss.qualcomm.com>
Cc: devicetree@vger.kernel.org, robh@kernel.org, conor+dt@kernel.org,
	media-ci@linuxtv.org
Subject: Re: [PATCH v13 9/9] arm64: defconfig: Enable Qualcomm CAMSS OPE driver
Date: Tue, 03 Nov 2026 20:51:40 +0000	[thread overview]
Message-ID: <sashiko-outbox-196017@kernel.org> (raw)
In-Reply-To: <20261103-camss-isp-ope-v13-9-c2cbf15a39b1@oss.qualcomm.com>

Thank you for your contribution! Sashiko AI review found 14 potential issue(s) to consider:
- [High] media: qcom: camss: ope: use-after-free on ope_v4l2_register() failure
- [Medium] arm64: dts: qcom: agatti: duplicate IOMMU stream IDs on camss and isp_ope
- [Medium] media: qcom: camss: premature consume and reset of params in ope_apply_params()
- [Medium] media: qcom: camss: vb2 ownership and poll bugs in ope_dqbuf() and ope_poll()
- [Medium] media: qcom: camss: invalid G_PARM and S_PARM on capture in ope_g_parm()
- [Medium] media: qcom: camss: wrong compose bounds origin in ope_proc_get_selection()
- [Medium] media: qcom: camss: missing enum_mbus_code callback in ope_disp_pad_ops
- [Medium] media: qcom: camss: max-size check and late validation in ope_buf_prepare()
- [Medium] media: qcom: camss: ope: 0 Hz OPP rounded up in ope_adjust_power()
- [Medium] media: qcom: camss: ope: split ioctl locks between vdev_lock and vbq_lock
- [Medium] media: qcom: camss: ope: unbalanced pipeline stop on unregistration
- [Medium] media: qcom: camss: premature ope_sched_resume() in ope_runtime_resume()
- [Low] media: qcom: camss: signed shift overflow in DS_RESOLUTION()
- [Low] media: qcom: camss: ope: incomplete kernel-doc in pipeline.h
--

commit e9b18dde076886d4503de413170d4ece303a05fe
Author: Nihal Kumar Gupta <nihal.gupta@oss.qualcomm.com>

arm64: defconfig: Enable Qualcomm CAMSS OPE driver

This series adds the Qualcomm Camera Subsystem Offline Processing Engine
(OPE) V4L2 memory-to-memory ISP driver, its media-controller pipeline and
parameter helpers, Devicetree nodes for Agatti and Shikra, and enables the
driver in arm64 defconfig.

Link: https://github.com/jwrdegoede/libcamera/commits/camss_pipeline_v2.1/
Link: https://github.com/loicpoulain/camss-isp-m2m-test

> diff --git a/arch/arm64/boot/dts/qcom/agatti.dtsi b/arch/arm64/boot/dts/qcom/agatti.dtsi
> index d9f4cbfd2861c..8f2b451b8f6e8 100644
> --- a/arch/arm64/boot/dts/qcom/agatti.dtsi
> +++ b/arch/arm64/boot/dts/qcom/agatti.dtsi
> @@ -1990,6 +1990,83 @@ port@1 {
>  			};
>  		};
>  
> +		isp_ope: isp@5c42000 {
> +			compatible = "qcom,qcm2290-camss-ope";

[ ... ]

> +			iommus = <&apps_smmu 0x800 0x0>,
> +				 <&apps_smmu 0x820 0x0>,
> +				 <&apps_smmu 0x840 0x0>;

[Severity: Medium]
Does isp_ope conflict with the stream IDs already claimed by camss in
arch/arm64/boot/dts/qcom/agatti.dtsi?

Right above isp_ope, the camss@5c11000 node still lists the OPE CDM, read,
and write stream IDs alongside the VFE stream ID:

	camss: camss@5c11000 {
		...
		iommus = <&apps_smmu 0x400 0x0>,
			 <&apps_smmu 0x800 0x0>,
			 <&apps_smmu 0x820 0x0>,
			 <&apps_smmu 0x840 0x0>;

Unlike shikra.dtsi (where camss@5c11000 only lists 0x400), agatti.dtsi keeps
0x800, 0x820, and 0x840 on camss. On boards or overlays that enable camss
alongside the default-enabled isp_ope node, both devices claim those three
stream IDs on apps_smmu. When isp_ope probes first, arm_smmu_device_group()
assigns a group to SMEs 0x800, 0x820, and 0x840, and when camss later calls
arm_smmu_device_group(), SME 0x400 has group == NULL while SMEs 0x800, 0x820,
and 0x840 match isp_ope's group, returning early without recording the group
for SME 0x400 and forcing both devices to share a single IOMMU group. Should
0x800, 0x820, and 0x840 be removed from camss@5c11000 in agatti.dtsi?

> diff --git a/drivers/media/platform/qcom/camss/ope/core.c b/drivers/media/platform/qcom/camss/ope/core.c
> new file mode 100644
> index 0000000000000..e111fd1b2b1a2
> --- /dev/null
> +++ b/drivers/media/platform/qcom/camss/ope/core.c

[ ... ]

> +/* Downscaler fixed-point helpers */
> +#define Q21(v)			(((uint64_t)(v)) << 21)
> +#define DS_Q21(n, d)		((uint32_t)div_u64((uint64_t)(n) << 21, (d)))
> +#define DS_RESOLUTION(in, out) \
> +	(((out) * 128 <= (in)) ? 0x0 : \
> +	 ((out) * 16  <= (in)) ? 0x1 : \
> +	 ((out) * 8   <= (in)) ? 0x2 : 0x3)

[ ... ]

> +	/* Downscalers */
> +	for (int i = 0; i < OPE_DS_MAX; i++) {
> +		struct ope_dsc_config *dsc = &stripe->dsc[i];
> +		u32 base = ope_ds_base[i];
> +		u32 cfg = 0;
> +
> +		if (dsc->input_width != dsc->output_width) {
> +			dsc->phase_step_h |=
> +				DS_RESOLUTION(dsc->input_width,
> +					      dsc->output_width) << 30;
> +			cfg |= OPE_PP_DOWNSCALE_MN_DS_CFG_H_SCALE_EN;
> +		}
> +		if (dsc->input_height != dsc->output_height) {
> +			dsc->phase_step_v |=
> +				DS_RESOLUTION(dsc->input_height,
> +					      dsc->output_height) << 30;
> +			cfg |= OPE_PP_DOWNSCALE_MN_DS_CFG_V_SCALE_EN;
> +		}

[Severity: Low]
Can shifting the result of DS_RESOLUTION() left by 30 bits in
ope_prog_stripe() trigger a signed 32-bit shift overflow?

DS_RESOLUTION() evaluates to signed int literals 0x0, 0x1, 0x2, or 0x3. When
the downscaling ratio is less than 16x (including every NV12 or NV16 frame
where OPE_DS_C_PRE scales 2x horizontally), DS_RESOLUTION() returns 0x2 or
0x3, and shifting that signed int by 30 overflows into the sign bit under
CONFIG_UBSAN_SHIFT. Could DS_RESOLUTION() use unsigned literals (0x0U..0x3U)
or FIELD_PREP(GENMASK(31, 30), ...)?

[ ... ]

> +static void ope_apply_params(struct ope_ctx *ctx)
> +{
> +	struct vb2_v4l2_buffer *vbuf;
> +	int ret;
> +
> +	vbuf = ope_queue_remove(ctx, OPE_QUEUE_PARAMS);
> +	if (!vbuf)
> +		return;
> +
> +	ret = camss_isp_params_apply(ctx->ope->dev, &vbuf->vb2_buf,
> +				     ctx->params_buf,
> +				     ope_params_type_info,
> +				     ope_params_handlers,
> +				     ARRAY_SIZE(ope_params_handlers),
> +				     ctx);
> +	if (ret)
> +		dev_warn_ratelimited(ctx->ope->dev, "invalid parameters\n");
> +
> +	vb2_buffer_done(&vbuf->vb2_buf, ret ? VB2_BUF_STATE_ERROR : VB2_BUF_STATE_DONE);
> +}

[Severity: Medium]
Can ope_apply_params() consume a parameter buffer before OPE_QUEUE_PARAMS has
finished starting streaming, and does stopping OPE_QUEUE_PARAMS abort active
video frames?

During VIDIOC_STREAMON on ope_params, vb2_start_streaming() passes pre-queued
buffers to ope_buf_queue() before calling ope_start_streaming():

	vb2_start_streaming()
	  ope_buf_queue()
	    list_add_tail(&buf->list, &ctx->queues[idx].rdy_queue);
	    ope_try_schedule(ctx);
	  ope_start_streaming()

While OPE_QUEUE_FRAME_IN and OPE_QUEUE_DISP_OUT are gated by ope_job_ready()
checking ctx->queues[...].streaming, ope_apply_params() calls
ope_queue_remove(ctx, OPE_QUEUE_PARAMS) without checking
ctx->queues[OPE_QUEUE_PARAMS].streaming. If ope_input and ope_disp_output are
already streaming, a job running before ope_start_streaming() sets
streaming = true will dequeue and complete the parameter buffer via
vb2_buffer_done(), triggering WARN_ON(!list_empty(&q->done_list)) in
vb2_start_streaming() if ope_start_streaming() fails.

In addition, in ope_stop_streaming() and ope_queues_release():

	ope_sched_cancel(ctx);
	ope_queue_drain(ctx, idx, VB2_BUF_STATE_ERROR);
	ope_sched_resume(ctx->ope);

stopping or closing ope_params while a video frame is in flight calls
ope_sched_cancel(ctx), which resets the OPE hardware via ope_hw_reset_sync()
and completes the active video frame buffers with VB2_BUF_STATE_ERROR, even
though parameter buffers are CPU-copied before hardware execution starts.

[ ... ]

> +static int ope_buf_prepare(struct vb2_buffer *vb)
> +{
> +	struct ope_ctx *ctx = ope_ctx_from_vq(vb->vb2_queue);
> +	unsigned int idx = ope_idx_from_vq(vb->vb2_queue);
> +	struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
> +	unsigned int sizeimage;
> +
> +	if (idx == OPE_QUEUE_FRAME_IN)
> +		sizeimage = ctx->fmt_in.sizeimage;
> +	else if (idx == OPE_QUEUE_DISP_OUT)
> +		sizeimage = ctx->fmt_out.sizeimage;
> +	else
> +		sizeimage = v4l2_isp_buffer_size(CAMSS_PARAMS_OPE_MAX_PAYLOAD);
> +
> +	if (V4L2_TYPE_IS_OUTPUT(vb->vb2_queue->type)) {
> +		if (vb->vb2_queue->type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) {
> +			if (vbuf->field == V4L2_FIELD_ANY)
> +				vbuf->field = V4L2_FIELD_NONE;
> +			if (vbuf->field != V4L2_FIELD_NONE)
> +				return -EINVAL;
> +		}
> +	}
> +
> +	if (vb2_plane_size(vb, 0) < sizeimage)
> +		return -EINVAL;

[Severity: Medium]
Does ope_buf_prepare() reject valid variable-length VB2_USERPTR parameter
buffers while deferring v4l2-isp buffer validation until the worker runs?

OPE_QUEUE_PARAMS enables VB2_USERPTR in ope_init_vq() and uses the extensible
variable-length v4l2-isp format. However, ope_buf_prepare() requires
vb2_plane_size(vb, 0) to be at least
v4l2_isp_buffer_size(CAMSS_PARAMS_OPE_MAX_PAYLOAD), which rejects USERPTR
buffers sized for only a subset of parameter blocks.

At the same time, ope_buf_prepare() does not call
v4l2_isp_params_validate_buffer_size() or v4l2_isp_params_validate_buffer()
at VIDIOC_PREPARE_BUF / VIDIOC_QBUF time; validation and copying only happen
asynchronously in ope_run_job() -> ope_apply_params(), allowing malformed
parameter buffers to succeed at VIDIOC_QBUF.

[ ... ]

> +static void ope_adjust_power(struct ope_dev *ope)
> +{

[ ... ]

> +	opp = dev_pm_opp_find_freq_ceil(ope->dev, &pixclk);
> +	if (IS_ERR(opp)) {
> +		unsigned long max_freq = ULONG_MAX;
> +
> +		dev_warn(ope->dev, "Requested pixel clock %luHz exceeds hardware limit\n", pixclk);
> +
> +		/* Clamp to the highest available OPP */
> +		opp = dev_pm_opp_find_freq_floor(ope->dev, &max_freq);
> +		if (!IS_ERR(opp)) {
> +			pixclk = max_freq;
> +			dev_pm_opp_put(opp);
> +		}
> +	} else {
> +		dev_pm_opp_put(opp);
> +	}
> +
> +	ret = dev_pm_opp_set_rate(ope->dev, pixclk);

[Severity: Medium]
As reported by Sashiko on v12 [1], when the last streaming context stops in
ope_stop_streaming(), pixclk is 0, but ope_adjust_power() unconditionally
calls dev_pm_opp_find_freq_ceil(ope->dev, &pixclk).

Because dev_pm_opp_find_freq_ceil() rounds pixclk = 0 up to the lowest OPP
frequency (19.2 MHz), dev_pm_opp_set_rate(ope->dev, pixclk) is never called
with 0, so the OPP table is not disabled and the required-opps vote on the cx
power domain remains active while the device is idle. Should the
dev_pm_opp_find_freq_ceil() lookup be skipped when pixclk is 0?

[ ... ]

> +static int ope_init_vq(struct ope_ctx *ctx, unsigned int idx)
> +{
> +	struct ope_dev *ope = ctx->ope;
> +	struct ope_vq_priv *qpriv;
> +	struct vb2_queue *q = &ctx->queues[idx].vq;
> +	int ret;
> +
> +	qpriv = kzalloc_obj(*qpriv);
> +	if (!qpriv)
> +		return -ENOMEM;
> +
> +	qpriv->ctx = ctx;
> +	qpriv->queue_idx = idx;
> +	qpriv->vdev = camss_isp_pipeline_get_vdev(ope->pipeline, ope_queue_to_entity[idx]);
> +	q->drv_priv = qpriv;
> +	q->ops = &ope_vb2_ops;
> +	q->lock = &ctx->vbq_lock;

[ ... ]

> +	/* Only valid because there is a single device-wide ope_ctx for now */
> +	qpriv->vdev->queue = q;
> +
> +	return 0;
> +}

[ ... ]

> +static int ope_s_fmt_vid_out(struct file *file, void *priv, struct v4l2_format *f)
> +{
> +	struct ope_ctx *ctx = ope_ctx_from_file(file);
> +	const struct ope_fmt *fmt;
> +
> +	if (vb2_is_busy(&ctx->queues[OPE_QUEUE_FRAME_IN].vq))
> +		return -EBUSY;
> +
> +	fmt = ope_fmt_try(ctx->ope, false, &f->fmt.pix_mp);
> +	ctx->fmt_in.fmt		    = fmt;
> +	ctx->fmt_in.width	    = f->fmt.pix_mp.width;
> +	ctx->fmt_in.height	    = f->fmt.pix_mp.height;

[ ... ]

> +static int ope_s_fmt_vid_cap(struct file *file, void *priv, struct v4l2_format *f)
> +{
> +	struct ope_ctx *ctx = ope_ctx_from_file(file);
> +	const struct ope_fmt *fmt;
> +
> +	if (vb2_is_busy(&ctx->queues[OPE_QUEUE_DISP_OUT].vq))
> +		return -EBUSY;
> +
> +	fmt = ope_fmt_try(ctx->ope, true, &f->fmt.pix_mp);
> +
> +	ctx->fmt_out.fmt	    = fmt;
> +	ctx->fmt_out.width	    = f->fmt.pix_mp.width;
> +	ctx->fmt_out.height	    = f->fmt.pix_mp.height;

[Severity: Medium]
As reported by Sashiko on v12 [2], does assigning vdev->lock =
&pipeline->vdev_lock in isp_register_vdev() while setting q->lock =
&ctx->vbq_lock and qpriv->vdev->queue = q in ope_init_vq() split
serialization between queue and format ioctls?

In v4l2_ioctl_get_lock(), ioctls with INFO_FL_QUEUE (VIDIOC_REQBUFS,
VIDIOC_CREATE_BUFS, VIDIOC_PREPARE_BUF, VIDIOC_QBUF, VIDIOC_STREAMON) lock
vdev->queue->lock (&ctx->vbq_lock), whereas non-queue ioctls (VIDIOC_S_FMT,
VIDIOC_G_FMT, VIDIOC_S_PARM, VIDIOC_G_PARM) lock vdev->lock
(&pipeline->vdev_lock). As a result, ope_s_fmt_vid_out(),
ope_s_fmt_vid_cap(), and ope_s_parm() check vb2_is_busy() and mutate
ctx->fmt_in, ctx->fmt_out, and ctx->framerate under pipeline->vdev_lock while
concurrent vb2 callbacks (ope_queue_setup(), ope_buf_prepare(),
ope_start_streaming(), ope_adjust_power()) read those fields under
ctx->vbq_lock.

Furthermore, ope_s_fmt_vid_out() and ope_s_fmt_vid_cap() only check
vb2_is_busy() on their own queue rather than checking whether the pipeline is
already streaming. Because video_device_pipeline_alloc_start() only validates
media links when the first video node starts streaming (when
pipe->start_count == 0), starting streaming on one node (such as ope_input or
ope_params) and then calling VIDIOC_S_FMT on another node whose queue has no
buffers yet allows ctx->fmt_out or ctx->fmt_in to be changed after link
validation has already completed.

[ ... ]

> +static int ope_g_parm(struct file *file, void *priv, struct v4l2_streamparm *sp)
> +{
> +	struct ope_ctx *ctx = ope_ctx_from_file(file);
> +
> +	if (!V4L2_TYPE_IS_OUTPUT(sp->type))
> +		return -EINVAL;
> +
> +	sp->parm.output.capability = V4L2_CAP_TIMEPERFRAME;
> +	sp->parm.output.timeperframe = ctx->fmt_in.timeperframe;
> +
> +	return 0;
> +}

[Severity: Medium]
Should VIDIOC_G_PARM and VIDIOC_S_PARM be disabled on the ope_disp_output
capture node?

Both ope_input and ope_disp_output share ope_video_ioctl_ops, which wires up
ope_g_parm() and ope_s_parm(). On ope_disp_output (VFL_DIR_RX), calling
VIDIOC_G_PARM or VIDIOC_S_PARM with V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE passes
check_fmt() in v4l2-ioctl.c and then fails with -EINVAL in ope_g_parm() /
ope_s_parm() because !V4L2_TYPE_IS_OUTPUT(sp->type) is true, whereas V4L2
expects -ENOTTY when an ioctl is not supported on a capture node. Could
v4l2_disable_ioctl() be used on ope_disp_output for VIDIOC_G_PARM and
VIDIOC_S_PARM (or separate ioctl_ops tables be used)?

[ ... ]

> +static int ope_dqbuf(struct file *file, void *priv, struct v4l2_buffer *buf)
> +{
> +	struct ope_ctx *ctx = ope_ctx_from_file(file);
> +	struct vb2_queue *vq = ope_vq_from_file(file, ctx);
> +
> +	return vq ? vb2_dqbuf(vq, buf, file->f_flags & O_NONBLOCK) : -EINVAL;
> +}
> +
> +static int ope_prepare_buf(struct file *file, void *priv, struct v4l2_buffer *buf)
> +{
> +	struct ope_ctx *ctx = ope_ctx_from_file(file);
> +	struct video_device *vdev = video_devdata(file);
> +	struct vb2_queue *vq = ope_vq_from_file(file, ctx);
> +
> +	if (!vq)
> +		return -EINVAL;
> +	if (vb2_queue_is_busy(vq, file))
> +		return -EBUSY;
> +
> +	return vb2_prepare_buf(vq, vdev->v4l2_dev->mdev, buf);
> +}
> +
> +static int ope_create_bufs(struct file *file, void *priv, struct v4l2_create_buffers *create)
> +{
> +	struct ope_ctx *ctx = ope_ctx_from_file(file);
> +	struct vb2_queue *vq = ope_vq_from_file(file, ctx);
> +	int ret;
> +
> +	if (!vq)
> +		return -EINVAL;
> +	if (vb2_queue_is_busy(vq, file))
> +		return -EBUSY;
> +	ret = vb2_create_bufs(vq, create);
> +	if (!ret && create->count)
> +		vq->owner = file->private_data;
> +
> +	return ret;
> +}

[ ... ]

> +static __poll_t ope_poll(struct file *file, poll_table *wait)
> +{
> +	unsigned int idx = ope_queue_idx_from_file(file);
> +	struct ope_ctx *ctx = ope_ctx_from_file(file);
> +	struct v4l2_fh *fh = file_to_v4l2_fh(file);
> +	struct vb2_queue *vq;
> +	unsigned long flags;
> +	__poll_t rc = 0;
> +
> +	if (idx >= OPE_QUEUE_COUNT)
> +		return EPOLLERR;
> +
> +	vq = &ctx->queues[idx].vq;
> +
> +	poll_wait(file, &vq->done_wq, wait);
> +	poll_wait(file, &fh->wait, wait);
> +
> +	spin_lock_irqsave(&vq->done_lock, flags);
> +	if (!vb2_is_streaming(vq) || vq->error) {
> +		rc |= EPOLLERR;
> +	} else if (!list_empty(&vq->done_list)) {
> +		if (V4L2_TYPE_IS_OUTPUT(vq->type))
> +			rc |= EPOLLOUT | EPOLLWRNORM;
> +		else
> +			rc |= EPOLLIN | EPOLLRDNORM;
> +	}
> +	spin_unlock_irqrestore(&vq->done_lock, flags);
> +
> +	if (v4l2_event_pending(fh))
> +		rc |= EPOLLPRI;
> +
> +	return rc;
> +}

[Severity: Medium]
As reported by Sashiko on v12 [3], why not use the standard vb2_ioctl_* and
vb2_fop_* helpers instead of custom wrappers in ope_dqbuf(),
ope_create_bufs(), and ope_poll()?

There are three issues in these custom wrappers:

1. ope_dqbuf() does not check vb2_queue_is_busy(vq, file), allowing a second
open file descriptor to dequeue buffers from an active stream owned by
another file handle.

2. ope_create_bufs() checks vb2_queue_is_busy(vq, file) even when
create->count == 0, causing read-only capability queries to fail with -EBUSY
instead of populating capabilities and max_num_buffers as
vb2_ioctl_create_bufs() does.

3. ope_poll() directly accesses private vb2_queue fields (done_wq, done_lock,
done_list, error) without holding vq->lock. Because it does not check
poll_requested_events(wait), polling only for EPOLLPRI events while the queue
is not streaming immediately returns EPOLLERR, and because it does not check
vq->waiting_for_buffers on capture queues, polling after VIDIOC_STREAMON with
no queued capture buffers blocks instead of returning EPOLLERR.

[ ... ]

> +static int ope_proc_get_selection(struct v4l2_subdev *sd,
> +				  const struct v4l2_subdev_client_info *info,
> +				  struct v4l2_subdev_state *state,
> +				  struct v4l2_subdev_selection *sel)
> +{

[ ... ]

> +	if (sel->pad == OPE_PROC_PAD_SOURCE) {
> +		const struct v4l2_rect *crop =
> +			v4l2_subdev_state_get_crop(state, OPE_PROC_PAD_SINK_IN);
> +
> +		switch (sel->target) {
> +		case V4L2_SEL_TGT_COMPOSE:
> +			sel->r = *v4l2_subdev_state_get_compose(state, OPE_PROC_PAD_SOURCE);
> +			return 0;
> +		case V4L2_SEL_TGT_COMPOSE_DEFAULT:
> +		case V4L2_SEL_TGT_COMPOSE_BOUNDS:
> +			sel->r = *crop;
> +			return 0;

[Severity: Medium]
In ope_proc_get_selection(), should V4L2_SEL_TGT_COMPOSE_DEFAULT and
V4L2_SEL_TGT_COMPOSE_BOUNDS on OPE_PROC_PAD_SOURCE return origin (0, 0)
instead of copying crop->left and crop->top?

In ope_proc_adjust_src_compose(), the compose rectangle is clamped against
bounds starting at (0, 0):

	bounds = (struct v4l2_rect){ 0, 0, sink_crop->width, sink_crop->height };
	ope_sd_adjust_crop_rect(compose, &bounds, halign, valign);

However, ope_proc_get_selection() sets sel->r = *crop, which includes any
non-zero crop->left and crop->top from OPE_PROC_PAD_SINK_IN. When a non-zero
sink crop offset is configured, a valid compose rectangle at (0, 0) falls
outside the reported V4L2_SEL_TGT_COMPOSE_BOUNDS.

[ ... ]

> +static const struct v4l2_subdev_pad_ops ope_disp_pad_ops = {
> +	.get_fmt       = v4l2_subdev_get_fmt,
> +	.set_fmt       = ope_disp_set_fmt,
> +	.link_validate = v4l2_subdev_link_validate_default,
> +};

[Severity: Medium]
Does ope_disp_pad_ops need an .enum_mbus_code callback?

isp_register_subdev() sets V4L2_SUBDEV_FL_HAS_DEVNODE on every subdev in the
pipeline, exposing a /dev/v4l-subdevN device node for ope_disp, but
ope_disp_pad_ops omits .enum_mbus_code, so VIDIOC_SUBDEV_ENUM_MBUS_CODE on
ope_disp always fails with -ENOTTY.

[ ... ]

> +	cpu_latency_qos_add_request(&ope->qos_req, PM_QOS_DEFAULT_VALUE);
> +
> +	/* ope ownership is transferred to ope_v4l2_register(), even on failure */
> +	ret = ope_v4l2_register(ope);
> +	if (ret) {
> +		cpu_latency_qos_remove_request(&ope->qos_req);
> +		platform_set_drvdata(pdev, NULL);
> +		return dev_err_probe(dev, ret, "V4L2 registration failed\n");
> +	}
> +
> +	/* Everything @ope_irq() touches is in place, let interrupts in */
> +	enable_irq(ope->irq);

[Severity: High]
As reported by Sashiko on v12 [4], if ope_v4l2_register(ope) fails in
ope_probe(), doesn't cpu_latency_qos_remove_request(&ope->qos_req) access
freed memory?

Every failure path in ope_v4l2_register() invokes ope_v4l2_dev_release(),
which frees ope via kfree(ope):

	ope_probe()
	  cpu_latency_qos_add_request(&ope->qos_req, PM_QOS_DEFAULT_VALUE)
	  ope_v4l2_register(ope)
	    ...
	    ope_v4l2_dev_release(&ope->v4l2_dev)
	      kfree(ope)
	  cpu_latency_qos_remove_request(&ope->qos_req)  <-- use-after-free

In addition, on the success path of ope_probe(), enable_irq(ope->irq) is
called after ope_v4l2_register(ope) has already published the V4L2 and media
device nodes to userspace, leaving a window where userspace can open the
device and start streaming before the IRQ is enabled.

[ ... ]

> +static int ope_runtime_resume(struct device *dev)
> +{
> +	struct ope_dev *ope = dev_get_drvdata(dev);
> +	int ret;
> +
> +	ret = clk_bulk_prepare_enable(ope->nclks, ope->clks);
> +	if (ret)
> +		return ret;
> +
> +	__ope_irq_init(ope);
> +
> +	/*
> +	 * Let the contexts that are still streaming pick up a new job. The job
> +	 * is started from a work item, which may run before runtime PM reports
> +	 * the device active again, i.e. while ope_irq() still drops interrupts.
> +	 * Programming a job takes long enough for that window to be closed by
> +	 * the time the hardware can raise one.
> +	 */
> +	ope_sched_resume(ope);
> +
> +	return 0;
> +}

[Severity: Medium]
Can scheduling ope->run_work from inside ope_runtime_resume() race with
pm_runtime_force_resume() and drop an edge-triggered hardware interrupt?

During system resume, pm_runtime_force_resume() invokes ope_runtime_resume()
while dev->power.disable_depth > 0, before calling pm_runtime_enable(dev).
Inside ope_runtime_resume(), ope_sched_resume(ope) queues ope->run_work on
system_wq, which can execute on another CPU and call ope_run_job() ->
ope_start(ope).

If the task running pm_runtime_force_resume() is preempted before reaching
pm_runtime_enable(dev), the hardware can raise its IRQ_TYPE_EDGE_RISING
interrupt while dev->power.disable_depth > 0. In that state,
pm_runtime_get_if_in_use(ope->dev) in ope_irq() returns -EINVAL, causing
ope_irq() to return IRQ_NONE without reading or clearing OPE_TOP_IRQ_STATUS
and stalling the active job.

> diff --git a/drivers/media/platform/qcom/camss/ope/pipeline.c b/drivers/media/platform/qcom/camss/ope/pipeline.c
> new file mode 100644
> index 0000000000000..9a8afbc30d323
> --- /dev/null
> +++ b/drivers/media/platform/qcom/camss/ope/pipeline.c

[ ... ]

> +void camss_isp_pipeline_unregister(struct camss_isp_pipeline *pipeline)
> +{
> +	int i;
> +
> +	/* Unregister entities in reverse order. */
> +	for (i = (int)pipeline->num_entities - 1; i >= 0; i--) {
> +		struct camss_isp_pipeline_entity *slot = &pipeline->entities[i];
> +
> +		if (slot->registered) {
> +			switch (slot->obj_type) {
> +			case MEDIA_ENTITY_TYPE_VIDEO_DEVICE:
> +				if (video_device_pipeline(&slot->vdev))
> +					video_device_pipeline_stop(&slot->vdev);
> +				video_unregister_device(&slot->vdev);
> +				break;

[Severity: Medium]
As reported by Sashiko on v12 [5], can camss_isp_pipeline_unregister() call
video_device_pipeline_stop() an unbalanced number of times or unregister
subdevs while their pads are still part of an active media_pipeline?

When any video node in the connected graph calls
video_device_pipeline_alloc_start() in ope_start_streaming(),
__media_pipeline_start() sets pad->pipe on every connected pad in the graph.
As a result, video_device_pipeline(&slot->vdev) becomes non-NULL on all three
video nodes (OPE_ENTITY_FRAME_IN at index 0, OPE_ENTITY_PARAMS at index 1,
and OPE_ENTITY_DISP_OUT at index 4) even if only one or two of them started
streaming.

Additionally, because OPE_ENTITY_DISP (index 3) and OPE_ENTITY_PROC (index 2)
are unregistered before OPE_ENTITY_PARAMS (index 1) and OPE_ENTITY_FRAME_IN
(index 0), if two or more video nodes are streaming during unbind, stopping
index 4 only decrements pipe->start_count without terminating the pipeline,
and the subdevs at indices 3 and 2 are unregistered while their pads are
still attached to the active media_pipeline.

> diff --git a/drivers/media/platform/qcom/camss/ope/pipeline.h b/drivers/media/platform/qcom/camss/ope/pipeline.h
> new file mode 100644
> index 0000000000000..62e4116012ad0
> --- /dev/null
> +++ b/drivers/media/platform/qcom/camss/ope/pipeline.h

[ ... ]

> +struct camss_isp_entity_desc {
> +	const char				*name;
> +	u32					obj_type;
> +	u32					function;
> +	const struct camss_isp_pad_desc		*pads;
> +
> +	union {
> +		/* MEDIA_ENTITY_TYPE_VIDEO_DEVICE */
> +		struct {
> +			u32					caps;
> +			void					*drvdata;
> +			const struct v4l2_file_operations	*fops;
> +			const struct v4l2_ioctl_ops		*ioctl_ops;
> +			const struct media_entity_operations	*entity_ops;
> +		} vdev;
> +		/* MEDIA_ENTITY_TYPE_V4L2_SUBDEV */
> +		struct {
> +			const struct v4l2_subdev_ops		*ops;
> +			const struct v4l2_subdev_internal_ops	*internal_ops;
> +			const struct media_entity_operations	*entity_ops;
> +		} subdev;
> +	};
> +};

[ ... ]

> +/**
> + * camss_isp_pipeline_register() - validate descriptors and register the graph
> + * @pipeline:    pipeline (allocated with camss_isp_pipeline_alloc())
> + * @v4l2_dev:    caller-owned and already-registered V4L2 device; its
> + *               associated media_device (v4l2_dev->mdev) must also be
> + *               initialised and registered before this call.

[Severity: Low]
As reported by Sashiko on v12 [6], this isn't a bug, but the kernel-doc
comment for struct camss_isp_entity_desc documents the nested @vdev.* and
@subdev.* members without @vdev: and @subdev: entries, which causes
scripts/kernel-doc to warn that struct members vdev and subdev are not
described.

Also, the kernel-doc comment for camss_isp_pipeline_register() states that
v4l2_dev->mdev must already be registered before the call, whereas
ope_v4l2_register() only calls media_device_init() beforehand and calls
media_device_register() after camss_isp_pipeline_register() succeeds.

[1] https://lore.kernel.org/r/20261102-camss-isp-ope-v12-9-702254689feb@oss.qualcomm.com
[2] https://lore.kernel.org/r/20261102-camss-isp-ope-v12-9-702254689feb@oss.qualcomm.com
[3] https://lore.kernel.org/r/20261102-camss-isp-ope-v12-9-702254689feb@oss.qualcomm.com
[4] https://lore.kernel.org/r/20261102-camss-isp-ope-v12-9-702254689feb@oss.qualcomm.com
[5] https://lore.kernel.org/r/20261102-camss-isp-ope-v12-9-702254689feb@oss.qualcomm.com
[6] https://lore.kernel.org/r/20261102-camss-isp-ope-v12-9-702254689feb@oss.qualcomm.com

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261103-camss-isp-ope-v13-0-c2cbf15a39b1@oss.qualcomm.com?part=9

      reply	other threads:[~2026-11-03 20:51 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-03 20:05 [PATCH v13 0/9] media: qcom: camss: CAMSS Offline Processing Engine support Loic Poulain
2026-11-03 20:05 ` [PATCH v13 1/9] media: qcom: camss: Add V4L2 meta format for CAMSS ISP parameters Loic Poulain
2026-11-03 20:05 ` [PATCH v13 2/9] dt-bindings: media: qcom: Add CAMSS Offline Processing Engine (OPE) Loic Poulain
2026-11-03 20:05 ` [PATCH v13 3/9] dt-bindings: media: qcom,qcm2290-camss-ope: Document shikra compatible Loic Poulain
2026-11-03 20:05 ` [PATCH v13 4/9] media: uapi: Add CAMSS ISP configuration definition Loic Poulain
2026-11-04 17:05   ` Jacopo Mondi
2026-11-04 20:00     ` Loic Poulain
2026-11-03 20:05 ` [PATCH v13 5/9] media: Documentation: uapi: Add qcom-camss ISP params documentation Loic Poulain
2026-11-03 20:05 ` [PATCH v13 6/9] media: qcom: camss: Add CAMSS Offline Processing Engine driver Loic Poulain
2026-11-03 20:41   ` sashiko-bot
2026-11-03 20:05 ` [PATCH v13 7/9] arm64: dts: qcom: agatti: Add OPE node Loic Poulain
2026-11-03 20:18   ` sashiko-bot
2026-11-03 20:05 ` [PATCH v13 8/9] arm64: dts: qcom: shikra: " Loic Poulain
2026-11-03 20:05 ` [PATCH v13 9/9] arm64: defconfig: Enable Qualcomm CAMSS OPE driver Loic Poulain
2026-11-03 20:51   ` sashiko-bot [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-196017@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=loic.poulain@oss.qualcomm.com \
    --cc=media-ci@linuxtv.org \
    --cc=robh@kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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.