CVE-2026-16395
Overview
Background
- `MediaFormatReader`
- Gecko’s format-agnostic media reader that demuxes and dispatches compressed samples to platform decoders.
- `CryptoSample`
- Per-sample encryption metadata carrying parallel
mPlainSizesandmEncryptedSizessubsample arrays describing the clear/encrypted byte layout of an encrypted sample. - `CheckedInt<size_t>`
- Mozilla’s overflow-detecting integer wrapper whose
isValid()reports whether any arithmetic operation overflowed. - Subsample
- A
(clear, encrypted)byte-length pair used by CENC/CBCS encryption to split a single media sample into alternating plaintext and ciphertext regions.
Root Cause Analysis
The vulnerable path is MediaFormatReader::DecodeDemuxedSamples, which forwarded an encrypted sample’s subsample layout (crypto.mPlainSizes / crypto.mEncryptedSizes) toward the decoder without validating it against the actual sample buffer. The code assumed two invariants that were never enforced: that the plain and encrypted subsample arrays have equal length, and that the sum of all subsample sizes exactly equals aSample->Size(). Because these sizes come from attacker-influenced container/pssh metadata, a crafted stream could declare subsample totals that overflow when summed or that describe more bytes than the buffer holds, letting downstream code read past the sample’s real allocation.
The fix adds an explicit length-equality check plus a CheckedInt<size_t> accumulation of every plain and encrypted size, rejecting the sample with NS_ERROR_DOM_MEDIA_DECODE_ERR if the addition overflows (!isValid()) or if the total does not match aSample->Size(). This closes the gap by establishing the size invariant at the reader boundary before any decoder consumes the subsample map.
aSample->Size() comparison before dispatch.Attack Path
- Craft encrypted media
Author a media file whose encrypted samples carry malicious
CryptoSamplesubsample metadata with mismatched array lengths or subsample sizes that overflow when summed. - Deliver to victim Serve the file to a page that plays it via the standard media pipeline (e.g. EME/MSE playback).
- Reach the reader
DecodeDemuxedSamplesaccepts the demuxed encrypted sample and, pre-fix, forwards the unvalidated subsample layout onward. - Trigger overflow
The summed subsample lengths overflow
size_tor exceed the real buffer size, so downstream code operates on a length larger than the backing allocation. - Out-of-bounds access The mismatched size drives reads beyond the sample buffer during crypto/decoder handoff.
Impact Assessment
MediaFormatReader (the content/media decoding context). The precondition is media playback of attacker-supplied encrypted content; Mozilla rates the impact moderate/medium with no assigned CVSS in the metadata.Files Changed
dom/media/MediaFormatReader.cpp
Audit Directions
- Unvalidated parallel arraysFlag any use of paired metadata arrays (like
mPlainSizes/mEncryptedSizes) where lengths are assumed equal without an explicit check. - Unchecked size summationLook for loops accumulating attacker-influenced sizes into a plain integer; require
CheckedIntor equivalent overflow detection before the total is used as a length or offset. - Missing buffer-total invariantAudit media/crypto handoff paths where declared subsample or fragment totals are trusted without comparing against the real
Size()of the underlying buffer.
Patch
diff --git a/dom/media/MediaFormatReader.cpp b/dom/media/MediaFormatReader.cpp
index 02a8ddd0c19..533f6fcf063 100644
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -21,6 +21,7 @@
#include "VideoUtils.h"
#include "mozilla/AbstractThread.h"
#include "mozilla/CDMProxy.h"
+#include "mozilla/CheckedInt.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/NotNull.h"
#include "mozilla/Preferences.h"
@@ -2100,6 +2101,30 @@ void MediaFormatReader::DecodeDemuxedSamples(TrackType aTrack,
aSample->mEOS ? " eos" : "");
decoder.StartRecordDecodingPerf(aTrack, aSample);
+
+ const CryptoSample& crypto = aSample->mCrypto;
+ if (crypto.IsEncrypted() && !crypto.mPlainSizes.IsEmpty()) {
+ if (crypto.mPlainSizes.Length() != crypto.mEncryptedSizes.Length()) {
+ NotifyError(aTrack,
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ "Mismatched crypto subsample array lengths"));
+ return;
+ }
+ CheckedInt<size_t> subsampleTotal = 0;
+ for (size_t i = 0; i < crypto.mPlainSizes.Length(); i++) {
+ subsampleTotal += crypto.mPlainSizes[i];
+ subsampleTotal += crypto.mEncryptedSizes[i];
+ }
+ if (!subsampleTotal.isValid() ||
+ subsampleTotal.value() != aSample->Size()) {
+ NotifyError(
+ aTrack,
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ "Crypto subsample sizes don't match sample size"));
+ return;
+ }
+ }
+
if (aSample->mCrypto.IsEncrypted() &&
(mMediaEngineId || (mCDMProxy && !!mCDMProxy->AsRemoteCDMProxy()))) {
aSample->mShouldCopyCryptoToRemoteRawData = true;