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


**Input.** An MP4 whose trailing `mfra` holds a `tfra` box with declared size 
0. With `-use_mfra_for pts`, or by default when the file starts with an 
`Anevia` `free` box (`mov_read_free()` switches `use_mfra_for` from auto to 
pts), the first `moof` makes the demuxer read the `mfra`. A 76-byte file 
(`ftyp`, `free "Anevia\x1a\x1a"`, empty `moof`, `mfra`) is enough to make a 
plain `ffmpeg -i file.mp4` spin at 100% CPU inside `avformat_open_input()` and 
never return.

**Cause.** `mov_read_mfra()` calls `read_tfra()` until it returns non-zero. 
`read_tfra()` ends with `avio_seek(f, pos + size, SEEK_SET); return 0;`, so 
with `size == 0` it seeks back to the start of the same box and gets parsed 
again, forever. The box is still in the AVIOContext buffer, so the seek only 
moves `buf_ptr` and no read reaches `retry_transfer_wrapper()`, which is where 
`ff_check_interrupt()` gets called. As a result, ffmpeg's `decode_interrupt_cb` 
is never checked. A single SIGTERM or SIGINT only increments 
`received_nb_signals`, and the process keeps spinning until it gets SIGKILL or 
a fourth signal.

**Fix.** Reject a `tfra` whose size is smaller than its 24-byte full-box header 
with `AVERROR_INVALIDDATA`. `mov_read_moof()` already treats a failed mfra read 
as non-fatal and carries on without the mfra index. Any accepted size is at 
least 24, so each iteration moves forward and the loop reaches a non-`tfra` box 
or EOF.

**Verification** (master 38c6f36f93, release build, Docker `--network none`):
- Before: the 76-byte file was still running at 100% CPU 5 s after SIGTERM. It 
needed SIGKILL (exit 137).
- After: `Invalid tfra size 0`, then `moov atom not found`. The open fails 
right away.
- New test `fate-mov-tfra-zero-size`: it generates a fragmented MP4 with the 
same command as the `fate-mov-trun-*` tests, sets the tfra size to 0 with 
`run_with_patched_temp`, and probes it with `-use_mfra_for pts`. Without the 
fix it hangs (I killed it after 30 s). With the fix it prints all 10 packets, 
the same as the unpatched file. No sample upload is needed.
- `make fate-mov fate-mpeg4-fragmented-seek fate-lavf-mov 
fate-lavf-mov_hybrid_frag fate-lavf-ismv fate-h264-bsf-redundant-pps-mov 
fate-binsub-movtextenc`: 110/110 pass before and after, plus the new test 
passes (111).

This issue was found by an LLM-Enhanced Static Analysis Engine and verified 
manually.

