Skip to content

Fix unbounded ReadGlyph allocation (issue #191)#192

Open
parasol-aser wants to merge 1 commit intogoogle:masterfrom
parasol-aser:fix/issue-191-readglyph-unbounded-allocation
Open

Fix unbounded ReadGlyph allocation (issue #191)#192
parasol-aser wants to merge 1 commit intogoogle:masterfrom
parasol-aser:fix/issue-191-readglyph-unbounded-allocation

Conversation

@parasol-aser
Copy link
Copy Markdown

Summary

Fixes #191Unbounded ReadGlyph allocation via uint16_t wraparound in ConvertTTFToWOFF2 (DoS, High).

ReadGlyph's simple-glyph branch (src/glyph.cc) never enforces the sfnt spec requirement that endPtsOfContours be monotonically non-decreasing. All operands are uint16_t, so a crafted later endpoint less than last_point_index wraps the subtraction and makes num_points land near 65535. With maxp.numGlyphs up to 32767, std::vector<Glyph::Point>::resize is driven to multi-GB allocations on a ~100 KB input — libFuzzer OOMs at 2.1–2.4 GB RSS inside a single ConvertTTFToWOFF2 invocation. The decoder side (ReconstructGlyf) already has equivalent guards; this brings the encoder to parity.

Fix

src/glyph.cc — two guards inside the per-contour loop:

  1. Monotonicity: reject point_index < last_point_index for i > 0 (option (a) from the issue).
  2. Belt-and-braces size cap: reject num_points > len - buffer.offset() — each point consumes at least one flag byte downstream, so this is a trivially sound upper bound against future variants where the arithmetic is well-defined but the aggregate allocation is still disproportionate (option (b) from the issue).

Both return FONT_COMPRESSION_FAILURE(), which already propagates cleanly through WriteNormalizedLoca → NormalizeGlyphs → NormalizeFont → NormalizeFontCollection → ConvertTTFToWOFF2.

No public API change. No decoder-side change (already guarded).

Changes

  • src/glyph.cc — add monotonicity and per-contour size guards in ReadGlyph.
  • src/convert_ttf2woff2_fuzzer.cc (new) — libFuzzer harness over ConvertTTFToWOFF2 using the existing WOFF2Params overload, matching the reporter's setup.
  • CMakeLists.txt — register convert_ttf2woff2_fuzzer next to convert_woff2ttf_fuzzer, and add an opt-in test suite (gated on BUILD_TESTING, auto-detects a Python with fontTools for fixture generation; silently skips with a status message otherwise).
  • test/test_read_glyph.cc (new) — unit tests that drive ReadGlyph directly with crafted glyf payloads (non-monotonic endpoints, num_points exceeding remaining buffer, valid monotonic cases).
  • test/test_convert_ttf2woff2.cc (new) — end-to-end test over ConvertTTFToWOFF2.
  • test/generate_fixtures.py (new) — fontTools-based generator for a minimal malformed TTF (numGlyphs == 2, each glyph with endPtsOfContours == [5, 2]) plus a valid TTF baseline.

Test plan

  • Before the fix: convert_ttf2woff2_fuzzer on a crafted ~100 KB sfnt matching the reporter's pattern reproduces out-of-memory (~2149 Mb; limit: 2048 Mb) with the stack from [Fuzzing] DoS: Unbounded ReadGlyph allocation via uint16_t wraparound in ConvertTTFToWOFF2 #191.
  • After the fix: the same artefact returns cleanly in milliseconds, RSS < 100 MB, no ASan/UBSan finding.
  • test_read_glyph covers: non-monotonic endpoints (rejected), equal endpoints (accepted — intentional, some real fonts rely on this tolerance), num_points exceeding remaining buffer (rejected), valid monotonic endpoints (accepted).
  • test_convert_ttf2woff2 runs ConvertTTFToWOFF2 over the generated fixtures and asserts the malformed input is rejected while the valid baseline round-trips.
  • Reviewer: a ≥1 h libFuzzer run with 4 workers against a normal sfnt corpus (encoder harness) to catch any legitimate-glyph regression.
  • Reviewer: byte-for-byte woff2_compress output on a known-good corpus (e.g. Google Fonts) must be identical pre- vs post-fix — the fix only rejects inputs that previously triggered undefined-by-spec wraparound.

🤖 Generated with Claude Code

In ReadGlyph's simple-glyph branch, endPtsOfContours is treated as a
uint16_t without enforcing monotonicity. A crafted sfnt with
point_index < last_point_index wraps the subtraction and drives
std::vector<Point>::resize to ~65535 entries per contour, producing
multi-GB allocations and OOM DoS in ConvertTTFToWOFF2.

Enforce the spec's monotonic-non-decreasing requirement on
endPtsOfContours, and bound num_points by the remaining glyph buffer
so allocations stay proportional to input size.

Add a libFuzzer harness (convert_ttf2woff2_fuzzer) mirroring the
reporter's setup, unit tests for ReadGlyph's per-contour guards, and
an end-to-end test over fontTools-generated TTF fixtures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@google-cla
Copy link
Copy Markdown

google-cla bot commented Apr 13, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fuzzing] DoS: Unbounded ReadGlyph allocation via uint16_t wraparound in ConvertTTFToWOFF2

1 participant