CVE-2026-13858
Overview
Files Changed
libavformat/mov.c
Patch
From a87f87d880452edb43738d90ae2948ba1c22581e Mon Sep 17 00:00:00 2001
From: Dale Curtis <dalecurtis@chromium.org>
Date: Tue, 05 May 2026 22:04:27 +0000
Subject: [PATCH] avformat/mov: Fix negative index given to can_seek_to_key_sample()
The potentially negative return value of av_index_search_timestamp()
wasn't being handled before passing it to can_seek_to_key_sample().
Found by Wongi Lee (@_qwerty_po) of Theori with Xint Code,
Jungwoo Lee (@physicube).
Signed-off-by: Dale Curtis <dalecurtis@chromium.org>
Bug: 507090179
Change-Id: I3037acb8e3d6cd5eedb7ef812569673d9be61879
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/third_party/ffmpeg/+/7819356
Reviewed-by: Thomas Guilbert <tguilbert@chromium.org>
---
diff --git a/libavformat/mov.c b/libavformat/mov.c
index 9050ad4..5530c53 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -11605,6 +11605,7 @@
if (sample >= sc->sample_offsets_count)
return 1;
+ av_assert0(sample >= 0);
key_sample_dts = sti->index_entries[sample].timestamp;
key_sample_pts = key_sample_dts + sc->sample_offsets[sample] + sc->dts_shift;
@@ -11648,6 +11649,8 @@
next_ts = timestamp - FFMAX(sc->min_sample_duration, 1);
requested_sample = av_index_search_timestamp(st, next_ts, flags);
+ if (requested_sample < 0)
+ return AVERROR_INVALIDDATA;
// If we've reached a different sample trying to find a good pts to
// seek to, give up searching because we'll end up seeking back to
Original Bug Report
Heap-buffer-overflow read in libavformat `mov_seek_stream` / `can_seek_to_key_sample` via a crafted HEVC MP4 and an HTMLMediaElement seek
Report description
Heap-buffer-overflow read in libavformat mov_seek_stream / can_seek_to_key_sample via a crafted HEVC MP4 and an HTMLMediaElement seek
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src
The problem
Please describe the technical details of the vulnerability
Summary
A crafted MP4 with one HEVC video track and one valid Opus audio track makes libavformat’s mov_seek_stream retry path call can_seek_to_key_sample(st, requested_sample, next_ts) with requested_sample = -1. The bounds guard at third_party/ffmpeg/libavformat/mov.c:11601 is a signed int compare (if (sample >= sc->sample_offsets_count)) and lets -1 through, after which line 11604 reads sti->index_entries[-1].timestamp — an 8-byte heap out-of-bounds READ 16 bytes before a 1536-byte index_entries buffer allocated by av_reallocp_array. Trigger is <video src=poc.mp4> plus a JS video.currentTime = 0 (or any value mapping to PTS 0 on the HEVC track); no user gesture, autoplay opt-in, or experimental feature flag is required for the web trigger.
Affected versions
- Crash reproduced and ASAN log captured on 149.0.7802.0 Linux ASAN. The Chromium
149.0.7802.0tag pinsthird_party/ffmpegto thechromium/third_party/ffmpegfork atb5e18fb9da84e26ceef30d4e4886696bf59337c0. - Source reviewed in a Chromium checkout from 2026-04-27 whose DEPS pin
third_party/ffmpegto64c21ea132c48271dd3ce497eeb008187eb7d3f1; the vulnerable hunk is still present there.third_party/ffmpegfile:lineanchors below refer to that pinned ffmpeg revision. Chromium-sideffmpeg_demuxer.ccreferences are by function name because line numbers may drift between Chromium checkouts. Both pins contain the prerequisite commit identified in the Bisect section.
Root cause
Three conditions compose:
-
can_seek_to_key_sampleuses a signed-intupper-bound compare.third_party/ffmpeg/libavformat/mov.c:11592-11616:static int can_seek_to_key_sample(AVStream *st, int sample, int64_t requested_pts) { MOVStreamContext *sc = st->priv_data; FFStream *const sti = ffstream(st); int64_t key_sample_dts, key_sample_pts; if (st->codecpar->codec_id != AV_CODEC_ID_HEVC) return 1; if (sample >= sc->sample_offsets_count) // sample is int, count is int — sample = -1 passes return 1; key_sample_dts = sti->index_entries[sample].timestamp; // line 11604: OOB read key_sample_pts = key_sample_dts + sc->sample_offsets[sample] + sc->dts_shift; if (is_open_key_sample(sc, sample) && key_sample_pts > requested_pts) return 0; return 1; }MOVStreamContext::sample_offsets_countisint(signed) atthird_party/ffmpeg/libavformat/isom.h:247, so the only guard rejectssample >= countbut neversample < 0. -
The retry path in
mov_seek_streamfeeds the result ofav_index_search_timestamp(..., flags)straight intocan_seek_to_key_sample.third_party/ffmpeg/libavformat/mov.c:11634-11655:for (;;) { sample = av_index_search_timestamp(st, timestamp, flags); if (sample < 0 && sti->nb_index_entries && timestamp < sti->index_entries[0].timestamp) sample = 0; // fix-up applies only to the first call if (sample < 0) return AVERROR_INVALIDDATA; if (!sample || can_seek_to_key_sample(st, sample, timestamp)) break; next_ts = timestamp - FFMAX(sc->min_sample_duration, 1); // crafted file: min_sample_duration = 0, so next_ts = timestamp - 1 requested_sample = av_index_search_timestamp(st, next_ts, flags);// returns -1 when no entry has ts <= next_ts if (sample != requested_sample && !can_seek_to_key_sample(st, requested_sample, next_ts)) break; // requested_sample = -1 reaches the function with no fix-up timestamp = next_ts; }The fix-up after the first
av_index_search_timestampis not mirrored onrequested_sample, so-1flows directly into condition (1). -
The crafted MP4 forces the first
can_seek_to_key_samplecall to return 0, so the retry path is taken. The firstav_index_search_timestampreturns a non-zero midpoint sample. For that sample,can_seek_to_key_sampleevaluatesis_open_key_sample(midpoint) && key_sample_pts > requested_pts; the crafted MP4 makes both true, so the function returns 0 and the loop falls into the retry path.make_poc_mp4.pybuilds a moov withsttscount=4 duration=0(somin_sample_duration = 0),cttsoffsets[100,200,300,400,...](sokey_sample_pts > 0for any midpoint),stssmarking every sample sync, ansgpdofgrouping_type=syncwhose first entry hasnal_unit_type = HEVC_NAL_CRA_NUT (21), and N separatesbgpentries each withcount=1, group_description_index=1. The split-sbgpshape is required becausebuild_open_gop_key_pointsatthird_party/ffmpeg/libavformat/mov.c:4631-4640uses a constantsample_idinside the inner loop and only ratchetssample_idbetween outersbgpentries:for (uint32_t i = 0; i < sc->sync_group_count; i++) { const MOVSbgp *sg = &sc->sync_group[i]; if (sg->index == cra_index) for (uint32_t j = 0; j < sg->count; j++) sc->open_key_samples[k++] = sample_id; // inner loop sees a fixed sample_id sample_id += sg->count; // ratchets only between outer entries }With one
sbgpentry ofcount=N,open_key_samplesbecomes[0,0,...,0]andis_open_key_sample(midpoint) = 0, so the firstcan_seek_to_key_samplereturns 1 and the retry path is never entered. With N per-samplesbgpentries ofcount=1,open_key_samples = [0,1,...,N-1],is_open_key_sample(midpoint) = 1, the first call returns 0, and the loop falls into the OOB second call.
The HEVC track does not have to be supported by the Chromium media stack. On Chromium-branded builds with ENABLE_PLATFORM_HEVC=false, FFmpegDemuxerStream::Create rejects the HEVC AVStream, but FFmpegDemuxer::OnFindStreamInfoDone only logs the unsupported video track and continues from the stream-construction loop. It does not set stream->discard = AVDISCARD_ALL for that HEVC AVStream. The HEVC stream therefore remains in format_context->streams[]. When a seek is then issued for the surviving Opus track, mov_read_seek walks every stream because seek_individually defaults to 1 (third_party/ffmpeg/libavformat/mov.c:11725-11742, default at :11773), so mov_seek_stream runs on the zombie HEVC stream and reaches the bug.
Reproduction
Extract the attached poc.html, poc.mp4, make_poc_mp4.py, and asan.log into an empty working directory and cd into it. Download the ASAN build into the same directory so that chrome and llvm-symbolizer sit next to the PoC files. All commands below are run from that directory; every path is relative.
-
Download the 149.0.7802.0 Linux ASAN build using
get_asan_chrome.py(which ships in the Chromium source tree attools/get_asan_chrome/get_asan_chrome.py):python3 get_asan_chrome.py --version 149.0.7802.0 --output-dir .Unzip the resulting archive and move
chrome,llvm-symbolizer, and the sibling runtime files into the working directory. -
Serve the PoC over a local HTTP origin (single command line):
python3 -m http.server 8000 -
In another shell, run the ASAN build against the PoC (single command line):
ASAN_OPTIONS=detect_leaks=0:symbolize=1:allocator_may_return_null=1:halt_on_error=1:external_symbolizer_path=./llvm-symbolizer ./chrome --no-sandbox --user-data-dir=/tmp/asan_profile --headless=new --no-first-run --disable-breakpad --disable-crash-reporter "http://127.0.0.1:8000/poc.html" 2> asan.log
make_poc_mp4.py is the deterministic builder for poc.mp4; running it regenerates the same 26 KB MP4 used to capture the attached asan.log. The attached asan.log was captured with the command above.
Crash evidence
From asan.log:
==343409==ERROR: AddressSanitizer: use-after-poison on address 0x7449f7300d70 at pc 0x59cbcbeeadcc bp 0x7299ee5992a0 sp 0x7299ee599298
READ of size 8 at 0x7449f7300d70 thread T4 (ThreadPoolForeg)
#0 0x59cbcbeeadcb in mov_seek_stream third_party/ffmpeg/libavformat/mov.c:11604:49
Allocation context (the buffer the read is past):
0x7449f7300d70 is located 16 bytes before 1536-byte region [0x7449f7300d80,0x7449f7301380)
allocated by thread T4 (ThreadPoolForeg) here:
#0 realloc
#1 av_reallocp_array third_party/ffmpeg/libavutil/mem.c:165:11
ASAN labels the report use-after-poison because the byte before the user region is poisoned by PartitionAlloc (Chromium’s heap allocator) via __asan_poison_memory_region rather than ASAN’s own malloc-shim left-redzone. The semantic is identical to heap-buffer-overflow — a read past the start of an allocation. The crashing process is the renderer, confirmed by the command line in the log’s ADDITIONAL INFO block (/proc/self/exe --type=renderer ...). The task trace shows the crash on the media demuxer seek path: media::FFmpegDemuxer::SeekInternal posts AVSeekFrame to a blocking task runner, and the OOB read fires on that runner’s ThreadPoolForeg thread.
Bisect
The bug landed when the retry path in mov_seek_stream was added; before that commit, the loop just decremented timestamp without making a second can_seek_to_key_sample call, so no caller ever passed a negative sample.
- Commit:
d1b96c380826c505a8c7e655b5ad4fdb0c2de167(FFmpeg upstream; carried into Chromium’schromium/third_party/ffmpegfork) - Subject:
avformat/mov: avoid seeking back to 0 on HEVC open GOP files - Date: 2024-05-21
- Upstream URL: https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/d1b96c380826c505a8c7e655b5ad4fdb0c2de167
The exact hunk that introduces the unguarded second call:
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -10133,7 +10133,7 @@ static int mov_seek_stream(AVFormatContext *s, AVStream *st, int64_t timestamp,
{
MOVStreamContext *sc = st->priv_data;
FFStream *const sti = ffstream(st);
- int sample, time_sample, ret;
+ int sample, time_sample, ret, next_ts, requested_sample;
unsigned int i;
@@ -10154,7 +10154,17 @@ static int mov_seek_stream(AVFormatContext *s, AVStream *st, int64_t timestamp,
if (!sample || can_seek_to_key_sample(st, sample, timestamp))
break;
- timestamp -= FFMAX(sc->min_sample_duration, 1);
+ next_ts = timestamp - FFMAX(sc->min_sample_duration, 1);
+ requested_sample = av_index_search_timestamp(st, next_ts, flags);
+ if (!can_seek_to_key_sample(st, requested_sample, next_ts) && sample != requested_sample)
+ break;
+ timestamp = next_ts;
}
The earlier prerequisite that introduced can_seek_to_key_sample itself is FFmpeg ab77b878f1 (https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/ab77b878f1, “avformat/mov: fix seeking with HEVC open GOP files”, 2022-05-19). The first chromium fork merge that pulled d1b96c3808 into chromium/third_party/ffmpeg is d4258358c7672ae3ccaac0ffb16c5169bbff273c (https://chromium.googlesource.com/chromium/third_party/ffmpeg/+/d4258358c7672ae3ccaac0ffb16c5169bbff273c, 2024-09-20). Both the 149.0.7802.0 pin (b5e18fb9da...) and the current pin (64c21ea132...) include all of these prerequisites.
Suggested patch
Tighten can_seek_to_key_sample’s bounds check to reject negative sample in addition to over-large values:
--- a/third_party/ffmpeg/libavformat/mov.c
+++ b/third_party/ffmpeg/libavformat/mov.c
@@ -11598,7 +11598,8 @@ static int can_seek_to_key_sample(AVStream *st, int sample, int64_t requested_pt
if (st->codecpar->codec_id != AV_CODEC_ID_HEVC)
return 1;
- if (sample >= sc->sample_offsets_count)
+ if (sample < 0 || sample >= sti->nb_index_entries ||
+ sample >= sc->sample_offsets_count)
return 1;
key_sample_dts = sti->index_entries[sample].timestamp;
Rationale: sample is int, and the function reads sti->index_entries[sample] and sc->sample_offsets[sample] immediately after. av_index_search_timestamp is documented to return a negative number when no matching entry exists; the existing fix-up at mov_seek_stream:11637 only catches the first call’s negative result. Rejecting sample < 0 and bounding against both nb_index_entries and sample_offsets_count at the function boundary closes the OOB read where it happens and matches the two array accesses on the next two lines.
Impact analysis
ASAN reports an out-of-bounds heap READ of size 8, landing 16 bytes before the start of a 1536-byte index_entries allocation, inside the renderer process (sandboxed). The trigger is a <video src=...> element pointing at the crafted MP4 plus a single video.currentTime seek.
The cause
What version of Chrome have you found the security issue in?
149.0.7802.0 dev
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption (in a sandboxed process)
How would you like to be publicly acknowledged for your report?
Wongi Lee (@_qwerty_po) of Theori with Xint Code, Jungwoo Lee (@physicube).
- http://127.0.0.1:8000/poc.html
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://chromium.googlesource.com/chromium/src
- https://chromium.googlesource.com/chromium/third_party/ffmpeg/+/d4258358c7672ae3ccaac0ffb16c5169bbff273c
- https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/ab77b878f1
- https://git.ffmpeg.org/gitweb/ffmpeg.git/commit/d1b96c380826c505a8c7e655b5ad4fdb0c2de167