From fc32bdbdb51e8594bbf3b8ce2166dca49f729de7 Mon Sep 17 00:00:00 2001 From: jeffhuang Date: Mon, 13 Apr 2026 13:08:01 +0000 Subject: [PATCH 1/2] Fix unbounded ReadGlyph allocation (issue #191) 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::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) --- CMakeLists.txt | 57 +++++ src/convert_ttf2woff2_fuzzer.cc | 18 ++ src/glyph.cc | 9 + test/generate_fixtures.py | 187 +++++++++++++++ test/test_convert_ttf2woff2.cc | 193 +++++++++++++++ test/test_read_glyph.cc | 409 ++++++++++++++++++++++++++++++++ 6 files changed, 873 insertions(+) create mode 100644 src/convert_ttf2woff2_fuzzer.cc create mode 100644 test/generate_fixtures.py create mode 100644 test/test_convert_ttf2woff2.cc create mode 100644 test/test_read_glyph.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index ecfbb83..fee0379 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,63 @@ add_library(convert_woff2ttf_fuzzer STATIC src/convert_woff2ttf_fuzzer.cc) target_link_libraries(convert_woff2ttf_fuzzer woff2dec) add_library(convert_woff2ttf_fuzzer_new_entry STATIC src/convert_woff2ttf_fuzzer_new_entry.cc) target_link_libraries(convert_woff2ttf_fuzzer_new_entry woff2dec) +add_library(convert_ttf2woff2_fuzzer STATIC src/convert_ttf2woff2_fuzzer.cc) +target_link_libraries(convert_ttf2woff2_fuzzer woff2enc) + +# Tests (issue #191 regression coverage) +option(BUILD_TESTING "Build tests" ON) +if (BUILD_TESTING) + enable_testing() + + # Unit test that drives ReadGlyph directly with crafted glyf byte payloads. + add_executable(test_read_glyph test/test_read_glyph.cc) + target_link_libraries(test_read_glyph woff2enc) + add_test(NAME test_read_glyph COMMAND test_read_glyph) + + # End-to-end test: feeds crafted TTFs through ConvertTTFToWOFF2. + add_executable(test_convert_ttf2woff2 test/test_convert_ttf2woff2.cc) + target_link_libraries(test_convert_ttf2woff2 woff2enc) + + # Generate fixtures at build time (requires Python + fontTools). CMake's + # Python3::Interpreter may resolve to a Python without fontTools; probe + # /usr/bin/python3 as well and pick whichever has fontTools importable. + find_package(Python3 COMPONENTS Interpreter) + set(FIXTURE_PYTHON "") + foreach(candidate "${Python3_EXECUTABLE}" "/usr/bin/python3" "python3") + if (candidate AND NOT FIXTURE_PYTHON) + execute_process( + COMMAND ${candidate} -c "import fontTools" + RESULT_VARIABLE _probe + OUTPUT_QUIET ERROR_QUIET) + if (_probe EQUAL 0) + set(FIXTURE_PYTHON "${candidate}") + endif() + endif() + endforeach() + if (FIXTURE_PYTHON) + message(STATUS "Using ${FIXTURE_PYTHON} for fixture generation (fontTools available)") + set(FIXTURE_DIR "${CMAKE_CURRENT_BINARY_DIR}/test_fixtures") + set(FIXTURE_STAMP "${FIXTURE_DIR}/.stamp") + add_custom_command( + OUTPUT ${FIXTURE_STAMP} + COMMAND ${CMAKE_COMMAND} -E make_directory ${FIXTURE_DIR} + COMMAND ${FIXTURE_PYTHON} + ${CMAKE_CURRENT_SOURCE_DIR}/test/generate_fixtures.py + ${FIXTURE_DIR} + COMMAND ${CMAKE_COMMAND} -E touch ${FIXTURE_STAMP} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/test/generate_fixtures.py + COMMENT "Generating TTF test fixtures for issue #191" + VERBATIM) + add_custom_target(test_fixtures DEPENDS ${FIXTURE_STAMP}) + add_dependencies(test_convert_ttf2woff2 test_fixtures) + add_test(NAME test_convert_ttf2woff2 + COMMAND test_convert_ttf2woff2 ${FIXTURE_DIR}) + else() + message(WARNING "No Python with fontTools found — skipping " + "test_convert_ttf2woff2 (fixture generation requires " + "`pip install fonttools`).") + endif() +endif() # PC files include(CMakeParseArguments) diff --git a/src/convert_ttf2woff2_fuzzer.cc b/src/convert_ttf2woff2_fuzzer.cc new file mode 100644 index 0000000..1abbba8 --- /dev/null +++ b/src/convert_ttf2woff2_fuzzer.cc @@ -0,0 +1,18 @@ +#include +#include +#include + +#include + +// Entry point for LibFuzzer. +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + size_t result_length = woff2::MaxWOFF2CompressedSize(data, size); + if (result_length == 0) { + return 0; + } + std::vector result(result_length); + woff2::WOFF2Params params; + params.brotli_quality = 1; + woff2::ConvertTTFToWOFF2(data, size, result.data(), &result_length, params); + return 0; +} diff --git a/src/glyph.cc b/src/glyph.cc index 5b49486..7366d69 100644 --- a/src/glyph.cc +++ b/src/glyph.cc @@ -95,7 +95,16 @@ bool ReadGlyph(const uint8_t* data, size_t len, Glyph* glyph) { if (!buffer.ReadU16(&point_index)) { return FONT_COMPRESSION_FAILURE(); } + // endPtsOfContours must be monotonically non-decreasing (sfnt spec). + // Reject point_index < last_point_index to prevent uint16_t subtraction + // wraparound that drives an unbounded resize (issue #191). + if (i > 0 && point_index < last_point_index) { + return FONT_COMPRESSION_FAILURE(); + } uint16_t num_points = point_index - last_point_index + (i == 0 ? 1 : 0); + if (num_points > len - buffer.offset()) { + return FONT_COMPRESSION_FAILURE(); + } glyph->contours[i].resize(num_points); last_point_index = point_index; } diff --git a/test/generate_fixtures.py b/test/generate_fixtures.py new file mode 100644 index 0000000..df0b460 --- /dev/null +++ b/test/generate_fixtures.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Generate TTF fixtures exercising issue #191 (unbounded ReadGlyph allocation). + +Produces: + valid_baseline.ttf — a legitimate TTF with monotonic endPts. + non_monotonic_endpts.ttf — one glyph with endPtsOfContours == [5, 2]. + non_monotonic_one_step.ttf — endPts == [1, 0] (smallest wrap-to-65535 case). + non_monotonic_large_drop.ttf — endPts == [0xFFFE, 0] (extreme downward jump). + dos_reproducer.ttf — many glyphs each crafted with wraparound. + large_endpoint.ttf — endPts == [0xFFFE, 0xFFFF] (valid arithmetic, + but num_points on contour 0 is 0xFFFF and the + record doesn't contain that many bytes). + +Each "non-monotonic" / "dos" fixture is built by starting from a valid font +and surgically rewriting the glyf/loca tables so the arithmetic problem +described in PLAN.md section 1 appears exactly where ReadGlyph reads it. +""" + +from fontTools.fontBuilder import FontBuilder +from fontTools.pens.ttGlyphPen import TTGlyphPen +from fontTools.ttLib import TTFont +import io +import os +import struct +import sys + + +def build_valid_font(num_glyphs=4): + """Return a minimal but spec-complete TTF as bytes.""" + fb = FontBuilder(1024, isTTF=True) + glyph_names = [".notdef"] + [f"g{i}" for i in range(1, num_glyphs)] + fb.setupGlyphOrder(glyph_names) + fb.setupCharacterMap({ord("A") + i - 1: glyph_names[i] + for i in range(1, num_glyphs)}) + + glyphs = {} + for name in glyph_names: + pen = TTGlyphPen(None) + # Draw a simple square so each glyph has at least one contour. + pen.moveTo((0, 0)) + pen.lineTo((100, 0)) + pen.lineTo((100, 100)) + pen.lineTo((0, 100)) + pen.closePath() + glyphs[name] = pen.glyph() + fb.setupGlyf(glyphs) + + metrics = {name: (500, 0) for name in glyph_names} + fb.setupHorizontalMetrics(metrics) + fb.setupHorizontalHeader(ascent=800, descent=-200) + fb.setupOS2(sTypoAscender=800, sTypoDescender=-200, usWinAscent=800, + usWinDescent=200) + fb.setupNameTable({"familyName": "Test", "styleName": "Regular"}) + fb.setupPost() + + buf = io.BytesIO() + fb.save(buf) + return buf.getvalue() + + +def rewrite_glyph(font_bytes, glyph_index, new_glyph_bytes): + """Replace the raw bytes of one glyph record in an sfnt. + + Reads the compiled font's glyf + loca tables, splices the new bytes into + the glyf table, rewrites loca to reflect the new offsets, and rebuilds the + sfnt via SFNTWriter so the raw bytes survive serialisation unchanged. + """ + from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter + + src = io.BytesIO(font_bytes) + reader = SFNTReader(src) + num_glyphs = TTFont(io.BytesIO(font_bytes))["maxp"].numGlyphs + head_table = TTFont(io.BytesIO(font_bytes))["head"] + if head_table.indexToLocFormat == 0: + fmt_char = "H" + scale = 2 + else: + fmt_char = "I" + scale = 1 + + glyf_data = reader["glyf"] + loca_data = reader["loca"] + loca_fmt = ">" + fmt_char * (num_glyphs + 1) + loca_values = list(struct.unpack(loca_fmt, loca_data)) + start = loca_values[glyph_index] * scale + end = loca_values[glyph_index + 1] * scale + + new_glyf = (glyf_data[:start] + new_glyph_bytes + glyf_data[end:]) + # Keep glyf padded to 4 bytes (sfnt convention for table data). + pad = (-len(new_glyf)) & 3 + new_glyf += b"\x00" * pad + + delta_bytes = len(new_glyph_bytes) - (end - start) + delta_units = delta_bytes // scale # works for both short and long loca + + new_loca_values = list(loca_values) + for i in range(glyph_index + 1, num_glyphs + 1): + new_loca_values[i] = loca_values[i] + delta_units + new_loca_bytes = struct.pack(loca_fmt, *new_loca_values) + + out = io.BytesIO() + writer = SFNTWriter(out, numTables=len(reader.tables), + sfntVersion=reader.sfntVersion) + for tag in reader.tables: + data = reader[tag] + if tag == "glyf": + data = new_glyf + elif tag == "loca": + data = new_loca_bytes + writer[tag] = data + writer.close() + return out.getvalue() + + +def simple_glyph_bytes(endpts, num_points_override=None, + include_coords=True, trailing_padding=0): + """Build the raw bytes of a simple glyph record. + + endpts: list of uint16 endPtsOfContours values (may be non-monotonic). + num_points_override: if given, writes exactly that many flag/x/y bytes. + Otherwise derives from endpts[-1] + 1 when include_coords is True. + """ + buf = struct.pack(">hhhhh", len(endpts), 0, 0, 100, 100) + for ep in endpts: + buf += struct.pack(">H", ep & 0xFFFF) + buf += struct.pack(">H", 0) # instructionLength + if include_coords: + n = (num_points_override if num_points_override is not None + else (endpts[-1] + 1 if endpts else 0)) + # flag 0x37: on-curve, x short (+), y short (+), no repeat + buf += bytes([0x37]) * n + buf += bytes([1]) * n # x coords + buf += bytes([1]) * n # y coords + buf += b"\x00" * trailing_padding + return buf + + +def main(): + out_dir = sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(__file__) + os.makedirs(out_dir, exist_ok=True) + + def write(name, data): + path = os.path.join(out_dir, name) + with open(path, "wb") as f: + f.write(data) + print(f"wrote {path} ({len(data)} bytes)") + + # 1. Baseline valid TTF — monotonic endpoints throughout. + baseline = build_valid_font() + write("valid_baseline.ttf", baseline) + + # 2. Non-monotonic endpoints [5, 2] on glyph index 1. + mal1 = rewrite_glyph(baseline, 1, simple_glyph_bytes([5, 2], + include_coords=False, + trailing_padding=6)) + write("non_monotonic_endpts.ttf", mal1) + + # 3. Minimal wrap: endpts = [1, 0]. + mal2 = rewrite_glyph(baseline, 1, simple_glyph_bytes([1, 0], + include_coords=False, + trailing_padding=6)) + write("non_monotonic_one_step.ttf", mal2) + + # 4. Extreme drop: endpts = [0xFFFE, 0]. + mal3 = rewrite_glyph(baseline, 1, simple_glyph_bytes([0xFFFE, 0], + include_coords=False, + trailing_padding=6)) + write("non_monotonic_large_drop.ttf", mal3) + + # 5. DoS reproducer: every writable glyph has a wraparound pattern. + dos = baseline + font = TTFont(io.BytesIO(baseline)) + num_glyphs = font["maxp"].numGlyphs + for i in range(1, num_glyphs): + dos = rewrite_glyph(dos, i, simple_glyph_bytes([100, 0], + include_coords=False, + trailing_padding=4)) + write("dos_reproducer.ttf", dos) + + # 6. Valid arithmetic but physically impossible: [0xFFFE, 0xFFFF]. + large = rewrite_glyph(baseline, 1, simple_glyph_bytes( + [0xFFFE, 0xFFFF], include_coords=False, trailing_padding=8)) + write("large_endpoint.ttf", large) + + +if __name__ == "__main__": + main() diff --git a/test/test_convert_ttf2woff2.cc b/test/test_convert_ttf2woff2.cc new file mode 100644 index 0000000..9fa23b7 --- /dev/null +++ b/test/test_convert_ttf2woff2.cc @@ -0,0 +1,193 @@ +// End-to-end tests for issue #191 — the ConvertTTFToWOFF2 entry point. +// +// Reads TTF fixtures generated by generate_fixtures.py (valid_baseline.ttf, +// non_monotonic_endpts.ttf, ...) and checks that ConvertTTFToWOFF2: +// * succeeds on a legitimate TTF, and +// * rejects (returns false, promptly, without huge allocations) every +// crafted input that previously triggered the uint16_t wraparound +// described in PLAN.md section 1. +// +// Fixtures are located via the WOFF2_FIXTURE_DIR environment variable when +// set, otherwise via argv[1]. This keeps the test invokable both from +// ctest (which passes the build-time fixture directory) and by hand. + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +int g_total = 0; +int g_failed = 0; + +#define CHECK_MSG(cond, msg) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, " FAIL: %s:%d: %s — %s\n", __FILE__, __LINE__, \ + #cond, msg); \ + ++g_failed; \ + return; \ + } \ + } while (0) + +bool ReadFile(const std::string& path, std::vector* out) { + std::ifstream f(path, std::ios::binary); + if (!f) return false; + f.seekg(0, std::ios::end); + std::streamsize n = f.tellg(); + f.seekg(0, std::ios::beg); + out->resize(static_cast(n)); + if (n > 0) f.read(reinterpret_cast(out->data()), n); + return static_cast(f); +} + +// Returns woff2's return value for the given input, using brotli_quality=1 +// to keep the test fast. The output buffer is sized per MaxWOFF2CompressedSize +// but capped at 32 MiB to contain any allocation damage if the fix regresses. +bool RunEncoder(const std::vector& input, size_t* out_size_bytes) { + size_t cap = woff2::MaxWOFF2CompressedSize(input.data(), input.size()); + const size_t kHardCap = 32 * 1024 * 1024; + if (cap > kHardCap) cap = kHardCap; + if (cap < 1024) cap = 1024; + std::vector out(cap); + size_t out_size = out.size(); + woff2::WOFF2Params params; + params.brotli_quality = 1; + bool ok = woff2::ConvertTTFToWOFF2(input.data(), input.size(), + out.data(), &out_size, params); + if (out_size_bytes) *out_size_bytes = ok ? out_size : 0; + return ok; +} + +std::string g_fixture_dir; + +std::string FixturePath(const char* name) { + std::string path = g_fixture_dir; + if (!path.empty() && path.back() != '/') path.push_back('/'); + path.append(name); + return path; +} + +// Tests ----------------------------------------------------------------------- + +#define TEST(name) \ + void name(); \ + void Run_##name() { \ + ++g_total; \ + std::fprintf(stderr, "[RUN ] %s\n", #name); \ + int before = g_failed; \ + name(); \ + if (g_failed == before) std::fprintf(stderr, "[ OK ] %s\n", #name); \ + } \ + void name() + +// Sanity: the fixture builder must produce a font that actually encodes. +// If this fails, the remaining assertions cannot be interpreted (we can't +// tell "rejected due to bug fix" from "rejected due to bad fixture"). +TEST(EncodesValidBaseline) { + std::vector input; + CHECK_MSG(ReadFile(FixturePath("valid_baseline.ttf"), &input), + "failed to read valid_baseline.ttf"); + CHECK_MSG(!input.empty(), "valid_baseline.ttf is empty"); + size_t written = 0; + bool ok = RunEncoder(input, &written); + CHECK_MSG(ok, "legitimate TTF must compress"); + CHECK_MSG(written > 0, "compressed output must be non-empty"); +} + +// Primary regression test: the exact endPts == [5, 2] case from PLAN.md 5.3. +TEST(RejectsNonMonotonicEndpts) { + std::vector input; + CHECK_MSG(ReadFile(FixturePath("non_monotonic_endpts.ttf"), &input), + "failed to read non_monotonic_endpts.ttf"); + bool ok = RunEncoder(input, nullptr); + CHECK_MSG(!ok, "TTF with non-monotonic endPts must be rejected"); +} + +// Smallest wrap: point_index < last_point_index by exactly one. +TEST(RejectsOneStepDown) { + std::vector input; + CHECK_MSG(ReadFile(FixturePath("non_monotonic_one_step.ttf"), &input), + "failed to read non_monotonic_one_step.ttf"); + bool ok = RunEncoder(input, nullptr); + CHECK_MSG(!ok, "endPts [1,0] must be rejected"); +} + +// Extreme downward drop from near-max to zero. +TEST(RejectsLargeDrop) { + std::vector input; + CHECK_MSG(ReadFile(FixturePath("non_monotonic_large_drop.ttf"), &input), + "failed to read non_monotonic_large_drop.ttf"); + bool ok = RunEncoder(input, nullptr); + CHECK_MSG(!ok, "endPts [0xFFFE,0] must be rejected"); +} + +// The DoS reproducer — many crafted glyphs, each wrapping. Before the fix +// this would allocate multi-GB of std::vector storage; after the fix +// it must return false within a few milliseconds. +TEST(RejectsDosReproducer) { + std::vector input; + CHECK_MSG(ReadFile(FixturePath("dos_reproducer.ttf"), &input), + "failed to read dos_reproducer.ttf"); + bool ok = RunEncoder(input, nullptr); + CHECK_MSG(!ok, "DoS reproducer must be rejected"); +} + +// Valid arithmetic, physically impossible: num_points = 0xFFFF but the glyph +// record is only a few hundred bytes. Belt-and-braces check (PLAN 3.2) +// must reject this. +TEST(RejectsOversizedEndpoint) { + std::vector input; + CHECK_MSG(ReadFile(FixturePath("large_endpoint.ttf"), &input), + "failed to read large_endpoint.ttf"); + bool ok = RunEncoder(input, nullptr); + CHECK_MSG(!ok, "endPts [0xFFFE,0xFFFF] with tiny glyph record must be " + "rejected by the buffer-bound check"); +} + +// Robustness: arbitrary short junk must be rejected cleanly (not crash, not +// OOM). Establishes that ConvertTTFToWOFF2 has sensible behaviour on garbage. +TEST(RejectsGarbageInput) { + std::vector junk(128, 0xAB); + bool ok = RunEncoder(junk, nullptr); + CHECK_MSG(!ok, "garbage input must be rejected"); +} + +// Robustness: empty input. +TEST(RejectsEmptyInput) { + std::vector empty; + bool ok = RunEncoder(empty, nullptr); + CHECK_MSG(!ok, "empty input must be rejected"); +} + +} // namespace + +int main(int argc, char** argv) { + const char* env = std::getenv("WOFF2_FIXTURE_DIR"); + if (env && *env) { + g_fixture_dir = env; + } else if (argc > 1) { + g_fixture_dir = argv[1]; + } else { + g_fixture_dir = "."; + } + std::fprintf(stderr, "fixture dir: %s\n", g_fixture_dir.c_str()); + + Run_EncodesValidBaseline(); + Run_RejectsNonMonotonicEndpts(); + Run_RejectsOneStepDown(); + Run_RejectsLargeDrop(); + Run_RejectsDosReproducer(); + Run_RejectsOversizedEndpoint(); + Run_RejectsGarbageInput(); + Run_RejectsEmptyInput(); + + std::fprintf(stderr, "\n%d/%d tests passed\n", g_total - g_failed, g_total); + return g_failed == 0 ? 0 : 1; +} diff --git a/test/test_read_glyph.cc b/test/test_read_glyph.cc new file mode 100644 index 0000000..508a89b --- /dev/null +++ b/test/test_read_glyph.cc @@ -0,0 +1,409 @@ +// Tests for issue #191: unbounded ReadGlyph allocation fix. +// +// Exercises src/glyph.cc's ReadGlyph directly by building the simple-glyph +// byte layout per the OpenType sfnt spec and asserting the contract described +// in PLAN.md section 4 (edge cases table). +// +// The simple-glyph payload layout targeted here (what ReadGlyph receives): +// int16 numberOfContours (must be > 0 for the simple-glyph branch) +// int16 xMin, yMin, xMax, yMax +// uint16 endPtsOfContours[numberOfContours] +// uint16 instructionLength +// uint8 instructions[instructionLength] +// uint8 flags[variable, one per point, with optional repeat] +// uint8/int16 xCoordinates[variable] +// uint8/int16 yCoordinates[variable] +// +// Each test builds just enough bytes to reach the behaviour under test and +// asserts ReadGlyph's return value (and, on success, the per-contour sizes). + +#include +#include +#include +#include +#include +#include + +#include "glyph.h" + +namespace { + +// Big-endian byte appenders --------------------------------------------------- + +void PutU8(std::vector& b, uint8_t v) { b.push_back(v); } +void PutU16(std::vector& b, uint16_t v) { + b.push_back(static_cast(v >> 8)); + b.push_back(static_cast(v & 0xff)); +} +void PutI16(std::vector& b, int16_t v) { + PutU16(b, static_cast(v)); +} + +// Builds the byte layout of a simple glyph record. +// endPts is the sequence of endPtsOfContours values (one per contour). +// If fill_points is true, the function appends minimal flags/x/y so that the +// rest of ReadGlyph can parse the glyph to completion (useful for the valid +// baseline tests). Each point costs 3 bytes (flag 0x37 + 1 byte x + 1 byte y). +// If fill_points is false, no flag/coord bytes are written; this is the right +// choice for "expected to be rejected before reaching point parsing" tests. +std::vector BuildSimpleGlyph(const std::vector& endPts, + bool fill_points, + size_t trailing_padding = 0) { + std::vector out; + PutI16(out, static_cast(endPts.size())); // numberOfContours + PutI16(out, 0); // xMin + PutI16(out, 0); // yMin + PutI16(out, 10); // xMax + PutI16(out, 10); // yMax + for (uint16_t ep : endPts) PutU16(out, ep); + PutU16(out, 0); // instructionLength + if (fill_points) { + // Total point count per the sfnt spec: endPts.back() + 1. Each point uses + // a simple flag = 0x37 (on-curve, x & y are 1-byte positive shorts). + size_t n_points = 0; + if (!endPts.empty()) n_points = static_cast(endPts.back()) + 1; + for (size_t i = 0; i < n_points; ++i) PutU8(out, 0x37); + for (size_t i = 0; i < n_points; ++i) PutU8(out, 1); // x coord + for (size_t i = 0; i < n_points; ++i) PutU8(out, 1); // y coord + } + for (size_t i = 0; i < trailing_padding; ++i) PutU8(out, 0); + return out; +} + +// Test runner scaffolding ----------------------------------------------------- + +int g_total = 0; +int g_failed = 0; + +#define CHECK(cond) \ + do { \ + if (!(cond)) { \ + std::fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + ++g_failed; \ + return; \ + } \ + } while (0) + +#define RUN(fn) \ + do { \ + ++g_total; \ + std::fprintf(stderr, "[RUN ] %s\n", #fn); \ + int before = g_failed; \ + fn(); \ + if (g_failed == before) std::fprintf(stderr, "[ OK ] %s\n", #fn); \ + } while (0) + +// Tests ----------------------------------------------------------------------- + +// Baseline: single-contour glyph with three points parses correctly. +void Test_Simple_SingleContour_Valid() { + auto bytes = BuildSimpleGlyph({2}, /*fill_points=*/true); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(ok); + CHECK(g.contours.size() == 1); + CHECK(g.contours[0].size() == 3); +} + +// Spec-clean monotonic endpoints [3, 7, 11] -> contour sizes [4, 4, 4]. +void Test_Simple_Monotonic_Endpoints() { + auto bytes = BuildSimpleGlyph({3, 7, 11}, /*fill_points=*/true); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(ok); + CHECK(g.contours.size() == 3); + CHECK(g.contours[0].size() == 4); + CHECK(g.contours[1].size() == 4); + CHECK(g.contours[2].size() == 4); +} + +// Equal endpoints are tolerated (PLAN.md explicitly says do NOT tighten < to <=). +// endPts [3, 3, 5] -> sizes [4, 0, 2]. +void Test_Simple_Equal_Endpoints_Tolerated() { + auto bytes = BuildSimpleGlyph({3, 3, 5}, /*fill_points=*/true); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(ok); + CHECK(g.contours.size() == 3); + CHECK(g.contours[0].size() == 4); + CHECK(g.contours[1].size() == 0); + CHECK(g.contours[2].size() == 2); +} + +// First contour with point_index == 0 is well-defined: num_points = 1 +// because of the (i == 0 ? 1 : 0) addend. +void Test_Simple_FirstContour_PointIndexZero() { + auto bytes = BuildSimpleGlyph({0}, /*fill_points=*/true); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(ok); + CHECK(g.contours.size() == 1); + CHECK(g.contours[0].size() == 1); +} + +// Upper bound on per-contour points that any *legitimate* post-rejection +// glyph should have touched. The bug's allocation land at ~65533 points; +// any fix that prevents the wraparound will leave the contour untouched +// (or with a small legitimate value). +constexpr size_t kSaneContourCap = 1000; + +// THE BUG FIX: two contours with endPts [5, 2] -> without the fix the +// subtraction wraps to 65533 and resize allocates ~MB of Points. With the +// fix, ReadGlyph must return false AND must not have resize()d contours[1] +// to the wrapped value first. +void Test_Simple_NonMonotonic_Rejected() { + auto bytes = BuildSimpleGlyph({5, 2}, /*fill_points=*/false, + /*trailing_padding=*/4); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); + // Fix (a) requirement: the wrapped-size allocation must NOT have happened. + if (g.contours.size() > 1) { + CHECK(g.contours[1].size() < kSaneContourCap); + } +} + +// Minimal wrap case: endPts [1, 0] -> wrapped num_points = 65535 on contour 1. +// Without fix this is the worst-per-glyph allocation. Must be rejected. +void Test_Simple_NonMonotonic_OneStepDown_Rejected() { + auto bytes = BuildSimpleGlyph({1, 0}, /*fill_points=*/false, + /*trailing_padding=*/4); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); + if (g.contours.size() > 1) { + CHECK(g.contours[1].size() < kSaneContourCap); + } +} + +// Non-monotonic from a very large value to zero: [0xFFFE, 0]. +// Must be rejected; no giant allocation on contour 1 even though the wrap +// lands at num_points == 2 here (this one wouldn't have shown up as a DoS, +// but the monotonicity rule still applies per the sfnt spec). +void Test_Simple_NonMonotonic_LargeToZero_Rejected() { + auto bytes = BuildSimpleGlyph({0xFFFE, 0}, /*fill_points=*/false, + /*trailing_padding=*/4); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); +} + +// Three contours, non-monotonic at the third slot: [3, 7, 4]. Must be +// rejected; contours[2] must not be grown to the wrapped size 65533. +void Test_Simple_NonMonotonic_AtLaterContour_Rejected() { + auto bytes = BuildSimpleGlyph({3, 7, 4}, /*fill_points=*/false, + /*trailing_padding=*/4); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); + if (g.contours.size() > 2) { + CHECK(g.contours[2].size() < kSaneContourCap); + } +} + +// Arithmetically valid but physically impossible: a single contour with +// endPtsOfContours[0] = 1000 and a glyph record far too small to contain +// the downstream flags/coords. The belt-and-braces check (PLAN section 3.2) +// requires rejection before resize() allocates 1001 Points. +void Test_Simple_NumPoints_ExceedsRemainingBuffer() { + // Construct manually so the record is exactly 16 bytes: + // header (10) + endPts (2) + instructionLength (2) + 2 filler bytes. + // num_points = 1001, remaining after endPts read = 16 - 12 = 4 bytes. + std::vector bytes; + PutI16(bytes, 1); // numberOfContours + PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 10); PutI16(bytes, 10); + PutU16(bytes, 1000); // endPts[0] = 1000 -> num_points = 1001 + PutU16(bytes, 0); // instructionLength + PutU8(bytes, 0); PutU8(bytes, 0); // two stray bytes + assert(bytes.size() == 16); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); + // Fix (b) requirement: the over-budget allocation must not have happened. + if (!g.contours.empty()) CHECK(g.contours[0].size() < 500); +} + +// Another buffer-bound variant with multiple contours: +// endPts [3, 127], num_points on contour 1 = 124, but only ~4 bytes remain. +void Test_Simple_NumPoints_ExceedsRemaining_MultiContour() { + // header (10) + 2 endPts (4) + instructionLength (2) = 16 bytes so far. + // Give only a few trailing bytes so the total is well under 124 points. + std::vector bytes; + PutI16(bytes, 2); + PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 10); PutI16(bytes, 10); + PutU16(bytes, 3); + PutU16(bytes, 127); + PutU16(bytes, 0); // instructionLength + for (int i = 0; i < 4; ++i) PutU8(bytes, 0); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); +} + +// Scaled-up reproducer of the reported DoS: many contours where every odd +// contour has a point_index below its predecessor. Without the fix this +// would try to resize() many vectors to ~65535 points each. With the fix +// the first non-monotonic transition is rejected outright -- aggregate +// allocation across contours must stay tiny. +void Test_Simple_DoS_Reproducer_ManyContours() { + std::vector endPts; + endPts.reserve(200); + for (int i = 0; i < 100; ++i) { + endPts.push_back(100); + endPts.push_back(0); // wraps on every even-indexed contour (i>0) + } + auto bytes = BuildSimpleGlyph(endPts, /*fill_points=*/false, + /*trailing_padding=*/4); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); + size_t total_points = 0; + for (const auto& c : g.contours) total_points += c.size(); + // Spec-legal rejection leaves at most ~100 points allocated on the very + // first contour; the wrap would produce >1M if the fix regressed. + CHECK(total_points < 10000); +} + +// numberOfContours == 0 denotes an empty glyph. PLAN.md marks the behaviour +// as "early return at line 84 | unchanged". We don't assume a specific +// return value, we only assert the call doesn't crash and doesn't allocate +// spurious contours on success. +void Test_Simple_ZeroContours_NoCrash() { + // Minimum payload: just the 10-byte bbox header, no endPts. + std::vector bytes; + PutI16(bytes, 0); + PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 0); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + // Either outcome is acceptable per PLAN.md; on success, no simple contours. + if (ok) CHECK(g.contours.empty()); + (void)ok; +} + +// numberOfContours < -1 must be rejected (PLAN.md section 4 row 3). +void Test_NumContours_LessThanNegativeOne_Rejected() { + std::vector bytes; + PutI16(bytes, -2); + PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 0); + // Add plenty of padding in case the code reads further before deciding. + for (int i = 0; i < 32; ++i) PutU8(bytes, 0); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); +} + +// Truncated input: simple glyph header promises 5 contours but the buffer +// only contains 2 endPts. Must be rejected by the existing ReadU16 guard. +void Test_Simple_Truncated_Endpts_Rejected() { + std::vector bytes; + PutI16(bytes, 5); // numberOfContours + PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 0); + PutU16(bytes, 1); // endPts[0] + PutU16(bytes, 2); // endPts[1] — only 2 of 5 present + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); +} + +// Input so short we can't even read the numberOfContours field. +void Test_TooShort_ForHeader_Rejected() { + std::vector bytes = {0x00}; + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); +} + +// len == 0: empty input. Must not crash; must return false. +void Test_EmptyInput_Rejected() { + woff2::Glyph g; + // Passing nullptr would be UB; use a non-null, zero-length buffer. + uint8_t stub = 0; + bool ok = woff2::ReadGlyph(&stub, 0, &g); + CHECK(!ok); +} + +// Two contours with equal endpoints at zero: [0, 0]. Monotonic check must +// NOT trigger (point_index == last_point_index is allowed). Contour sizes +// are [1, 0] (the +1 applies only to i==0). +void Test_Simple_TwoContours_BothZero_Accepted() { + auto bytes = BuildSimpleGlyph({0, 0}, /*fill_points=*/true); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(ok); + CHECK(g.contours.size() == 2); + CHECK(g.contours[0].size() == 1); + CHECK(g.contours[1].size() == 0); +} + +// Boundary: endPts[i] one below last_point_index -- the smallest possible +// violation. [5, 4] must be rejected; without the fix num_points wraps to +// 65535 and contours[1] would grow to 65535 entries. +void Test_Simple_NonMonotonic_OneUnderPrev_Rejected() { + auto bytes = BuildSimpleGlyph({5, 4}, /*fill_points=*/false, + /*trailing_padding=*/4); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); + if (g.contours.size() > 1) { + CHECK(g.contours[1].size() < kSaneContourCap); + } +} + +// Strictly-increasing-by-one endpoints: [0, 1, 2, 3]. Every contour has +// exactly one point (sizes [1, 1, 1, 1]). +void Test_Simple_IncrementByOne() { + auto bytes = BuildSimpleGlyph({0, 1, 2, 3}, /*fill_points=*/true); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(ok); + CHECK(g.contours.size() == 4); + for (size_t i = 0; i < 4; ++i) CHECK(g.contours[i].size() == 1); +} + +// endPts exactly at uint16 max: [0xFFFE, 0xFFFF]. Monotonic, so the +// monotonicity check passes; num_points on contour 1 = 1. But contour 0 +// wants num_points = 0xFFFE + 1 = 0xFFFF, and providing that many bytes is +// infeasible -- the belt-and-braces check (PLAN 3.2) should reject it +// because num_points exceeds remaining buffer, BEFORE resize(). +void Test_Simple_Max_Endpoint_RejectedByBufferBound() { + std::vector bytes; + PutI16(bytes, 2); + PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 10); PutI16(bytes, 10); + PutU16(bytes, 0xFFFE); + PutU16(bytes, 0xFFFF); + PutU16(bytes, 0); // instructionLength + // Trailing bytes nowhere near 0xFFFF. + for (int i = 0; i < 8; ++i) PutU8(bytes, 0); + woff2::Glyph g; + bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); + CHECK(!ok); + if (!g.contours.empty()) CHECK(g.contours[0].size() < 1000); +} + +} // namespace + +int main() { + RUN(Test_Simple_SingleContour_Valid); + RUN(Test_Simple_Monotonic_Endpoints); + RUN(Test_Simple_Equal_Endpoints_Tolerated); + RUN(Test_Simple_FirstContour_PointIndexZero); + RUN(Test_Simple_NonMonotonic_Rejected); + RUN(Test_Simple_NonMonotonic_OneStepDown_Rejected); + RUN(Test_Simple_NonMonotonic_LargeToZero_Rejected); + RUN(Test_Simple_NonMonotonic_AtLaterContour_Rejected); + RUN(Test_Simple_NumPoints_ExceedsRemainingBuffer); + RUN(Test_Simple_NumPoints_ExceedsRemaining_MultiContour); + RUN(Test_Simple_DoS_Reproducer_ManyContours); + RUN(Test_Simple_ZeroContours_NoCrash); + RUN(Test_NumContours_LessThanNegativeOne_Rejected); + RUN(Test_Simple_Truncated_Endpts_Rejected); + RUN(Test_TooShort_ForHeader_Rejected); + RUN(Test_EmptyInput_Rejected); + RUN(Test_Simple_TwoContours_BothZero_Accepted); + RUN(Test_Simple_NonMonotonic_OneUnderPrev_Rejected); + RUN(Test_Simple_IncrementByOne); + RUN(Test_Simple_Max_Endpoint_RejectedByBufferBound); + + std::fprintf(stderr, "\n%d/%d tests passed\n", g_total - g_failed, g_total); + return g_failed == 0 ? 0 : 1; +} From 4bdbcee2462be3ce64cac2a0afe28270fc549b71 Mon Sep 17 00:00:00 2001 From: jeffhuang Date: Fri, 8 May 2026 13:28:34 +0000 Subject: [PATCH 2/2] Remove invalid ReadGlyph byte-bound guard --- src/glyph.cc | 3 -- test/generate_fixtures.py | 8 ---- test/test_convert_ttf2woff2.cc | 13 ------ test/test_read_glyph.cc | 83 +++++++++++----------------------- 4 files changed, 26 insertions(+), 81 deletions(-) diff --git a/src/glyph.cc b/src/glyph.cc index 7366d69..6455fe6 100644 --- a/src/glyph.cc +++ b/src/glyph.cc @@ -102,9 +102,6 @@ bool ReadGlyph(const uint8_t* data, size_t len, Glyph* glyph) { return FONT_COMPRESSION_FAILURE(); } uint16_t num_points = point_index - last_point_index + (i == 0 ? 1 : 0); - if (num_points > len - buffer.offset()) { - return FONT_COMPRESSION_FAILURE(); - } glyph->contours[i].resize(num_points); last_point_index = point_index; } diff --git a/test/generate_fixtures.py b/test/generate_fixtures.py index df0b460..c10de73 100644 --- a/test/generate_fixtures.py +++ b/test/generate_fixtures.py @@ -7,9 +7,6 @@ non_monotonic_one_step.ttf — endPts == [1, 0] (smallest wrap-to-65535 case). non_monotonic_large_drop.ttf — endPts == [0xFFFE, 0] (extreme downward jump). dos_reproducer.ttf — many glyphs each crafted with wraparound. - large_endpoint.ttf — endPts == [0xFFFE, 0xFFFF] (valid arithmetic, - but num_points on contour 0 is 0xFFFF and the - record doesn't contain that many bytes). Each "non-monotonic" / "dos" fixture is built by starting from a valid font and surgically rewriting the glyf/loca tables so the arithmetic problem @@ -177,11 +174,6 @@ def write(name, data): trailing_padding=4)) write("dos_reproducer.ttf", dos) - # 6. Valid arithmetic but physically impossible: [0xFFFE, 0xFFFF]. - large = rewrite_glyph(baseline, 1, simple_glyph_bytes( - [0xFFFE, 0xFFFF], include_coords=False, trailing_padding=8)) - write("large_endpoint.ttf", large) - if __name__ == "__main__": main() diff --git a/test/test_convert_ttf2woff2.cc b/test/test_convert_ttf2woff2.cc index 9fa23b7..6abde1b 100644 --- a/test/test_convert_ttf2woff2.cc +++ b/test/test_convert_ttf2woff2.cc @@ -139,18 +139,6 @@ TEST(RejectsDosReproducer) { CHECK_MSG(!ok, "DoS reproducer must be rejected"); } -// Valid arithmetic, physically impossible: num_points = 0xFFFF but the glyph -// record is only a few hundred bytes. Belt-and-braces check (PLAN 3.2) -// must reject this. -TEST(RejectsOversizedEndpoint) { - std::vector input; - CHECK_MSG(ReadFile(FixturePath("large_endpoint.ttf"), &input), - "failed to read large_endpoint.ttf"); - bool ok = RunEncoder(input, nullptr); - CHECK_MSG(!ok, "endPts [0xFFFE,0xFFFF] with tiny glyph record must be " - "rejected by the buffer-bound check"); -} - // Robustness: arbitrary short junk must be rejected cleanly (not crash, not // OOM). Establishes that ConvertTTFToWOFF2 has sensible behaviour on garbage. TEST(RejectsGarbageInput) { @@ -184,7 +172,6 @@ int main(int argc, char** argv) { Run_RejectsOneStepDown(); Run_RejectsLargeDrop(); Run_RejectsDosReproducer(); - Run_RejectsOversizedEndpoint(); Run_RejectsGarbageInput(); Run_RejectsEmptyInput(); diff --git a/test/test_read_glyph.cc b/test/test_read_glyph.cc index 508a89b..9c6073d 100644 --- a/test/test_read_glyph.cc +++ b/test/test_read_glyph.cc @@ -17,7 +17,6 @@ // Each test builds just enough bytes to reach the behaviour under test and // asserts ReadGlyph's return value (and, on success, the per-contour sizes). -#include #include #include #include @@ -70,6 +69,20 @@ std::vector BuildSimpleGlyph(const std::vector& endPts, return out; } +void AppendRepeatedSamePointFlags(size_t n_points, std::vector* out) { + const uint8_t kSamePointFlag = 0x01 | 0x10 | 0x20; + while (n_points > 0) { + size_t run = n_points > 256 ? 256 : n_points; + if (run == 1) { + PutU8(*out, kSamePointFlag); + } else { + PutU8(*out, kSamePointFlag | 0x08); + PutU8(*out, static_cast(run - 1)); + } + n_points -= run; + } +} + // Test runner scaffolding ----------------------------------------------------- int g_total = 0; @@ -201,43 +214,21 @@ void Test_Simple_NonMonotonic_AtLaterContour_Rejected() { } } -// Arithmetically valid but physically impossible: a single contour with -// endPtsOfContours[0] = 1000 and a glyph record far too small to contain -// the downstream flags/coords. The belt-and-braces check (PLAN section 3.2) -// requires rejection before resize() allocates 1001 Points. -void Test_Simple_NumPoints_ExceedsRemainingBuffer() { - // Construct manually so the record is exactly 16 bytes: - // header (10) + endPts (2) + instructionLength (2) + 2 filler bytes. - // num_points = 1001, remaining after endPts read = 16 - 12 = 4 bytes. - std::vector bytes; - PutI16(bytes, 1); // numberOfContours - PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 10); PutI16(bytes, 10); - PutU16(bytes, 1000); // endPts[0] = 1000 -> num_points = 1001 - PutU16(bytes, 0); // instructionLength - PutU8(bytes, 0); PutU8(bytes, 0); // two stray bytes - assert(bytes.size() == 16); - woff2::Glyph g; - bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); - CHECK(!ok); - // Fix (b) requirement: the over-budget allocation must not have happened. - if (!g.contours.empty()) CHECK(g.contours[0].size() < 500); -} - -// Another buffer-bound variant with multiple contours: -// endPts [3, 127], num_points on contour 1 = 124, but only ~4 bytes remain. -void Test_Simple_NumPoints_ExceedsRemaining_MultiContour() { - // header (10) + 2 endPts (4) + instructionLength (2) = 16 bytes so far. - // Give only a few trailing bytes so the total is well under 124 points. +// Flags are run-length encoded, so many logical points may be represented by +// far fewer flag bytes. This guards against reintroducing a remaining-bytes +// bound that assumes one stored flag byte per point. +void Test_Simple_RleFlags_CanRepresentManyPoints() { std::vector bytes; - PutI16(bytes, 2); + PutI16(bytes, 1); PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 10); PutI16(bytes, 10); - PutU16(bytes, 3); - PutU16(bytes, 127); - PutU16(bytes, 0); // instructionLength - for (int i = 0; i < 4; ++i) PutU8(bytes, 0); + PutU16(bytes, 1000); // 1001 logical points. + PutU16(bytes, 0); // instructionLength + AppendRepeatedSamePointFlags(1001, &bytes); woff2::Glyph g; bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); - CHECK(!ok); + CHECK(ok); + CHECK(g.contours.size() == 1); + CHECK(g.contours[0].size() == 1001); } // Scaled-up reproducer of the reported DoS: many contours where every odd @@ -360,26 +351,6 @@ void Test_Simple_IncrementByOne() { for (size_t i = 0; i < 4; ++i) CHECK(g.contours[i].size() == 1); } -// endPts exactly at uint16 max: [0xFFFE, 0xFFFF]. Monotonic, so the -// monotonicity check passes; num_points on contour 1 = 1. But contour 0 -// wants num_points = 0xFFFE + 1 = 0xFFFF, and providing that many bytes is -// infeasible -- the belt-and-braces check (PLAN 3.2) should reject it -// because num_points exceeds remaining buffer, BEFORE resize(). -void Test_Simple_Max_Endpoint_RejectedByBufferBound() { - std::vector bytes; - PutI16(bytes, 2); - PutI16(bytes, 0); PutI16(bytes, 0); PutI16(bytes, 10); PutI16(bytes, 10); - PutU16(bytes, 0xFFFE); - PutU16(bytes, 0xFFFF); - PutU16(bytes, 0); // instructionLength - // Trailing bytes nowhere near 0xFFFF. - for (int i = 0; i < 8; ++i) PutU8(bytes, 0); - woff2::Glyph g; - bool ok = woff2::ReadGlyph(bytes.data(), bytes.size(), &g); - CHECK(!ok); - if (!g.contours.empty()) CHECK(g.contours[0].size() < 1000); -} - } // namespace int main() { @@ -391,8 +362,7 @@ int main() { RUN(Test_Simple_NonMonotonic_OneStepDown_Rejected); RUN(Test_Simple_NonMonotonic_LargeToZero_Rejected); RUN(Test_Simple_NonMonotonic_AtLaterContour_Rejected); - RUN(Test_Simple_NumPoints_ExceedsRemainingBuffer); - RUN(Test_Simple_NumPoints_ExceedsRemaining_MultiContour); + RUN(Test_Simple_RleFlags_CanRepresentManyPoints); RUN(Test_Simple_DoS_Reproducer_ManyContours); RUN(Test_Simple_ZeroContours_NoCrash); RUN(Test_NumContours_LessThanNegativeOne_Rejected); @@ -402,7 +372,6 @@ int main() { RUN(Test_Simple_TwoContours_BothZero_Accepted); RUN(Test_Simple_NonMonotonic_OneUnderPrev_Rejected); RUN(Test_Simple_IncrementByOne); - RUN(Test_Simple_Max_Endpoint_RejectedByBufferBound); std::fprintf(stderr, "\n%d/%d tests passed\n", g_total - g_failed, g_total); return g_failed == 0 ? 0 : 1;