PR #23766 opened by michaelni
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23766
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/23766.patch

Found-by: BapToutatis


>From d9bcec86fbcef7f95e715a809d1804c7511f0c22 Mon Sep 17 00:00:00 2001
From: BapToutatis <[email protected]>
Date: Sat, 11 Jul 2026 01:38:34 +0200
Subject: [PATCH 1/2] avformat/hls: bound the number of segments parsed from a
 single playlist

parse_playlist() appended a segment for every #EXTINF/URL pair with no
upper bound. A malicious server responding with Transfer-Encoding: chunked
that never sends the terminating chunk keeps avio_feof() false forever, so
the loop allocates segments until memory is exhausted. This runs during
hls_read_header(), where the existing max_reload limit does not apply.

Add a max_segments option (default 100000, enough for long-form VOD) and
stop parsing via the existing pls->broken flag once it is reached.

Fixes: OOM
Fixes: infinite_segments_server.py
Fixes: v3rmKcAVRW03
Found-by: BapToutatis
---
 libavformat/hls.c | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/libavformat/hls.c b/libavformat/hls.c
index e76e68ed51..01ab40b7d8 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -240,6 +240,7 @@ typedef struct HLSContext {
     char *allowed_segment_extensions;
     int extension_picky;
     int max_reload;
+    int max_segments;
     int http_persistent;
     int http_multiple;
     int http_seekable;
@@ -1047,6 +1048,13 @@ static int parse_playlist(HLSContext *c, const char *url,
                 ret = ensure_playlist(c, &pls, url);
                 if (ret < 0)
                     goto fail;
+                if (pls->n_segments >= c->max_segments) {
+                    av_log(c->ctx, AV_LOG_WARNING,
+                           "Too many segments (%d) in playlist %s, stopping 
parse\n",
+                           c->max_segments, url);
+                    pls->broken = 1;
+                    break;
+                }
                 seg = av_malloc(sizeof(struct segment));
                 if (!seg) {
                     ret = AVERROR(ENOMEM);
@@ -3053,6 +3061,8 @@ static const AVOption hls_options[] = {
         OFFSET(extension_picky), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, FLAGS},
     {"max_reload", "Maximum number of times a insufficient list is attempted 
to be reloaded",
         OFFSET(max_reload), AV_OPT_TYPE_INT, {.i64 = 100}, 0, INT_MAX, FLAGS},
+    {"max_segments", "Maximum number of segments accepted in a single playlist 
parse",
+        OFFSET(max_segments), AV_OPT_TYPE_INT, {.i64 = 100000}, 1, INT_MAX, 
FLAGS},
     {"m3u8_hold_counters", "The maximum number of times to load m3u8 when it 
refreshes without new segments",
         OFFSET(m3u8_hold_counters), AV_OPT_TYPE_INT, {.i64 = 1000}, 0, 
INT_MAX, FLAGS},
     {"http_persistent", "Use persistent HTTP connections",
-- 
2.52.0


>From 9706d64d843d01d00dfc9cb7460445a1d834a544 Mon Sep 17 00:00:00 2001
From: BapToutatis <[email protected]>
Date: Sat, 11 Jul 2026 01:38:44 +0200
Subject: [PATCH 2/2] avformat/hls: time-bound parsing of a single playlist

As defense in depth against a non-terminating playlist stream, abort the
parse loop once it has run longer than parse_timeout (default 5 minutes,
-1 to disable). Legitimate manifests parse in well under a second.

The monotonic av_gettime_relative() clock is used so a wall-clock jump
cannot skip or trip the limit, and the timeout exits through the normal
cleanup path.

Fixes: OOM
Fixes: infinite_segments_server.py
Fixes: v3rmKcAVRW03
Found-by: BapToutatis
---
 libavformat/hls.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/libavformat/hls.c b/libavformat/hls.c
index 01ab40b7d8..54dfe81267 100644
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -241,6 +241,7 @@ typedef struct HLSContext {
     int extension_picky;
     int max_reload;
     int max_segments;
+    int64_t parse_timeout;
     int http_persistent;
     int http_multiple;
     int http_seekable;
@@ -888,7 +889,15 @@ static int parse_playlist(HLSContext *c, const char *url,
         pls->finished = 0;
         pls->type = PLS_TYPE_UNSPECIFIED;
     }
+    int64_t parse_start = av_gettime_relative();
     while (!avio_feof(in)) {
+        if (c->parse_timeout >= 0 &&
+            av_gettime_relative() - parse_start > c->parse_timeout) {
+            av_log(c->ctx, AV_LOG_ERROR,
+                   "Playlist parsing timed out on %s\n", url);
+            ret = AVERROR(ETIMEDOUT);
+            goto fail;
+        }
         ff_get_chomp_line(in, line, sizeof(line));
         if (av_strstart(line, "#EXT-X-STREAM-INF:", &ptr)) {
             if (pls) {
@@ -3063,6 +3072,8 @@ static const AVOption hls_options[] = {
         OFFSET(max_reload), AV_OPT_TYPE_INT, {.i64 = 100}, 0, INT_MAX, FLAGS},
     {"max_segments", "Maximum number of segments accepted in a single playlist 
parse",
         OFFSET(max_segments), AV_OPT_TYPE_INT, {.i64 = 100000}, 1, INT_MAX, 
FLAGS},
+    {"parse_timeout", "Maximum time spent parsing a single playlist before 
aborting (-1 to disable)",
+        OFFSET(parse_timeout), AV_OPT_TYPE_DURATION, {.i64 = 300000000}, -1, 
INT64_MAX, FLAGS},
     {"m3u8_hold_counters", "The maximum number of times to load m3u8 when it 
refreshes without new segments",
         OFFSET(m3u8_hold_counters), AV_OPT_TYPE_INT, {.i64 = 1000}, 0, 
INT_MAX, FLAGS},
     {"http_persistent", "Use persistent HTTP connections",
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to