Critical firefox Memory Corruption 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactcritical
DescriptionMemory safety bugs present in Firefox ESR 115.35.0, Firefox ESR 140.10.0 and Firefox 150.0.0. Some of these bugs showed evidence of memory corruption and we presume that with enough effort some of these could have been exploited to run arbitrary code.
ComponentCore
Bug ClassMemory Corruption
Tracker2021904
Fix commit9968df8fbe02 (firefox) +61/-6
CISA KEVNot listed
CreditedC.M.Chang, Christian Holler, Steve Fink and the Mozilla Fuzzing Team
Disclosed2026-04-28

Files Changed

  • xpcom/io/SlicedInputStream.cpp
  • xpcom/io/SlicedInputStream.h
  • xpcom/io/nsICloneableInputStream.idl
  • xpcom/tests/gtest/TestSlicedInputStream.cpp
diff --git a/xpcom/io/SlicedInputStream.cpp b/xpcom/io/SlicedInputStream.cpp
index 5a338e45d08..11da3a1395b 100644
--- a/xpcom/io/SlicedInputStream.cpp
+++ b/xpcom/io/SlicedInputStream.cpp
@@ -41,6 +41,15 @@ NS_INTERFACE_MAP_BEGIN(SlicedInputStream)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
 NS_INTERFACE_MAP_END
 
+// It is highly unlikely for a stream to exceed INT64_MAX bytes in length, so we
+// clamp these values here to allow callers as well as SlicedInputStream to
+// avoid unnecessary integer overflow checks. Out of range start/length values
+// should behave as expected in all realistic situations.
+//
+// Some stream APIs use an int64_t (e.g. Tell), so we use INT64_MAX as the
+// maximum internal offset for start/end.
+static constexpr uint64_t kMaxStreamPos = INT64_MAX;
+
 SlicedInputStream::SlicedInputStream(
     already_AddRefed<nsIInputStream> aInputStream, uint64_t aStart,
     uint64_t aLength)
