[subset] Reject condition pinning with subnormal region scalars A pinned float region product can be nonzero but subnormal. Multiplying by a retained axis may then round it to zero, while positive rescaling of the residual condition removes that threshold and selects a different substitution after partial instancing. Detect these source regions and reject affected variable conditions instead of emitting a false activation. Keep full instancing and ordinary fractional partial instancing working. Avoid scanning region axes when their count cannot produce a subnormal product, and handle set OOM. Add a valid 20-axis fixture and API regression. The partial request incorrectly succeeds before the fix; controls check source/full output and an ordinary partial request. The full Meson suite passes: 272 tests, one skip, no failures. Assisted-by: OpenAI Codex
diff --git a/src/hb-ot-layout-common.hh b/src/hb-ot-layout-common.hh index a7cd032..68970e9 100644 --- a/src/hb-ot-layout-common.hh +++ b/src/hb-ot-layout-common.hh
@@ -2842,6 +2842,12 @@ return_trace (true); } + hb_array_t<const VarRegionAxis> get_region_axes (unsigned region_index) const + { + if (unlikely (region_index >= regionCount)) return {}; + return hb_array (axesZ.arrayZ + region_index * axisCount, (unsigned) axisCount); + } + bool get_var_region (unsigned region_index, const hb_map_t& axes_old_index_tag_map, hb_hashmap_t<hb_tag_t, Triple>& axis_tuples /* OUT */) const
diff --git a/src/hb-ot-var-common.hh b/src/hb-ot-var-common.hh index 9f5bdd3..dc324b9 100644 --- a/src/hb-ot-var-common.hh +++ b/src/hb-ot-var-common.hh
@@ -1876,6 +1876,30 @@ auto &condition_map = plan->layout_condition_idx_value_map; condition_map.clear (); if (!plan->layout_condition_values) return true; + // A subnormal pinned scalar can round to zero at some retained locations. + // Positive rescaling cannot preserve that source underflow boundary. + const VarRegionList ®ions = varStore.get_region_list (); + hb_set_t subnormal_regions; + // Eight positive F2DOT14 factors cannot reach the float subnormal range. + if (regions.axisCount > 8) + for (unsigned r = 0; r < regions.regionCount; r++) + { + float scalar = 1.f; + bool variable = false; + auto axes = regions.get_region_axes (r); + for (unsigned i = 0; i < axes.length; i++) + { + const Triple *limit; + hb_tag_t tag = plan->axes_old_index_tag_map.get (i); + if (plan->axes_location.has (tag, &limit) && limit->is_point ()) + scalar *= axes[i].evaluate (plan->normalized_coords[i]); + else + variable |= axes[i].peakCoord.to_int () != 0; + } + if (variable && scalar != 0.f && scalar < FLT_MIN) + subnormal_regions.add (r); + } + if (unlikely (subnormal_regions.in_error ())) return false; using condition_value_t = hb_pair_t<uintptr_t, hb_pair_t<unsigned, int>>; hb_vector_t<condition_value_t> conditions; for (auto condition : plan->layout_condition_values.iter ()) @@ -1924,6 +1948,13 @@ continue; } + const VarData &source_data = varStore.get_sub_table (old_outer); + if (!subnormal_regions.is_empty ()) + for (unsigned column = 0; column < source_data.get_region_index_count (); column++) + if (subnormal_regions.has (source_data.get_region_index (column)) && + source_data.get_item_delta (old_idx & 0xFFFF, column)) + return false; + double scale = 1.; while (maximum * scale > INT_MAX) scale *= .5;
diff --git a/test/api/fonts/feature-variation-subnormal.ttf b/test/api/fonts/feature-variation-subnormal.ttf new file mode 100644 index 0000000..5ccee6f --- /dev/null +++ b/test/api/fonts/feature-variation-subnormal.ttf Binary files differ
diff --git a/test/api/test-subset.c b/test/api/test-subset.c index 2a4bb52..c734beb 100644 --- a/test/api/test-subset.c +++ b/test/api/test-subset.c
@@ -298,6 +298,61 @@ } static void +test_subset_condition_value_subnormal (void) +{ + hb_face_t *face = hb_test_open_font_file ("fonts/feature-variation-subnormal.ttf"); + /* One positive 20-axis region, delta 1, and a format-2 condition with + * default 0. The first 18 pins contribute 2^-144; the last contributes + * 2^-5, leaving the smallest nonzero float. At ZZZZ=1/4 the original + * scalar rounds to zero, but rescaling a residual row would make it true. */ + for (unsigned mode = 0; mode < 3; mode++) + { + hb_subset_input_t *input = hb_subset_input_create_or_fail (); + hb_subset_input_set_flags (input, HB_SUBSET_FLAGS_RETAIN_GIDS); + hb_set_add_range (hb_subset_input_glyph_set (input), 0, 3); + int coords[20]; + for (unsigned i = 0; i < 19; i++) + { + hb_tag_t tag = HB_TAG ('A', '0', '0' + i / 10, '0' + i % 10); + float value = mode == 2 ? 0.5f : i == 18 ? 0.03125f : 0.00390625f; + coords[i] = (int) (value * 16384); + g_assert_true (hb_subset_input_pin_axis_location (input, face, tag, value)); + } + coords[19] = 4096; + if (mode == 1) + g_assert_true (hb_subset_input_pin_axis_location ( + input, face, HB_TAG ('Z','Z','Z','Z'), 0.25f)); + hb_face_t *subset = hb_subset_or_fail (face, input); + if (mode) + { + g_assert_nonnull (subset); + hb_font_t *fonts[] = {hb_font_create (face), hb_font_create (subset)}; + hb_font_set_var_coords_normalized (fonts[0], coords, 20); + if (mode == 2) + hb_font_set_var_coords_normalized (fonts[1], &coords[19], 1); + for (unsigned i = 0; i < 2; i++) + { + hb_buffer_t *buffer = hb_buffer_create (); + hb_buffer_add_utf8 (buffer, "A", -1, 0, -1); + hb_buffer_guess_segment_properties (buffer); + hb_shape (fonts[i], buffer, NULL, 0); + unsigned length; + const hb_glyph_info_t *info = hb_buffer_get_glyph_infos (buffer, &length); + g_assert_cmpuint (length, ==, 1); + g_assert_cmpuint (info[0].codepoint, ==, mode == 1 ? 2 : 3); + hb_buffer_destroy (buffer); + hb_font_destroy (fonts[i]); + } + } + else + g_assert_null (subset); + hb_face_destroy (subset); + hb_subset_input_destroy (input); + } + hb_face_destroy (face); +} + +static void test_subset_condition_value_precision (void) { const char *filenames[] = { @@ -991,6 +1046,7 @@ hb_test_add (test_subset_lookup_variations); hb_test_add (test_subset_lookup_variation_fractional_value); hb_test_add (test_subset_condition_value_precision); + hb_test_add (test_subset_condition_value_subnormal); hb_test_add (test_subset_feature_variation_conditions); hb_test_add (test_subset_feature_variation_universal); #endif