Closed Bug 2038439 (CVE-2026-8947) Opened 4 months ago Closed 4 months ago

Heap-use-after-free in StyleSheet::RuleAdded via adoptedStyleSheets proxy

Categories

(Core :: DOM: Bindings (WebIDL), defect, P2)

defect

Tracking

()

RESOLVED FIXED
152 Branch
Tracking Status
firefox-esr115 151+ fixed
firefox-esr140 151+ fixed
firefox150 --- wontfix
firefox151 + fixed
firefox152 + fixed

People

(Reporter: t.satoki111, Assigned: edgar)

Details

(Keywords: csectype-uaf, reporter-external, sec-high, Whiteboard: [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+])

Attachments

(6 files)

Attached file PoC.html

== Summary ==
A heap-use-after-free exists when the adoptedStyleSheets ObservableArray proxy is used after its backing ShadowRoot has been freed by the cycle collector. The proxy retains a raw void* to the freed ShadowRoot in its reserved slot (OBSERVABLE_ARRAY_DOM_INTERFACE_SLOT). Using proxy.push() calls OnSetAdoptedStyleSheets on the freed object, which stores a dangling DocumentOrShadowRoot* in StyleSheet::mAdopters. A subsequent sheet modification (insertRule/replaceSync) triggers the NOTIFY macro, which iterates mAdopters and dereferences the freed ShadowRoot pointer.
A minimal PoC (poc_adopted_uaf_minimal.html) is attached to this report.
== Steps to Reproduce ==

  1. Open the attached PoC (poc_adopted_uaf_minimal.html) in an ASan-enabled Firefox Nightly build
  2. Wait ~60 seconds for cycle collection pressure to trigger
    Or run from CLI:
    ASAN_OPTIONS="detect_leaks=0:halt_on_error=0:print_stacktrace=1:quarantine_size_mb=256"
    MOZ_DISABLE_CONTENT_SANDBOX=1
    ./firefox --headless --no-remote --profile /tmp/poc_profile
    poc_adopted_uaf_minimal.html
    == Actual Results ==
    ASan reports heap-use-after-free:
    ==PID==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
    READ of size 8 at 0x... thread T0 (file:// Content)
    #0 AsNode DocumentOrShadowRoot.h:66
    #1 mozilla::StyleSheet::RuleAdded StyleSheet.cpp:739
    #2 InsertRuleInternal StyleSheet.cpp:1427
    #3 mozilla::StyleSheet::InsertRule StyleSheet.cpp:548
    #4 CSSStyleSheet_Binding::insertRule CSSStyleSheetBinding.cpp:718
    freed by:
    #0 free
    #1 nsIContent::Destroy() FragmentOrElement.cpp:131
    #2 SnowWhiteKiller::Visit nsCycleCollector.cpp:2702
    previously allocated by:
    #0 malloc
    #1 nsNodeInfoManager::Allocate nsNodeInfoManager.cpp:283
    #2 Element::AttachShadowWithoutNameChecks Element.cpp:1537
    == Expected Results ==
    No crash. The proxy should detect that the ShadowRoot has been freed and refuse the operation.
    == Root Cause ==
  3. The ObservableArray proxy for adoptedStyleSheets stores a raw void* to DocumentOrShadowRoot in OBSERVABLE_ARRAY_DOM_INTERFACE_SLOT.
  4. The proxy's slot is only invalidated during GC finalize, leaving it stale when the backing ShadowRoot is reclaimed via cycle collection (CC unlink) instead.
  5. StyleSheet::mAdopters is nsTArray<DocumentOrShadowRoot*> — raw pointers, not RefPtr. AddAdopter() appends without preventing the object from being freed.
    Attack chain:
    (a) proxy = sr.adoptedStyleSheets → proxy holds void* to ShadowRoot
    (b) host.remove(); sr = null → ShadowRoot becomes garbage
    (c) CC runs → SnowWhiteKiller frees the ShadowRoot
    (d) proxy.push(sheet) → OnSetAdoptedStyleSheets on freed memory
    → AddAdopter stores dangling ptr in mAdopters
    (e) sheet.insertRule(...) → RuleAdded → NOTIFY macro
    → iterates mAdopters
    → adopter->AsNode() dereferences freed memory → UAF
    == Security Impact ==
  • Web-accessible from content process (no special permissions required)
  • The freed ShadowRoot is a 264-byte heap allocation
  • The UAF dereferences a vtable-like pointer (AsNode), which could be controlled via heap spraying
  • Deterministic trigger with sufficient GC/CC pressure
    == Tested Version ==
    Firefox Nightly 152.0a1 (ASan build)
    BuildID: 9b901a2393dbc3f35c75b3f7fa4e2a7529df7367
    == Attachments ==
  1. PoC.html
Flags: sec-bounty?

ASan log

We've had a few ObservableArray issues before that ended up in DOM, but I'll put it in DOM:CSS for now. Emilio, any idea where this should go? Thanks.

Group: firefox-core-security → layout-core-security
Component: Security → DOM: CSS Object Model
Flags: needinfo?(emilio)
Product: Firefox → Core

If I'm understanding correctly, this seems to be on the bindings side. It seems the .adoptedStyleSheets proxy is not keeping alive the ShadowRoot, but it's keeping a pointer to it. That's the root cause, the rest falls off of being able to do things on that freed pointer.

Edgar, my guess is that either the proxy needs to keep alive the ShadowRoot, or alternatively (maybe preferably) around here we should clear the proxy pointer to the interface?

Component: DOM: CSS Object Model → DOM: Bindings (WebIDL)
Flags: needinfo?(emilio) → needinfo?(echen)
Group: layout-core-security → dom-core-security
Severity: -- → S2
Status: UNCONFIRMED → NEW
Ever confirmed: true
Priority: -- → P2

(In reply to Emilio Cobos Álvarez [:emilio] from comment #3)

Edgar, my guess is that either the proxy needs to keep alive the ShadowRoot, or alternatively (maybe preferably) around here we should clear the proxy pointer to the interface?

I wonder if it would make more sense to run the DocumentOrShadowRoot::Unlink() cleanup steps in ~ShadowRoot().
It might also be possible for a ShadowRoot to be destroyed without being unlinked, though I’m not sure how to trigger that case.

Flags: needinfo?(echen) → needinfo?(emilio)
Attached file (secure)
Assignee: nobody → echen
Status: NEW → ASSIGNED

(In reply to Edgar Chen [:edgar] from comment #4)

I wonder if it would make more sense to run the DocumentOrShadowRoot::Unlink() cleanup steps in ~ShadowRoot().
It might also be possible for a ShadowRoot to be destroyed without being unlinked, though I’m not sure how to trigger that case.

Yeah, that makes sense to me. And yeah that can definitely happen, we have code to deal with the stylesheet backpointer already, see bug 1475351.

Flags: needinfo?(emilio)

Comment on attachment 9585134 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: The patch shows that something is not being cleaned up properly during Document/ShadowRoot destruction, but it does not point to where the problem is.
  • 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?: Yes
  • If not, how different, hard to create, and risky will they be?: The patch should be applied cleanly.
  • How likely is this patch to cause regressions; how much testing does it need?: Do proper cleanup in destructor, should not cause regression.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9585134 - Flags: sec-approval?
Attachment #9585134 - Flags: sec-approval? → sec-approval+

https://hg-edge.mozilla.org/mozilla-central/rev/2f18b55a67b9

Please add Beta, ESR140, and ESR115 uplift requests on this.

Group: dom-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 4 months ago
Flags: needinfo?(echen)
Resolution: --- → FIXED
Target Milestone: --- → 152 Branch

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: UAF
  • Code covered by automated testing?: no
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: None
  • Risk associated with taking this patch: low
  • Explanation of risk level: low, patch does proper cleanup in destructor, should not cause regression.
  • String changes made/needed?: None
  • Is Android affected?: yes
Attachment #9585867 - Flags: approval-mozilla-beta?
Attached file (secure)

firefox-esr140 Uplift Approval Request

  • User impact if declined/Reason for urgency: UAF
  • Code covered by automated testing?: no
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: None
  • Risk associated with taking this patch: low
  • Explanation of risk level: low, patch does proper cleanup in destructor, should not cause regression.
  • String changes made/needed?: None
  • Is Android affected?: yes
Attachment #9585870 - Flags: approval-mozilla-esr140?
Attached file (secure)

firefox-esr115 Uplift Approval Request

  • User impact if declined/Reason for urgency: UAF
  • Code covered by automated testing?: no
  • Fix verified in Nightly?: yes
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing: None
  • Risk associated with taking this patch: low
  • Explanation of risk level: low, patch does proper cleanup in destructor, should not cause regression.
  • String changes made/needed?: None
  • Is Android affected?: yes
Attachment #9585877 - Flags: approval-mozilla-esr115?
Attached file (secure)
Flags: needinfo?(echen)
Attachment #9585867 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c152/b151]
Attachment #9585870 - Flags: approval-mozilla-esr140? → approval-mozilla-esr140+
Attachment #9585877 - Flags: approval-mozilla-esr115? → approval-mozilla-esr115+
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main151+][adv-main151+][adv-main151+][adv-esr115.36+][adv-esr115.36+][adv-esr115.36+][adv-esr140.11+][adv-esr140.11+][adv-esr140.11+]
Whiteboard: [client-bounty-form][adv-main151+][adv-main151+][adv-main151+][adv-esr115.36+][adv-esr115.36+][adv-esr115.36+][adv-esr140.11+][adv-esr140.11+][adv-esr140.11+] → [client-bounty-form][adv-main151+][adv-esr115.36+][adv-esr140.11+]
Flags: sec-bounty? → sec-bounty+
Alias: CVE-2026-8947
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: