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]

Reply via email to