From 316531e61cf234ce184de7e750410bded7c9bb14 Mon Sep 17 00:00:00 2001 From: Zhao Zhili Date: Wed, 25 Mar 2026 00:35:19 +0800 Subject: [PATCH] avfilter/vidstabtransform: always use in-place transform path libvidstab's vsTransformPrepare() takes different internal code paths for in-place (src == dest) vs. separate-buffer operation. The separate-buffer path stores a shallow copy of the source frame pointer in td->src without allocating internal memory (srcMalloced stays 0). When a subsequent frame takes the in-place path, vsFrameIsNull(&td->src) is false so vsFrameAllocate() is skipped, and vsFrameCopy() writes into the stale pointer left over from the previous frame, corrupting memory that the caller no longer owns. Whether a given frame is writable depends on pipeline scheduling and frame reference management, which can change between FFmpeg versions. Since FFmpeg 8.1, changes in the scheduler caused some frames to arrive as non-writable, leading to alternation between in-place and separate-buffer paths that triggered the bug. Fix this by marking the input pad with AVFILTERPAD_FLAG_NEEDS_WRITABLE. Fix #22595 --- libavfilter/vf_vidstabtransform.c | 42 +++++++++++-------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/libavfilter/vf_vidstabtransform.c b/libavfilter/vf_vidstabtransform.c index a725eaacca..012521b9c0 100644 --- a/libavfilter/vf_vidstabtransform.c +++ b/libavfilter/vf_vidstabtransform.c @@ -232,46 +232,20 @@ static int filter_frame(AVFilterLink *inlink, AVFrame *in) VSTransformData* td = &(tc->td); AVFilterLink *outlink = ctx->outputs[0]; - int direct = 0; - AVFrame *out; VSFrame inframe; int plane; - if (av_frame_is_writable(in)) { - direct = 1; - out = in; - } else { - out = ff_get_video_buffer(outlink, outlink->w, outlink->h); - if (!out) { - av_frame_free(&in); - return AVERROR(ENOMEM); - } - av_frame_copy_props(out, in); - } - for (plane = 0; plane < vsTransformGetSrcFrameInfo(td)->planes; plane++) { inframe.data[plane] = in->data[plane]; inframe.linesize[plane] = in->linesize[plane]; } - if (direct) { - vsTransformPrepare(td, &inframe, &inframe); - } else { // separate frames - VSFrame outframe; - for (plane = 0; plane < vsTransformGetDestFrameInfo(td)->planes; plane++) { - outframe.data[plane] = out->data[plane]; - outframe.linesize[plane] = out->linesize[plane]; - } - vsTransformPrepare(td, &inframe, &outframe); - } + vsTransformPrepare(td, &inframe, &inframe); vsDoTransform(td, vsGetNextTransform(td, &tc->trans)); vsTransformFinish(td); - if (!direct) - av_frame_free(&in); - - return ff_filter_frame(outlink, out); + return ff_filter_frame(outlink, in); } static const AVFilterPad avfilter_vf_vidstabtransform_inputs[] = { @@ -280,6 +254,18 @@ static const AVFilterPad avfilter_vf_vidstabtransform_inputs[] = { .type = AVMEDIA_TYPE_VIDEO, .filter_frame = filter_frame, .config_props = config_input, + /* libvidstab's vsTransformPrepare() takes different internal code paths + * for in-place (src == dest) vs. separate-buffer operation. The + * separate-buffer path stores a shallow copy of the source frame + * pointer in td->src without allocating internal memory. When a + * subsequent frame takes the in-place path, it skips allocation + * (td->src is non-null) and copies into the stale pointer, + * corrupting memory that the caller no longer owns. + * Whether a frame is writable depends on pipeline scheduling, so + * always ensure the frame is writable to consistently take the + * in-place path. + */ + .flags = AVFILTERPAD_FLAG_NEEDS_WRITABLE, }, };