avcodec/bsf/dts2pts: evict unconsumable nodes from the poc tree

Each pending packet consumes up to poc_diff tree nodes when it is
output, so the packets in the FIFO can consume at most nb_pending nodes
in total. Frames whose tree lookup misses on output leave their nodes
behind, and damaged or crafted streams can make that happen
indefinitely, growing the tree without limit.

Track the node count and insertion order, keep the leftovers of up to
MAX_DAMAGED_FRAMES frames and beyond that evict the nodes unconsumed
the longest. Timestamps of valid frames are unaffected unless more
frames than that are damaged. No eviction is done at EOF, where nodes
are deliberately kept to regenerate timestamps from.

Of all h264/hevc conformance samples only MR3_TANDBERG_B.264 triggers
evictions, and no sample changes output.

Fixes: unbounded memory growth with damaged streams

Co-Authored-By: Fable-5
(cherry picked from commit dd941af8ac)
Signed-off-by: Michael Niedermayer <michael@niedermayer.cc>
This commit is contained in:
Michael Niedermayer
2026-07-08 16:14:48 +02:00
parent 1f37509a06
commit 76398b2b4f

View File

@@ -41,11 +41,17 @@
#include "libavcodec/h264_ps.h"
#include "libavcodec/hevc/ps.h"
// Damaged frames leave their up to 2 timestamp nodes behind unconsumed.
// This many damaged frames are tolerated before the oldest leftovers are
// evicted; no timestamp of a valid frame is lost below this.
#define MAX_DAMAGED_FRAMES 32
typedef struct DTS2PTSNode {
int64_t dts;
int64_t duration;
int poc;
int gop;
int64_t serial; // insertion order, evicting the stalest node first
struct DTS2PTSNode *next; // valid only during same-gop re-keying
} DTS2PTSNode;
@@ -90,6 +96,9 @@ typedef struct DTS2PTSContext {
DTS2PTSHEVCContext hevc;
} u;
int nb_nodes;
int nb_pending;
int64_t serial;
int nb_frame;
int gop;
int eof;
@@ -129,11 +138,21 @@ static int free_node(void *opaque, void *elem)
return 0;
}
static int find_stalest(void *opaque, void *elem)
{
DTS2PTSNode **stalest = opaque;
DTS2PTSNode *node = elem;
if (!*stalest || node->serial < (*stalest)->serial)
*stalest = node;
return 0;
}
// Shared functions
static int alloc_and_insert_node(AVBSFContext *ctx, int64_t ts, int64_t duration,
int poc, int poc_diff, int gop)
{
DTS2PTSContext *s = ctx->priv_data;
for (int i = 0; i < poc_diff; i++) {
struct AVTreeNode *node = av_tree_node_alloc();
DTS2PTSNode *poc_node, *ret;
@@ -146,13 +165,14 @@ static int alloc_and_insert_node(AVBSFContext *ctx, int64_t ts, int64_t duration
}
if (i && ts != AV_NOPTS_VALUE)
ts += duration / poc_diff;
*poc_node = (DTS2PTSNode) { ts, duration, poc++, gop };
*poc_node = (DTS2PTSNode) { ts, duration, poc++, gop, s->serial++ };
ret = av_tree_insert(&s->root, poc_node, cmp_insert, &node);
if (ret && ret != poc_node) {
*ret = *poc_node;
av_refstruct_unref(&poc_node);
av_free(node);
}
} else
s->nb_nodes++;
}
return 0;
}
@@ -230,6 +250,7 @@ static int h264_queue_frame(AVBSFContext *ctx, AVPacket *pkt, int poc, int *queu
frame = (DTS2PTSFrame) { pkt, poc, poc_diff, s->gop };
ret = av_fifo_write(s->fifo, &frame, 1);
av_assert2(ret >= 0);
s->nb_pending += poc_diff;
*queued = 1;
return 0;
@@ -477,6 +498,7 @@ static int hevc_queue_frame(AVBSFContext *ctx, AVPacket *pkt, int poc, bool *que
*r = *node;
av_refstruct_unref(&node);
av_free(tnode);
s->nb_nodes--;
}
}
}
@@ -498,6 +520,7 @@ static int hevc_queue_frame(AVBSFContext *ctx, AVPacket *pkt, int poc, bool *que
ret = av_fifo_write(s->fifo, &frame, 1);
if (ret < 0)
return ret;
s->nb_pending += frame.poc_diff;
*queued = true;
@@ -665,6 +688,7 @@ static int dts2pts_filter(AVBSFContext *ctx, AVPacket *out)
// Fetch a packet from the FIFO
ret = av_fifo_read(s->fifo, &frame, 1);
av_assert2(ret >= 0);
s->nb_pending -= frame.poc_diff;
av_packet_move_ref(out, frame.pkt);
av_packet_free(&frame.pkt);
@@ -691,6 +715,7 @@ static int dts2pts_filter(AVBSFContext *ctx, AVPacket *out)
av_tree_insert(&s->root, poc_node, cmp_insert, &node);
av_refstruct_unref(&poc_node);
av_free(node);
s->nb_nodes--;
poc_node = av_tree_find(s->root, &dup, cmp_find, NULL);
}
}
@@ -715,6 +740,24 @@ static int dts2pts_filter(AVBSFContext *ctx, AVPacket *out)
av_log(ctx, AV_LOG_WARNING, "No timestamp for POC %d in tree\n", frame.poc);
} else
av_log(ctx, AV_LOG_WARNING, "No timestamp for POC %d in tree\n", frame.poc);
// The pending packets consume nb_pending nodes; frames whose lookup above
// missed leave nodes behind which nothing consumes anymore. Keep the
// leftovers of up to MAX_DAMAGED_FRAMES frames, then evict the nodes
// unconsumed the longest.
// At EOF nodes are deliberately kept to regenerate timestamps from.
while (!s->eof && s->nb_nodes > s->nb_pending + 2 * MAX_DAMAGED_FRAMES) {
DTS2PTSNode *stale = NULL;
struct AVTreeNode *tnode = NULL;
av_tree_enumerate(s->root, &stale, NULL, find_stalest);
av_log(ctx, AV_LOG_WARNING, "Evicting unconsumed POC %d, GOP %d\n",
stale->poc, stale->gop);
av_tree_insert(&s->root, stale, cmp_insert, &tnode);
av_refstruct_unref(&stale);
av_free(tnode);
s->nb_nodes--;
}
av_log(ctx, AV_LOG_DEBUG, "Returning frame for POC %d, GOP %d, dts %"PRId64", pts %"PRId64"\n",
frame.poc, frame.gop, out->dts, out->pts);
@@ -736,7 +779,9 @@ static void dts2pts_flush(AVBSFContext *ctx)
av_tree_enumerate(s->root, NULL, NULL, free_node);
av_tree_destroy(s->root);
s->root = NULL;
s->root = NULL;
s->nb_nodes = 0;
s->nb_pending = 0;
ff_cbs_fragment_reset(&s->au);
if (s->cbc)