[fontations] Fix OOB write on glyph-name callback with size == 0 (#5946) (#5947)
* [fontations] Fix OOB write on glyph-name callback with size == 0
The Rust `_hb_fontations_glyph_name()` callback computed
`glyph_name.len().min(size as usize - 1)`. When a caller passed
`size == 0`, `size as usize - 1` underflowed a `usize` to
`usize::MAX`, the `min()` clamp collapsed to `glyph_name.len()`,
and the subsequent `copy_nonoverlapping` / trailing NUL wrote
`glyph_name.len() + 1` bytes into a zero-capacity caller buffer.
This is an out-of-bounds write reachable from the public API
whenever the build was configured with `-Dfontations=enabled`
and the application had called `hb_fontations_font_set_funcs()`.
Short-circuit the `size == 0` case before the subtraction.
Return `true` in that case to match the existing post and CFF1
backends (`hb-ot-post-table.hh`, `hb-ot-cff1-table.hh`), which
both early-out with `return true` on `!buf_len` — the glyph
does have a name; we simply had no buffer to write it into.
Regression tests added in `test/api/test-glyph-names.c` cover
the zero-size path on post and CFF1 backends, a sub-font, a
NULL buffer, and `size == 1` / small-buffer / exact-fit cases
so the truncation path does not silently regress.
Fixes: https://github.com/harfbuzz/harfbuzz/issues/5946
Assisted-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [font] Reject glyph-name lookups with size == 0 at the wrapper
`hb_font_t::get_glyph_name()` previously pre-wrote the trailing
NUL only when `size != 0` but still dispatched to the backend
callback with `size == 0`. Every current glyph-name backend has
to re-validate that argument: `post` and `cff1` short-circuit
with `return true`, the new Rust `fontations` callback now
short-circuits too (prior commit), and the CoreText callback in
`hb-coretext-font.cc` has a structurally similar `(CFIndex)size
- 1` pattern.
Normalize this at the wrapper layer: return `false` immediately
on `size == 0`, before any backend dispatch and before the
`*name = '\0'` write. This makes the zero-size case a single,
auditable no-op for every glyph-name provider (native or Rust,
current or future) and removes the CoreText underflow from the
reachable surface through the same chokepoint.
Observable change: callers that invoked
`hb_font_get_glyph_name(font, gid, buf, 0)` on post/cff1 backed
fonts used to get `true` back; they now get `false`. `size == 0`
queries carry no useful information for the caller, so this is
considered a safe hardening rather than a contract break.
Assisted-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [font] Preserve glyph-name zero-size probe semantics
Keep `hb_font_get_glyph_name(..., NULL, 0)` as a valid
return-value-only probe instead of rejecting `size == 0` in the
wrapper.
Fix the remaining backend-specific zero-size paths in hb-ft and
hb-coretext-font so they answer the probe without dereferencing the
caller buffer, and update the API tests to assert the preserved
behavior.
Testing: CCACHE_DISABLE=1 meson test -C build test-glyph-names
Testing: CCACHE_DISABLE=1 meson test -C build test-ft
Assisted-by: OpenAI Codex <codex@openai.com>
---------
Co-authored-by: jeffhuang <jeff@docker.xydrsucermoubd24xgo33yhsgd.bx.internal.cloudapp.net>
diff --git a/src/hb-coretext-font.cc b/src/hb-coretext-font.cc
index 939e620..9f46928 100644
--- a/src/hb-coretext-font.cc
+++ b/src/hb-coretext-font.cc
@@ -409,6 +409,12 @@
if (!cf_name)
return false;
+ if (!size)
+ {
+ CFRelease (cf_name);
+ return true;
+ }
+
CFIndex len = CFStringGetLength (cf_name);
if (len > (CFIndex)size - 1)
len = (CFIndex)size - 1;
diff --git a/src/hb-ft.cc b/src/hb-ft.cc
index 5fef168..8ac5db7 100644
--- a/src/hb-ft.cc
+++ b/src/hb-ft.cc
@@ -766,6 +766,12 @@
hb_lock_t lock (ft_font->lock);
FT_Face ft_face = ft_font->ft_face;
+ if (!size)
+ {
+ char buf[128];
+ return !FT_Get_Glyph_Name (ft_face, glyph, buf, sizeof (buf)) && *buf;
+ }
+
hb_bool_t ret = !FT_Get_Glyph_Name (ft_face, glyph, name, size);
if (ret && (size && !*name))
ret = false;
diff --git a/src/rust/font.rs b/src/rust/font.rs
index 8e6b526..efe0c7c 100644
--- a/src/rust/font.rs
+++ b/src/rust/font.rs
@@ -981,9 +981,12 @@
let data = unsafe { &mut *(font_data as *mut FontationsData) };
if let Some(glyph_name) = data.glyph_names.get(GlyphId::new(glyph)) {
+ if size == 0 {
+ return true as hb_bool_t;
+ }
let glyph_name = glyph_name.as_str();
// Copy the glyph name into the buffer, up to size-1 bytes
- let len = glyph_name.len().min(size as usize - 1);
+ let len = glyph_name.len().min((size as usize) - 1);
unsafe {
std::slice::from_raw_parts_mut(name as *mut u8, len)
.copy_from_slice(&glyph_name.as_bytes()[..len]);
diff --git a/test/api/test-ft.c b/test/api/test-ft.c
index ae792cf..e32e0b6 100644
--- a/test/api/test-ft.c
+++ b/test/api/test-ft.c
@@ -80,7 +80,7 @@
init_freetype ();
- ft_face = get_ft_face ("fonts/Cantarell.A.otf");
+ ft_face = get_ft_face ("fonts/adwaita.ttf");
g_assert_nonnull (ft_face);
g_assert_nonnull (FT_Get_Font_Format (ft_face));
@@ -135,6 +135,77 @@
cleanup_freetype ();
}
+static void
+test_native_ft_glyph_name_zero_size_probe (void)
+{
+ static const char *files[] = {
+ "fonts/adwaita.ttf",
+ "fonts/SourceSansPro-Regular.otf",
+ "fonts/Cantarell.A.otf",
+ };
+ FT_Face ft_face = NULL;
+ hb_font_t *font = NULL;
+ hb_bool_t ret;
+ hb_codepoint_t glyph = 0;
+ char name[64];
+ char guard[4] = { 0x7f, 0x7f, 0x7f, 0x7f };
+ unsigned int i;
+
+ init_freetype ();
+
+ for (i = 0; i < G_N_ELEMENTS (files); i++)
+ {
+ unsigned int gid;
+
+ ft_face = get_ft_face (files[i]);
+ g_assert_nonnull (ft_face);
+
+ font = hb_ft_font_create_referenced (ft_face);
+ g_assert_nonnull (font);
+ hb_ft_font_set_funcs (font);
+
+ for (gid = 0; gid < (unsigned int) ft_face->num_glyphs; gid++)
+ if (hb_font_get_glyph_name (font, gid, name, sizeof (name)))
+ {
+ glyph = gid;
+ goto found;
+ }
+
+ hb_font_destroy (font);
+ font = NULL;
+ FT_Done_Face (ft_face);
+ ft_face = NULL;
+ }
+
+ g_test_skip ("No FreeType glyph-name test font available");
+ cleanup_freetype ();
+ return;
+
+found:
+
+ ret = hb_font_get_glyph_name (font, glyph, name, sizeof (name));
+ g_assert_true (ret);
+ g_assert_true (*name);
+
+ ret = hb_font_get_glyph_name (font, glyph, NULL, 0);
+ g_assert_true (ret);
+
+ ret = hb_font_get_glyph_name (font, glyph, guard, 0);
+ g_assert_true (ret);
+ g_assert_cmpint (guard[0], ==, 0x7f);
+ g_assert_cmpint (guard[1], ==, 0x7f);
+ g_assert_cmpint (guard[2], ==, 0x7f);
+ g_assert_cmpint (guard[3], ==, 0x7f);
+
+ ret = hb_font_get_glyph_name (font, 0xFFFFFF, NULL, 0);
+ g_assert_false (ret);
+
+ hb_font_destroy (font);
+ FT_Done_Face (ft_face);
+
+ cleanup_freetype ();
+}
+
int
main (int argc, char **argv)
{
@@ -142,6 +213,7 @@
hb_test_add (test_native_ft_basic);
hb_test_add (test_native_ft_set_funcs_preserves_load_flags);
+ hb_test_add (test_native_ft_glyph_name_zero_size_probe);
return hb_test_run ();
}
diff --git a/test/api/test-glyph-names.c b/test/api/test-glyph-names.c
index 48ca06b..50dd104 100644
--- a/test/api/test-glyph-names.c
+++ b/test/api/test-glyph-names.c
@@ -100,6 +100,277 @@
hb_face_destroy (face);
}
+/* Regression tests for issue #5946: hb_font_get_glyph_name (..., size=0)
+ * must not underflow and write past a zero-capacity buffer, while preserving
+ * the long-standing "return value only" probe behavior. */
+
+static void
+test_glyph_names_zero_size (void)
+{
+ hb_face_t *face;
+ hb_font_t *font;
+ char guard[4] = { 0x7f, 0x7f, 0x7f, 0x7f };
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ /* size == 0 must not write to the buffer. */
+ g_assert_true (hb_font_get_glyph_name (font, 1, guard, 0));
+ g_assert_cmpint (guard[0], ==, 0x7f);
+ g_assert_cmpint (guard[1], ==, 0x7f);
+ g_assert_cmpint (guard[2], ==, 0x7f);
+ g_assert_cmpint (guard[3], ==, 0x7f);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_zero_size_post (void)
+{
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char guard[8] = { 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f };
+ unsigned int i;
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ /* Named glyph, size == 0: keep the old return-value-only probe behavior
+ * and leave the buffer untouched. */
+ ret = hb_font_get_glyph_name (font, 1, guard, 0);
+ g_assert_true (ret);
+ for (i = 0; i < 8; i++)
+ g_assert_cmpint (guard[i], ==, 0x7f);
+
+ /* .notdef, size == 0: same invariant. Confirms it is not glyph-specific. */
+ ret = hb_font_get_glyph_name (font, 0, guard, 0);
+ g_assert_true (ret);
+ for (i = 0; i < 8; i++)
+ g_assert_cmpint (guard[i], ==, 0x7f);
+
+ /* Perform a successful sized lookup first, then retry with size == 0.
+ * Confirms no residual state corrupts the zero-size path. */
+ {
+ char name[64];
+ ret = hb_font_get_glyph_name (font, 2, name, 64);
+ g_assert_true (ret);
+ g_assert_cmpstr (name, ==, "icon0");
+ }
+ ret = hb_font_get_glyph_name (font, 2, guard, 0);
+ g_assert_true (ret);
+ for (i = 0; i < 8; i++)
+ g_assert_cmpint (guard[i], ==, 0x7f);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_zero_size_cff (void)
+{
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char guard[8] = { 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f };
+ unsigned int i;
+
+ face = hb_test_open_font_file ("fonts/SourceSansPro-Regular.otf");
+ font = hb_font_create (face);
+
+ /* CFF-backed named glyph, size == 0: preserve the return-value-only probe
+ * behavior without writing into the buffer. */
+ ret = hb_font_get_glyph_name (font, 2, guard, 0);
+ g_assert_true (ret);
+ for (i = 0; i < 8; i++)
+ g_assert_cmpint (guard[i], ==, 0x7f);
+
+ /* .notdef on CFF. */
+ ret = hb_font_get_glyph_name (font, 0, guard, 0);
+ g_assert_true (ret);
+ for (i = 0; i < 8; i++)
+ g_assert_cmpint (guard[i], ==, 0x7f);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_zero_size_unnamed_glyph (void)
+{
+ /* size == 0 on a glyph beyond the font's range must also return false
+ * and must not write. */
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char guard[4] = { 0x7f, 0x7f, 0x7f, 0x7f };
+ unsigned int i;
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ ret = hb_font_get_glyph_name (font, 100, guard, 0);
+ g_assert_false (ret);
+ for (i = 0; i < 4; i++)
+ g_assert_cmpint (guard[i], ==, 0x7f);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_zero_size_null_buffer (void)
+{
+ /* name == NULL with size == 0 is a valid return-value-only probe. */
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ /* Glyph with a name. */
+ ret = hb_font_get_glyph_name (font, 1, NULL, 0);
+ g_assert_true (ret);
+
+ /* Glyph beyond the font's range. */
+ ret = hb_font_get_glyph_name (font, 100, NULL, 0);
+ g_assert_false (ret);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_size_one (void)
+{
+ /* Boundary: size == 1. The backend must produce exactly a trailing NUL
+ * at name[0] (len = min(|name|, size-1) = 0 bytes of name copied) and
+ * must not touch any byte past name[0]. */
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char buf[4] = { 0x7f, 0x7f, 0x7f, 0x7f };
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ ret = hb_font_get_glyph_name (font, 1, buf, 1);
+ g_assert_true (ret);
+ g_assert_cmpint (buf[0], ==, 0);
+ /* Bytes beyond the caller-declared capacity must remain intact. */
+ g_assert_cmpint (buf[1], ==, 0x7f);
+ g_assert_cmpint (buf[2], ==, 0x7f);
+ g_assert_cmpint (buf[3], ==, 0x7f);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_small_buffer_truncation (void)
+{
+ /* Non-regression sanity: a small-but-nonzero buffer must truncate to
+ * size-1 bytes of the name plus a trailing NUL. */
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char buf[4] = { 0x7f, 0x7f, 0x7f, 0x7f };
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ /* ".notdef" truncated to capacity 4 -> ".no\0". */
+ ret = hb_font_get_glyph_name (font, 0, buf, 4);
+ g_assert_true (ret);
+ g_assert_cmpint (buf[0], ==, '.');
+ g_assert_cmpint (buf[1], ==, 'n');
+ g_assert_cmpint (buf[2], ==, 'o');
+ g_assert_cmpint (buf[3], ==, 0);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_exact_fit_buffer (void)
+{
+ /* Non-regression sanity: a buffer exactly sized to fit the full name
+ * (including the trailing NUL) must hold the full name. For ".notdef"
+ * that is 8 bytes. */
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char buf[8] = { 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f, 0x7f };
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ ret = hb_font_get_glyph_name (font, 0, buf, 8);
+ g_assert_true (ret);
+ g_assert_cmpstr (buf, ==, ".notdef");
+ g_assert_cmpint (buf[7], ==, 0);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_nonzero_sizes_unchanged (void)
+{
+ /* Non-regression sanity: the existing behaviour at typical sizes must
+ * be preserved by the fix. */
+ hb_face_t *face;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char name[64];
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ font = hb_font_create (face);
+
+ ret = hb_font_get_glyph_name (font, 0, name, 64);
+ g_assert_true (ret);
+ g_assert_cmpstr (name, ==, ".notdef");
+
+ ret = hb_font_get_glyph_name (font, 1, name, 64);
+ g_assert_true (ret);
+ g_assert_cmpstr (name, ==, ".space");
+
+ /* Beyond last glyph still returns false. */
+ ret = hb_font_get_glyph_name (font, 100, name, 64);
+ g_assert_false (ret);
+
+ hb_font_destroy (font);
+ hb_face_destroy (face);
+}
+
+static void
+test_glyph_names_zero_size_sub_font (void)
+{
+ /* Sub-fonts delegate to the parent's glyph-name funcs. Preserve the
+ * return-value-only probe behavior through the parent chain as well. */
+ hb_face_t *face;
+ hb_font_t *parent;
+ hb_font_t *font;
+ hb_bool_t ret;
+ char guard[4] = { 0x7f, 0x7f, 0x7f, 0x7f };
+ unsigned int i;
+
+ face = hb_test_open_font_file ("fonts/adwaita.ttf");
+ parent = hb_font_create (face);
+ font = hb_font_create_sub_font (parent);
+
+ ret = hb_font_get_glyph_name (font, 1, guard, 0);
+ g_assert_true (ret);
+ for (i = 0; i < 4; i++)
+ g_assert_cmpint (guard[i], ==, 0x7f);
+
+ hb_font_destroy (font);
+ hb_font_destroy (parent);
+ hb_face_destroy (face);
+}
+
int
main (int argc, char **argv)
{
@@ -107,6 +378,16 @@
hb_test_add (test_glyph_names_post);
hb_test_add (test_glyph_names_cff);
+ hb_test_add (test_glyph_names_zero_size);
+ hb_test_add (test_glyph_names_zero_size_post);
+ hb_test_add (test_glyph_names_zero_size_cff);
+ hb_test_add (test_glyph_names_zero_size_unnamed_glyph);
+ hb_test_add (test_glyph_names_zero_size_null_buffer);
+ hb_test_add (test_glyph_names_size_one);
+ hb_test_add (test_glyph_names_small_buffer_truncation);
+ hb_test_add (test_glyph_names_exact_fit_buffer);
+ hb_test_add (test_glyph_names_nonzero_sizes_unchanged);
+ hb_test_add (test_glyph_names_zero_size_sub_font);
return hb_test_run();
}