From: sashiko-bot@kernel.org
To: "Zhentao Guo" <zhentao.guo@amlogic.com>
Cc: linux-amlogic@lists.infradead.org, devicetree@vger.kernel.org,
robh@kernel.org, conor+dt@kernel.org, neil.armstrong@linaro.org,
media-ci@linuxtv.org
Subject: Re: [PATCH v12 5/6] arm64: dts: amlogic: Add video decoder driver support for S4 SOCs
Date: Thu, 22 Oct 2026 06:01:18 +0000 [thread overview]
Message-ID: <sashiko-outbox-181046@kernel.org> (raw)
In-Reply-To: <20261022-b4-s4-vdec-upstream-v12-5-616240978792@amlogic.com>
Thank you for your contribution! Sashiko AI review found 13 potential issue(s) to consider:
- [Critical] media: amlogic: vdec: OOB read and canvas leaks in h264 reference list handling
- [Critical] media: amlogic: vdec: colocate buffer OOB DMA write in h264_config_decode_buf()
- [High] media: amlogic: vdec: aml_vdec_init_dec_inst() returns -1 on queue restart
- [High] media: amlogic: vdec: invalid IS_ERR() check on meson_sm_get() in probe
- [High] media: amlogic: vdec: poc_is_in_dpb() and get_dpb_by_poc() ignore active flag
- [High] media: amlogic: vdec: dos clock in vdec_clock_gate_init() is never enabled
- [High] media: amlogic: vdec: ignored power and clock errors in aml_vdec_hw.c
- [High] media: amlogic: vdec: aml_vdec_drv_remove() powers off hw before unregistering
- [High] media: amlogic: vdec: broken TEE deferral and error codes in aml_vdec_tee_fw.c
- [High] media: amlogic: vdec: infinite poll under ctx_mutex in h264_config_decode_buf()
- [Medium] media: amlogic: vdec: missing REGMAP_MMIO and MEDIA_CONTROLLER selects
- [Medium] media: amlogic: vdec: aml_vdec_drv_probe() registers video_device too early
- [Medium] media: amlogic: vdec: missing set_pic_info() in reset_output_fmts()
--
commit 7d77b0e5560dd6f7c9fdd465c00fffdff91c7e55
Author: Zhentao Guo <zhentao.guo@amlogic.com>
arm64: dts: amlogic: Add video decoder driver support for S4 SOCs
This series adds the Amlogic S4 V4L2 stateless H.264 video decoder driver in
commit 18a11b2c70a7ef55c42ed897bc1624397ec3430c ("decoder: Add V4L2 stateless
H.264 decoder driver") and enables the corresponding vdec, canvas, and optee
device tree nodes in meson-s4.dtsi.
> diff --git a/drivers/media/platform/amlogic/vdec/Kconfig b/drivers/media/platform/amlogic/vdec/Kconfig
> new file mode 100644
> index 0000000000000..2c5dd84529621
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/Kconfig
> @@ -0,0 +1,18 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR MIT)
> +
> +config VIDEO_AMLOGIC_VDEC
> + tristate "Amlogic Video Decoder Driver"
> + depends on ARCH_MESON || COMPILE_TEST
> + depends on VIDEO_DEV
> + depends on V4L_MEM2MEM_DRIVERS
> + depends on TEE
> + depends on MESON_SM
> + select VIDEOBUF2_DMA_CONTIG
> + select V4L2_H264
> + select V4L2_MEM2MEM_DEV
> + select MESON_CANVAS
[Severity: Medium]
Should VIDEO_AMLOGIC_VDEC also select REGMAP_MMIO and MEDIA_CONTROLLER here?
In drivers/media/platform/amlogic/vdec/aml_vdec_hw.c,
dev_request_hw_resources() calls devm_regmap_init_mmio(), which fails to link
when CONFIG_REGMAP_MMIO is disabled.
In addition, aml_vdec_drv_probe() in
drivers/media/platform/amlogic/vdec/aml_vdec_drv.c registers a media device
with aml_m2m_media_ops (vb2_request_validate and v4l2_m2m_request_queue) for
stateless H.264 decoding, which compiles to no-op stubs if
CONFIG_MEDIA_CONTROLLER is disabled. In
drivers/media/platform/amlogic/vdec/aml_vdec.c, should aml_vdec_queue_init()
also set src_vq->requires_requests = true alongside
src_vq->supports_requests = true so non-request buffers are rejected?
[ ... ]
> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec.c b/drivers/media/platform/amlogic/vdec/aml_vdec.c
> new file mode 100644
> index 0000000000000..e65ebb2ce311f
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec.c
[ ... ]
> +static int aml_vdec_init_dec_inst(struct aml_vdec_ctx *ctx)
> +{
> + struct aml_vdec_dev *dev = ctx->dev;
> + struct aml_video_fmt *fmt_out = &ctx->dec_fmt[AML_FMT_SRC];
> + int ret = -1;
> +
> + ctx->codec_ops = &dev->pvdec_data->codec_ops[fmt_out->codec_type];
> + /*
> + * The codec instance is only released when both queues are
> + * streamed off: do not allocate a new one (leaking the previous
> + * instance) when the output queue is restarted while the
> + * capture queue is still streaming.
> + */
> + if (ctx->codec_ops->init && !ctx->codec_priv) {
> + ret = ctx->codec_ops->init(ctx);
> + if (ret < 0)
> + return ret;
> + }
> + ctx->curr_dec_type = fmt_out->codec_type;
> + dev_info(&dev->plat_dev->dev, "%s set curr_dec_type %s\n",
> + __func__, dec_type_to_name(ctx->curr_dec_type));
> +
> + return ret;
> +}
[Severity: High]
When the output queue is stopped and restarted while the capture queue remains
streaming, ctx->codec_priv is already non-NULL, so aml_vdec_init_dec_inst()
skips ctx->codec_ops->init(ctx) and returns the initial ret = -1.
In vb2ops_vdec_start_streaming():
if (aml_vdec_init_dec_inst(ctx) < 0) {
while ((src_buf = v4l2_m2m_src_buf_remove(ctx->m2m_ctx)))
v4l2_m2m_buf_done(src_buf, VB2_BUF_STATE_QUEUED);
return -EINVAL;
}
Won't this cause vb2ops_vdec_start_streaming() to always fail with -EINVAL on
output queue restart unless ret is initialized to 0 in
aml_vdec_init_dec_inst()?
[ ... ]
> +static int vdec_try_fmt_mp(struct aml_vdec_ctx *ctx, enum v4l2_buf_type type,
> + struct v4l2_pix_format_mplane *pix_mp,
> + const struct aml_video_fmt *dec_fmt)
> +{
> + int i, align;
> +
> + pix_mp->field = V4L2_FIELD_NONE;
> + align = dec_fmt->align;
> +
> + if (V4L2_TYPE_IS_OUTPUT(type)) {
> + pix_mp->num_planes = dec_fmt->num_planes;
> + pix_mp->pixelformat = dec_fmt->fourcc;
> + if (!pix_mp->plane_fmt[0].sizeimage)
> + pix_mp->plane_fmt[0].sizeimage =
> + (pix_mp->height * pix_mp->width * 3) / 2;
> + pix_mp->height = ALIGN(pix_mp->height, align);
> + pix_mp->width = ALIGN(pix_mp->width, align);
> + } else {
> + pix_mp->height = ctx->pix_fmt[AML_FMT_SRC].height;
> + pix_mp->width = ctx->pix_fmt[AML_FMT_SRC].width;
> + }
> +
> + v4l2_apply_frmsize_constraints(&pix_mp->width, &pix_mp->height,
> + &dec_fmt->stepwise);
[ ... ]
> +static int vdec_s_fmt_output(struct aml_vdec_ctx *ctx, struct v4l2_format *f)
> +{
> + struct v4l2_pix_format_mplane *pix_mp = &f->fmt.pix_mp;
> + struct v4l2_pix_format_mplane *cap_fmt = &ctx->pix_fmt[AML_FMT_DST];
> + const struct aml_video_fmt *out_fmt;
> + struct vb2_queue *vq, *peer_vq;
> + int ret;
> +
> + vq = v4l2_m2m_get_vq(ctx->m2m_ctx, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE);
> + if (vb2_is_busy(vq) &&
> + pix_mp->pixelformat != ctx->pix_fmt[AML_FMT_SRC].pixelformat)
> + return -EBUSY;
> +
> + /*
> + * Since s_fmt output will reset the width and height in the capture queue,
> + * s_fmt_output with resolution change should not take effect when capture
> + * queue is busy.
> + */
> + peer_vq = v4l2_m2m_get_vq(ctx->m2m_ctx, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE);
> + if (vb2_is_busy(peer_vq) &&
> + (pix_mp->width != cap_fmt->width ||
> + pix_mp->height != cap_fmt->height))
> + return -EBUSY;
[ ... ]
> +static void reset_output_fmts(struct aml_vdec_ctx *ctx)
> +{
> + struct aml_vdec_dev *dev = ctx->dev;
> + const struct aml_video_fmt *out_fmt;
> + struct v4l2_pix_format_mplane fmt;
> +
> + /* reset default output fmt to V4L2_PIX_FMT_H264_SLICE */
> + out_fmt = aml_vdec_get_video_fmt(dev, V4L2_PIX_FMT_H264_SLICE);
> + if (!out_fmt)
> + return;
> +
> + ctx->dec_fmt[AML_FMT_SRC] = *out_fmt;
> +
> + memset(&fmt, 0, sizeof(struct v4l2_pix_format_mplane));
> +
> + fmt.height = out_fmt->stepwise.min_height;
> + fmt.width = out_fmt->stepwise.min_width;
> + /* reset bytesperline to 0 for output fmts */
> + fmt.plane_fmt[0].bytesperline = 0;
> + fmt.colorspace = V4L2_COLORSPACE_DEFAULT;
> + fmt.ycbcr_enc = V4L2_YCBCR_ENC_DEFAULT;
> + fmt.quantization = V4L2_QUANTIZATION_DEFAULT;
> + fmt.xfer_func = V4L2_XFER_FUNC_DEFAULT;
> + vdec_try_fmt_mp(ctx, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, &fmt,
> + out_fmt);
> +
> + ctx->pix_fmt[AML_FMT_SRC] = fmt;
> +}
[Severity: Medium]
In fops_vcodec_open() -> aml_vdec_reset_fmts() -> reset_output_fmts(),
set_pic_info() is not called after vdec_try_fmt_mp(). If userspace starts
streaming without calling vdec_s_fmt_output(), won't ctx->pic_info.fb_size[0]
remain 0, causing h264_config_decode_spec() and h264_config_ref_spec() in
drivers/media/platform/amlogic/vdec/h264.c to set c_dma_addr equal to
y_dma_addr for single-planar NV12/NV21 capture buffers?
Also in vdec_try_fmt_mp(), pix_mp->plane_fmt[0].sizeimage is calculated from
pix_mp->height * pix_mp->width before ALIGN() and
v4l2_apply_frmsize_constraints() clamp the dimensions, and in
vdec_s_fmt_output(), pix_mp->width and pix_mp->height are compared against the
64-aligned cap_fmt dimensions before vdec_try_fmt_mp() aligns them. Should
sizeimage and the busy resolution check use the aligned and clamped dimensions
instead?
[ ... ]
> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_drv.c b/drivers/media/platform/amlogic/vdec/aml_vdec_drv.c
> new file mode 100644
> index 0000000000000..1870d56659cf4
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_drv.c
[ ... ]
> + ret = video_register_device(vfd_dec, VFL_TYPE_VIDEO, -1);
> + if (ret) {
> + v4l2_err(&dev->v4l2_dev, "Failed to register video device");
> + goto err_vid_dev_register;
> + }
> +
> + dev->mdev.dev = &pdev->dev;
> + strscpy(dev->mdev.model, AML_VDEC_DRV_NAME, sizeof(dev->mdev.model));
> + media_device_init(&dev->mdev);
> + dev->mdev.ops = &aml_m2m_media_ops;
> + dev->v4l2_dev.mdev = &dev->mdev;
> +
> + ret = v4l2_m2m_register_media_controller(dev->m2m_dev_dec, vfd_dec,
> + MEDIA_ENT_F_PROC_VIDEO_DECODER);
> + if (ret) {
> + v4l2_err(&dev->v4l2_dev, "Failed to init mem2mem media controller\n");
> + goto error_m2m_mc_register;
> + }
> +
> + ret = media_device_register(&dev->mdev);
> + if (ret) {
> + v4l2_err(&dev->v4l2_dev, "Failed to register media device");
> + goto err_media_dev_register;
> + }
> + vdec_enable(dev->dec_hw);
> + return 0;
[Severity: Medium]
In aml_vdec_drv_probe(), video_register_device() exposes the video node to
userspace before dev->v4l2_dev.mdev is initialized, before the media
controller entities are registered, and before vdec_enable(dev->dec_hw) powers
on the VDEC hardware domain and clock.
Could a userspace process open the video device immediately after
video_register_device() and race with media controller setup or trigger
m2mops_vdec_device_run() before vdec_enable() has run?
[ ... ]
> +static void aml_vdec_drv_remove(struct platform_device *pdev)
> +{
> + struct aml_vdec_dev *dev = platform_get_drvdata(pdev);
> +
> + /*
> + * The IRQ is devm-managed and only released after remove()
> + * returns: mask it and wait for any in-flight threaded handler
> + * before powering off the VDEC domain, otherwise the handler
> + * could access powered-down registers.
> + */
> + disable_irq(dev->dec_hw->dec_irq);
> + vdec_disable(dev->dec_hw);
> + media_device_unregister(&dev->mdev);
> + v4l2_m2m_unregister_media_controller(dev->m2m_dev_dec);
> + video_unregister_device(dev->vfd);
> + media_device_cleanup(&dev->mdev);
> +
> + v4l2_m2m_release(dev->m2m_dev_dec);
> + dev->pvdec_data->destroy_hw_resource(dev);
> + v4l2_device_unregister(&dev->v4l2_dev);
> +}
[Severity: High]
In aml_vdec_drv_remove(), disable_irq() and vdec_disable(dev->dec_hw) power
off the VDEC domain and disable the VDEC clock before unregistering the media
and video devices or releasing M2M contexts.
If a decode job is in flight in aml_h264_dec_run() or an open file handle is
subsequently closed via fops_vcodec_release() -> vb2ops_vdec_stop_streaming()
-> aml_h264_exit(), won't read_dos_reg() and aml_stop_vdec_hw() perform MMIO
accesses to DOS_BUS registers that have already been powered down and
unclocked?
> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
> new file mode 100644
> index 0000000000000..043f0bbf32516
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_hw.c
[ ... ]
> +void dos_enable(struct aml_vdec_hw *hw)
> +{
> + dos_reg_write_bits(hw, DOS_GCLK_EN0, 0x3ff, 0, 10);
> +
> + regmap_write(hw->map[DOS_BUS], GCLK_EN, 0x3ff);
> +
> + regmap_update_bits(hw->map[DOS_BUS], MDEC_PIC_DC_CTRL, (1 << 31), 0);
> +}
[ ... ]
> +static int vdec_clock_gate_init(struct aml_vdec_hw *hw)
> +{
> + hw->gates[CLK_DOS].id = "dos";
> + hw->gates[CLK_VDEC].id = "vdec";
> + hw->gates[CLK_HVDEC].id = "hvdec";
> +
> + return devm_clk_bulk_get(hw->dev, CLK_MAX, hw->gates);
> +}
[Severity: High]
vdec_clock_gate_init() acquires hw->gates[CLK_DOS] ("dos"), but
pm_vdec_power_domain_power_on() and pm_vdec_power_domain_power_off() only
enable and disable the "vdec" and "hvdec" clocks, and dos_enable() only writes
internal DOS registers.
Where is hw->gates[CLK_DOS].clk prepared and enabled before dos_local_config()
and dos_enable() access DOS_BUS registers? If CLKID_DOS is gated by the
bootloader or clk_disable_unused, won't these MMIO accesses hang or abort?
[ ... ]
> +static int pm_vdec_power_domain_init(struct aml_vdec_hw *hw)
> +{
> + int i, err;
> + const struct power_manager_s *pm = hw->pm;
> + struct pm_pd_s *pd = pm->pd_data;
> +
> + mutex_init(&hw->pm_mutex);
> +
> + for (i = 0; i < VDEC_MAX; i++) {
> + pd[i].dev = dev_pm_domain_attach_by_name(hw->dev, pd[i].name);
> + if (IS_ERR_OR_NULL(pd[i].dev)) {
> + err = PTR_ERR(pd[i].dev);
> + dev_dbg(hw->dev, "Get %s failed, pm-domain: %d\n",
> + pd[i].name, err);
> + pd[i].dev = NULL;
> + continue;
> + }
[ ... ]
> +static void pm_vdec_power_domain_power_on(struct aml_vdec_hw *hw, int id)
> +{
> + const struct power_manager_s *pm = hw->pm;
> + struct device *dev = pm->pd_data[id].dev;
> + struct clk_bulk_data *gate_node = NULL;
> + int ret = 0;
> +
> + if (id == VDEC)
> + gate_node = vdec_get_clk_by_name(hw, "vdec");
> + else if (id == HVDEC)
> + gate_node = vdec_get_clk_by_name(hw, "hvdec");
> +
> + if (gate_node) {
> + ret = clk_prepare_enable(gate_node->clk);
> + if (ret) {
> + dev_err(hw->dev, "failed to enable %s clock: %d\n",
> + gate_node->id, ret);
> + } else {
> + if (id == VDEC) {
> + clk_set_rate(gate_node->clk, 499999992);
> + dev_dbg(hw->dev, "after set, vdec clock is %lu Hz\n",
> + clk_get_rate(gate_node->clk));
> + }
> + dev_dbg(hw->dev, "the %-15s clock on\n", gate_node->id);
> + }
> + } else {
> + dev_info(hw->dev, "clk %d, unreachable\n", id);
> + ret = -ENODEV;
> + }
> +
> + if (!ret && dev) {
> + ret = pm_runtime_resume_and_get(dev);
> + if (ret < 0)
> + dev_err(hw->dev, "failed to power on %s domain: %d\n",
> + pm->pd_data[id].name, ret);
> + else
> + dev_dbg(dev, "dev: %p link %p the %-15s power on\n",
> + dev, pm->pd_data[id].link, pm->pd_data[id].name);
> + }
> +
> + /*
> + * Do not touch the DOS registers when the clock or the power
> + * domain could not be enabled, the MMIO access would abort.
> + */
> + if (!ret)
> + dos_local_config(hw, 1, id);
> +}
> +
> +static void pm_vdec_power_domain_power_off(struct aml_vdec_hw *hw, int id)
> +{
> + const struct power_manager_s *pm = hw->pm;
> + struct device *dev = pm->pd_data[id].dev;
> + struct clk_bulk_data *gate_node = NULL;
> +
> + dos_local_config(hw, 0, id);
> +
> + if (dev) {
> + pm_runtime_put_sync(dev);
> + dev_dbg(dev, "dev: %p link %p the %-15s power off\n",
> + dev, pm->pd_data[id].link, pm->pd_data[id].name);
> + }
> +
> + if (id == VDEC)
> + gate_node = vdec_get_clk_by_name(hw, "vdec");
> + else if (id == HVDEC)
> + gate_node = vdec_get_clk_by_name(hw, "hvdec");
> +
> + if (gate_node) {
> + clk_disable_unprepare(gate_node->clk);
> + dev_dbg(hw->dev, "the %-15s clock off\n", gate_node->id);
> + } else {
> + dev_info(hw->dev, "clk %d, unreachable\n", id);
> + }
> +}
> +
> +static void vdec_poweron(struct aml_vdec_hw *hw, enum vdec_type_e core)
> +{
> + if (core >= VDEC_MAX)
> + return;
> +
> + mutex_lock(&hw->pm_mutex);
> + if (!hw->pm->pd_data[core].dev)
> + goto out;
> +
> + hw->pm->pd_data[core].ref_count++;
> + if (hw->pm->pd_data[core].ref_count > 1)
> + goto out;
> +
> + hw->pm->power_on(hw, core);
> +
> +out:
> + mutex_unlock(&hw->pm_mutex);
> +}
[Severity: High]
There appear to be two error-handling issues in the power domain and clock
helpers here:
1. In pm_vdec_power_domain_init(), if dev_pm_domain_attach_by_name() returns
an error pointer (such as -EPROBE_DEFER) or NULL, pd[i].dev is set to NULL and
the loop continues while returning 0. Then vdec_poweron() sees
!hw->pm->pd_data[core].dev and exits without enabling the clock or power
domain, while vdec_enable() still returns 0. Won't subsequent MMIO writes in
m2mops_vdec_device_run() -> dos_enable() access unpowered and unclocked DOS
registers?
2. In pm_vdec_power_domain_power_on(), if clk_prepare_enable(gate_node->clk)
succeeds and pm_runtime_resume_and_get(dev) fails, gate_node->clk is left
enabled and the failure is not propagated to vdec_poweron() or vdec_enable(),
leaving ref_count at 1. When vdec_disable() -> vdec_poweroff() ->
pm_vdec_power_domain_power_off() later runs, won't it call
dos_local_config(hw, 0, id) on the unpowered domain and call
pm_runtime_put_sync(dev) despite pm_runtime_resume_and_get(dev) having failed?
[ ... ]
> + hw->canvas = meson_canvas_get(&pdev->dev);
> + if (IS_ERR_OR_NULL(hw->canvas))
> + return PTR_ERR(hw->canvas);
> +
> + sm_np = of_parse_phandle(pdev->dev.of_node, "amlogic,secure-monitor", 0);
> + if (!sm_np)
> + return -ENODEV;
> +
> + hw->sec_fw = meson_sm_get(sm_np);
> + of_node_put(sm_np);
> + if (IS_ERR(hw->sec_fw))
> + return PTR_ERR(hw->sec_fw);
[Severity: High]
In drivers/firmware/meson/meson_sm.c, meson_sm_get() returns NULL (not an
ERR_PTR) when the secure-monitor platform device has not probed yet.
Because IS_ERR(NULL) is false, dev_request_hw_resources() succeeds with
hw->sec_fw == NULL instead of returning -EPROBE_DEFER. When decoding starts in
m2mops_vdec_device_run() -> load_firmware() -> meson_sm_call(hw->sec_fw, ...),
won't meson_sm_call() dereference a NULL hw->sec_fw pointer on fw->chip?
Similarly, above that check, if meson_canvas_get() were to return NULL,
if (IS_ERR_OR_NULL(hw->canvas)) return PTR_ERR(hw->canvas); would return 0.
Should hw->canvas use IS_ERR(hw->canvas) and hw->sec_fw check !hw->sec_fw and
return -EPROBE_DEFER?
[ ... ]
> diff --git a/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
> new file mode 100644
> index 0000000000000..9af9eae88fb89
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/aml_vdec_tee_fw.c
[ ... ]
> + /* Open session */
> + memcpy(sess_arg.uuid, uuid.b, TEE_IOCTL_UUID_LEN);
> + sess_arg.clnt_login = TEE_IOCTL_LOGIN_PUBLIC;
> + sess_arg.num_params = TEE_PARAM_NUM;
> + ret = tee_client_open_session(ctx, &sess_arg, param_init);
> + if (ret < 0 || sess_arg.ret != TEEC_SUCCESS) {
> + dev_info(hw->dev,
> + "%s open session failed, cmd = %u, ret = %d, res = 0x%x, origin = 0x%x\n",
> + __func__, cmd, ret, sess_arg.ret, sess_arg.ret_origin);
> + if (!ret)
> + ret = sess_arg.ret;
> + goto free_shm;
> + }
> +
> + inv_arg.func = cmd;
> + inv_arg.session = sess_arg.session;
> + inv_arg.num_params = TEE_PARAM_NUM;
> +
> + while (sent_size < fw_size) {
> + memset(shm_va, 0, ONCE_SENT_SIZE);
> + if (fw_size - sent_size > ONCE_SENT_SIZE) {
> + memcpy(shm_va, (firmware_data + sent_size),
> + ONCE_SENT_SIZE);
> + param_invoke[0].u.memref.size = ONCE_SENT_SIZE;
> + } else {
> + memcpy(shm_va, (firmware_data + sent_size),
> + fw_size - sent_size);
> + param_invoke[0].u.memref.size = (fw_size - sent_size);
> + }
> + param_invoke[0].u.memref.shm = shm;
> + ret = tee_client_invoke_func(ctx, &inv_arg, param_invoke);
> + if (ret < 0 || (inv_arg.ret != TEEC_SUCCESS && inv_arg.ret != TEEC_ERROR_BUSY)) {
> + dev_info(hw->dev,
> + "%s invoke func failed, cmd = %u, ret= %d, res = 0x%x, origin = 0x%x\n",
> + __func__, cmd, ret, inv_arg.ret,
> + inv_arg.ret_origin);
> + if (!ret)
> + ret = inv_arg.ret;
> + goto close_session;
> + }
> + sent_size += param_invoke[0].u.memref.size;
> + }
[ ... ]
> + ctx = tee_client_open_context(NULL, optee_ctx_match, NULL, NULL);
> + if (IS_ERR(ctx)) {
> + dev_info(hw->dev, "Failed to open TEE context\n");
> + ret = PTR_ERR(ctx);
> + goto free_firmware;
> + }
[Severity: High]
In aml_vdec_drv_probe() -> dev_request_hw_resources() -> aml_tee_fw_preload()
-> pass_firmware_to_tee():
1. When the OP-TEE driver has not registered its TEE device yet,
tee_client_open_context() returns ERR_PTR(-ENOENT). Propagating -ENOENT causes
aml_vdec_drv_probe() to fail permanently instead of deferring probe with
-EPROBE_DEFER.
2. In tee_pta_invoke_cmd(), when tee_client_open_session() or
tee_client_invoke_func() returns 0 with sess_arg.ret != TEEC_SUCCESS or
inv_arg.ret != TEEC_SUCCESS, ret is assigned the raw u32 TEEC status code
rather than a negative errno (which aml_vdec_drv_probe() only checks for
ret < 0).
3. In tee_pta_invoke_cmd(), when inv_arg.ret == TEEC_ERROR_BUSY, the error
branch is skipped and sent_size += param_invoke[0].u.memref.size advances past
the current 128 KB chunk without retrying it. Won't this silently drop a
chunk of firmware data?
[ ... ]
> diff --git a/drivers/media/platform/amlogic/vdec/h264.c b/drivers/media/platform/amlogic/vdec/h264.c
> new file mode 100644
> index 0000000000000..7ec7af73d44c7
> --- /dev/null
> +++ b/drivers/media/platform/amlogic/vdec/h264.c
[ ... ]
> +static int poc_is_in_dpb(int poc, const struct v4l2_h264_dpb_entry *dpb)
> +{
> + int i;
> + int ret = 0;
> +
> + for (i = 0; i < V4L2_H264_NUM_DPB_ENTRIES; i++) {
> + if (poc == dpb[i].top_field_order_cnt) {
> + ret = 1;
> + break;
> + }
> + }
> +
> + return ret;
> +}
[Severity: High]
In poc_is_in_dpb(), dpb[i].top_field_order_cnt is matched across all 16
entries of dpb[] without checking dpb[i].flags & V4L2_H264_DPB_ENTRY_FLAG_ACTIVE.
The same issue also occurs in get_dpb_by_poc() further below:
static struct v4l2_h264_dpb_entry *
get_dpb_by_poc(struct v4l2_ctrl_h264_decode_params *decode, int poc)
{
int i;
for (i = 0; i < V4L2_H264_NUM_DPB_ENTRIES; i++) {
if (decode->dpb[i].top_field_order_cnt == poc)
return &decode->dpb[i];
}
return NULL;
}
Because unused DPB entries have flags == 0 and top_field_order_cnt == 0 (and
evicted entries may retain their stale top_field_order_cnt with the active
flag cleared):
1. In h264_config_decode_spec() -> clear_unused_col_buf(), poc_is_in_dpb()
matches inactive DPB slots, preventing canvas_pos and colocated_buf slots from
being released on non-IDR frames and exhausting the 17 canvas_pos slots
(especially on multi-slice frames where h264_config_decode_spec() is called on
every slice).
2. In h264_config_decode_buf(), if an inactive DPB slot with
top_field_order_cnt == 0 precedes an active reference picture with POC 0,
get_dpb_by_poc(decode, 0) returns the inactive slot, so
if (dpb && (dpb->flags & V4L2_H264_DPB_ENTRY_FLAG_ACTIVE)) evaluates to false
and writes 0 into H264_BUFFER_INFO_DATA for the active reference frame.
Should both poc_is_in_dpb() and get_dpb_by_poc() require
(dpb[i].flags & V4L2_H264_DPB_ENTRY_FLAG_ACTIVE)?
[ ... ]
> +static int get_refidx_by_picnum(struct aml_h264_ctx *h264_ctx, int pic_num,
> + int curr_list)
> +{
> + int i;
> + struct h264_decode_buf_spec *ref_list;
> +
> + if (curr_list == 0)
> + ref_list = &h264_ctx->ref_list0[0];
> + else
> + ref_list = &h264_ctx->ref_list1[0];
> +
> + for (i = 0; ref_list[i].dpb; i++) {
> + if (pic_num == ref_list[i].dpb->pic_num)
> + return i;
> + }
> +
> + return -1;
> +}
> +
> +static struct h264_decode_buf_spec *get_st_refpic_by_num(struct aml_h264_ctx *h264_ctx,
> + int pic_num, int curr_list)
> +{
> + int i;
> + struct h264_decode_buf_spec *ref_list;
> +
> + if (curr_list == 0)
> + ref_list = &h264_ctx->ref_list0_unreordered[0];
> + else
> + ref_list = &h264_ctx->ref_list1_unreordered[0];
> +
> + for (i = 0; ref_list[i].dpb; i++) {
> + if (pic_num == ref_list[i].dpb->pic_num && ref_list[i].long_term_flag == 0)
> + return &ref_list[i];
> + }
> +
> + return NULL;
> +}
> +
> +static struct h264_decode_buf_spec *get_lt_refpic_by_num(struct aml_h264_ctx *h264_ctx,
> + int pic_num, int curr_list)
> +{
> + int i;
> + struct h264_decode_buf_spec *ref_list;
> +
> + if (curr_list == 0)
> + ref_list = &h264_ctx->ref_list0_unreordered[0];
> + else
> + ref_list = &h264_ctx->ref_list1_unreordered[0];
> +
> + for (i = 0; ref_list[i].dpb; i++) {
> + if (pic_num == ref_list[i].dpb->pic_num && ref_list[i].long_term_flag == 1)
> + return &ref_list[i];
> + }
> +
> + return NULL;
> +}
> +
> +static void reorder_short_term(struct slice *curr_slice, int cur_list,
> + int pic_num_lx, int *ref_idx_lx)
> +{
[ ... ]
> + for (c_idx = num_ref_idx_lx_active; c_idx > *ref_idx_lx; c_idx--)
> + memcpy(&ref_list_reordered[c_idx], &ref_list_reordered[c_idx - 1],
> + sizeof(struct h264_decode_buf_spec));
> +
> + memcpy(&ref_list_reordered[*ref_idx_lx], pic_lx, sizeof(struct h264_decode_buf_spec));
> + dev_dbg(&ctx->dev->plat_dev->dev, "%s : RefPicListX[%d ] = pic %p pic_num(%d)\n", __func__,
> + *ref_idx_lx, pic_lx, ref_list_reordered[*ref_idx_lx].dpb->pic_num);
> + *ref_idx_lx = *ref_idx_lx + 1;
> +
> + n_idx = *ref_idx_lx;
> + for (c_idx = *ref_idx_lx; c_idx <= num_ref_idx_lx_active; c_idx++) {
> + if (ref_list_reordered[c_idx].long_term_flag || !ref_list_reordered[c_idx].dpb ||
> + ref_list_reordered[c_idx].dpb->pic_num != pic_num_lx)
> + memcpy(&ref_list_reordered[n_idx++], &ref_list_reordered[c_idx],
> + sizeof(struct h264_decode_buf_spec));
> + }
> +
> + h264_ctx->list_size[cur_list] = num_ref_idx_lx_active;
> +}
[Severity: Critical]
Can reference list reordering and canvas allocation read out of bounds,
dereference a NULL dpb pointer, and leak hardware canvases?
1. In reorder_short_term() and reorder_long_term(), when
num_ref_idx_lx_active == 16 (V4L2_H264_NUM_DPB_ENTRIES), the shift loop copies
ref_list[15] into ref_list[16] and sets list_size[cur_list] = 16. Because
h264_release_decode_spec() only clears indices 0 .. list_size[cur_list] - 1,
ref_list[16].dpb remains non-NULL. On a subsequent call where
ref_list[0..16].dpb are all non-NULL, the loops in get_refidx_by_picnum(),
get_st_refpic_by_num(), and get_lt_refpic_by_num() have no upper bound on i
and read past the end of ref_list0[] / ref_list1[].
2. When num_ref_idx_lx_active exceeds the number of valid entries built by
h264_config_ref_lists() (or when h264_config_ref_spec() fails on an entry),
reorder_short_term() and reorder_long_term() set
h264_ctx->list_size[cur_list] = num_ref_idx_lx_active. Then get_canvas_index()
allocates hardware canvases via config_decode_canvas() for every entry up to
list_size[cur_list] without checking buf->used or buf->valid (also writing DMA
address 0 to ANC0_CANVAS_ADDR for canvas_pos 0), whereas
h264_release_decode_spec() only frees canvases when if (buf->used) is true,
permanently leaking canvases from the meson_canvas pool. In addition, in
h264_threaded_isr_func(), h264_release_decode_spec() is only called before
get_canvas_index() when first_mb_in_slice != 0, so a second
H264_SLICE_HEADER_DONE interrupt with first_mb_in_slice == 0 leaks the
canvases allocated for the first slice.
3. In h264_config_decode_buf(), when h264_ctx->list_size[1] > 0,
colocate_pic = &h264_ctx->ref_list1[0] dereferences
colocate_pic->dpb->top_field_order_cnt without checking colocate_pic->valid or
colocate_pic->dpb != NULL.
[ ... ]
> + read_poll_timeout(read_dos_reg, reg_val,
> + !(reg_val & 0x800),
> + 10, 0, true, hw, H264_CO_MB_RW_CTL);
[Severity: High]
In h264_config_decode_buf(), read_poll_timeout() is called with timeout_us = 0
while vdec_threaded_isr_handler() holds hw->ctx_mutex.
Since timeout_us = 0 polls indefinitely in read_poll_timeout(), if bit 11
(0x800) of H264_CO_MB_RW_CTL does not clear, won't vdec_threaded_isr_handler()
spin forever under hw->ctx_mutex and cause aml_h264_dec_run() to deadlock on
mutex_lock(&dec_hw->ctx_mutex) when wait_event_interruptible_timeout()
expires?
> +
> + /* col buf for curr frame */
> + colocate_adr_offset = COL_SIZE_FOR_ONE_MB;
> + if (use_mode_8x8_flag)
> + colocate_adr_offset >>= 2;
> + colocate_adr_offset *= curr_slice->first_mb_in_slice;
> +
> + if (h264_ctx->curr_spec.col_buf_index >= 0 &&
> + h264_ctx->curr_spec.col_buf_index < h264_ctx->colocated_buf_num) {
> + /* collated_cma_addr is limited to 32bit by flag GFP_DMA32 when allocated */
> + colocate_wr_adr = (unsigned int)h264_ctx->collated_cma_addr +
> + ((h264_ctx->one_col_buf_size *
> + h264_ctx->curr_spec.col_buf_index) >> (use_mode_8x8_flag ? 2 : 0));
> + if (colocate_adr_offset > h264_ctx->one_col_buf_size ||
> + colocate_wr_adr + h264_ctx->one_col_buf_size >
> + (unsigned int)h264_ctx->collated_cma_addr_end) {
> + dev_err(hw->dev,
> + "Error, colocate buf is not enough, index is %d\n",
> + h264_ctx->curr_spec.col_buf_index);
> + return -1;
> + }
> + regmap_write(hw->map[DOS_BUS], H264_CO_MB_WR_ADDR,
> + (colocate_wr_adr + colocate_adr_offset));
> + dev_dbg(hw->dev, "col buffer addr = 0x%x col_buf_index %d\n",
> + (colocate_wr_adr + colocate_adr_offset),
> + h264_ctx->curr_spec.col_buf_index);
> + } else {
> + regmap_write(hw->map[DOS_BUS], H264_CO_MB_WR_ADDR, 0xffffffff);
> + dev_dbg(hw->dev, "col buffer addr = 0xffffffff\n");
> + }
> +
> + if (h264_ctx->list_size[1] > 0) {
> + struct h264_decode_buf_spec *colocate_pic =
> + &h264_ctx->ref_list1[0];
> + struct h264_decode_buf_spec *curr_pic = &h264_ctx->curr_spec;
> + int l10_structure = 2; /* for pic struct == FRAME, default to 2 */
> + int cur_colocate_ref_type;
> + unsigned int colocate_rd_adr;
> + unsigned int colocate_rd_adr_offset = 0;
> + unsigned int val;
> +
> + cur_colocate_ref_type =
> + (abs(curr_pic->poc - colocate_pic->dpb->top_field_order_cnt) <
> + abs(curr_pic->poc - colocate_pic->dpb->bottom_field_order_cnt)) ? 0 : 1;
> + colocate_rd_adr_offset = COL_SIZE_FOR_ONE_MB;
> + if (use_mode_8x8_flag)
> + colocate_rd_adr_offset >>= 2;
> +
> + colocate_rd_adr_offset *= curr_slice->first_mb_in_slice;
> + if (colocate_pic->col_buf_index >= 0 &&
> + colocate_pic->col_buf_index < h264_ctx->colocated_buf_num) {
> + /* collated_cma_addr is limited to 32bit by flag GFP_DMA32 when allocated */
> + colocate_rd_adr = (unsigned int)h264_ctx->collated_cma_addr +
> + ((h264_ctx->one_col_buf_size *
> + colocate_pic->col_buf_index) >> (use_mode_8x8_flag
> + ? 2 : 0));
> + if (colocate_rd_adr_offset > h264_ctx->one_col_buf_size ||
> + colocate_rd_adr + h264_ctx->one_col_buf_size >
> + (unsigned int)h264_ctx->collated_cma_addr_end) {
> + dev_err(hw->dev,
> + "Error, colocate rd buf is not enough, index is %d\n",
> + colocate_pic->col_buf_index);
> + return -1;
> + }
> + val = ((colocate_rd_adr_offset + colocate_rd_adr) >> 3) |
> + (cur_colocate_ref_type << 29) |
> + (l10_structure << 30);
> + regmap_write(hw->map[DOS_BUS], H264_CO_MB_RD_ADDR, val);
[Severity: Critical]
In h264_config_decode_buf(), can a slice with a non-zero first_mb_in_slice
program H264_CO_MB_WR_ADDR and H264_CO_MB_RD_ADDR to DMA past the end of the
colocated buffer slot or past h264_ctx->collated_cma_addr_end?
When use_mode_8x8_flag is set, each slot's stride is
h264_ctx->one_col_buf_size >> 2, but colocate_adr_offset and
colocate_rd_adr_offset are compared against the unshifted
h264_ctx->one_col_buf_size, allowing the offset to cross into the next 3
colocated buffer slots.
Furthermore, the end-address check tests
colocate_wr_adr + h264_ctx->one_col_buf_size > h264_ctx->collated_cma_addr_end
without adding colocate_adr_offset (and likewise for colocate_rd_adr without
colocate_rd_adr_offset), so when col_buf_index is at the end of the buffer and
colocate_adr_offset > 0, colocate_wr_adr + colocate_adr_offset can point up to
one_col_buf_size bytes past collated_cma_addr_end.
[ ... ]
> +}
[ ... ]
> --
> 2.43.0
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261022-b4-s4-vdec-upstream-v12-0-616240978792@amlogic.com?part=5
_______________________________________________
linux-amlogic mailing list
linux-amlogic@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-amlogic
next prev parent reply other threads:[~2026-10-22 6:01 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 5:26 [PATCH v12 0/6] Add Amlogic stateless H.264 video decoder for S4 Zhentao Guo via B4 Relay
2026-10-22 5:26 ` [PATCH v12 1/6] firmware: meson: sm: video firmware loading via secure monitor Zhentao Guo via B4 Relay
2026-10-22 5:27 ` [PATCH v12 2/6] firmware: meson: sm: Add video firmware loading SMC call Zhentao Guo via B4 Relay
2026-10-22 5:27 ` [PATCH v12 3/6] media: dt-bindings: Add Amlogic V4L2 video decoder Zhentao Guo via B4 Relay
2026-10-22 5:27 ` [PATCH v12 4/6] decoder: Add V4L2 stateless H.264 decoder driver Zhentao Guo via B4 Relay
2026-10-22 5:27 ` [PATCH v12 5/6] arm64: dts: amlogic: Add video decoder driver support for S4 SOCs Zhentao Guo via B4 Relay
2026-10-22 6:01 ` sashiko-bot [this message]
2026-10-23 11:25 ` Zhentao Guo
2026-10-22 5:27 ` [PATCH v12 6/6] arm64: defconfig: Enable CONFIG_VIDEO_AMLOGIC_VDEC Zhentao Guo via B4 Relay
2026-10-22 5:53 ` sashiko-bot
2026-10-23 9:46 ` Zhentao Guo
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-181046@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=linux-amlogic@lists.infradead.org \
--cc=media-ci@linuxtv.org \
--cc=neil.armstrong@linaro.org \
--cc=robh@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
--cc=zhentao.guo@amlogic.com \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®