@@ -51,8 +60,8 @@ SlicedInputStream::SlicedInputStream(
       mWeakAsyncInputStream(nullptr),
       mWeakInputStreamLength(nullptr),
       mWeakAsyncInputStreamLength(nullptr),
-      mStart(aStart),
-      mLength(aLength),
+      mStart(std::clamp<uint64_t>(aStart, 0, kMaxStreamPos)),
+      mLength(std::clamp<uint64_t>(aLength, 0, kMaxStreamPos - mStart)),
       mCurPos(0),
       mClosed(false),
       mAsyncWaitFlags(0),
@@ -489,11 +498,11 @@ bool SlicedInputStream::Deserialize(
 
   const SlicedInputStreamParams& params = aParams.get_SlicedInputStreamParams();
 
-  auto end = CheckedUint64(params.start()) + params.length();
-  if (!end.isValid()) {
+  if (params.start() > kMaxStreamPos ||
+      params.length() > kMaxStreamPos - params.start()) {
     return false;
   }
-  if (params.curPos() > end.value()) {
+  if (params.curPos() > params.start() + params.length()) {
     return false;
   }
 
diff --git a/xpcom/io/SlicedInputStream.h b/xpcom/io/SlicedInputStream.h
index c7a28d5ff3e..a266bbf5f7b 100644
--- a/xpcom/io/SlicedInputStream.h
+++ b/xpcom/io/SlicedInputStream.h
@@ -44,7 +44,11 @@ class SlicedInputStream final : public nsIAsyncInputStream,
   // than aStart bytes, reading from SlicedInputStream returns no data.  If
   // aInputStream contains more than aStart bytes, but fewer than aStart +
   // aLength bytes, reading from SlicedInputStream returns as many bytes as can
-  // be consumed from aInputStream after reading aLength bytes.
+  // be consumed from aInputStream after reading aStart bytes.
+  //
+  // It is safe to specify an arbitrarily large aLength (e.g. UINT64_MAX). Doing
+  // so is treated as allowing an arbitrary number of additional bytes following
+  // aStart.
   //
   // aInputStream should not be read from after constructing a
   // SlicedInputStream wrapper around it.
diff --git a/xpcom/io/nsICloneableInputStream.idl b/xpcom/io/nsICloneableInputStream.idl
index adefd0f4285..98790bd2d97 100644
--- a/xpcom/io/nsICloneableInputStream.idl
+++ b/xpcom/io/nsICloneableInputStream.idl
@@ -27,5 +27,12 @@ interface nsICloneableInputStream : nsISupports
 [scriptable, builtinclass, uuid(ece853c3-aded-4cef-8f51-0d1493d60bd5)]
 interface nsICloneableInputStreamWithRange : nsICloneableInputStream
 {
+  // Create a copy of the input stream, but with the data range reduced to a
+  // sub-slice. The copy will begin at `start` bytes, and extends for a maximum
+  // of `length` bytes. If the underlying stream contains more than `start`
+  // bytes, but fewer than `length` bytes, reading from the stream returns all
+  // remaining bytes.
+  //
+  // See SlicedInputStream's constructor for more details.
   nsIInputStream cloneWithRange(in uint64_t start, in uint64_t length);
 };
diff --git a/xpcom/tests/gtest/TestSlicedInputStream.cpp b/xpcom/tests/gtest/TestSlicedInputStream.cpp
index a2c1a077e48..ea9f606da09 100644
--- a/xpcom/tests/gtest/TestSlicedInputStream.cpp
+++ b/xpcom/tests/gtest/TestSlicedInputStream.cpp
@@ -307,6 +307,41 @@ TEST(TestSlicedInputStream, LengthBiggerThan)
   ASSERT_EQ((uint64_t)500, count);
 }
 
+// Like LengthBiggerThan, but for an overflowing aStart + aLength pair.
+TEST(TestSlicedInputStream, LengthMuchBiggerThan)
+{
+  nsCString buf;
+  RefPtr<SlicedInputStream> sis =
+      CreateNonSeekableStreams(500, 100, UINT64_MAX - 1, buf);
+
+  uint64_t length;
+  ASSERT_EQ(NS_OK, sis->Available(&length));
+  ASSERT_EQ((uint64_t)500 - 100, length);
+
+  char buf2[4096];
+  uint32_t count;
+  ASSERT_EQ(NS_OK, sis->Read(buf2, sizeof(buf2), &count));
+  ASSERT_EQ((uint64_t)(500 - 100), count);
+  ASSERT_EQ(Substring(buf, 100, count), Substring(buf2, count));
+}
+
+// Like LengthMuchBiggerThan, but with a massive aStart value.
+TEST(TestSlicedInputStream, StartMuchBiggerThan)
+{
+  nsCString buf;
+  RefPtr<SlicedInputStream> sis =
+      CreateNonSeekableStreams(500, UINT64_MAX - 1, 100, buf);
+
+  uint64_t length;
+  ASSERT_EQ(NS_OK, sis->Available(&length));
+  ASSERT_EQ((uint64_t)0, length);
+
+  char buf2[4096];
+  uint32_t count;
+  ASSERT_EQ(NS_OK, sis->Read(buf2, sizeof(buf2), &count));
+  ASSERT_EQ((uint64_t)0, count);
+}
+
 // What if the length is 0?
 TEST(TestSlicedInputStream, Length0)
 {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/xpcom/tests/gtest/TestSlicedInputStream.cpp b/xpcom/tests/gtest/TestSlicedInputStream.cpp
index a2c1a077e48..ea9f606da09 100644
--- a/xpcom/tests/gtest/TestSlicedInputStream.cpp
+++ b/xpcom/tests/gtest/TestSlicedInputStream.cpp
@@ -307,6 +307,41 @@ TEST(TestSlicedInputStream, LengthBiggerThan)
   ASSERT_EQ((uint64_t)500, count);
 }
 
+// Like LengthBiggerThan, but for an overflowing aStart + aLength pair.
+TEST(TestSlicedInputStream, LengthMuchBiggerThan)
+{
+  nsCString buf;
+  RefPtr<SlicedInputStream> sis =
+      CreateNonSeekableStreams(500, 100, UINT64_MAX - 1, buf);
+
+  uint64_t length;
+  ASSERT_EQ(NS_OK, sis->Available(&length));
+  ASSERT_EQ((uint64_t)500 - 100, length);
+
+  char buf2[4096];
+  uint32_t count;
+  ASSERT_EQ(NS_OK, sis->Read(buf2, sizeof(buf2), &count));
+  ASSERT_EQ((uint64_t)(500 - 100), count);
+  ASSERT_EQ(Substring(buf, 100, count), Substring(buf2, count));
+}
+
+// Like LengthMuchBiggerThan, but with a massive aStart value.
+TEST(TestSlicedInputStream, StartMuchBiggerThan)
+{
+  nsCString buf;
+  RefPtr<SlicedInputStream> sis =
+      CreateNonSeekableStreams(500, UINT64_MAX - 1, 100, buf);
+
+  uint64_t length;
+  ASSERT_EQ(NS_OK, sis->Available(&length));
+  ASSERT_EQ((uint64_t)0, length);
+
+  char buf2[4096];
+  uint32_t count;
+  ASSERT_EQ(NS_OK, sis->Read(buf2, sizeof(buf2), &count));
+  ASSERT_EQ((uint64_t)0, count);
+}
+
 // What if the length is 0?
 TEST(TestSlicedInputStream, Length0)
 {
Loading diff…