ANT-2026-CEP8Y9M0 · ffmpeg/ffmpeg

use-after-free high

Severity Claude high · Security research firm - · Maintainer -

REPORT

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

  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

[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

UPSTREAM FIX

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

TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-12 Sent to maintainer
  2. 2026-03-12 Maintainer acknowledged
  3. 2026-03-15 Patch released
  4. 2026-05-14 Reported to tracker
  5. 2026-08-17 Publicly revealed
PROVENANCE

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
}