[SCM] PostGIS branch master updated. 3.7.0beta2-48-g3e1eba5f5
git at osgeo.org
git at osgeo.org
Sat Aug 15 09:58:26 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 3e1eba5f5181f357f7d32309f0ea4132247103d5 (commit)
via ef96a35be0f3b5a7978133c07e0bb276d20e3d33 (commit)
from 40b61a5fc02f0bb2d07be263e180e7f24c6c5a95 (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 3e1eba5f5181f357f7d32309f0ea4132247103d5
Merge: 40b61a5fc ef96a35be
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date: Sat Aug 15 09:58:25 2026 -0700
Merge pull request 'liblwgeom: validate GSERIALIZED ring counts against buffer, not geometry validity' (!741) from Komzpa/postgis:fix/gserialized-ringcount-buffer-guard-20260815 into master
Pull request [724](https://gitea.osgeo.org/postgis/postgis/pulls/724) added `gserialized{1,2}_validate_polygon_ring_count()`, rejecting any non-empty polygon ring whose declared point count is 1, 2 or 3, to guard against OSSFuzz 544800490. That guard duplicates, and is stricter than, `gserialized{1,2}_validate_geometry_buffer()`, introduced two weeks earlier in commit c1f681540 ("Harden GSERIALIZED payload readers"). The earlier validator already walks every ring's declared point count with overflow-checked arithmetic and rejects any count whose implied byte size overflows or exceeds the remaining buffer, recursively for collection members, before `lwpoly_from_gserialized{1,2}_buffer` ever reads a ring, and that reader is static with no other caller — so by the time a ring is walked, its buffer bounds are already proven safe.
The added guard therefore contributed no memory safety and instead rejected geometrically invalid but memory-safe polygons that PostGIS has always accepted at deserialization time: 1-, 2- and 3-point rings built by GeoJSON input (https://trac.osgeo.org/postgis/ticket/4470) and legacy paths pinned by https://trac.osgeo.org/postgis/ticket/408. This change removes the redundant per-ring point-count guard and its call sites in both GSERIALIZED layouts, restoring pre-724 deserialization behavior while leaving `gserialized{1,2}_validate_geometry_buffer()`, and the fuzz-safety invariant it enforces, untouched.
Un-breaks regress/core/clean test 9, regress/core/geos310 test 31, regress/core/tickets tests 408.3/408.4, regress/core/in_geojson tests 4470.a/4470.c, and the "postgis extension upgrade 3.3.9--3.7.0dev" regression, all red on master since Woodpecker pipeline 7002. PR 724 was not backpatched to stable-3.6 or stable-3.5, so no NEWS entry and no stable-branch action is needed.
Repurposes the malformed-ring-count CUnit tests in cu_gserialized{1,2}.c to craft a ring count whose implied byte size exceeds the buffer, which is the actual OSSFuzz 544800490 vector, and adds short-ring round-trip coverage for 1- and 3-point rings. Full CUnit suite green (376 tests, 5908 asserts); the four regress tests and the upgrade path validated red at 89cd60efd and green after the fix.
References https://gitea.osgeo.org/postgis/postgis/pulls/724
References https://trac.osgeo.org/postgis/ticket/408
References https://trac.osgeo.org/postgis/ticket/4470
Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/741
commit ef96a35be0f3b5a7978133c07e0bb276d20e3d33
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Sat Aug 15 20:17:43 2026 +0400
liblwgeom: validate GSERIALIZED ring counts against buffer, not geometry validity
Pull request 724 added gserialized{1,2}_validate_polygon_ring_count(),
rejecting any non-empty polygon ring whose declared point count is 1,
2 or 3, to guard against OSSFuzz 544800490. That guard is redundant
with, and stricter than, gserialized{1,2}_validate_geometry_buffer(),
introduced two weeks earlier in commit c1f681540 ("Harden GSERIALIZED
payload readers"). That function already walks every ring's declared
point count with overflow-checked arithmetic and rejects any count
whose implied byte size overflows or exceeds the remaining buffer,
and it runs on the whole geometry tree, recursively for collection
members, before lwpoly_from_gserialized{1,2}_buffer ever reads a
ring. lwpoly_from_gserialized{1,2}_buffer is static and has no other
caller, so by the time it walks a ring, the buffer bounds for that
ring are already proven safe.
The added guard therefore contributed no memory safety and instead
rejected geometrically invalid but memory-safe polygons that PostGIS
has always accepted at deserialization time: 1-, 2- and 3-point rings
built by GeoJSON input (Trac #4470), ST_IsValid intermediates, and
other legacy paths (Trac #408). This narrows the check back to what
buffer-bounds validation already guarantees by removing the redundant
per-ring point-count guard and its call sites in both GSERIALIZED
layouts, restoring pre-724 deserialization behavior while leaving
gserialized{1,2}_validate_geometry_buffer(), and the fuzz-safety
invariant it enforces, untouched.
Un-breaks regress/core/clean test 9, regress/core/geos310 test 31,
regress/core/tickets tests 408.3/408.4 and regress/core/in_geojson
tests 4470.a/4470.c, and the postgis extension upgrade
3.3.9--3.7.0dev regression, all broken by 724 on master since
Woodpecker pipeline 7002. Confirmed not backpatched to stable-3.6 or
stable-3.5.
Repurposes cu_gserialized{1,2}.c's malformed-ring-count CUnit test to
craft a ring count whose implied byte size exceeds the buffer, the
actual OSSFuzz 544800490 vector, and adds a short-ring round-trip test
covering 1- and 3-point rings.
References https://gitea.osgeo.org/postgis/postgis/pulls/724
References https://trac.osgeo.org/postgis/ticket/408
References https://trac.osgeo.org/postgis/ticket/4470
diff --git a/liblwgeom/cunit/cu_gserialized1.c b/liblwgeom/cunit/cu_gserialized1.c
index 1153c1a8b..58c7cd373 100644
--- a/liblwgeom/cunit/cu_gserialized1.c
+++ b/liblwgeom/cunit/cu_gserialized1.c
@@ -1307,6 +1307,13 @@ gserialized1_test_payload_p(GSERIALIZED *g)
return (uint8_t *)(void *)g + gserialized1_header_size(g);
}
+/*
+ * A ring point count that claims far more points than the serialized
+ * buffer actually holds implies a read past the end of the payload; that
+ * is the memory-safety hazard OSSFuzz 544800490 found, and it must stay
+ * rejected regardless of whether the claimed count is itself a
+ * geometrically plausible ring size.
+ */
static void
test_gserialized1_malformed_polygon_ring_count(void)
{
@@ -1316,7 +1323,7 @@ test_gserialized1_malformed_polygon_ring_count(void)
uint8_t *payload;
uint32_t type;
uint32_t nrings;
- uint32_t npoints = 1;
+ uint32_t npoints = 0x10000000; /* claims ~4GB of ring ordinates */
CU_ASSERT_PTR_NOT_NULL_FATAL(lwgeom);
g = gserialized1_from_lwgeom(lwgeom, NULL);
@@ -1338,6 +1345,51 @@ test_gserialized1_malformed_polygon_ring_count(void)
lwfree(g);
}
+/*
+ * Rings with 1-3 points are geometrically invalid (not simple closed
+ * rings) but memory-safe: their declared point count still fits the
+ * serialized buffer. PostGIS intentionally stores and round-trips such
+ * polygons; ST_IsValid, not deserialization, is where that gets flagged.
+ * See Trac #408 and #4470, and ST_GeomFromGeoJSON's degenerate-ring
+ * handling.
+ */
+static void
+test_gserialized1_short_polygon_ring_roundtrip(void)
+{
+ const char *short_ring_wkt[] = {
+ "POLYGON((0 0))",
+ "POLYGON((0 0, 1 0, 1 1))",
+ };
+ const uint32_t expected_npoints[] = {1, 3};
+ size_t i;
+
+ for (i = 0; i < sizeof(short_ring_wkt) / sizeof(short_ring_wkt[0]); i++)
+ {
+ LWGEOM *lwgeom = lwgeom_from_wkt(short_ring_wkt[i], LW_PARSER_CHECK_NONE);
+ GSERIALIZED *g;
+ LWGEOM *roundtrip;
+ LWPOLY *poly;
+
+ CU_ASSERT_PTR_NOT_NULL_FATAL(lwgeom);
+ g = gserialized1_from_lwgeom(lwgeom, NULL);
+ CU_ASSERT_PTR_NOT_NULL_FATAL(g);
+
+ cu_error_msg_reset();
+ roundtrip = lwgeom_from_gserialized1(g);
+ CU_ASSERT_PTR_NOT_NULL_FATAL(roundtrip);
+ CU_ASSERT_EQUAL(strlen(cu_error_msg), 0);
+
+ CU_ASSERT_EQUAL(roundtrip->type, POLYGONTYPE);
+ poly = (LWPOLY *)roundtrip;
+ CU_ASSERT_EQUAL(poly->nrings, 1);
+ CU_ASSERT_EQUAL(poly->rings[0]->npoints, expected_npoints[i]);
+
+ lwgeom_free(lwgeom);
+ lwgeom_free(roundtrip);
+ lwfree(g);
+ }
+}
+
static void
test_gserialized1_malformed_declared_size(void)
{
@@ -1393,5 +1445,6 @@ void gserialized1_suite_setup(void)
PG_ADD_TEST(suite, test_signum_macro);
PG_ADD_TEST(suite, test_gserialized1_peek_first_point);
PG_ADD_TEST(suite, test_gserialized1_malformed_polygon_ring_count);
+ PG_ADD_TEST(suite, test_gserialized1_short_polygon_ring_roundtrip);
PG_ADD_TEST(suite, test_gserialized1_malformed_declared_size);
}
diff --git a/liblwgeom/cunit/cu_gserialized2.c b/liblwgeom/cunit/cu_gserialized2.c
index 6ee2f877c..49bd3f4d8 100644
--- a/liblwgeom/cunit/cu_gserialized2.c
+++ b/liblwgeom/cunit/cu_gserialized2.c
@@ -589,6 +589,13 @@ test_gserialized2_malformed_nurbs_degree(void)
lwfree(g);
}
+/*
+ * A ring point count that claims far more points than the serialized
+ * buffer actually holds implies a read past the end of the payload; that
+ * is the memory-safety hazard OSSFuzz 544800490 found, and it must stay
+ * rejected regardless of whether the claimed count is itself a
+ * geometrically plausible ring size.
+ */
static void
test_gserialized2_malformed_polygon_ring_count(void)
{
@@ -598,7 +605,7 @@ test_gserialized2_malformed_polygon_ring_count(void)
uint8_t *payload;
uint32_t type;
uint32_t nrings;
- uint32_t npoints = 1;
+ uint32_t npoints = 0x10000000; /* claims ~4GB of ring ordinates */
CU_ASSERT_PTR_NOT_NULL_FATAL(lwgeom);
g = gserialized2_from_lwgeom(lwgeom, NULL);
@@ -620,6 +627,51 @@ test_gserialized2_malformed_polygon_ring_count(void)
lwfree(g);
}
+/*
+ * Rings with 1-3 points are geometrically invalid (not simple closed
+ * rings) but memory-safe: their declared point count still fits the
+ * serialized buffer. PostGIS intentionally stores and round-trips such
+ * polygons; ST_IsValid, not deserialization, is where that gets flagged.
+ * See Trac #408 and #4470, and ST_GeomFromGeoJSON's degenerate-ring
+ * handling.
+ */
+static void
+test_gserialized2_short_polygon_ring_roundtrip(void)
+{
+ const char *short_ring_wkt[] = {
+ "POLYGON((0 0))",
+ "POLYGON((0 0, 1 0, 1 1))",
+ };
+ const uint32_t expected_npoints[] = {1, 3};
+ size_t i;
+
+ for (i = 0; i < sizeof(short_ring_wkt) / sizeof(short_ring_wkt[0]); i++)
+ {
+ LWGEOM *lwgeom = lwgeom_from_wkt(short_ring_wkt[i], LW_PARSER_CHECK_NONE);
+ GSERIALIZED *g;
+ LWGEOM *roundtrip;
+ LWPOLY *poly;
+
+ CU_ASSERT_PTR_NOT_NULL_FATAL(lwgeom);
+ g = gserialized2_from_lwgeom(lwgeom, NULL);
+ CU_ASSERT_PTR_NOT_NULL_FATAL(g);
+
+ cu_error_msg_reset();
+ roundtrip = lwgeom_from_gserialized2(g);
+ CU_ASSERT_PTR_NOT_NULL_FATAL(roundtrip);
+ CU_ASSERT_EQUAL(strlen(cu_error_msg), 0);
+
+ CU_ASSERT_EQUAL(roundtrip->type, POLYGONTYPE);
+ poly = (LWPOLY *)roundtrip;
+ CU_ASSERT_EQUAL(poly->nrings, 1);
+ CU_ASSERT_EQUAL(poly->rings[0]->npoints, expected_npoints[i]);
+
+ lwgeom_free(lwgeom);
+ lwgeom_free(roundtrip);
+ lwfree(g);
+ }
+}
+
static void
test_gserialized2_malformed_nurbs_short_vectors(void)
{
@@ -763,6 +815,7 @@ void gserialized2_suite_setup(void)
PG_ADD_TEST(suite, test_gserialized2_malformed_collection_count);
PG_ADD_TEST(suite, test_gserialized2_malformed_nurbs_degree);
PG_ADD_TEST(suite, test_gserialized2_malformed_polygon_ring_count);
+ PG_ADD_TEST(suite, test_gserialized2_short_polygon_ring_roundtrip);
PG_ADD_TEST(suite, test_gserialized2_malformed_nurbs_short_vectors);
PG_ADD_TEST(suite, test_gserialized2_malformed_declared_size);
PG_ADD_TEST(suite, test_gserialized2_malformed_short_allocation);
diff --git a/liblwgeom/gserialized1.c b/liblwgeom/gserialized1.c
index 2c68f2c71..406f36c3f 100644
--- a/liblwgeom/gserialized1.c
+++ b/liblwgeom/gserialized1.c
@@ -273,17 +273,6 @@ gserialized1_pointarray_payload_size(uint32_t npoints, lwflags_t lwflags, size_t
return LW_SUCCESS;
}
-static int
-gserialized1_validate_polygon_ring_count(uint32_t npoints)
-{
- if (npoints > 0 && npoints < 4)
- {
- lwerror("%s: invalid non-empty polygon ring point count %u", __func__, npoints);
- return LW_FAILURE;
- }
- return LW_SUCCESS;
-}
-
static int
gserialized1_validate_geometry_buffer(uint8_t *data_ptr, uint8_t *data_end, lwflags_t lwflags, size_t *size)
{
@@ -1523,15 +1512,14 @@ static LWPOLY* lwpoly_from_gserialized1_buffer(uint8_t *data_ptr, lwflags_t lwfl
{
uint32_t npoints = 0;
- /* Read in the number of points. */
+ /* Read in the number of points. gserialized1_validate_geometry_buffer() has
+ * already bounds-checked every ring's declared point count against the
+ * remaining buffer and rejected overflowing byte sizes; a ring with fewer
+ * than four points is geometrically invalid but not a memory-safety
+ * problem, and PostGIS intentionally stores and reports such rings (see
+ * Trac #408, #4470). */
npoints = gserialized1_get_uint32_t(data_ptr);
data_ptr += 4;
- if (gserialized1_validate_polygon_ring_count(npoints) == LW_FAILURE)
- {
- poly->nrings = i;
- lwpoly_free(poly);
- return NULL;
- }
/* Make a point array for the ring, and move the ordinate pointer past the ring ordinates. */
poly->rings[i] = ptarray_construct_reference_data(FLAGS_GET_Z(lwflags), FLAGS_GET_M(lwflags), npoints, ordinate_ptr);
diff --git a/liblwgeom/gserialized2.c b/liblwgeom/gserialized2.c
index f0a6bdd7c..f3422ae17 100644
--- a/liblwgeom/gserialized2.c
+++ b/liblwgeom/gserialized2.c
@@ -398,17 +398,6 @@ gserialized2_pointarray_payload_size(uint32_t npoints, lwflags_t lwflags, size_t
return LW_SUCCESS;
}
-static int
-gserialized2_validate_polygon_ring_count(uint32_t npoints)
-{
- if (npoints > 0 && npoints < 4)
- {
- lwerror("%s: invalid non-empty polygon ring point count %u", __func__, npoints);
- return LW_FAILURE;
- }
- return LW_SUCCESS;
-}
-
static int
gserialized2_validate_geometry_buffer(uint8_t *data_ptr, uint8_t *data_end, lwflags_t lwflags, size_t *size)
{
@@ -2067,15 +2056,14 @@ lwpoly_from_gserialized2_buffer(uint8_t *data_ptr, lwflags_t lwflags, size_t *si
{
uint32_t npoints = 0;
- /* Read in the number of points. */
+ /* Read in the number of points. gserialized2_validate_geometry_buffer() has
+ * already bounds-checked every ring's declared point count against the
+ * remaining buffer and rejected overflowing byte sizes; a ring with fewer
+ * than four points is geometrically invalid but not a memory-safety
+ * problem, and PostGIS intentionally stores and reports such rings (see
+ * Trac #408, #4470). */
npoints = gserialized2_get_uint32_t(data_ptr);
data_ptr += 4;
- if (gserialized2_validate_polygon_ring_count(npoints) == LW_FAILURE)
- {
- poly->nrings = i;
- lwpoly_free(poly);
- return NULL;
- }
/* Make a point array for the ring, and move the ordinate pointer past the ring ordinates. */
poly->rings[i] = ptarray_construct_reference_data(FLAGS_GET_Z(lwflags), FLAGS_GET_M(lwflags), npoints, ordinate_ptr);
-----------------------------------------------------------------------
Summary of changes:
liblwgeom/cunit/cu_gserialized1.c | 55 ++++++++++++++++++++++++++++++++++++++-
liblwgeom/cunit/cu_gserialized2.c | 55 ++++++++++++++++++++++++++++++++++++++-
liblwgeom/gserialized1.c | 24 +++++------------
liblwgeom/gserialized2.c | 24 +++++------------
4 files changed, 120 insertions(+), 38 deletions(-)
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list