🤖 Generated with [Claude Code](https://claude.com/claude-code)



From 7bfa56dffe505362155cfd914869c17dbea38c51 Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Tue, 27 Oct 2026 10:45:34 +0800
Subject: [PATCH 1/2] avformat/mov: reject tfra boxes smaller than their header

mov_read_mfra() calls read_tfra() until it returns non-zero, and
read_tfra() ends by seeking to the box start plus its declared size.
A tfra box with size 0 therefore seeks back onto itself and is parsed
again forever. Since the whole box stays in the AVIOContext buffer, no
read reaches the protocol layer and the interrupt callback is never
checked, so the demuxer cannot be interrupted either.

The mfra is read on the first moof when use_mfra_for is enabled, which
an "Anevia" free box does by default, so a 76-byte file can make
avformat_open_input() spin indefinitely.

Signed-off-by: Martin Mirchev <[email protected]>
---
 libavformat/mov.c | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/libavformat/mov.c b/libavformat/mov.c
index e2f33643f78..83bf9fa8d4f 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -11013,6 +11013,12 @@ static int read_tfra(MOVContext *mov, AVIOContext *f)
     if (avio_rb32(f) != MKBETAG('t', 'f', 'r', 'a')) {
         return 1;
     }
+    /* A tfra box can not be smaller than its 24-byte header. A size of 0
+     * would make mov_read_mfra() re-read the same box forever. */
+    if (size < 24) {
+        av_log(mov->fc, AV_LOG_ERROR, "Invalid tfra size %"PRIu32"\n", size);
+        return AVERROR_INVALIDDATA;
+    }
     av_log(mov->fc, AV_LOG_VERBOSE, "found tfra\n");
 
     version = avio_r8(f);
-- 
2.54.0


From 89751e9f254ceae998c98bf94204b97a5cfebbf0 Mon Sep 17 00:00:00 2001
From: Martin Mirchev <[email protected]>
Date: Tue, 27 Oct 2026 10:45:34 +0800
Subject: [PATCH 2/2] tests/fate/mov: add a test for a tfra box with size 0

Generate a fragmented file, set the size of the tfra box in its mfra to
zero and check that the packets are still read with -use_mfra_for pts.

Signed-off-by: Martin Mirchev <[email protected]>
---
 tests/fate/mov.mak                | 16 +++++++++++++++-
 tests/ref/fate/mov-tfra-zero-size | 10 ++++++++++
 2 files changed, 25 insertions(+), 1 deletion(-)
 create mode 100644 tests/ref/fate/mov-tfra-zero-size

diff --git a/tests/fate/mov.mak b/tests/fate/mov.mak
index e467acdafbd..7de934cef10 100644
--- a/tests/fate/mov.mak
+++ b/tests/fate/mov.mak
@@ -402,7 +402,8 @@ FATE_MOV_FFMPEG_FFPROBE-$(call ALLYES, COLOR_FILTER 
SETPTS_FILTER MPEG4_ENCODER
 FATE_MOV_FFMPEG_FFPROBE-$(call ALLYES, COLOR_FILTER SETPTS_FILTER 
MPEG4_ENCODER \
                                       MP4_MUXER MOV_DEMUXER FILE_PROTOCOL)     
 \
                                       += fate-mov-trun-large-sample-duration \
-                                         fate-mov-trun-zero-sample-size
+                                         fate-mov-trun-zero-sample-size \
+                                         fate-mov-tfra-zero-size
 
 # Generate a fragmented VFR file, then replace the last sample duration in
 # its first trun with UINT32_MAX.
@@ -429,6 +430,19 @@ fate-mov-trun-zero-sample-size: CMD = 
run_with_patched_temp \
     -show_entries packet=pts,dts,duration,size,pos -print_format compact \
     -select_streams v -v 0" mp4 927 "\\0000\\0000\\0000\\0000"
 
+# Set the size of the tfra box (at offset 1198, inside the trailing mfra) to
+# zero. Reading the mfra must fail instead of re-reading the same tfra box
+# forever; the packets are then read without the mfra.
+fate-mov-tfra-zero-size: CMD = run_with_patched_temp \
+    "$(FFMPEG) -nostdin -v error \
+    -filter_complex color=c=black:s=2x2:r=1,setpts=N*N \
+    -frames:v 10 -fps_mode vfr -c:v mpeg4 -g 5 -bf 0 -q:v 2 -threads 1 \
+    -flags +bitexact -fflags +bitexact \
+    -movflags empty_moov+frag_keyframe+default_base_moof -f mp4 -y" \
+    "ffprobe$(PROGSSUF)$(EXESUF) -use_mfra_for pts -show_packets \
+    -show_entries packet=pts,dts,duration -print_format compact \
+    -select_streams v -v 0" mp4 1198 "\\0000\\0000\\0000\\0000"
+
 FATE_MOV_FFMPEG_FFPROBE-$(call ALLYES, LAVFI_INDEV ANULLSRC_FILTER 
AFORMAT_FILTER \
                                       PCM_S16$(if $(filter 
yes,$(HAVE_BIGENDIAN)),BE,LE)_DECODER \
                                       PCM_S16BE_ENCODER                        
  \
diff --git a/tests/ref/fate/mov-tfra-zero-size 
b/tests/ref/fate/mov-tfra-zero-size
new file mode 100644
index 00000000000..20570f51465
--- /dev/null
+++ b/tests/ref/fate/mov-tfra-zero-size
@@ -0,0 +1,10 @@
+packet|pts=0|dts=0|duration=16384
+packet|pts=16384|dts=16384|duration=49152
+packet|pts=65536|dts=65536|duration=81920
+packet|pts=147456|dts=147456|duration=114688
+packet|pts=262144|dts=262144|duration=147456
+packet|pts=409600|dts=409600|duration=180224
+packet|pts=589824|dts=589824|duration=212992
+packet|pts=802816|dts=802816|duration=245760
+packet|pts=1048576|dts=1048576|duration=278528
+packet|pts=1327104|dts=1327104|duration=16384
-- 
2.54.0

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

Reply via email to