[ot] Fix NULL-deref when nullable output arrays are passed to list getters
Several public "list getter" APIs document their output array as nullable
(pass NULL to just query the total count without storing anything), but
their implementations gated the copy only on the count pointer:
if (count)
{
+ source.sub_array (start_offset, count)
| hb_sink (hb_array (out, *count)); // writes through out even if NULL
}
Passing a non-NULL count with a NULL output array on a populated table
therefore wrote through the NULL pointer and crashed (SIGSEGV).
Gate each copy on the output pointer as well (the same idiom already used
by hb_color_line_get_color_stops: "if (count && out)"). This is a no-op
when the output array is non-NULL and restores the documented behaviour of
returning the total count when it is NULL. Affected getters:
- hb_ot_color_palette_get_colors (CPAL)
- hb_ot_color_glyph_get_layers (COLR v0)
- hb_ot_meta_get_entry_tags (meta)
- hb_aat_layout_get_feature_types (AAT feat)
- hb_aat_layout_feature_type_get_selector_infos (AAT feat)
- hb_ot_layout_feature_get_characters (GSUB cvXX)
- hb_ot_layout_lookup_get_glyph_alternates (GSUB aalt)
Add test/api/test-ot-nullable-output.c exercising each route with a NULL
output array; every case crashed before this change.
Fixes: GHSA-v5mv-8hhw-vff8
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
diff --git a/src/OT/Color/COLR/COLR.hh b/src/OT/Color/COLR/COLR.hh
index dd1dbd1..39a8231 100644
--- a/src/OT/Color/COLR/COLR.hh
+++ b/src/OT/Color/COLR/COLR.hh
@@ -2121,7 +2121,7 @@
hb_array_t<const LayerRecord> all_layers = (this+layersZ).as_array (numLayers);
hb_array_t<const LayerRecord> glyph_layers = all_layers.sub_array (record.firstLayerIdx,
record.numLayers);
- if (count)
+ if (count && layers)
{
+ glyph_layers.sub_array (start_offset, count)
| hb_sink (hb_array (layers, *count))
diff --git a/src/OT/Color/CPAL/CPAL.hh b/src/OT/Color/CPAL/CPAL.hh
index 9364fd9..ade2c1f 100644
--- a/src/OT/Color/CPAL/CPAL.hh
+++ b/src/OT/Color/CPAL/CPAL.hh
@@ -209,7 +209,7 @@
hb_array_t<const BGRAColor> all_colors ((this+colorRecordsZ).arrayZ, numColorRecords);
hb_array_t<const BGRAColor> palette_colors = all_colors.sub_array (start_index,
numColors);
- if (color_count)
+ if (color_count && colors)
{
+ palette_colors.sub_array (start_offset, color_count)
| hb_sink (hb_array (colors, *color_count))
diff --git a/src/OT/Layout/GSUB/AlternateSet.hh b/src/OT/Layout/GSUB/AlternateSet.hh
index 0437cff..2488358 100644
--- a/src/OT/Layout/GSUB/AlternateSet.hh
+++ b/src/OT/Layout/GSUB/AlternateSet.hh
@@ -82,7 +82,7 @@
unsigned *alternate_count /* IN/OUT. May be NULL. */,
hb_codepoint_t *alternate_glyphs /* OUT. May be NULL. */) const
{
- if (alternates.len && alternate_count)
+ if (alternates.len && alternate_count && alternate_glyphs)
{
+ alternates.as_array ().sub_array (start_offset, alternate_count)
| hb_sink (hb_array (alternate_glyphs, *alternate_count))
diff --git a/src/hb-aat-layout-feat-table.hh b/src/hb-aat-layout-feat-table.hh
index 4fbec33..2f02de3 100644
--- a/src/hb-aat-layout-feat-table.hh
+++ b/src/hb-aat-layout-feat-table.hh
@@ -114,7 +114,7 @@
if (pdefault_index)
*pdefault_index = default_index;
- if (selectors_count)
+ if (selectors_count && selectors)
{
+ settings_table.sub_array (start_offset, selectors_count)
| hb_map ([=] (const SettingName& setting) { return setting.get_info (default_selector); })
@@ -168,7 +168,7 @@
unsigned int *count,
hb_aat_layout_feature_type_t *features) const
{
- if (count)
+ if (count && features)
{
+ namesZ.as_array (featureNameCount).sub_array (start_offset, count)
| hb_map (&FeatureName::get_feature_type)
diff --git a/src/hb-ot-layout-common.hh b/src/hb-ot-layout-common.hh
index 09326da..17f5990 100644
--- a/src/hb-ot-layout-common.hh
+++ b/src/hb-ot-layout-common.hh
@@ -628,7 +628,7 @@
unsigned
get_characters (unsigned start_offset, unsigned *char_count, hb_codepoint_t *chars) const
{
- if (char_count)
+ if (char_count && chars)
{
+ characters.as_array ().sub_array (start_offset, char_count)
| hb_sink (hb_array (chars, *char_count))
diff --git a/src/hb-ot-meta-table.hh b/src/hb-ot-meta-table.hh
index 658db58..5365774 100644
--- a/src/hb-ot-meta-table.hh
+++ b/src/hb-ot-meta-table.hh
@@ -83,7 +83,7 @@
unsigned int *count,
hb_ot_meta_tag_t *entries) const
{
- if (count)
+ if (count && entries)
{
+ table->dataMaps.as_array ().sub_array (start_offset, count)
| hb_map (&DataMap::get_tag)
diff --git a/test/api/meson.build b/test/api/meson.build
index 51d0a01..7e91fa0 100644
--- a/test/api/meson.build
+++ b/test/api/meson.build
@@ -34,6 +34,7 @@
'test-ot-layout.c',
'test-ot-ligature-carets.c',
'test-ot-name.c',
+ 'test-ot-nullable-output.c',
'test-ot-math.c',
'test-ot-meta.c',
'test-ot-metrics.c',
diff --git a/test/api/test-ot-nullable-output.c b/test/api/test-ot-nullable-output.c
new file mode 100644
index 0000000..56e6813
--- /dev/null
+++ b/test/api/test-ot-nullable-output.c
@@ -0,0 +1,131 @@
+/*
+ * Copyright © 2026 Behdad Esfahbod
+ *
+ * This is part of HarfBuzz, a text shaping library.
+ *
+ * Permission is hereby granted, without written agreement and without
+ * license or royalty fees, to use, copy, modify, and distribute this
+ * software and its documentation for any purpose, provided that the
+ * above copyright notice and the following two paragraphs appear in
+ * all copies of this software.
+ *
+ * IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
+ * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
+ * IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ * THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
+ * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ * FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+
+#include "hb-test.h"
+
+#include <hb-ot.h>
+#include <hb-aat.h>
+
+/* Regression test for GHSA-v5mv-8hhw-vff8: the "list getter" APIs document
+ * their output array as nullable ("may be NULL"; pass NULL to just query the
+ * count). Passing a NULL output array together with a non-NULL count on a
+ * populated table used to write through the NULL pointer and crash. Each case
+ * below calls the getter with a non-NULL count and a NULL output array on a
+ * font whose table is populated, and checks it returns the total without
+ * crashing. */
+
+static void
+test_nullable_output_cpal_colors (void)
+{
+ hb_face_t *face = hb_test_open_font_file ("fonts/cpal-v0.ttf");
+ unsigned int count = 100;
+ g_assert_cmpuint (hb_ot_color_palette_get_colors (face, 0, 0, &count, NULL), ==, 2);
+ hb_face_destroy (face);
+}
+
+static void
+test_nullable_output_colr_layers (void)
+{
+ hb_face_t *face = hb_test_open_font_file ("fonts/cpal-v1.ttf");
+ unsigned int count = 100;
+ g_assert_cmpuint (hb_ot_color_glyph_get_layers (face, 2, 0, &count, NULL), ==, 2);
+ hb_face_destroy (face);
+}
+
+static void
+test_nullable_output_meta_entries (void)
+{
+ hb_face_t *face = hb_test_open_font_file ("fonts/meta.ttf");
+ unsigned int count = 100;
+ unsigned int total = hb_ot_meta_get_entry_tags (face, 0, &count, NULL);
+ g_assert_cmpuint (total, >, 0);
+ hb_face_destroy (face);
+}
+
+#ifndef HB_NO_AAT
+static void
+test_nullable_output_aat_feature_types (void)
+{
+ hb_face_t *face = hb_test_open_font_file ("fonts/aat-feat.ttf");
+ unsigned int count = 100;
+ g_assert_cmpuint (hb_aat_layout_get_feature_types (face, 0, &count, NULL), ==, 11);
+ hb_face_destroy (face);
+}
+
+static void
+test_nullable_output_aat_selector_infos (void)
+{
+ hb_face_t *face = hb_test_open_font_file ("fonts/aat-feat.ttf");
+ unsigned int count = 100;
+ g_assert_cmpuint (hb_aat_layout_feature_type_get_selector_infos (
+ face, HB_AAT_LAYOUT_FEATURE_TYPE_DESIGN_COMPLEXITY_TYPE,
+ 0, &count, NULL, NULL), ==, 4);
+ hb_face_destroy (face);
+}
+#endif
+
+static void
+test_nullable_output_cv_characters (void)
+{
+ hb_face_t *face = hb_test_open_font_file ("fonts/cv01.otf");
+ unsigned int feature_index;
+ if (hb_ot_layout_language_find_feature (face, HB_OT_TAG_GSUB, 0,
+ HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX,
+ HB_TAG ('c','v','0','1'), &feature_index))
+ {
+ unsigned int count = 100;
+ g_assert_cmpuint (hb_ot_layout_feature_get_characters (face, HB_OT_TAG_GSUB,
+ feature_index, 0,
+ &count, NULL), ==, 2);
+ }
+ hb_face_destroy (face);
+}
+
+static void
+test_nullable_output_gsub_alternates (void)
+{
+ hb_face_t *face = hb_test_open_font_file ("fonts/SourceSansPro-Regular.otf");
+ unsigned int count = 100;
+ g_assert_cmpuint (hb_ot_layout_lookup_get_glyph_alternates (face, 1, 1091, 0,
+ &count, NULL), ==, 7);
+ hb_face_destroy (face);
+}
+
+int
+main (int argc, char **argv)
+{
+ hb_test_init (&argc, &argv);
+
+ hb_test_add (test_nullable_output_cpal_colors);
+ hb_test_add (test_nullable_output_colr_layers);
+ hb_test_add (test_nullable_output_meta_entries);
+#ifndef HB_NO_AAT
+ hb_test_add (test_nullable_output_aat_feature_types);
+ hb_test_add (test_nullable_output_aat_selector_infos);
+#endif
+ hb_test_add (test_nullable_output_cv_characters);
+ hb_test_add (test_nullable_output_gsub_alternates);
+
+ return hb_test_run ();
+}