Commit 260d32d06d for ffmpeg

commit 260d32d06d9b158ccbf31569421d074eafc5043c
Author: Jun Zhao <barryjzhao@tencent.com>
Date:   Sat Oct 24 16:05:24 2026 +0800

    avformat/id3v2: tighten v2.4 non-synchsafe frame size boundary check

    A valid synchsafe integer has the MSB cleared in every byte, and a
    candidate next-frame boundary inside an ID3v2.4 tag must be followed by
    the end of the tag, zero padding, or a plausible 10-byte frame header
    whose size fits in the remaining tag bytes.

    Previously, size_to_syncsafe(tlen) was probed even when tlen had bit 7
    set in one of its bytes (such as 0x000000b6), and check_tag() accepted
    any four uppercase ASCII characters (such as "LLLL" inside a chapter
    title) as a valid next frame ID without checking the following frame
    size. It could also read past the end of the ID3v2 tag when the last
    frame had fewer than 4 bytes of padding.

    Check tlen with is_syncsafe() before probing size_to_syncsafe(tlen), bound
    the probe to the remaining tag bytes, and verify in check_tag() that a
    candidate frame header fits in the remaining tag length (including empty
    frames, which id3v2_parse() skips).

    Signed-off-by: Jun Zhao <barryjzhao@tencent.com>

diff --git a/libavformat/id3v2.c b/libavformat/id3v2.c
index 22a15ab794..20d37f345b 100644
--- a/libavformat/id3v2.c
+++ b/libavformat/id3v2.c
@@ -182,6 +182,14 @@ static unsigned int get_size(AVIOContext *s, int len)
     return v;
 }

+static int is_syncsafe(unsigned int size)
+{
+    return !(size & ((0x80U <<  0) |
+                     (0x80U <<  8) |
+                     (0x80U << 16) |
+                     (0x80U << 24)));
+}
+
 static unsigned int size_to_syncsafe(unsigned int size)
 {
     return (((size) & (0x7f <<  0)) >> 0) +
@@ -208,20 +216,38 @@ static int is_tag(const char *buf, unsigned int len)
 }

 /**
- * Return 1 if the tag of length len at the given offset is valid, 0 if not, -1 on error
+ * Return 1 if the candidate frame boundary at offset (with rem bytes left in
+ * the ID3v2 tag after it) is followed by the end of the tag, zero padding, or
+ * a plausible 10-byte ID3v2.3/2.4 frame header; 0 if not, -1 on error.
  */
-static int check_tag(AVIOContext *s, int offset, unsigned int len)
+static int check_tag(AVIOContext *s, int64_t offset, unsigned int rem)
 {
     char tag[4];
+    unsigned int nsize, nlen;

-    if (len > 4 ||
-        avio_seek(s, offset, SEEK_SET) < 0 ||
-        avio_read(s, tag, len) < (int)len)
+    if (!rem)
+        return 1;
+    if (avio_seek(s, offset, SEEK_SET) < 0)
         return -1;
-    else if (!AV_RB32(tag) || is_tag(tag, len))
+    if (rem < sizeof(tag)) {
+        while (rem--)
+            if (avio_r8(s))
+                return 0;
         return 1;
-
-    return 0;
+    }
+    if (avio_read(s, tag, sizeof(tag)) < (int)sizeof(tag))
+        return -1;
+    if (!AV_RB32(tag))
+        return 1;
+    if (rem < ID3v2_HEADER_SIZE || !is_tag(tag, sizeof(tag)))
+        return 0;
+    nsize = avio_rb32(s);
+    nlen  = is_syncsafe(nsize) ? size_to_syncsafe(nsize) : nsize;
+    /* ID3v2 requires frames to be at least 1 byte, but id3v2_parse()
+     * explicitly skips empty frames (tlen == 0) to recover from broken tags.
+     * Accept nlen == 0 here as well so a valid synchsafe boundary followed by
+     * an empty frame is not rejected. */
+    return nlen <= rem - ID3v2_HEADER_SIZE;
 }

 /**
@@ -944,18 +970,23 @@ static void id3v2_parse(AVIOContext *pb, AVDictionary **metadata,
                  * so check the next tag to see which one to use */
                 tlen = avio_rb32(pb);
                 if (tlen > 0x7f) {
-                    if (tlen < len) {
+                    unsigned int rem = len - taghdrlen;
+
+                    if (tlen <= rem) {
                         int64_t cur = avio_tell(pb);
+                        unsigned int ss_tlen = size_to_syncsafe(tlen);

-                        if (ffio_ensure_seekback(pb, 2 /* tflags */ + tlen + 4 /* next tag */))
+                        if (ffio_ensure_seekback(pb, 2 /* tflags */ +
+                                                 FFMIN(rem, tlen + ID3v2_HEADER_SIZE)))
                             break;

-                        if (check_tag(pb, cur + 2 + size_to_syncsafe(tlen), 4) == 1)
-                            tlen = size_to_syncsafe(tlen);
-                        else if (check_tag(pb, cur + 2 + tlen, 4) != 1)
+                        if (is_syncsafe(tlen) &&
+                            check_tag(pb, cur + 2 + ss_tlen, rem - ss_tlen) == 1)
+                            tlen = ss_tlen;
+                        else if (check_tag(pb, cur + 2 + tlen, rem - tlen) != 1)
                             break;
                         avio_seek(pb, cur, SEEK_SET);
-                    } else
+                    } else if (is_syncsafe(tlen))
                         tlen = size_to_syncsafe(tlen);
                 }
             }