SKIA-0005
high verifiedSkOTUtils::RenameFont: unchecked 'name' table offset/length → heap OOB write from a crafted font (Windows font load)
SkFontMgr::makeFromStream / makeFromData / makeFromFile (GDI backend)public API Public SkFontMgr GDI API — SkFontMgr_New_GDI()->makeFromStream(crafted ttf, ttcIndex=0) — on a Windows MSVC cl.exe 14.38 + GN sanitize=ASAN build of Skia 488a6fc (GDI font backend). ASan heap-buffer-overflow WRITE 243300 bytes into a 280-byte buffer, full public-API stack (main → SkFontMgr::makeFromStream → SkFontMgrGDI::onMakeFromStreamIndex → create_from_stream → SkOTUtils::RenameFont:91). This is the topmost public API driven end-to-end → public_api_reachable. (Earlier clang-12 harness on the real SkOTUtils.cpp = harness_reproduced, superseded.) Reproduction command (asan)
SKIA=~/Projects/skia pocs/skia/skottable/verify.sh (clang-12, ASan; links the real src/sfnt/SkOTUtils.cpp + SkStream/SkData/SkOTTable_name TUs, no GN build)
PUBLIC API (public_api_reachable, Windows GDI): skia0005_gdi_harness.exe malicious_verdana.ttf → ASan heap-OOB-WRITE through SkFontMgr_New_GDI()->makeFromStream (GN target //:skia0005_gdi_harness, MSVC sanitize=ASAN build). CHEAP harness (harness_reproduced, Linux/clang-12, no GN): pocs/skia/skottable/poc_renamefont_oobwrite (synthetic) AND poc_renamefont_fromfile <corrupted.ttf> (real font); verify.sh runs both.
# expect: AddressSanitizer: heap-buffer-overflow WRITE, 0 bytes to the right of the (newDataSize)-byte region, in SkMemoryStream::read -> sk_careful_memcpy <- SkOTUtils::RenameFont (SkOTUtils.cpp:91). Synthetic: WRITE size 900 past a 188-byte buffer. Real font: WRITE size ~54504 past a 320-byte buffer (corrupted ttf-bitstream-vera VeraMoIt.ttf). Classification
| Target | Skia |
|---|---|
| Component | SFNT / OpenType table structs |
| Location | src/sfnt/SkOTUtils.cpp · SkOTUtils::RenameFont:85-91 |
| Entry point | SkFontMgr::makeFromStream / makeFromData / makeFromFile (GDI backend) public API SkFontMgr_New_GDI()->makeFromStream/makeFromData/makeFromFile (public SkFontMgr API, attacker font bytes) — base dispatch SkFontMgr.cpp:184-205, only a nullptr check → SkFontMgr_GDI::onMakeFromStreamIndex (SkFontHost_win.cpp:2285) — sole gate is ttcIndex==0 (the default); makeFromData funnels via SkMemoryStream, makeFromFile via SkStream::MakeFromFile, both into the same path → create_from_stream (SkFontHost_win.cpp:1799) — RenameFont is the FIRST operation on the stream, before activate_font/AddFontMemResourceEx; no content validation → SkOTUtils::RenameFont(stream.get(), familyName, ...) (SkFontHost_win.cpp:1809) → SkOTUtils.cpp:91 sk_careful_memcpy → heap OOB WRITEGENUINELY REACHABLE (this is the one real finding among the Skia set — NOT a false-positive). On the Skia GDI font backend, create_from_stream passes the attacker-controlled font stream straight into RenameFont before GDI sees it (confirmed at SkFontHost_win.cpp:1799-1809). Config caveat: GDI backend only — modern DirectWrite Chrome does not hit it; affects Skia-GDI-configured embedders (legacy Chromium, some doc/image renderers) loading untrusted fonts on Windows. evidence:harness_reproduced because the PoC calls RenameFont directly (mirroring create_from_stream) on the real linked SkOTUtils.cpp; NEXT rung is to reproduce end-to-end through the public SkFontMgr::makeFromStream on a Windows GDI build → public_api_reachable, then file upstream as a security bug. WINDOWS-BUILD VERIFICATION ATTEMPTED 2026-06-22 — BLOCKED (env, not the bug): this host has only MSVC cl.exe (no clang-cl) and the Skia checkout is unsynced (no third_party/externals, no gn, no out). Modern Skia's GN build REQUIRES clang, and the GDI SkFontMgr pulls in the whole font/text core, so the literal makeFromStream public-API repro needs a full Skia GDI build that can't be done here. To reach public_api_reachable a future session needs: clang-cl installed + `python3 tools/git-sync-deps` (multi-GB) + a GN Windows ASan build (`is_official_build=false sanitize=ASAN`, GDI font mgr) + a driver calling SkFontMgr_New_GDI()->makeFromStream(crafted). MSVC `cl /fsanitize=address` could instead reproduce RenameFont via the create_from_stream sequence natively (still harness_reproduced, just Windows-ABI). User decision 2026-06-22: finalize at harness_reproduced + source-traced reachability; upstream security report is ready to file (pocs/skia/skottable/SKIA-0005/UPSTREAM-REPORT.md + fix.patch). PUBLIC-API REACHABILITY RE-VERIFIED AT SOURCE 2026-06-23 (this session): traced the WHOLE path on the current checkout (488a6fc) and confirmed it is airtight and UNVALIDATED — SkFontMgr::makeFromStream/Data/File (SkFontMgr.cpp:177-205, nullptr check only) → SkFontMgr_GDI::onMakeFromStreamIndex (SkFontHost_win.cpp:2285, sole gate ttcIndex==0) → create_from_stream (:1799) where RenameFont (:1809) is the FIRST operation on the attacker stream, before any GDI call (activate_font/ AddFontMemResourceEx come after). RenameFont still has EXACTLY ONE caller tree-wide (grep). So this is NOT a false-positive for real-world reach: the realistic public caller passes attacker font bytes straight through with no bounds/length/NUL handling. The harness (verify.sh, both PoCs) re-confirmed deterministic ASan heap-OOB-WRITE on the real SkOTUtils.cpp this session. PUBLIC-API REPRODUCTION COMPLETED END-TO-END ON WINDOWS GDI 2026-06-23 → PROMOTED TO evidence:public_api_reachable / status:verified. The previously-blocked environmental step was done: Skia 488a6fc built for Windows with MSVC cl.exe 14.38 + GN sanitize="ASAN" (is_clang=false → /fsanitize=address; cl auto-links the ASan runtime, no clang-cl/LLVM needed), GDI font backend on. DEPS synced natively in WSL, gn.exe/ninja.exe bootstrapped, source mapped as drive Z: (UNC trips gn's CWD). A driver (pocs/skia/skottable/SKIA-0005/skia0005_gdi_harness.cc, GN target //:skia0005_gdi_harness) called the PUBLIC API SkFontMgr_New_GDI()->makeFromStream(malicious_verdana.ttf, ttcIndex=0) → ASan heap-buffer-overflow WRITE of 243300 bytes, 0 bytes past a 280-byte region, with the exact reported stack: main → SkFontMgr::makeFromStream (SkFontMgr.cpp:189) → SkFontMgrGDI::onMakeFromStreamIndex (SkFontHost_win.cpp:2290) → create_from_stream (:1809) → SkOTUtils::RenameFont (SkOTUtils.cpp:91) → SkMemoryStream::read (SkStream.cpp:349); buffer from SkData::MakeUninitialized ← RenameFont (SkOTUtils.cpp:88). Full log: pocs/skia/skottable/SKIA-0005/asan_gdi_e2e.txt. Fix confirmed end-to-end too: with fix.patch applied + rebuilt, the same crafted font is rejected (RenameFont→null, makeFromStream→null typeface, no ASan error) while a legitimate verdana.ttf still loads. Next rung (real_application_verified) would be a shipping app (e.g. a Skia-GDI document/HTML-to-image renderer) loading the font; the public-API runtime fact is now established. |
| Vuln class | oob-write |
| CVE | — |
| CVSS | — |
| Discovered | 2026-06-22 |
Verification
| Evidence | ✓ Public API reachable (real-world verified) |
|---|---|
| Verified through | Public SkFontMgr GDI API — SkFontMgr_New_GDI()->makeFromStream(crafted ttf, ttcIndex=0) — on a Windows MSVC cl.exe 14.38 + GN sanitize=ASAN build of Skia 488a6fc (GDI font backend). ASan heap-buffer-overflow WRITE 243300 bytes into a 280-byte buffer, full public-API stack (main → SkFontMgr::makeFromStream → SkFontMgrGDI::onMakeFromStreamIndex → create_from_stream → SkOTUtils::RenameFont:91). This is the topmost public API driven end-to-end → public_api_reachable. (Earlier clang-12 harness on the real SkOTUtils.cpp = harness_reproduced, superseded.) |
| Harness fired | ✅ yes |
| Protocol | 2.0 |
| Sanitizer | asan |
| Crash type | heap-buffer-overflow (write) |
| Repro | WINDOWS PUBLIC-API END-TO-END (2026-06-23): Skia 488a6fc built for Windows with MSVC cl.exe 14.38 + GN sanitize="ASAN" (is_clang=false → /fsanitize=address), GDI font backend enabled. Harness skia0005_gdi_harness.cc (GN target //:skia0005_gdi_harness) calls SkFontMgr_New_GDI()->makeFromStream(malicious_verdana.ttf, /*ttcIndex=*/0), where malicious_verdana.ttf is stock verdana.ttf with its 'name' directory entry patched to offset=243300, logicalLength=243240 (via corrupt_name_table.py). ASan: heap-buffer-overflow WRITE of size 243300, 0 bytes to the right of a 280-byte region, allocated by SkData::MakeUninitialized ← RenameFont (SkOTUtils.cpp:88), stack main → SkFontMgr::makeFromStream (SkFontMgr.cpp:189) → SkFontMgrGDI::onMakeFromStreamIndex (SkFontHost_win.cpp:2290) → create_from_stream (:1809) → SkOTUtils::RenameFont (SkOTUtils.cpp:91) → SkMemoryStream::read (SkStream.cpp:349). Log: pocs/skia/skottable/SKIA-0005/asan_gdi_e2e.txt. Fix verified end-to-end: with fix.patch applied + rebuilt, the crafted font is rejected (null typeface, no ASan error) and a legitimate verdana.ttf still loads. Earlier Linux harness (clang-12, real SkOTUtils.cpp, synthetic + corrupted-VeraMoIt PoCs via verify.sh) independently reproduces the same OOB write at harness_reproduced level. |
Reproduce / self-verify
# build
SKIA=~/Projects/skia pocs/skia/skottable/verify.sh (clang-12, ASan; links the real src/sfnt/SkOTUtils.cpp + SkStream/SkData/SkOTTable_name TUs, no GN build)
# run
PUBLIC API (public_api_reachable, Windows GDI): skia0005_gdi_harness.exe malicious_verdana.ttf → ASan heap-OOB-WRITE through SkFontMgr_New_GDI()->makeFromStream (GN target //:skia0005_gdi_harness, MSVC sanitize=ASAN build). CHEAP harness (harness_reproduced, Linux/clang-12, no GN): pocs/skia/skottable/poc_renamefont_oobwrite (synthetic) AND poc_renamefont_fromfile <corrupted.ttf> (real font); verify.sh runs both.
# expect (asan):
AddressSanitizer: heap-buffer-overflow WRITE, 0 bytes to the right of the (newDataSize)-byte region, in SkMemoryStream::read -> sk_careful_memcpy <- SkOTUtils::RenameFont (SkOTUtils.cpp:91). Synthetic: WRITE size 900 past a 188-byte buffer. Real font: WRITE size ~54504 past a 320-byte buffer (corrupted ttf-bitstream-vera VeraMoIt.ttf). cost: Trivial to verify (no GN build). Two repros: (1) poc_renamefont_oobwrite.cc builds a synthetic 1000-byte SFNT ('name' entry offset=900, logicalLength=980); (2) SKIA-0005/corrupt_name_table.py takes ANY real .ttf/.otf and patches its 'name' directory entry's offset (->read count) and logicalLength (->shrinks buffer) to the trigger condition, then poc_renamefont_fromfile.cc loads the file via SkStream::MakeFromFile and calls RenameFont (mirroring create_from_stream). Both fire on the real src/sfnt/SkOTUtils.cpp under ASan. REACHABILITY (Windows + GDI backend ONLY): the sole caller is create_from_stream (SkFontHost_win.cpp:1799/1809), reached via SkFontMgr_GDI::onMakeFromStream/Data/File (i.e. SkFontMgr_New_GDI()->makeFromData/Stream/File with attacker font bytes). NOT the DirectWrite backend, so default modern Chrome (DirectWrite) does not hit it; it affects Skia-GDI-configured apps/embedders (legacy Chromium, some doc/image renderers) loading untrusted fonts.
Disclosure
| Reported to | Skia issue tracker (issues.skia.org), component Skia > Fonts, Security template — issue 527031551 |
|---|---|
| Reported | 2026-06-23 |
| Vendor ack | — |
| Embargo until | — |
| Public | 2026-06-24 |
| Patched in | — |
PoC: pocs/skia/skottable/SKIA-0005/poc_renamefont_oobwrite.cc
Writeup
Summary
SkOTUtils::RenameFont() rewrites a font’s name table. It sizes the output
buffer from the font’s declared table sizes but then copies using the name
entry’s declared offset without checking either against the file length or
the allocation:
// src/sfnt/SkOTUtils.cpp
size_t oldNameTablePhysicalSize = (SkEndian_SwapBE32(tableEntry.logicalLength) + 3) & ~3; // attacker
size_t oldNameTableOffset = SkEndian_SwapBE32(tableEntry.offset); // attacker
size_t originalDataSize = fontData->getLength() - oldNameTablePhysicalSize; // (1) underflow-prone
size_t newDataSize = originalDataSize + nameTablePhysicalSize;
auto rewrittenFontData = SkData::MakeUninitialized(newDataSize);
SK_OT_BYTE* data = static_cast<SK_OT_BYTE*>(rewrittenFontData->writable_data());
if (fontData->read(data, oldNameTableOffset) < oldNameTableOffset) { // (2) writes oldNameTableOffset
return nullptr; // bytes into newDataSize buf
}
...
if (fontData->read(data + oldNameTableOffset,
originalDataSize - oldNameTableOffset) < ...) { ... } // (3) also unchecked
Nothing enforces the SFNT invariant oldNameTableOffset + oldNameTablePhysicalSize <= getLength() (nor oldNameTableOffset <= newDataSize). A crafted font whose
name entry declares a large logicalLength (shrinking newDataSize) and a
large offset (the read count) makes read(data, oldNameTableOffset) write
oldNameTableOffset attacker bytes into the smaller newDataSize buffer — a heap
buffer overflow write with attacker-controlled length and contents. (When
oldNameTablePhysicalSize > getLength(), line (1) additionally underflows size_t,
which can wrap newDataSize to a tiny value and widen the overflow.)
Reproduction
pocs/skia/skottable/SKIA-0005/poc_renamefont_oobwrite.cc builds a 1000-byte SFNT
with a single name TableDirectoryEntry { offset = 900, logicalLength = 980 }
and calls RenameFont(stream, "X", 1).
SKIA=~/Projects/skia pocs/skia/skottable/verify.sh
ASan (clang-12, real SkOTUtils.cpp):
calling SkOTUtils::RenameFont (font=1000 bytes, name offset=900, logicalLength=980)...
==ERROR: AddressSanitizer: heap-buffer-overflow ... WRITE of size 900
#2 SkMemoryStream::read SkStream.cpp:349
... sk_careful_memcpy <- SkOTUtils::RenameFont
0x6100000000fc is located 0 bytes to the right of 188-byte region
... SkData::MakeUninitialized(188)
newDataSize = (1000 − 980) + nameTablePhysicalSize(168) = 188; the read of 900
bytes overflows it by 712 bytes.
Root cause
The function trusts the attacker-controlled name table directory entry
(offset, logicalLength) to be internally consistent with the file, but never
validates it. Fix: before allocating/copying, require
oldNameTableOffset <= getLength(), oldNameTablePhysicalSize <= getLength(),
and oldNameTableOffset + oldNameTablePhysicalSize <= getLength() (reject
otherwise), and use checked arithmetic (SkSafeMath) for originalDataSize /
newDataSize.
Verification evidence
verification.verified: true, evidence: public_api_reachable. Verified at two levels:
Public API, end-to-end (Windows GDI, 2026-06-23) — the rung that counts. Skia
488a6fc was built for Windows with MSVC cl.exe 14.38 + GN sanitize="ASAN"
(is_clang=false → /fsanitize=address; cl auto-links the ASan runtime, so no
clang-cl/LLVM is needed), GDI font backend enabled. The harness
pocs/skia/skottable/SKIA-0005/skia0005_gdi_harness.cc (GN target
//:skia0005_gdi_harness) drives the public API
SkFontMgr_New_GDI()->makeFromStream(malicious_verdana.ttf, /*ttcIndex=*/0). ASan
fires a heap-buffer-overflow WRITE of 243300 bytes, 0 bytes past a 280-byte
region, with the full public-API stack:
==ERROR: AddressSanitizer: heap-buffer-overflow WRITE of size 243300
#1 SkMemoryStream::read src/core/SkStream.cpp:349
#2 SkOTUtils::RenameFont src/sfnt/SkOTUtils.cpp:91
#3 create_from_stream src/ports/SkFontHost_win.cpp:1809
#4 SkFontMgrGDI::onMakeFromStreamIndex src/ports/SkFontHost_win.cpp:2290
#5 SkFontMgr::makeFromStream src/core/SkFontMgr.cpp:189
#6 main ← public API
0 bytes to the right of 280-byte region
... SkData::MakeUninitialized <- SkOTUtils::RenameFont SkOTUtils.cpp:88
Full log: pocs/skia/skottable/SKIA-0005/asan_gdi_e2e.txt. The source-only
reachability claim is now a runtime fact through the real public GDI API.
Fix confirmed end-to-end too: with fix.patch applied and rebuilt, the same
crafted font is rejected (RenameFont→null → makeFromStream→null typeface, no
ASan error), while a legitimate verdana.ttf still loads.
Cheap harness (Linux/clang-12, no GN) — harness_reproduced, corroborating. The
PoCs in verify.sh link the actual src/sfnt/SkOTUtils.cpp (not a logic replica)
and ASan reports the same heap OOB write inside the shipping RenameFont via
SkMemoryStream::read — RenameFont is platform-agnostic, so this reproduces the
core bug with trivial cost.
Impact
High (scoped to the Windows GDI font backend). This is an
attacker-controlled-length, attacker-controlled-content heap buffer overflow
write during font loading. Call chain: SkFontMgr_GDI::onMakeFromStream / onMakeFromData / onMakeFromFile (SkFontHost_win.cpp:2285-2307) →
create_from_stream (:1799) → SkOTUtils::RenameFont (:1809), which renames a
stream-loaded font before AddFontMemResourceEx. Any
SkFontMgr_New_GDI()->makeFromData/Stream/File(<untrusted font>) reaches it.
Reachability caveat (important, and a correction to an earlier overstatement):
this is the GDI backend ONLY — RenameFont has exactly one caller in the tree
and the DirectWrite backend (SkScalerContext_win_dw) does not call it. Modern
Chrome on Windows uses DirectWrite, so default Chrome web-font loading does NOT hit
this. It affects Windows apps/embedders configured with the GDI font manager
(legacy Chromium; some Skia-based document/SVG/HTML-to-image renderers; embedded
Windows software) that load untrusted fonts via stream/data/file (web @font-face,
document-embedded fonts, user-supplied font files). Within that scope it is a
controlled-data heap-corruption-write primitive (potential RCE, subject to heap
layout/mitigations). RenameFont is also a public SkOTUtils API, so any embedder
that renames untrusted fonts is exposed. Worth an upstream report with the
bounds-check fix above (one caller, simple fix).
Fix (verified) & upstream report
A one-hunk fix — reject the entry unless oldNameTableOffset + oldNameTablePhysicalSize <= getLength() before allocating/copying — is in
pocs/skia/skottable/SKIA-0005/fix.patch. Verified at both levels: with the
patch applied, the Linux PoCs return nullptr (malformed font rejected) and ASan
reports no error (rebuilt via verify.sh); AND end-to-end on the Windows GDI ASan
build, the public-API harness rejects malicious_verdana.ttf (null typeface, no
ASan error) while a legitimate verdana.ttf still loads — confirming the fix is
correct and not over-broad. A submission-ready writeup is in
pocs/skia/skottable/SKIA-0005/UPSTREAM-REPORT.md (Skia tracker, Skia > Fonts,
Security template), with the completed Windows end-to-end verification recorded.
Disclosed upstream 2026-06-23 — Skia issue tracker issue
527031551 (Skia > Fonts, Security
template); made public 2026-06-24 (issue opened to public view; embargo lifted).
Status stays verified pending a CVE/vendor ack (the build gates disclosed on a
cve); promote to disclosed once an ID is assigned and to patched when the fix lands.