Closed Bug 2016358 (CVE-2026-2778) Opened 5 months ago Closed 5 months ago

StructuredCloneBlob OOB Heap Read via Crafted blobCount

Categories

(Core :: DOM: Core & HTML, defect)

defect

Tracking

()

RESOLVED FIXED
149 Branch
Tracking Status
firefox-esr115 148+ fixed
firefox-esr140 148+ fixed
firefox147 --- wontfix
firefox148 + fixed
firefox149 + fixed

People

(Reporter: prodigysml555, Assigned: mccr8)

References

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main148+] [adv-esr115.33+] [adv-esr140.8+])

Attachments

(4 files)

Compromised content process -> parent process via ContentParent::RecvAsyncMessage.

StructuredCloneBlob::Holder::ReadStructuredCloneInternal (StructuredCloneBlob.cpp:176) reads blobOffset and blobCount from the attacker-controlled StructuredClone buffer, then calls BlobImpls().AppendElements(&aHolder->BlobImpls()[blobOffset], blobCount). The AppendElements(const T*, size_type) overload takes a raw pointer and copies blobCount elements via std::uninitialized_copy with no source bounds checking. No release-build validation exists; the #ifdef FUZZING guard only checks blobOffset, not blobOffset + blobCount.

SCTAG_DOM_STRUCTURED_CLONE_HOLDER (StructuredCloneHolder.cpp:1121) has no CloneScope() == SameProcess guard, unlike SCTAG_DOM_IMAGEBITMAP at line 1105. So it's reachable from cross-process (IPC) scope.

The Bug

StructuredCloneBlob.cpp:165-177:

uint32_t blobOffset;
uint32_t blobCount;
if (!JS_ReadUint32Pair(aReader, &blobOffset, &blobCount)) {
    return false;
}
if (blobCount) {
#ifdef FUZZING
    if (blobOffset >= aHolder->BlobImpls().Length()) {  // incomplete even when present
        return false;
    }
#endif
    BlobImpls().AppendElements(&aHolder->BlobImpls()[blobOffset], blobCount);  // OOB
}

With 1 blob in the IPC message and blobCount=100: AppendElements reads 99 elements past the 1-element array. Each 8-byte OOB value is treated as a RefPtr<BlobImpl>, and the copy constructor calls AddRef() on non-null values, which dereferences garbage heap pointers.

ASAN Proof

SUMMARY: AddressSanitizer: heap-buffer-overflow (XUL:arm64+0x1b3128)
  in nsTArray_Impl::AppendElementsInternal

0x6020002278e0 is located 0 bytes after 16-byte region
  [0x6020002278d0,0x6020002278e0)

PoC

dom/base/test/gtest/TestStructuredCloneBlobOOB.cpp:

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "gtest/gtest.h"

#include "nsTArray.h"
#include "nsISupportsImpl.h"
#include "mozilla/RefPtr.h"

namespace {

class SimpleRefCounted {
 public:
  NS_INLINE_DECL_REFCOUNTING(SimpleRefCounted)
  uint64_t mData = 0;

 private:
  ~SimpleRefCounted() = default;
};

}  // namespace

TEST(StructuredCloneBlobOOB, AppendElementsOOBRead)
{
  nsTArray<uint64_t> sourceArray;
  sourceArray.AppendElement(0xDEADBEEFCAFEBABEULL);
  ASSERT_EQ(sourceArray.Length(), 1u);

  uint32_t blobOffset = 0;
  uint32_t blobCount = 16;  // only 1 element, reading 16

  ASSERT_LT(blobOffset, sourceArray.Length());

  // ASAN: heap-buffer-overflow READ
  nsTArray<uint64_t> destArray;
  destArray.AppendElements(&sourceArray[blobOffset], blobCount);

  ASSERT_EQ(destArray[0], 0xDEADBEEFCAFEBABEULL);
  ASSERT_EQ(destArray.Length(), (uint32_t)blobCount);
}

TEST(StructuredCloneBlobOOB, DISABLED_RefPtrAddRefOnGarbage)
{
  nsTArray<RefPtr<SimpleRefCounted>> sourceArray;
  sourceArray.AppendElement(MakeRefPtr<SimpleRefCounted>());
  ASSERT_EQ(sourceArray.Length(), 1u);

  uint32_t blobOffset = 0;
  uint32_t blobCount = 4;

  // ASAN: heap-buffer-overflow READ (before the AddRef crash)
  nsTArray<RefPtr<SimpleRefCounted>> destArray;
  destArray.AppendElements(&sourceArray[blobOffset], blobCount);
}

TEST(StructuredCloneBlobOOB, ExactCountNoOverflow)
{
  nsTArray<uint64_t> sourceArray;
  sourceArray.AppendElement(0x1111111111111111ULL);
  sourceArray.AppendElement(0x2222222222222222ULL);
  sourceArray.AppendElement(0x3333333333333333ULL);

  uint32_t blobOffset = 1;
  uint32_t blobCount = 2;

  nsTArray<uint64_t> destArray;
  destArray.AppendElements(&sourceArray[blobOffset], blobCount);

  ASSERT_EQ(destArray.Length(), 2u);
  ASSERT_EQ(destArray[0], 0x2222222222222222ULL);
  ASSERT_EQ(destArray[1], 0x3333333333333333ULL);
}

Run: ./mach gtest "StructuredCloneBlobOOB.AppendElementsOOBRead"

Fix

Add bounds validation:

if (blobCount) {
    if (blobOffset >= aHolder->BlobImpls().Length() ||
        blobCount > aHolder->BlobImpls().Length() - blobOffset) {
        return false;
    }
    BlobImpls().AppendElements(&aHolder->BlobImpls()[blobOffset], blobCount);
}

Also add a CloneScope() == SameProcess guard for SCTAG_DOM_STRUCTURED_CLONE_HOLDER in StructuredCloneHolder.cpp:1121.

Flags: sec-bounty?
Keywords: ai-involved

How is this an out of bounds read? AppendElements adds new elements, it doesn't read anything.

Also aHolder->BlobImpls() returns an nsTArray, so indexing at blobOffset will crash safely and not go out-of-bounds.

Group: firefox-core-security → core-security
Component: Security → DOM: Core & HTML
Flags: needinfo?(prodigysml555)
Product: Firefox → Core

Well, I can reproduce an ASan crash locally so I'll take a look some more at it.

Flags: needinfo?(prodigysml555)

I talked over this with Nika and we both this this is a valid issue. Sorry for my initial skepticism, but this seemed like such an obvious problem, and we clearly have at least a bit of fuzzing coverage here due to the FUZZING but clearly we have some work to do there.

Status: UNCONFIRMED → NEW
Ever confirmed: true
Group: core-security → dom-core-security
Keywords: sec-high
Severity: -- → S2
Assignee: nobody → continuation
Depends on: 2016498

