[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 &regions = 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