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
This commit is contained in:
Zhao Zhili
2026-03-25 00:35:19 +08:00
parent c695ad1197
commit 316531e61c

View File

@@ -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);
}
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,
},
};