I looked up other places that do BlobImpls()[ and the others are just looking at a single element and not doing this weird raw pointer span thing.

Attached file (secure)

Comment on attachment 9544722 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Probably pretty obvious, although maybe anybody who has been paying attention could have already found this.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: all
  • If not all supported branches, which bug introduced the flaw?: None
  • Do you have backports for the affected branches?: No
  • If not, how different, hard to create, and risky will they be?: Shouldn't be a problem, as this code hasn't changed much. Note that this patch requires bug 2016498, or it'll just end up as another sec bug.
  • How likely is this patch to cause regressions; how much testing does it need?: This should only cause problems in the cases where we're out of bounds anyways.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9544722 - Flags: sec-approval?

Comment on attachment 9544722 [details]
(secure)

sec-approval+

Attachment #9544722 - Flags: sec-approval? → sec-approval+

If tomorrow is really the last day for beta, then maybe I should wait for the next release cycle, come to think of it, as I really want to make sure bug 2016498 has stuck and can be uplifted before landing this.

I realized I didn't post an actual explanation of what is going on here, which I should do because it isn't obvious and it took me a bit of staring to figure out.

The problematic code is BlobImpls().AppendElements(&aHolder->BlobImpls()[blobOffset], blobCount). This involves 2 nsTArrays, but the type of the elements doesn't matter so let's pretend they are nsTArray<T>.
I'll make some definitions so the code is shorter

  • array1 is aHolder->BlobImpls() and array2 is BlobImpls()
  • x is blobOffset and y is blobCount.

The original code is then array2.AppendElements(&array1[x], y). Thing go awry with the &. Breaking it down from the inside out:

  1. array1[x] is a reference to the xth element of the nsTArray array1, with type T&.
  2. &array1[x] is a pointer to the xth element of the nsTArray array1, with type T*.

The overload of AppendElements we are calling takes two arguments: a C-style array pointer, and the length of that array (in this case y). It then copies all y of those elements of the array onto the nsTArray (in this case array2).

The issue is that we have done nothing to ensure that the length of array2 is at least x + y. array1[x] is safe because array1 is an nsTArray and we do runtime bounds checking, but by the time we're dealing with the x + y we've dropped the nsTArray and only have a raw pointer.

x, y, and array1 are all controlled by the sender of the message, so it is trivial for the sender to make x + y larger than the length of array1. If the attacker manages to do heap grooming and control memory located after array1, then it can put whatever it wants into array2, which will be used later by the parent process.

What my patch does is change the above code to array2.AppendElements(Span(array1).Subspan(x, y)). A mozilla::Span is a C-style array plus a length, and access is bounds checked.

  1. Span(array1) creates a span out of the entire nsTArray array1, which knows its length.
  2. Span(array1).Subspan(x, y) then creates a subspan of length y starting from element x. Span does the appropriate runtime checks to ensure this is valid. Well, it is supposed to at least (see bug 2016498).

Anyways, as you can see the basic idea is that we never drop into a raw C-style array that can't be checked.

firefox-beta Uplift Approval Request

  • User impact if declined: Very old sec-high bug that could be used as a sandbox escape. Kind of an obvious problem in hindsight so it would be nice to get it fixed quickly despite how old it is.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: This adds a new check that should only fail if we are in a bad state, so we probably prefer a crash. Note that this depends on bug 2016498, which is another sec-high tweak to a check.
  • String changes made/needed: none
  • Is Android affected?: yes
Attachment #9544975 - Flags: approval-mozilla-beta?
Attached file (secure)
Attached file (secure)
Attachment #9544983 - Flags: approval-mozilla-esr140?

firefox-esr140 Uplift Approval Request

  • User impact if declined: Very old sec-high bug that could be used as a sandbox escape. Kind of an obvious problem in hindsight so it would be nice to get it fixed quickly despite how old it is.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: This adds a new check that should only fail if we are in a bad state, so we probably prefer a crash. Note that this depends on bug 2016498, which is another sec-high tweak to a check.
  • String changes made/needed: none
  • Is Android affected?: yes

firefox-esr115 Uplift Approval Request

  • User impact if declined: Very old sec-high bug that could be used as a sandbox escape. Kind of an obvious problem in hindsight so it would be nice to get it fixed quickly despite how old it is.
  • Code covered by automated testing: yes
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: This adds a new check that should only fail if we are in a bad state, so we probably prefer a crash. Note that this depends on bug 2016498, which is another sec-high tweak to a check.
  • String changes made/needed: none
  • Is Android affected?: yes
Attachment #9544987 - Flags: approval-mozilla-esr115?
Attached file (secure)
Group: dom-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 149 Branch
Attachment #9544975 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9544983 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attachment #9544987 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main148+]
QA Whiteboard: [sec] [uplift] [qa-triage-done-c149/b148]
Whiteboard: [client-bounty-form][adv-main148+] → [client-bounty-form][adv-main148+] [adv-esr115.33+]
Whiteboard: [client-bounty-form][adv-main148+] [adv-esr115.33+] → [client-bounty-form][adv-main148+] [adv-esr115.33+] [adv-esr140.8+]
Alias: CVE-2026-2778
Flags: sec-bounty? → sec-bounty+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: