ANT-2026-CEP8Y9M0 · ffmpeg/ffmpeg
use-after-free high
Severity Claude high · Security research firm - · Maintainer -
Anthropic's analysis of this finding, sealed at approval.
ANT-2026-CEP8Y9M0: avformat/mpegts: reset PES state before JPEG-XS error return
In the MPEG-TS demuxer, new_pes_packet() begins transferring ownership of pes->buffer into pkt->buf but only nulls out pes->buffer at the end of the function. The JPEG-XS header-size check added in 16f89d342e returns AVERROR_INVALIDDATA mid-function, so on that path both pkt->buf and pes->buffer point to the same refcount-1 buffer. mpegts_read_packet() sees the error, calls av_packet_unref() (freeing the buffer), then its flush loop re-enters new_pes_packet() on the same PES (reset was also skipped so data_index > 0), dereferencing the dangling pes->buffer. A second path reaches the stale pointer via av_buffer_unref() in the seek-reset code in handle_packets, yielding a double-free and an atomic decrement through a freed AVBuffer*. The bug is triggered simply by running ffprobe on a crafted file.
Target
Project: ffmpeg
Version: git master only (introduced in commit 16f89d342e, Dec 2025; not present in 8.0.1 or any release)
Location: libavformat/mpegts.c:1024 (new_pes_packet)
Technical Details
ASAN: "heap-use-after-free ... READ of size 8 at 0x509000000808" in new_pes_packet libavformat/mpegts.c:1024. The root cause is an uncommitted ownership move: pkt->buf = pes->buffer starts the transfer, but the early return AVERROR_INVALIDDATA in the JPEG-XS branch skips both pes->buffer = NULL and reset_pes_packet_state(pes), leaving a dangling alias that is freed via av_packet_unref() and then reused.
Crash trace:
[mpegts @ ...] Invalid JPEG-XS header size 4294967295 > packet size 359
==2565539==ERROR: AddressSanitizer: heap-use-after-free on address 0x509000000808
READ of size 8 at 0x509000000808 thread T0
#0 new_pes_packet libavformat/mpegts.c:1024:30
#1 mpegts_read_packet libavformat/mpegts.c:3495:27
...
#4 avformat_find_stream_info libavformat/demux.c:2800:15
freed by thread T0 here:
#3 av_packet_unref libavcodec/packet.c:436:5
#4 mpegts_read_packet libavformat/mpegts.c:3489:9
Reproduction
- Craft an MPEG-TS stream with a PES packet where stream_id==0xbd, stream_type==0x32, bytes 4..7 == 'jxes', and AV_RB32(data) > packet size
- Deliver the file to the target (e.g.
ffprobe file.ts) - new_pes_packet() takes the early return, leaving pes->buffer == pkt->buf
- mpegts_read_packet() calls av_packet_unref(), freeing the buffer
- The flush loop (or later seek-reset in handle_packets) touches the dangling pes->buffer, producing a UAF read or double-free
[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]
Suggested Fix
Before the early return in the JPEG-XS header_size > pkt->size branch, perform the same cleanup as the success path: set pes->buffer = NULL and call reset_pes_packet_state(pes) (dropping the now-redundant pes->flags |= AV_PKT_FLAG_CORRUPT since reset clears flags). Patch submitted as a PR.
Acknowledgement
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-CEP8Y9M0.
Reference: ANT-2026-CEP8Y9M0
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure
The change that resolved this finding.
diff --git a/libavformat/mpegts.c b/libavformat/mpegts.c
index 0ee10f9a77492..bfbdbf5b19ecd 100644
--- a/libavformat/mpegts.c
+++ b/libavformat/mpegts.c
@@ -1041,10 +1041,10 @@ static int new_pes_packet(PESContext *pes, AVPacket *pkt)
"Invalid JPEG-XS header size %"PRIu32" > packet size %d\n",
header_size, pkt->size);
pes->flags |= AV_PKT_FLAG_CORRUPT;
- return AVERROR_INVALIDDATA;
+ } else {
+ pkt->data += header_size;
+ pkt->size -= header_size;
}
- pkt->data += header_size;
- pkt->size -= header_size;
}
memset(pkt->data + pkt->size, 0, AV_INPUT_BUFFER_PADDING_SIZE);https://github.com/FFmpeg/FFmpeg/commit/55bf0e6cd5a46b26b0ebd2374ad2625a7133e4ee
Dates from discovery through public reveal.
- 2026-03-12 Sent to maintainer
- 2026-03-12 Maintainer acknowledged
- 2026-03-15 Patch released
- 2026-05-14 Reported to tracker
- 2026-08-17 Publicly revealed
SHA-3-512 hash:
8512eaac6a2c21e5748f3d8801d6e310a02bede98fdf4a39607a4656343fe1e73fd4b2714a2643a607b19b4cf9d369d88e1c460a94c090492b871f0683fc5719
Committed 2026-03-13 11:31 PT
Revealed 2026-08-17 11:17 PT
Verify (download preimage.json)
Show preimage JSON
{
"ant_id": "ANT-2026-CEP8Y9M0",
"bug_class": "Use-After-Free",
"claude_severity": "high",
"commit_sha": null,
"created_at": "2026-05-14T22:41:33+00:00",
"description": "In the MPEG-TS demuxer, new_pes_packet() begins transferring ownership of pes->buffer into pkt->buf but only nulls out pes->buffer at the end of the function. The JPEG-XS header-size check added in 16f89d342e returns AVERROR_INVALIDDATA mid-function, so on that path both pkt->buf and pes->buffer point to the same refcount-1 buffer. mpegts_read_packet() sees the error, calls av_packet_unref() (freeing the buffer), then its flush loop re-enters new_pes_packet() on the same PES (reset was also skipped so data_index > 0), dereferencing the dangling pes->buffer. A second path reaches the stale pointer via av_buffer_unref() in the seek-reset code in handle_packets, yielding a double-free and an atomic decrement through a freed AVBuffer*. The bug is triggered simply by running ffprobe on a crafted file.",
"discovered_at": "2026-03-13T18:31:10+00:00",
"location": "libavformat/mpegts.c:1024 (new_pes_packet)",
"poc_sha256": null,
"preimage_version": 1,
"project": "ffmpeg",
"reproduction": [
"1. Craft an MPEG-TS stream with a PES packet where stream_id==0xbd, stream_type==0x32, bytes 4..7 == 'jxes', and AV_RB32(data) > packet size",
"2. Deliver the file to the target (e.g. `ffprobe file.ts`)",
"3. new_pes_packet() takes the early return, leaving pes->buffer == pkt->buf",
"4. mpegts_read_packet() calls av_packet_unref(), freeing the buffer",
"5. The flush loop (or later seek-reset in handle_packets) touches the dangling pes->buffer, producing a UAF read or double-free"
],
"technical_details": "Hi --\n\nI've filed a PR for a fix that has a similar ffprobe-level failure.\n\nThis one is master-only (the vulnerable code landed in 16f89d342e,\n\nDecember 2025, after 8.0.1 was cut) so there's no released version to\n\nworry about.\n\nIt's a use-after-free in the MPEG-TS demuxer's JPEG-XS handling, again\n\ntriggers on\n\n ffprobe arbitrary_file\n\nThe specific vulnerability here is that in libavformat/mpegts.c the\n\nfunction new_pes_packet() moves a buffer from one place to another\n\n(pes into pkt) with the line pkt->buf = pes->buffer. This keeps the\n\nrefcount at 1. Then it does JPEG-XS validation, and commits the move\n\nwith pes->buffer = NULL at the bottom. But the validation has an early\n\nreturn that skips the commit:\n\n // libavformat/mpegts.c, new_pes_packet()\n\n pkt->buf = pes->buffer; // move started\n\n pkt->data = pes->buffer->data;\n\n pkt->size = pes->data_index;\n\n ...\n\n if (pes->stream_id == 0xbd && pes->stream_type == 0x32 &&\n\n pkt->size >= 8 && memcmp(pkt->data + 4, \"jxes\", 4) == 0)\n\n {\n\n uint32_t header_size = AV_RB32(pkt->data);\n\n if (header_size > pkt->size) {\n\n ...\n\n return AVERROR_INVALIDDATA; // move never committed\n\n }\n\n }\n\n ...\n\n pes->buffer = NULL; // not reached\n\n reset_pes_packet_state(pes);\n\nAfter the early return, pkt->buf and pes->buffer both hold the same\n\npointer. But mpegts_read_packet sees the error and calls\n\nav_packet_unref(ts->pkt), freeing the buffer through pkt->buf. Then\n\nits flush loop re-calls new_pes_packet on the same pes -- because\n\nreset_pes_packet_state was also skipped, data_index is still > 0 --\n\nand the dangling pes->buffer gets dereferenced at line 1024.\n\nThere's a second path where the dangling pointer survives dormant and\n\nlater gets hit by av_buffer_unref(&pes->buffer) in the seek-reset\n\ncode at handle_packets.c, which goes to buffer_replace and gives\n\na double-free plus an atomic-decrement through the stale AVBuffer*.\n\nI tested this against current master:\n\n [mpegts @ ...] Invalid JPEG-XS header size 4294967295 > packet size 359\n\n ==2565539==ERROR: AddressSanitizer: heap-use-after-free on address\n\n0x509000000808\n\n READ of size 8 at 0x509000000808 thread T0\n\n #0 new_pes_packet libavformat/mpegts.c:1024:30\n\n #1 mpegts_read_packet libavformat/mpegts.c:3495:27\n\n ...\n\n #4 avformat_find_stream_info libavformat/demux.c:2800:15\n\n freed by thread T0 here:\n\n #3 av_packet_unref libavcodec/packet.c:436:5\n\n #4 mpegts_read_packet libavformat/mpegts.c:3489:9\n\nI think the fix is pretty clear, which is to do the same cleanup the\n\nsuccess path does before the early return. I've sent this as a PR that\n\ndoes this:\n\ndiff --git a/libavformat/mpegts.c b/libavformat/mpegts.c\n\nindex 0ee10f9a77..43b983660f 100644\n\n--- a/libavformat/mpegts.c\n\n+++ b/libavformat/mpegts.c\n\n@@ -1040,7 +1040,8 @@ static int new_pes_packet(PESContext *pes, AVPacket *pkt)\n\n av_log(pes->stream, AV_LOG_WARNING,\n\n \"Invalid JPEG-XS header size %\"PRIu32\" > packet size %d\\n\",\n\n header_size, pkt->size);\n\n- pes->flags |= AV_PKT_FLAG_CORRUPT;\n\n+ pes->buffer = NULL;\n\n+ reset_pes_packet_state(pes);\n\n return AVERROR_INVALIDDATA;\n\n }\n\n pkt->data += header_size;\n\n(The removal for pes->flags is because the call to\n\nreset_pes_packet_state clears the flags.)\n\nThanks,\n\nNicholas",
"title": "avformat/mpegts: reset PES state before JPEG-XS error return",
"vendor_severity": null
}