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]
