[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