[fuzzing] Fix hb-raster/vector fuzzer issues (#5816)

* [fuzzing] Guard raster/vector create_or_fail results

The raster and vector fuzzers called create_or_fail() and then
immediately dereferenced the returned pointers. Under CIFuzz's
allocation-failure coverage, that turns a valid NULL return into a
harness crash instead of treating it as an out-of-memory case.

Return early after cleaning up when draw or paint context creation
fails. This keeps the fuzzers aligned with the create_or_fail()
contract and avoids null-deref crashes unrelated to library logic.

Tested:
- meson compile -C build hb-raster-fuzzer hb-vector-fuzzer
- build/test/fuzzing/hb-vector-fuzzer test/fuzzing/fonts/TwemojiMozilla.subset.ttf
- build/test/fuzzing/hb-raster-fuzzer test/fuzzing/fonts/TwemojiMozilla.subset.ttf

Assisted-by: OpenAI Codex

* [vector] Avoid OOM leak in paint initialization

hb_vector_paint_ensure_initialized() used group_stack.push() and
then treated the returned pointer as a normal success indicator.
On allocation failure push() returns the Crap() sentinel, so the
subsequent root->alloc(4096) allocated backing storage into the
failure sink and leaked under malloc-failure fuzzing.

Use push_or_fail() instead and only allocate the root body after a
real push succeeds.

Tested:
- build/test/fuzzing/hb-vector-fuzzer test/fuzzing/fonts/TwemojiMozilla.subset.ttf

Assisted-by: OpenAI Codex

* [vector] Avoid double-free on blob creation failure

hb_svg_blob_from_buffer() handed buffer ownership to hb_blob_create()
in the non-recycled path and then ran its own failure cleanup. When
hb_blob_create() failed, it had already invoked the destroy callback,
so the local cleanup freed the same buffer again.

Use hb_blob_create_or_fail() there instead so allocation failure
returns nullptr without triggering the empty-blob fallback and its
conflicting cleanup semantics.

Tested:
- meson compile -C build hb-vector-fuzzer

Assisted-by: OpenAI Codex

* [vector] Avoid double-free in draw blob creation

Mirror the blob-creation failure fix from vector paint in the SVG
draw path. The non-recycled branch handed ownership to
hb_blob_create(), which may already run the destroy callback on
failure, and then fell into local cleanup that freed the same buffer
again.

Switch that branch to hb_blob_create_or_fail() so failure reports
nullptr without conflicting cleanup.

Tested:
- meson compile -C build hb-vector-fuzzer

Assisted-by: OpenAI Codex

* [vector] Fix OOM handling in SVG subset id collection

hb_svg_add_unique_id() used v->push() and tested the returned
pointer for null. hb_vector_t::push() reports allocation failure via
the vector error state and returns the Crap() sentinel, so the old
check did not reliably stop on OOM.

Check v->in_error() after push() before writing into the new slot.

Assisted-by: OpenAI Codex

* [vector] Guard paint render after init failure

hb_vector_paint_render() assumed ensure_initialized() had created
the root group body and indexed group_stack[0] unconditionally.
Under malloc-failure fuzzing, initialization can fail while extents
are already set, leaving group_stack empty and causing a null-deref.

Return nullptr if initialization did not produce the root group.

Assisted-by: OpenAI Codex

* [vector] Propagate paint init failures

hb_vector_paint_ensure_initialized() can fail under malloc-failure
fuzzing, but several paint callbacks assumed the root group existed
immediately afterward and accessed current_body() or group_stack[0].

Make initialization report success and bail out at the callers that
require the root group. Also use push_or_fail() for the extra group
stack entries created by grouping and glyph caching.

Assisted-by: OpenAI Codex

* [raster] Bound SVG path parsing work

hb_raster_svg_parse_path_data() had no complexity cap, unlike the
other new raster SVG subsystems that already bound recursion depth,
attribute count, and gradient stops. Malformed path data could
therefore spend excessive time in parsing and bbox/render work under
fuzzing.

Add a hard limit on parsed path segments and expose it through
hb-limits.hh so the budget is defined alongside the other shared
parser/work limits.

Assisted-by: OpenAI Codex

* [raster] Bound temporary clip mask buffers

hb_raster_paint_push_clip_from_emitter() and the general clip
rectangle path could allocate and clear full-surface temporary A8 clip
masks with no explicit size cap, leading to timeout-scale work on
malformed inputs.

Reuse the raster buffer size policy for these temporary masks as well,
and move that limit into hb-limits.hh so raster images and clip masks
share the same bound.

Assisted-by: OpenAI Codex

* [raster] Reduce buffer size cap

Lower HB_RASTER_MAX_BUFFER_SIZE from 1<<30 to 1<<24 so the shared
raster image / clip-mask limit meaningfully constrains pathological
fuzzer inputs instead of allowing timeout-scale temporary buffers.

Assisted-by: OpenAI Codex
diff --git a/src/hb-limits.hh b/src/hb-limits.hh
index 857e183..5b9954d 100644
--- a/src/hb-limits.hh
+++ b/src/hb-limits.hh
@@ -108,5 +108,13 @@
 #define HB_MAX_COMPOSITE_OPERATIONS_PER_GLYPH 64
 #endif
 
+#ifndef HB_SVG_MAX_PATH_SEGMENTS
+#define HB_SVG_MAX_PATH_SEGMENTS 262144
+#endif
+
+#ifndef HB_RASTER_MAX_BUFFER_SIZE
+#define HB_RASTER_MAX_BUFFER_SIZE ((size_t) 1 << 24)
+#endif
+
 
 #endif /* HB_LIMITS_HH */
diff --git a/src/hb-raster-image.cc b/src/hb-raster-image.cc
index f339c00..ea89aa6 100644
--- a/src/hb-raster-image.cc
+++ b/src/hb-raster-image.cc
@@ -395,8 +395,6 @@
 
 /* hb_raster_image_t */
 
-#define HB_RASTER_IMAGE_MAX_BUFFER_SIZE ((size_t) 1 << 30)
-
 unsigned
 hb_raster_image_t::bytes_per_pixel (hb_raster_format_t format)
 {
@@ -423,7 +421,7 @@
     return false;
 
   size_t buf_size = (size_t) extents.stride * extents.height;
-  if (buf_size > HB_RASTER_IMAGE_MAX_BUFFER_SIZE)
+  if (buf_size > HB_RASTER_MAX_BUFFER_SIZE)
     return false;
   if (unlikely (!buffer.resize_dirty (buf_size)))
     return false;
diff --git a/src/hb-raster-paint.cc b/src/hb-raster-paint.cc
index a88e0f0..84657cd 100644
--- a/src/hb-raster-paint.cc
+++ b/src/hb-raster-paint.cc
@@ -213,7 +213,9 @@
   }
 
   /* Allocate alpha buffer and intersect with previous clip */
-  if (unlikely (!new_clip.alpha.resize (new_clip.stride * h)))
+  size_t clip_size = (size_t) new_clip.stride * h;
+  if (unlikely (clip_size > HB_RASTER_MAX_BUFFER_SIZE ||
+                !new_clip.alpha.resize ((unsigned) clip_size)))
   {
     hb_raster_draw_recycle_image (rdr, mask_img);
     hb_raster_paint_push_empty_clip (c, w, h);
@@ -434,12 +436,14 @@
   {
     /* General case: rasterize transformed quad as alpha mask */
     new_clip.is_rect = false;
-    if (unlikely (!new_clip.alpha.resize (new_clip.stride * h)))
+    size_t clip_size = (size_t) new_clip.stride * h;
+    if (unlikely (clip_size > HB_RASTER_MAX_BUFFER_SIZE ||
+                  !new_clip.alpha.resize ((unsigned) clip_size)))
     {
       hb_raster_paint_push_empty_clip (c, w, h);
       return;
     }
-    hb_memset (new_clip.alpha.arrayZ, 0, new_clip.stride * h);
+    hb_memset (new_clip.alpha.arrayZ, 0, (unsigned) clip_size);
 
     /* Convert quad corners to pixel-relative coords */
     float qx[4], qy[4];
diff --git a/src/hb-raster-svg-parse.cc b/src/hb-raster-svg-parse.cc
index ecf6896..0bdb471 100644
--- a/src/hb-raster-svg-parse.cc
+++ b/src/hb-raster-svg-parse.cc
@@ -246,9 +246,12 @@
   float last_cx = 0, last_cy = 0;
   char last_cmd = 0;
   char cmd = 0;
+  unsigned segments = 0;
 
   while (fp.p < fp.end)
   {
+    if (unlikely (segments++ >= HB_SVG_MAX_PATH_SEGMENTS))
+      break;
     fp.skip_ws_comma ();
     if (fp.p >= fp.end) break;
 
diff --git a/src/hb-vector-svg-draw.cc b/src/hb-vector-svg-draw.cc
index 99379e3..dd80f74 100644
--- a/src/hb-vector-svg-draw.cc
+++ b/src/hb-vector-svg-draw.cc
@@ -217,7 +217,9 @@
   else
   {
     hb_svg_blob_meta_set_buffer (meta, data, allocated);
-    blob = hb_blob_create (data, len, HB_MEMORY_MODE_WRITABLE, meta, hb_svg_blob_meta_destroy);
+    blob = hb_blob_create_or_fail (data, len, HB_MEMORY_MODE_WRITABLE, meta, hb_svg_blob_meta_destroy);
+    if (unlikely (!blob))
+      return nullptr;
   }
 
   if (unlikely (blob == hb_blob_get_empty ()))
diff --git a/src/hb-vector-svg-paint.cc b/src/hb-vector-svg-paint.cc
index 0ab69be..31e61bc 100644
--- a/src/hb-vector-svg-paint.cc
+++ b/src/hb-vector-svg-paint.cc
@@ -203,7 +203,9 @@
   else
   {
     hb_svg_blob_meta_set_buffer (meta, data, allocated);
-    blob = hb_blob_create (data, len, HB_MEMORY_MODE_WRITABLE, meta, hb_svg_blob_meta_destroy);
+    blob = hb_blob_create_or_fail (data, len, HB_MEMORY_MODE_WRITABLE, meta, hb_svg_blob_meta_destroy);
+    if (unlikely (!blob))
+      return nullptr;
   }
 
   if (unlikely (blob == hb_blob_get_empty ()))
@@ -1011,14 +1013,15 @@
   return static_vector_paint_funcs.get_unconst ();
 }
 
-static void
+static hb_bool_t
 hb_vector_paint_ensure_initialized (hb_vector_paint_t *paint)
 {
   if (paint->group_stack.length)
-    return;
-  auto *root = paint->group_stack.push ();
-  if (likely (root))
-    root->alloc (4096);
+    return true;
+  if (unlikely (!paint->group_stack.push_or_fail ()))
+    return false;
+  paint->group_stack.tail ().alloc (4096);
+  return !paint->group_stack.in_error ();
 }
 
 static void
@@ -1030,7 +1033,8 @@
                                 void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
 
   if (unlikely (paint->transform_group_overflow_depth))
   {
@@ -1076,7 +1080,8 @@
                                void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
   if (unlikely (paint->transform_group_overflow_depth))
   {
     paint->transform_group_overflow_depth--;
@@ -1099,7 +1104,8 @@
                                  void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
 
   if (!hb_set_has (paint->defined_outlines, glyph))
   {
@@ -1137,7 +1143,8 @@
                                      void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
 
   unsigned clip_id = paint->clip_rect_counter++;
   hb_svg_append_str (&paint->defs, "<clipPath id=\"c");
@@ -1163,7 +1170,8 @@
                           void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
   hb_svg_append_str (&paint->current_body (), "</g>\n");
 }
 
@@ -1175,7 +1183,8 @@
                        void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
 
   hb_color_t c = color;
   if (is_foreground)
@@ -1202,7 +1211,8 @@
                        void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return false;
 
   auto &body = paint->current_body ();
   if (format == HB_TAG ('s','v','g',' '))
@@ -1287,7 +1297,8 @@
                                  void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
 
   hb_vector_t<hb_color_stop_t> &stops = paint->color_stops_scratch;
   if (!hb_svg_get_color_stops (paint, color_line, &stops) || !stops.length)
@@ -1328,7 +1339,8 @@
                                  void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
 
   hb_vector_t<hb_color_stop_t> &stops = paint->color_stops_scratch;
   if (!hb_svg_get_color_stops (paint, color_line, &stops) || !stops.length)
@@ -1376,7 +1388,8 @@
                                 void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
 
   hb_vector_t<hb_color_stop_t> &stops = paint->color_stops_scratch;
   if (!hb_svg_get_color_stops (paint, color_line, &stops) || !stops.length)
@@ -1400,8 +1413,9 @@
                             void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
-  paint->group_stack.push (hb_vector_t<char> {});
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
+  paint->group_stack.push_or_fail (hb_vector_t<char> {});
 }
 
 static void
@@ -1411,7 +1425,8 @@
                            void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return;
   if (paint->group_stack.length < 2)
     return;
 
@@ -1439,7 +1454,8 @@
                              void *)
 {
   auto *paint = (hb_vector_paint_t *) paint_data;
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return false;
   hb_codepoint_t old_gid = paint->current_svg_image_glyph;
   hb_face_t *old_face = paint->current_face;
   paint->current_svg_image_glyph = glyph;
@@ -1898,7 +1914,8 @@
     }
   }
 
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return false;
 
   bool can_cache = !paint->flat;
   hb_svg_color_glyph_cache_key_t cache_key = hb_svg_color_glyph_cache_key (glyph,
@@ -1933,7 +1950,8 @@
   bool has_svg_image = false;
   if (can_cache)
   {
-    paint->group_stack.push (hb_vector_t<char> {});
+    if (unlikely (!paint->group_stack.push_or_fail (hb_vector_t<char> {})))
+      return false;
     paint->current_color_glyph_has_svg_image = false;
   }
 
@@ -2090,7 +2108,8 @@
   if (!paint->has_extents)
     return nullptr;
 
-  hb_vector_paint_ensure_initialized (paint);
+  if (unlikely (!hb_vector_paint_ensure_initialized (paint)))
+    return nullptr;
 
   hb_vector_t<char> out;
   hb_svg_recover_recycled_buffer (paint->recycled_blob, &out);
diff --git a/src/hb-vector-svg-subset.cc b/src/hb-vector-svg-subset.cc
index d0b35c6..3cc06a9 100644
--- a/src/hb-vector-svg-subset.cc
+++ b/src/hb-vector-svg-subset.cc
@@ -178,7 +178,8 @@
   if (unlikely (!seen_ids->set (key, true)))
     return false;
   auto *slot = v->push ();
-  if (!slot) return false;
+  if (unlikely (v->in_error ()))
+    return false;
   *slot = key;
   return true;
 }
diff --git a/test/fuzzing/hb-raster-fuzzer.cc b/test/fuzzing/hb-raster-fuzzer.cc
index 713d5d6..277f820 100644
--- a/test/fuzzing/hb-raster-fuzzer.cc
+++ b/test/fuzzing/hb-raster-fuzzer.cc
@@ -56,6 +56,13 @@
 
   hb_raster_draw_t *draw = hb_raster_draw_create_or_fail ();
   hb_raster_paint_t *paint = hb_raster_paint_create_or_fail ();
+  if (!draw || !paint)
+  {
+    hb_raster_draw_destroy (draw);
+    hb_raster_paint_destroy (paint);
+    hb_buffer_destroy (buffer);
+    return 0;
+  }
   hb_raster_paint_set_foreground (paint, HB_COLOR (0, 0, 0, 255));
 
   draw_shaped_text (input.font, buffer, draw, paint);
@@ -101,4 +108,3 @@
   hb_buffer_destroy (buffer);
   return counter ? 0 : 0;
 }
-
diff --git a/test/fuzzing/hb-vector-fuzzer.cc b/test/fuzzing/hb-vector-fuzzer.cc
index 1fdb389..a1c19b6 100644
--- a/test/fuzzing/hb-vector-fuzzer.cc
+++ b/test/fuzzing/hb-vector-fuzzer.cc
@@ -56,6 +56,13 @@
 
   hb_vector_draw_t *draw = hb_vector_draw_create_or_fail (HB_VECTOR_FORMAT_SVG);
   hb_vector_paint_t *paint = hb_vector_paint_create_or_fail (HB_VECTOR_FORMAT_SVG);
+  if (!draw || !paint)
+  {
+    hb_vector_draw_destroy (draw);
+    hb_vector_paint_destroy (paint);
+    hb_buffer_destroy (buffer);
+    return 0;
+  }
   hb_vector_paint_set_foreground (paint, HB_COLOR (0, 0, 0, 255));
 
   unsigned precision = size ? data[size - 1] % 5 : 0;
@@ -105,4 +112,3 @@
   hb_buffer_destroy (buffer);
   return counter ? 0 : 0;
 }
-