PR #24769 opened by Kacper Michajłow (kasper93) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24769 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24769.patch
From 42698ed66f00c98d306a7f9040d603b57eb8a408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]> Date: Tue, 29 Sep 2026 11:34:11 +0200 Subject: [PATCH 1/7] avformat/hls: fix subtitle playlist IO buffer leaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IO context of a playlist owns its buffer. Allocate only the buffer it is initialized with, and free the previous one when init_subtitle_context() sets it up again. Fixes: 540482604/clusterfuzz-testcase-minimized-ffmpeg_dem_HLS_fuzzer-6393605404295168 Signed-off-by: Kacper Michajłow <[email protected]> --- libavformat/hls.c | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/libavformat/hls.c b/libavformat/hls.c index daca226f67..93ef1bd0c0 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -102,7 +102,6 @@ enum PlaylistType { struct playlist { char url[MAX_URL_SIZE]; FFIOContext pb; - uint8_t* read_buffer; AVIOContext *input; int input_read_done; int input_reuse; @@ -1929,19 +1928,21 @@ static int init_subtitle_context(struct playlist *pls) HLSContext *c = pls->parent->priv_data; const AVInputFormat *in_fmt; AVDictionary *opts = NULL; + uint8_t *buf; int ret; if (!(pls->ctx = avformat_alloc_context())) return AVERROR(ENOMEM); - pls->read_buffer = av_malloc(INITIAL_BUFFER_SIZE); - if (!pls->read_buffer) { + buf = av_malloc(INITIAL_BUFFER_SIZE); + if (!buf) { avformat_free_context(pls->ctx); pls->ctx = NULL; return AVERROR(ENOMEM); } - ffio_init_context(&pls->pb, pls->read_buffer, INITIAL_BUFFER_SIZE, 0, pls, + av_freep(&pls->pb.pub.buffer); + ffio_init_context(&pls->pb, buf, INITIAL_BUFFER_SIZE, 0, pls, read_data_subtitle_segment, NULL, NULL); pls->pb.pub.seekable = 0; pls->ctx->pb = &pls->pb.pub; @@ -2412,6 +2413,8 @@ static int hls_read_header(AVFormatContext *s) char *url; AVDictionary *options = NULL; struct segment *seg = NULL; + uint8_t *buf; + int buf_size; if (!(pls->ctx = avformat_alloc_context())) return AVERROR(ENOMEM); @@ -2435,19 +2438,21 @@ static int hls_read_header(AVFormatContext *s) pls->cur_seq_no = highest_cur_seq_no; } - pls->read_buffer = av_malloc(INITIAL_BUFFER_SIZE); - if (!pls->read_buffer){ + if (pls->is_subtitle) { + buf_size = strlen("WEBVTT\n"); + buf = av_memdup("WEBVTT\n", buf_size); + } else { + buf_size = INITIAL_BUFFER_SIZE; + buf = av_malloc(buf_size); + } + if (!buf) { avformat_free_context(pls->ctx); pls->ctx = NULL; return AVERROR(ENOMEM); } - if (pls->is_subtitle) - ffio_init_context(&pls->pb, (unsigned char*)av_strdup("WEBVTT\n"), (int)strlen("WEBVTT\n"), 0, pls, - NULL, NULL, NULL); - else - ffio_init_context(&pls->pb, pls->read_buffer, INITIAL_BUFFER_SIZE, 0, pls, - read_data_continuous, NULL, NULL); + ffio_init_context(&pls->pb, buf, buf_size, 0, pls, + pls->is_subtitle ? NULL : read_data_continuous, NULL, NULL); /* * If encryption scheme is SAMPLE-AES, try to read ID3 tags of -- 2.52.0 From 418387d17715e97589320578375ef5e7f74b5f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]> Date: Tue, 29 Sep 2026 11:34:13 +0200 Subject: [PATCH 2/7] avformat/hls: free the previous segments if parsing a playlist fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kacper Michajłow <[email protected]> --- libavformat/hls.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libavformat/hls.c b/libavformat/hls.c index 93ef1bd0c0..730e8b98fe 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -1156,11 +1156,11 @@ static int parse_playlist(HLSContext *c, const char *url, av_log(c->ctx, AV_LOG_WARNING, "Media sequence changed unexpectedly: %"PRId64" -> %"PRId64"\n", prev_start_seq_no, pls->start_seq_no); } - free_segment_dynarray(prev_segments, prev_n_segments); - av_freep(&prev_segments); } fail: + free_segment_dynarray(prev_segments, prev_n_segments); + av_freep(&prev_segments); if (pls) pls->last_load_time = load_start; av_free(new_url); -- 2.52.0 From 4888d19bd181616095fbe339bd7d0504c8778241 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]> Date: Tue, 29 Sep 2026 11:34:15 +0200 Subject: [PATCH 3/7] avformat/hls: use the playlist of the new variant in ensure_playlist() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new_playlist() returns the known playlist for a duplicate URL, which is not necessarily the last one in c->playlists. Take it from the variant, so media tags of a Master Playlist do not end up in an unrelated playlist. Fixes: 54f0296dfe Signed-off-by: Kacper Michajłow <[email protected]> --- libavformat/hls.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libavformat/hls.c b/libavformat/hls.c index 730e8b98fe..97755da3a3 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -657,9 +657,10 @@ static int ensure_playlist(HLSContext *c, struct playlist **pls, const char *url { if (*pls) return 0; - if (!new_variant(c, NULL, url, NULL)) + struct variant *var = new_variant(c, NULL, url, NULL); + if (!var) return AVERROR(ENOMEM); - *pls = c->playlists[c->n_playlists - 1]; + *pls = var->playlists[0]; return 0; } -- 2.52.0 From 7bc15f4ef960ed57b2c0aaa3e3d93e0db1a603c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]> Date: Tue, 29 Sep 2026 11:34:17 +0200 Subject: [PATCH 4/7] avformat/hls: free the subtitle context if copying the whitelists fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Free it like the other error paths of init_subtitle_context() do. Signed-off-by: Kacper Michajłow <[email protected]> --- libavformat/hls.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libavformat/hls.c b/libavformat/hls.c index 97755da3a3..5c530215e0 100644 --- a/libavformat/hls.c +++ b/libavformat/hls.c @@ -1950,8 +1950,11 @@ static int init_subtitle_context(struct playlist *pls) pls->ctx->io_open = nested_io_open; ret = ff_copy_whiteblacklists(pls->ctx, pls->parent); - if (ret < 0) + if (ret < 0) { + avformat_free_context(pls->ctx); + pls->ctx = NULL; return ret; + } in_fmt = av_find_input_format("webvtt"); av_dict_copy(&opts, c->seg_format_opts, 0); -- 2.52.0 From 65fc00969da0a22c55e0c08bbeda20bcd3ac762e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]> Date: Tue, 29 Sep 2026 11:34:18 +0200 Subject: [PATCH 5/7] avformat/dashdec: free the old IO buffer when reopening a representation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a failed reopen it is still there without a demuxer context, so close_demux_for_component() does not free it. Signed-off-by: Kacper Michajłow <[email protected]> --- libavformat/dashdec.c | 1 + 1 file changed, 1 insertion(+) diff --git a/libavformat/dashdec.c b/libavformat/dashdec.c index 740ac6bc1c..2ce6fe6c56 100644 --- a/libavformat/dashdec.c +++ b/libavformat/dashdec.c @@ -1964,6 +1964,7 @@ static int reopen_demux_for_component(AVFormatContext *s, struct representation pls->ctx = NULL; goto fail; } + av_freep(&pls->pb.pub.buffer); ffio_init_context(&pls->pb, avio_ctx_buffer, INITIAL_BUFFER_SIZE, 0, pls, read_data, NULL, c->is_live ? NULL : seek_data); pls->pb.pub.seekable = 0; -- 2.52.0 From 4305e0b249a79cd2ec77ef8cacb382ea70ea602c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]> Date: Tue, 29 Sep 2026 11:34:20 +0200 Subject: [PATCH 6/7] avformat/dashdec: free the context if copying the whitelists fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kacper Michajłow <[email protected]> --- libavformat/dashdec.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/libavformat/dashdec.c b/libavformat/dashdec.c index 2ce6fe6c56..76352c1708 100644 --- a/libavformat/dashdec.c +++ b/libavformat/dashdec.c @@ -1969,8 +1969,11 @@ static int reopen_demux_for_component(AVFormatContext *s, struct representation pls, read_data, NULL, c->is_live ? NULL : seek_data); pls->pb.pub.seekable = 0; - if ((ret = ff_copy_whiteblacklists(pls->ctx, s)) < 0) + if ((ret = ff_copy_whiteblacklists(pls->ctx, s)) < 0) { + avformat_free_context(pls->ctx); + pls->ctx = NULL; goto fail; + } pls->ctx->flags = AVFMT_FLAG_CUSTOM_IO; pls->ctx->probesize = s->probesize > 0 ? s->probesize : 1024 * 4; -- 2.52.0 From e0c3da55f5ead98045984ad1798a8e3aaae77fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= <[email protected]> Date: Tue, 29 Sep 2026 11:34:20 +0200 Subject: [PATCH 7/7] tests/fate/demux: add HLS WebVTT rendition and self-referencing master tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first test reads all segments of a WebVTT subtitle rendition. The second opens a Master Playlist that lists itself as a variant, with a stray segment and a subtitle rendition that cannot be opened. Signed-off-by: Kacper Michajłow <[email protected]> --- tests/fate/demux.mak | 27 +++++++++++++++++++++++++++ tests/ref/fate/hls-self-variant-demux | 1 + tests/ref/fate/hls-webvtt-demux | 10 ++++++++++ 3 files changed, 38 insertions(+) create mode 100644 tests/ref/fate/hls-self-variant-demux create mode 100644 tests/ref/fate/hls-webvtt-demux diff --git a/tests/fate/demux.mak b/tests/fate/demux.mak index b02f0d5010..c7a88ee145 100644 --- a/tests/fate/demux.mak +++ b/tests/fate/demux.mak @@ -186,6 +186,33 @@ FATE_FFPROBE_DEMUX-$(call DEMDEC, MPEGTS HLS) += fate-ts-timed-id3-hls-demux fate-ts-timed-id3-hls-demux: tests/data/id3.m3u8 fate-ts-timed-id3-hls-demux: CMD = ffprobe_demux $(TARGET_PATH)/tests/data/id3.m3u8 +tests/data/hls-webvtt.m3u8: TAG = GEN +tests/data/hls-webvtt.m3u8: ffmpeg$(PROGSSUF)$(EXESUF) | tests/data + $(M)$(TARGET_EXEC) $(TARGET_PATH)/$< -nostdin \ + -f lavfi -i "aevalsrc=0:d=6" -c:a mp2fixed -flags +bitexact -muxdelay 0 -muxpreload 0 \ + -f hls -hls_time 2 -hls_list_size 0 \ + -hls_segment_filename $(TARGET_PATH)/tests/data/hls-webvtt-a%d.ts \ + $(TARGET_PATH)/tests/data/hls-webvtt-a.m3u8 2>/dev/null + $(Q)printf '#EXTM3U\n#EXT-X-TARGETDURATION:2\n#EXTINF:2,\nhls-webvtt-s0.vtt\n#EXTINF:2,\nhls-webvtt-s1.vtt\n#EXTINF:2,\nhls-webvtt-s2.vtt\n#EXT-X-ENDLIST\n' > tests/data/hls-webvtt-s.m3u8 + $(Q)printf 'WEBVTT\n\n00:00.500 --> 00:01.500\nfirst\n' > tests/data/hls-webvtt-s0.vtt + $(Q)printf 'WEBVTT\n\n00:02.500 --> 00:03.500\nsecond\n' > tests/data/hls-webvtt-s1.vtt + $(Q)printf 'WEBVTT\n\n00:04.500 --> 00:05.500\nthird\n' > tests/data/hls-webvtt-s2.vtt + $(Q)printf '#EXTM3U\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="s",NAME="en",URI="hls-webvtt-s.m3u8"\n#EXT-X-STREAM-INF:BANDWIDTH=64000,SUBTITLES="s"\nhls-webvtt-a.m3u8\n' > $@ + +FATE_FFMPEG-$(call FILTERDEMDECENCMUX, AEVALSRC ARESAMPLE, HLS MPEGTS WEBVTT, MP2 PCM_F64LE, MP2FIXED, HLS MPEGTS NULL WEBVTT, LAVFI_INDEV) += fate-hls-webvtt-demux +fate-hls-webvtt-demux: tests/data/hls-webvtt.m3u8 +fate-hls-webvtt-demux: CLEANFILES = tests/data/hls-webvtt*.m3u8 tests/data/hls-webvtt-*.ts tests/data/hls-webvtt-*.vtt +fate-hls-webvtt-demux: CMD = fmtstdout webvtt -copyts -i $(TARGET_PATH)/tests/data/hls-webvtt.m3u8 -map 0:a -c copy -f null - -map 0:s -c copy + +tests/data/hls-self-variant.m3u8: TAG = GEN +tests/data/hls-self-variant.m3u8: | tests/data + $(Q)printf '#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=64000\nhls-self-variant.m3u8\n#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="s",NAME="en",URI="hls-self-variant-missing.m3u8"\n#EXTINF:2,\nhls-self-variant.vtt\n' > $@ + +FATE_FFPROBE-$(call DEMDEC, HLS) += fate-hls-self-variant-demux +fate-hls-self-variant-demux: tests/data/hls-self-variant.m3u8 +fate-hls-self-variant-demux: CLEANFILES = tests/data/hls-self-variant.m3u8 +fate-hls-self-variant-demux: CMD = ffprobe_demux $(TARGET_PATH)/tests/data/hls-self-variant.m3u8 + FATE_SAMPLES_DEMUX-$(call PARSERDEM, JPEGXS, IMAGE_JPEGXS_PIPE, CONCAT_PROTOCOL) += fate-jxs-concat-demux fate-jxs-concat-demux: CMD = framecrc "-i concat:$(TARGET_SAMPLES)/jxs/lena.jxs|$(TARGET_SAMPLES)/jxs/lena.jxs -c:v copy" diff --git a/tests/ref/fate/hls-self-variant-demux b/tests/ref/fate/hls-self-variant-demux new file mode 100644 index 0000000000..cac15f5202 --- /dev/null +++ b/tests/ref/fate/hls-self-variant-demux @@ -0,0 +1 @@ +format|filename=hls-self-variant.m3u8|nb_streams=0|nb_programs=2|nb_stream_groups=0|format_name=hls|start_time=N/A|duration=N/A|size=183|bit_rate=N/A|probe_score=100 diff --git a/tests/ref/fate/hls-webvtt-demux b/tests/ref/fate/hls-webvtt-demux new file mode 100644 index 0000000000..854c47a0e0 --- /dev/null +++ b/tests/ref/fate/hls-webvtt-demux @@ -0,0 +1,10 @@ +WEBVTT + +00:00.500 --> 00:01.500 +first + +00:02.500 --> 00:03.500 +second + +00:04.500 --> 00:05.500 +third -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
