CVE-2026-5902
Overview
Background
- NDK Video Encode Accelerator
- Chrome’s Android hardware video encoder wrapper (
NdkVideoEncodeAccelerator) that drives the platform NDK media encoder and produces encoded bitstream buffers. - Temporal scalability (SVC) parsing
- Post-encode analysis of the output bitstream to extract temporal-layer metadata, performed here by
svc_parser_->ParseChunk. - Shared-memory output buffer
- The destination (
output_dst) backed by memory shared with the (less-trusted) renderer, into which encoded data is copied for delivery. - Double fetch / TOCTOU
- Reading the same data from shared memory twice (once to copy out, once to re-parse) so that a concurrent writer can change it between the two reads.
Root Cause Analysis
The output-handling path copied the encoder’s bitstream into the shared-memory destination output_dst and then re-read that same shared-memory region to parse temporal-scalability metadata, calling svc_parser_->ParseChunk(output_dst.first(mc_buffer_size), ...). Because output_dst is backed by memory the renderer can also map, a compromised renderer could modify the bytes after the copy but before (or during) the metadata parse, so the browser/GPU-process parser operated on data different from what was encoded — a classic time-of-check/time-of-use double-fetch on shared memory.
The fix changes the parse to read from out_buffer_data, a process-local buffer holding the encoder’s own copy of the data, instead of the renderer-writable output_dst. With the parser consuming trusted local memory, the renderer can no longer influence the metadata parse after the fact, closing the race window described in the commit message.
ParseChunk at the process-local out_buffer_data buffer.Attack Path
- Drive hardware encoding with SVC
A renderer uses a media/WebCodecs encode path that reaches the Android NDK encoder with temporal scalability enabled, so each output buffer is parsed by
svc_parser_->ParseChunk. - Compromised renderer maps the output shared memory
An already-compromised renderer retains write access to the shared-memory region backing
output_dst. - Race the parse
After the browser/GPU process copies the bitstream into
output_dstbut before it parses the metadata, the renderer rewrites the shared bytes, so the parser consumes attacker-controlled, inconsistent data (the race window). - Downstream effect (inferred)
What the mismatched/malformed parsed metadata leads to (memory corruption or further misbehavior in the parser/consumer) is not shown in this diff and is
(inferred).
Impact Assessment
(inferred) because the patch only shows the change of parse source.Files Changed
media/gpu/android/ndk_video_encode_accelerator.cc
Audit Directions
- Parse-from-shared-memoryAudit GPU/browser-process code that both copies data into renderer-shared buffers and then re-reads those same buffers for validation or metadata parsing; parse from a local copy instead.
- Double fetch on IPC/shared buffersSearch for patterns where a size or structure is read once and the payload read again from the same untrusted mapping, especially in media/codec accelerators where large shared buffers are common.
- Hardware-encoder metadata pathsReview other VEA backends (V4L2, Media Foundation, VideoToolbox) for SVC/temporal-layer or header parsers that consume the shared output region rather than an internal buffer.
Patch
From cf230cf0cda34477afd4c14096ffc6692fa79994 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Wed, 11 Feb 2026 14:34:45 -0800
Subject: [PATCH] media: Fix race condition in NDK VEA's temporal scalability parsing
Previously, the encoder copied bitstream data to shared memory and then
read it back for metadata parsing. This created a race window where a
compromised renderer could modify the shared memory before the browser
parsed it.
Bug: 483109205
Change-Id: Ic6aec49b840c03fed1b01e2f9c2e553e44eec158
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7567414
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1583529}
---
diff --git a/media/gpu/android/ndk_video_encode_accelerator.cc b/media/gpu/android/ndk_video_encode_accelerator.cc
index 30bd5ca..1287329 100644
--- a/media/gpu/android/ndk_video_encode_accelerator.cc
+++ b/media/gpu/android/ndk_video_encode_accelerator.cc
@@ -1484,8 +1484,8 @@
}
TemporalScalabilityIdExtractor::BitstreamMetadata bits_md;
- if (!svc_parser_->ParseChunk(output_dst.first(mc_buffer_size),
- input_since_keyframe_count_, bits_md)) {
+ if (!svc_parser_->ParseChunk(out_buffer_data, input_since_keyframe_count_,
+ bits_md)) {
NotifyErrorStatus({EncoderStatus::Codes::kEncoderHardwareDriverError,
"Parse bitstream failed"});
return;
Original Bug Report
TOCTOU in NdkVideoEncodeAccelerator: shared memory re-read after write allows attacker-controlled bitstream parsing
VULNERABILITY DETAILS
The Android NDK Video Encode Accelerator (NdkVideoEncodeAccelerator) writes encoded bitstream data into an UnsafeSharedMemoryRegion, then re-reads from that same shared memory to parse temporal scalability metadata. The renderer retains write access to this region. A compromised renderer can mutate the buffer contents between the encoder’s write and the metadata parse, feeding attacker-controlled bitstream data to H.264, HEVC, VP9, and AV1 parsers running in the GPU process.
The vulnerable code is in media/gpu/android/ndk_video_encode_accelerator.cc:
- Line 1460: Encoder output copied into shared memory via output_dst.copy_prefix_from
- Line 1487: svc_parser_->ParseChunk() re-reads from the same shared memory span
Between these two lines, the renderer can overwrite the buffer with crafted NALUs/OBUs. The parsers (H264Parser, H265NaluParser, Vp9Parser, libgav1 ObuParser) were not designed to handle adversarial input on this code path. They expect trusted encoder output.
Other platform encoders are not affected. VAAPI derives metadata from encoder-internal state (vaapi_video_encode_accelerator.cc:524-556). V4L2 parses from its kernel mmap’d capture buffer before copying to shared memory (v4l2_video_encode_accelerator.cc:1467-1485). MediaFoundation parses from its MF COM buffer (media_foundation_video_encode_accelerator_win.cc:2398-2413). Only the NDK path parses from the shared memory span after writing to it.
The fix is to move the ParseChunk() call to operate on the MediaCodec output buffer (out_buffer_data) before copying to shared memory, matching the pattern used by V4L2 and MediaFoundation.
VERSION
Chrome Version: trunk (verified against current main branch) Operating System: Android (all versions using NDK MediaCodec encoder)
REPRODUCTION CASE
A standalone PoC demonstrates the race condition by simulating the GPU and renderer threads operating on the same UnsafeSharedMemoryRegion. Results: 1.9 million race wins out of 62 million iterations (3.06% hit rate) in 2 seconds.
The PoC is attached. It can also be built as a Chromium unit test.
The race window exists between ndk_video_encode_accelerator.cc lines 1460 and 1487. In real exploitation, the attacker substitutes arbitrary H.264/VP9/AV1 bitstream data, feeding crafted input to TemporalScalabilityIdExtractor::ParseChunk() (media/parsers/temporal_scalability_id_extractor.cc:56), which dispatches to:
- ParseH264 (line 77): H264Parser::AdvanceToNextNALU on attacker data
- ParseHEVC (line 108): H265NaluParser::AdvanceToNextNALU on attacker data
- ParseVP9 (line 132): Vp9Parser::ParseNextFrame on attacker data, reads ref_frame_idx[] used to index into vp9_ref_buffer_[kVp9NumRefFrames=8]
- ParseAV1 (line 175): libgav1::ObuParser::ParseOneFrame on attacker data, complex parser touching DecoderState and BufferPool
The VP9 and AV1 paths maintain persistent reference frame state across frames, so corrupted state cascades to subsequent frames.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: GPU process. Attacker-controlled data reaches bitstream parsers. Impact depends on parser robustness to adversarial input. Potential outcomes include wrong metadata propagation, parser state corruption, and possible out-of-bounds reads in complex parsers (especially libgav1).
CREDIT INFORMATION
Reporter credit: Luke Francis