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);
}
}