[glyf] Charge point loading to the work budget before doing the work (#6256)

The glyf outline budget was only charged after a glyph loaded
successfully: Glyph::get_points() spent 16 units per node up front, and
glyf_accelerator_t::get_points() charged one unit per point only after
the whole tree came back.  A glyph that loaded ~175K points and then
tripped a structural cap (HB_GLYF_MAX_POINTS, edge count) paid for the
nodes but not the points.  Driven from a COLR paint walk through
hb_font_get_glyph_extents(), one such glyph was loaded and thrown away
over 8000 times per session within the budget, ~1.4G point loads.

Charge at the point where the work is attempted instead:

- SimpleGlyph::get_contour_points() takes the budget and charges
  num_points before allocating, clearing, and reading, so truncated or
  later-rejected glyphs pay for the points they allocate.

- Composite levels charge the component subtree size before
  transform_points(), since each nesting level transforms every point
  below it and that multiplication was previously invisible to the
  budget.

The terminal per-point charge for path emission is unchanged.

Local hb-vector-fuzzer run on the testcase: 4.3s -> 0.12s.  Full test
suite passes.  The testcase is added as a fuzz seed.

Fixes: https://oss-fuzz.com/testcase-detail/5714817420427264


Claude-Session: https://claude.ai/code/session_013ksLGe7wUVwbuANqR56YaN

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
diff --git a/src/OT/glyf/Glyph.hh b/src/OT/glyf/Glyph.hh
index 1376d53..3a1e620 100644
--- a/src/OT/glyf/Glyph.hh
+++ b/src/OT/glyf/Glyph.hh
@@ -344,7 +344,7 @@
         head_maxp_info->maxContours = hb_max (head_maxp_info->maxContours, (unsigned) header->numberOfContours);
       if (depth > 0 && composite_contours)
         *composite_contours += (unsigned) header->numberOfContours;
-      if (unlikely (!SimpleGlyph (*header, bytes).get_contour_points (all_points, phantom_only)))
+      if (unlikely (!SimpleGlyph (*header, bytes).get_contour_points (all_points, phantom_only, budget)))
 	return false;
       break;
     case COMPOSITE:
@@ -476,6 +476,15 @@
 	  for (unsigned int i = 0; i < PHANTOM_COUNT; i++)
 	    phantoms[i] = comp_points[comp_points.length - PHANTOM_COUNT + i];
 
+	/* Each composite level transforms the whole subtree below it;
+	 * charge that work here so deep nesting is bounded by the budget
+	 * rather than by the per-draw point cap alone. */
+	if (unlikely (!hb_budget_spend (*budget, HB_BUDGET_1, comp_points.length)))
+	{
+	  points.resize (old_length);
+	  return false;
+	}
+
 	if (comp_points) // Empty in case of phantom_only
 	{
 	  float matrix[4];
diff --git a/src/OT/glyf/SimpleGlyph.hh b/src/OT/glyf/SimpleGlyph.hh
index 74c6509..a61b286 100644
--- a/src/OT/glyf/SimpleGlyph.hh
+++ b/src/OT/glyf/SimpleGlyph.hh
@@ -178,7 +178,8 @@
   }
 
   bool get_contour_points (contour_point_vector_t &points /* OUT */,
-			   bool phantom_only = false) const
+			   bool phantom_only = false,
+			   int64_t *budget = nullptr) const
   {
     const HBUINT16 *endPtsOfContours = &StructAfter<HBUINT16> (header);
     int num_contours = header.numberOfContours;
@@ -188,6 +189,10 @@
     unsigned int num_points = endPtsOfContours[num_contours - 1] + 1;
     if (unlikely (num_points < (unsigned) num_contours)) return false;
 
+    /* Charge before allocating and reading, so that glyphs that fail
+     * later (truncated data, composite caps) still pay for this work. */
+    if (budget && unlikely (!hb_budget_spend (*budget, HB_BUDGET_1, num_points))) return false;
+
     unsigned old_length = points.length;
     points.alloc (points.length + num_points + 4); // Allocate for phantom points, to avoid a possible copy
     if (unlikely (!points.resize_dirty (points.length + num_points))) return false;
diff --git a/test/fuzzing/fonts/clusterfuzz-testcase-minimized-hb-vector-fuzzer-5714817420427264 b/test/fuzzing/fonts/clusterfuzz-testcase-minimized-hb-vector-fuzzer-5714817420427264
new file mode 100644
index 0000000..72d1c74
--- /dev/null
+++ b/test/fuzzing/fonts/clusterfuzz-testcase-minimized-hb-vector-fuzzer-5714817420427264
Binary files differ