[SCM] PostGIS branch master updated. 3.7.0beta1-88-g7455b1db34
git at osgeo.org
git at osgeo.org
Mon Jul 27 13:15:20 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 7455b1db34856e3de8c24f0d265ed6cd52c0aded (commit)
via 50359fe4deed9f1c52b7ca6d05dd8da08b637e8a (commit)
via 8af773e2e566c2975fe78142c4c10f38a61127c4 (commit)
via 2cce9fe044a5aa0d77ff5f53dd7ba143db284425 (commit)
from 8a7bf4fe01ddf247a4d056e6e3feaaae512134b0 (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 7455b1db34856e3de8c24f0d265ed6cd52c0aded
Merge: 8a7bf4fe01 50359fe4de
Author: Darafei Praliaskouski <komzpa at gmail.com>
Date: Mon Jul 27 13:15:19 2026 -0700
Merge pull request 'Require attributed fuzzer corpus replay' (!525) from Komzpa/postgis:codex/fuzzer-corpus-required-polyline-geohash-20260726 into master
Reviewed-on: https://gitea.osgeo.org/postgis/postgis/pulls/525
commit 50359fe4deed9f1c52b7ca6d05dd8da08b637e8a
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Mon Jul 27 17:27:36 2026 +0400
Strengthen GSERIALIZED bytea roundtrip fuzzers
diff --git a/fuzzers/gserialized_from_bytea_fuzzer.cpp b/fuzzers/gserialized_from_bytea_fuzzer.cpp
index dfdb162bea..41c37ea859 100644
--- a/fuzzers/gserialized_from_bytea_fuzzer.cpp
+++ b/fuzzers/gserialized_from_bytea_fuzzer.cpp
@@ -42,26 +42,44 @@ postgis_fuzzer_assert(int condition)
abort();
}
+static lwvarlena_t *
+geometry_to_bytea(const LWGEOM *lwgeom)
+{
+ return lwgeom_to_wkb_varlena(lwgeom, WKB_NDR | WKB_EXTENDED);
+}
+
static void
-assert_idempotent_serialization(LWGEOM *lwgeom)
+assert_equal_gserialized(const GSERIALIZED *left, size_t left_size, const GSERIALIZED *right, size_t right_size)
+{
+ postgis_fuzzer_assert(left_size == LWSIZE_GET(left->size));
+ postgis_fuzzer_assert(right_size == LWSIZE_GET(right->size));
+ postgis_fuzzer_assert(left_size == right_size);
+ postgis_fuzzer_assert(memcmp(left, right, left_size) == 0);
+}
+
+static void
+assert_gserialized_bytea_roundtrip(LWGEOM *lwgeom)
{
size_t first_size = 0;
size_t second_size = 0;
GSERIALIZED *first = gserialized_from_lwgeom(lwgeom, &first_size);
- LWGEOM *roundtrip = lwgeom_from_gserialized(first);
- GSERIALIZED *second = gserialized_from_lwgeom(roundtrip, &second_size);
-
postgis_fuzzer_assert(first != NULL);
- postgis_fuzzer_assert(roundtrip != NULL);
- postgis_fuzzer_assert(second != NULL);
- postgis_fuzzer_assert(first_size == LWSIZE_GET(first->size));
- postgis_fuzzer_assert(second_size == LWSIZE_GET(second->size));
- postgis_fuzzer_assert(first_size == second_size);
- postgis_fuzzer_assert(memcmp(first, second, first_size) == 0);
- lwgeom_free(roundtrip);
+ lwvarlena_t *bytea = geometry_to_bytea(lwgeom);
+ postgis_fuzzer_assert(bytea != NULL);
+
+ LWGEOM *from_bytea =
+ lwgeom_from_wkb((uint8_t *)bytea->data, LWSIZE_GET(bytea->size) - LWVARHDRSZ, LW_PARSER_CHECK_ALL);
+ postgis_fuzzer_assert(from_bytea != NULL);
+
+ GSERIALIZED *second = gserialized_from_lwgeom(from_bytea, &second_size);
+ postgis_fuzzer_assert(second != NULL);
+ assert_equal_gserialized(first, first_size, second, second_size);
+
+ lwgeom_free(from_bytea);
lwfree(first);
lwfree(second);
+ lwfree(bytea);
}
int
@@ -92,7 +110,7 @@ LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len)
LWGEOM *lwgeom = lwgeom_from_gserialized(gserialized);
if (lwgeom != NULL)
{
- assert_idempotent_serialization(lwgeom);
+ assert_gserialized_bytea_roundtrip(lwgeom);
lwgeom_free(lwgeom);
}
diff --git a/fuzzers/gserialized_from_lwgeom_fuzzer.cpp b/fuzzers/gserialized_from_lwgeom_fuzzer.cpp
index c9074a35f8..0ac1454abf 100644
--- a/fuzzers/gserialized_from_lwgeom_fuzzer.cpp
+++ b/fuzzers/gserialized_from_lwgeom_fuzzer.cpp
@@ -16,6 +16,7 @@
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
+#include <string.h>
extern "C" {
#include "geos_stub.h"
@@ -42,6 +43,24 @@ postgis_fuzzer_assert(int condition)
abort();
}
+static lwvarlena_t *
+geometry_to_bytea(const LWGEOM *lwgeom)
+{
+ return lwgeom_to_wkb_varlena(lwgeom, WKB_NDR | WKB_EXTENDED);
+}
+
+static void
+assert_equal_bytea(const lwvarlena_t *left, const lwvarlena_t *right)
+{
+ size_t left_size = LWSIZE_GET(left->size);
+ size_t right_size = LWSIZE_GET(right->size);
+
+ postgis_fuzzer_assert(left_size >= LWVARHDRSZ);
+ postgis_fuzzer_assert(right_size >= LWVARHDRSZ);
+ postgis_fuzzer_assert(left_size == right_size);
+ postgis_fuzzer_assert(memcmp(left->data, right->data, left_size - LWVARHDRSZ) == 0);
+}
+
static void
assert_matching_gbox(const LWGEOM *input, const LWGEOM *roundtrip)
{
@@ -95,9 +114,17 @@ LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len)
assert_matching_gbox(input, roundtrip);
postgis_fuzzer_assert(lwgeom_same(input, roundtrip));
+ lwvarlena_t *input_bytea = geometry_to_bytea(input);
+ lwvarlena_t *roundtrip_bytea = geometry_to_bytea(roundtrip);
+ postgis_fuzzer_assert(input_bytea != NULL);
+ postgis_fuzzer_assert(roundtrip_bytea != NULL);
+ assert_equal_bytea(input_bytea, roundtrip_bytea);
+
lwgeom_free(input);
lwgeom_free(roundtrip);
lwfree(serialized);
+ lwfree(input_bytea);
+ lwfree(roundtrip_bytea);
postgis_lwgeom_fuzzer_cleanup_allocations();
return 0;
}
commit 8af773e2e566c2975fe78142c4c10f38a61127c4
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Sun Jul 26 12:27:31 2026 +0300
Require attributed fuzzer corpus replay
diff --git a/.woodpecker/qa.yml b/.woodpecker/qa.yml
index 2befd93b63..87bce37ad7 100644
--- a/.woodpecker/qa.yml
+++ b/.woodpecker/qa.yml
@@ -68,6 +68,7 @@ steps:
LDFLAGS="-Wl,-Bsymbolic-functions -Wl,-z,relro"
- make -j1
- make check RUNTESTFLAGS="--verbose $${QA_REGRESS_HOOKS} --after-create-db-script $${SRCDIR}/regress/hooks/standard-conforming-strings-off.sql"
+ - make check-fuzzers FUZZER_CHECK_REQUIRED=1
# Every matrix entry is an independent workflow. Keep this file to cheap modes
# that should protect every pull request; expensive modes live in qa-expensive.
diff --git a/fuzzers/Makefile b/fuzzers/Makefile
index 6064b7a220..491d3de23c 100644
--- a/fuzzers/Makefile
+++ b/fuzzers/Makefile
@@ -14,7 +14,7 @@ POSTGIS_CONFIGURED_SANITIZE_FLAGS := $(shell cxx="$(CXX)"; set -- $$cxx; test "$
POSTGIS_FUZZER_CPPFLAGS = $(POSTGIS_CONFIGURED_CPPFLAGS) $(CPPFLAGS)
POSTGIS_FUZZER_LDFLAGS = $(POSTGIS_CONFIGURED_SANITIZE_FLAGS) $(POSTGIS_CONFIGURED_LDFLAGS) $(LDFLAGS)
-.PHONY: clean dummyfuzzers check check-corpus
+.PHONY: clean dummyfuzzers check check-required check-corpus
clean:
$(RM) -f *.o *.a
@@ -29,30 +29,38 @@ dummyfuzzers: fuzzingengine.o
CC="${CC}" CXX="${CXX}" CPPFLAGS="$(POSTGIS_FUZZER_CPPFLAGS)" CXXFLAGS="-L$(CURDIR) ${CXXFLAGS}" LDFLAGS="$(POSTGIS_FUZZER_LDFLAGS)" POSTGIS_BUILD_DIR="$(POSTGIS_BUILD_DIR)" SRC="$(FUZZER_SRC_DIR)" OUT="$(FUZZER_OUT)" $(FUZZER_SRC_DIR)/build_seed_corpus.sh
check:
- @cxx="$${CXX:-$(CXX)}"; \
+ @required="$(FUZZER_CHECK_REQUIRED)"; \
+ skip_fuzzer_check() { \
+ echo "FUZZER CORPUS REPLAY SKIPPED: $$1"; \
+ if [ "$$required" = "1" ]; then \
+ echo "FUZZER_CHECK_REQUIRED=1, failing because corpus replay did not run."; \
+ exit 1; \
+ fi; \
+ exit 0; \
+ }; \
+ cxx="$${CXX:-$(CXX)}"; \
set -- $${cxx}; \
if [ "$$#" -eq 0 ] || ! command -v "$$1" >/dev/null 2>&1 || ! "$$@" --version >/dev/null 2>&1; then \
- echo "C++ compiler '$${cxx}' not available; skipping fuzzer smoke check."; \
+ skip_fuzzer_check "C++ compiler '$${cxx}' not available."; \
elif ! command -v pkg-config >/dev/null 2>&1 || ! pkg-config --exists json-c proj; then \
- echo "pkg-config metadata for fuzzer dependencies not found; skipping fuzzer smoke check."; \
+ skip_fuzzer_check "pkg-config metadata for json-c/proj not found."; \
elif ! command -v xml2-config >/dev/null 2>&1 && ! pkg-config --exists libxml-2.0; then \
- echo "libxml2 fuzzer dependency helper not found; skipping fuzzer smoke check."; \
+ skip_fuzzer_check "libxml2 dependency helper not found."; \
elif ! command -v geos-config >/dev/null 2>&1 || ! command -v gdal-config >/dev/null 2>&1; then \
- echo "GEOS/GDAL fuzzer dependency helpers not found; skipping fuzzer smoke check."; \
+ skip_fuzzer_check "GEOS/GDAL dependency helpers not found."; \
else \
set -e; \
if { command -v python3 >/dev/null 2>&1 && python3 -m zipfile --help >/dev/null 2>&1; } || \
{ command -v unzip >/dev/null 2>&1 && unzip -v >/dev/null 2>&1; }; then \
$(MAKE) -f "$(FUZZER_MAKEFILE)" check-corpus; \
else \
- echo "No seed corpus zip extractor found; running fuzzer smoke check without corpus replay."; \
- $(MAKE) -f "$(FUZZER_MAKEFILE)" dummyfuzzers; \
- for fuzzer in $(FUZZER_NAMES); do \
- "$(FUZZER_OUT)/$${fuzzer}"; \
- done; \
+ skip_fuzzer_check "No seed corpus zip extractor found; install python3 or unzip."; \
fi; \
fi
+check-required:
+ $(MAKE) -f "$(FUZZER_MAKEFILE)" check FUZZER_CHECK_REQUIRED=1
+
check-corpus: dummyfuzzers
@set -e; \
for fuzzer in $(FUZZER_NAMES); do \
@@ -77,6 +85,7 @@ check-corpus: dummyfuzzers
corpus_name=$$(basename "$${corpus}"); \
fuzzer=$${corpus_name%_seed_corpus.zip}; \
corpus_dir="$(FUZZER_WORK)/$${fuzzer}"; \
+ echo "Replaying seed corpus for $${fuzzer}: $${corpus_name}"; \
mkdir -p "$${corpus_dir}"; \
if [ "$${extract_zip}" = "python3" ]; then \
python3 -m zipfile -e "$${corpus}" "$${corpus_dir}"; \
diff --git a/fuzzers/encoded_polyline_import_fuzzer.cpp b/fuzzers/encoded_polyline_import_fuzzer.cpp
new file mode 100644
index 0000000000..b9af192777
--- /dev/null
+++ b/fuzzers/encoded_polyline_import_fuzzer.cpp
@@ -0,0 +1,59 @@
+/******************************************************************************
+ *
+ * Project: PostGIS
+ * Purpose: Encoded polyline input fuzzer
+ *
+ ******************************************************************************
+ * Copyright (C) 2026 Darafei Praliaskouski <me at komzpa.net>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ ****************************************************************************/
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern "C" {
+#include "geos_stub.h"
+#include "proj_stub.h"
+}
+
+#include "liblwgeom_fuzzer.hpp"
+
+extern "C" int
+LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/)
+{
+ postgis_lwgeom_fuzzer_initialize();
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len)
+{
+ if (len < 2)
+ return 0;
+
+ char *polyline = static_cast<char *>(malloc(len));
+ if (polyline == NULL)
+ return 0;
+
+ memcpy(polyline, buf + 1, len - 1);
+ polyline[len - 1] = '\0';
+ int precision = buf[0] % 10;
+
+ if (!POSTGIS_LWGEOM_FUZZER_SETJMP())
+ {
+ LWGEOM *lwgeom = lwgeom_from_encoded_polyline(polyline, precision);
+ lwgeom_free(lwgeom);
+ }
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ free(polyline);
+ return 0;
+}
diff --git a/fuzzers/encoded_polyline_import_fuzzer.options b/fuzzers/encoded_polyline_import_fuzzer.options
new file mode 100644
index 0000000000..9fda93fcb3
--- /dev/null
+++ b/fuzzers/encoded_polyline_import_fuzzer.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 4096
diff --git a/fuzzers/encoded_polyline_import_fuzzer_seed_corpus.zip b/fuzzers/encoded_polyline_import_fuzzer_seed_corpus.zip
new file mode 100644
index 0000000000..ff183cf977
Binary files /dev/null and b/fuzzers/encoded_polyline_import_fuzzer_seed_corpus.zip differ
diff --git a/fuzzers/geohash_import_fuzzer.cpp b/fuzzers/geohash_import_fuzzer.cpp
new file mode 100644
index 0000000000..6dd4b22732
--- /dev/null
+++ b/fuzzers/geohash_import_fuzzer.cpp
@@ -0,0 +1,59 @@
+/******************************************************************************
+ *
+ * Project: PostGIS
+ * Purpose: GeoHash input fuzzer
+ *
+ ******************************************************************************
+ * Copyright (C) 2026 Darafei Praliaskouski <me at komzpa.net>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ ****************************************************************************/
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern "C" {
+#include "geos_stub.h"
+#include "proj_stub.h"
+#include "liblwgeom_internal.h"
+}
+
+#include "liblwgeom_fuzzer.hpp"
+
+extern "C" int
+LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/)
+{
+ postgis_lwgeom_fuzzer_initialize();
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len);
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len)
+{
+ if (len < 2)
+ return 0;
+
+ char *geohash = static_cast<char *>(malloc(len));
+ if (geohash == NULL)
+ return 0;
+
+ memcpy(geohash, buf + 1, len - 1);
+ geohash[len - 1] = '\0';
+ int precision = (buf[0] % 2) ? -1 : (int)(buf[0] % len);
+ double lat[2];
+ double lon[2];
+
+ if (!POSTGIS_LWGEOM_FUZZER_SETJMP())
+ decode_geohash_bbox(geohash, lat, lon, precision);
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ free(geohash);
+ return 0;
+}
diff --git a/fuzzers/geohash_import_fuzzer.dict b/fuzzers/geohash_import_fuzzer.dict
new file mode 100644
index 0000000000..45f4aa9728
--- /dev/null
+++ b/fuzzers/geohash_import_fuzzer.dict
@@ -0,0 +1,5 @@
+"0123456789bcdefghjkmnpqrstuvwxyz"
+"ezs42"
+"u4pruydqqvj"
+"b"
+"z"
diff --git a/fuzzers/geohash_import_fuzzer.options b/fuzzers/geohash_import_fuzzer.options
new file mode 100644
index 0000000000..9fda93fcb3
--- /dev/null
+++ b/fuzzers/geohash_import_fuzzer.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 4096
diff --git a/fuzzers/geohash_import_fuzzer_seed_corpus.zip b/fuzzers/geohash_import_fuzzer_seed_corpus.zip
new file mode 100644
index 0000000000..4a0140fd02
Binary files /dev/null and b/fuzzers/geohash_import_fuzzer_seed_corpus.zip differ
commit 2cce9fe044a5aa0d77ff5f53dd7ba143db284425
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Sun Jul 26 13:30:20 2026 +0400
Add GSERIALIZED fuzz targets
diff --git a/fuzzers/Makefile b/fuzzers/Makefile
index 5ab9c0e314..6064b7a220 100644
--- a/fuzzers/Makefile
+++ b/fuzzers/Makefile
@@ -26,7 +26,7 @@ dummyfuzzers: fuzzingengine.o
mkdir -p "$(FUZZER_OUT)"
$(AR) r libFuzzingEngine.a fuzzingengine.o
CC="${CC}" CXX="${CXX}" CPPFLAGS="$(POSTGIS_FUZZER_CPPFLAGS)" CXXFLAGS="-L$(CURDIR) ${CXXFLAGS}" LDFLAGS="$(POSTGIS_FUZZER_LDFLAGS)" POSTGIS_BUILD_DIR="$(POSTGIS_BUILD_DIR)" SRC="$(FUZZER_SRC_DIR)" OUT="$(FUZZER_OUT)" $(FUZZER_SRC_DIR)/build_google_oss_fuzzers.sh
- OUT="$(FUZZER_OUT)" $(FUZZER_SRC_DIR)/build_seed_corpus.sh
+ CC="${CC}" CXX="${CXX}" CPPFLAGS="$(POSTGIS_FUZZER_CPPFLAGS)" CXXFLAGS="-L$(CURDIR) ${CXXFLAGS}" LDFLAGS="$(POSTGIS_FUZZER_LDFLAGS)" POSTGIS_BUILD_DIR="$(POSTGIS_BUILD_DIR)" SRC="$(FUZZER_SRC_DIR)" OUT="$(FUZZER_OUT)" $(FUZZER_SRC_DIR)/build_seed_corpus.sh
check:
@cxx="$${CXX:-$(CXX)}"; \
@@ -70,8 +70,8 @@ check-corpus: dummyfuzzers
rm -rf "$(FUZZER_WORK)"; \
mkdir -p "$(FUZZER_WORK)"; \
trap 'rm -rf "$(FUZZER_WORK)"' EXIT HUP INT TERM; \
- for corpus in $(SEED_CORPORA) __none__; do \
- if [ "$${corpus}" = "__none__" ]; then \
+ for corpus in $(SEED_CORPORA) "$(FUZZER_OUT)"/*_seed_corpus.zip __none__; do \
+ if [ "$${corpus}" = "__none__" ] || [ ! -e "$${corpus}" ]; then \
continue; \
fi; \
corpus_name=$$(basename "$${corpus}"); \
diff --git a/fuzzers/build_google_oss_fuzzers.sh b/fuzzers/build_google_oss_fuzzers.sh
index 30a39a4c74..b95c733e63 100755
--- a/fuzzers/build_google_oss_fuzzers.sh
+++ b/fuzzers/build_google_oss_fuzzers.sh
@@ -122,7 +122,7 @@ package_runtime_libs()
fi
}
-for F in "$FUZZERS_DIR"/*.cpp; do
+for F in "$FUZZERS_DIR"/*_fuzzer.cpp; do
[ -e "$F" ] || continue
fuzzerName=$(basename "$F" .cpp)
build_fuzzer "$fuzzerName" "$F"
diff --git a/fuzzers/build_gserialized_seed_corpus.cpp b/fuzzers/build_gserialized_seed_corpus.cpp
new file mode 100644
index 0000000000..f3fa8ef2cd
--- /dev/null
+++ b/fuzzers/build_gserialized_seed_corpus.cpp
@@ -0,0 +1,162 @@
+/******************************************************************************
+ *
+ * Project: PostGIS
+ * Purpose: GSERIALIZED seed corpus builder
+ *
+ ******************************************************************************
+ * Copyright (C) 2026 Darafei Praliaskouski <me at komzpa.net>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ ****************************************************************************/
+
+#include <errno.h>
+#include <setjmp.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <fstream>
+#include <string>
+
+extern "C" {
+#include "geos_stub.h"
+#include "proj_stub.h"
+}
+
+#include "liblwgeom_fuzzer.hpp"
+
+extern "C" GSERIALIZED *gserialized1_from_lwgeom(LWGEOM *geom, size_t *size);
+
+static int
+mkdir_if_needed(const std::string &path)
+{
+ if (mkdir(path.c_str(), 0777) == 0 || errno == EEXIST)
+ return 1;
+ perror(path.c_str());
+ return 0;
+}
+
+static int
+write_file(const std::string &path, const void *data, size_t len)
+{
+ FILE *file = fopen(path.c_str(), "wb");
+ if (file == NULL)
+ {
+ perror(path.c_str());
+ return 0;
+ }
+ if (len != 0 && fwrite(data, 1, len, file) != len)
+ {
+ perror(path.c_str());
+ fclose(file);
+ return 0;
+ }
+ if (fclose(file) != 0)
+ {
+ perror(path.c_str());
+ return 0;
+ }
+ return 1;
+}
+
+static int
+write_seed(const std::string &base, const std::string &name, const std::string &wkt)
+{
+ LWGEOM *geom = lwgeom_from_wkt(wkt.c_str(), LW_PARSER_CHECK_NONE);
+ if (geom == NULL)
+ return 0;
+
+ lwvarlena_t *wkb = lwgeom_to_wkb_varlena(geom, WKB_NDR | WKB_EXTENDED);
+ size_t gserialized1_size = 0;
+ size_t gserialized2_size = 0;
+ GSERIALIZED *gserialized1 = gserialized1_from_lwgeom(geom, &gserialized1_size);
+ GSERIALIZED *gserialized2 = gserialized_from_lwgeom(geom, &gserialized2_size);
+
+ int ok = 1;
+ if (wkb != NULL)
+ {
+ ok &= write_file(base + "/gserialized_from_lwgeom_fuzzer_seed_corpus/" + name,
+ wkb->data,
+ LWSIZE_GET(wkb->size) - LWVARHDRSZ);
+ }
+ if (gserialized1 != NULL)
+ {
+ ok &= write_file(base + "/gserialized_from_bytea_fuzzer_seed_corpus/" + name + "_v1",
+ gserialized1,
+ gserialized1_size);
+ }
+ if (gserialized2 != NULL)
+ {
+ ok &= write_file(base + "/gserialized_from_bytea_fuzzer_seed_corpus/" + name + "_v2",
+ gserialized2,
+ gserialized2_size);
+ }
+
+ lwfree(wkb);
+ lwfree(gserialized1);
+ lwfree(gserialized2);
+ lwgeom_free(geom);
+ return ok;
+}
+
+int
+main(int argc, char **argv)
+{
+ if (argc != 3)
+ {
+ fprintf(stderr, "usage: %s OUT seed-list\n", argv[0]);
+ return 1;
+ }
+
+ const std::string out = argv[1];
+ if (!mkdir_if_needed(out) || !mkdir_if_needed(out + "/gserialized_from_bytea_fuzzer_seed_corpus") ||
+ !mkdir_if_needed(out + "/gserialized_from_lwgeom_fuzzer_seed_corpus"))
+ {
+ return 1;
+ }
+
+ postgis_lwgeom_fuzzer_initialize();
+
+ std::ifstream input(argv[2]);
+ if (!input)
+ {
+ perror(argv[2]);
+ return 1;
+ }
+
+ std::string line;
+ int ok = 1;
+ while (std::getline(input, line))
+ {
+ if (line.empty() || line[0] == '#')
+ continue;
+
+ const std::string::size_type separator = line.find('|');
+ if (separator == std::string::npos)
+ {
+ fprintf(stderr, "invalid seed line: %s\n", line.c_str());
+ ok = 0;
+ continue;
+ }
+
+ const std::string name = line.substr(0, separator);
+ const std::string wkt = line.substr(separator + 1);
+ if (POSTGIS_LWGEOM_FUZZER_SETJMP())
+ {
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ ok = 0;
+ continue;
+ }
+ ok &= write_seed(out, name, wkt);
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ }
+
+ return ok ? 0 : 1;
+}
diff --git a/fuzzers/build_seed_corpus.sh b/fuzzers/build_seed_corpus.sh
index 3d7f0945ca..b64a2348a3 100755
--- a/fuzzers/build_seed_corpus.sh
+++ b/fuzzers/build_seed_corpus.sh
@@ -7,4 +7,43 @@ if [ "$OUT" == "" ]; then
exit 1
fi
-# Nothing for now
+if [ "$CXX" == "" ]; then
+ echo "CXX env var not defined"
+ exit 1
+fi
+
+if ! command -v python3 >/dev/null 2>&1; then
+ echo "python3 not found; skipping seed corpus archive generation"
+ exit 0
+fi
+
+POSTGIS_SOURCE_DIR="${POSTGIS_SOURCE_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
+POSTGIS_BUILD_DIR="${POSTGIS_BUILD_DIR:-$POSTGIS_SOURCE_DIR}"
+FUZZERS_DIR="$POSTGIS_SOURCE_DIR/fuzzers"
+JSON_C_LIBS=$(pkg-config --libs json-c)
+GEOS_LIBS=$(geos-config --clibs)
+PROJ_LIBS=$(pkg-config --libs proj)
+if command -v xml2-config >/dev/null 2>&1; then
+ XML2_LIBS=$(xml2-config --libs)
+else
+ XML2_LIBS=$(pkg-config --libs libxml-2.0)
+fi
+
+seed_builder="$OUT/build_gserialized_seed_corpus"
+mkdir -p "$OUT"
+
+"$CXX" $CXXFLAGS -std=c++11 \
+ -I"$POSTGIS_SOURCE_DIR/liblwgeom" -I"$POSTGIS_BUILD_DIR/liblwgeom" $CPPFLAGS \
+ "$FUZZERS_DIR/build_gserialized_seed_corpus.cpp" -o "$seed_builder" \
+ $LDFLAGS "$POSTGIS_BUILD_DIR/liblwgeom/.libs/liblwgeom.a" \
+ $JSON_C_LIBS $GEOS_LIBS $PROJ_LIBS $XML2_LIBS
+
+"$seed_builder" "$OUT" "$FUZZERS_DIR/gserialized_seed_corpus.in"
+
+for corpus_dir in \
+ "$OUT/gserialized_from_bytea_fuzzer_seed_corpus" \
+ "$OUT/gserialized_from_lwgeom_fuzzer_seed_corpus"; do
+ corpus_zip="$OUT/$(basename "$corpus_dir").zip"
+ rm -f "$corpus_zip"
+ python3 -m zipfile -c "$corpus_zip" "$corpus_dir"/*
+done
diff --git a/fuzzers/gserialized_from_bytea_fuzzer.cpp b/fuzzers/gserialized_from_bytea_fuzzer.cpp
new file mode 100644
index 0000000000..dfdb162bea
--- /dev/null
+++ b/fuzzers/gserialized_from_bytea_fuzzer.cpp
@@ -0,0 +1,102 @@
+/******************************************************************************
+ *
+ * Project: PostGIS
+ * Purpose: GSERIALIZED input fuzzer
+ *
+ ******************************************************************************
+ * Copyright (C) 2026 Darafei Praliaskouski <me at komzpa.net>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ ****************************************************************************/
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+extern "C" {
+#include "geos_stub.h"
+#include "proj_stub.h"
+}
+
+#include "liblwgeom_fuzzer.hpp"
+
+extern "C" int
+LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/)
+{
+ postgis_lwgeom_fuzzer_initialize();
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len);
+
+static void
+postgis_fuzzer_assert(int condition)
+{
+ if (!condition)
+ abort();
+}
+
+static void
+assert_idempotent_serialization(LWGEOM *lwgeom)
+{
+ size_t first_size = 0;
+ size_t second_size = 0;
+ GSERIALIZED *first = gserialized_from_lwgeom(lwgeom, &first_size);
+ LWGEOM *roundtrip = lwgeom_from_gserialized(first);
+ GSERIALIZED *second = gserialized_from_lwgeom(roundtrip, &second_size);
+
+ postgis_fuzzer_assert(first != NULL);
+ postgis_fuzzer_assert(roundtrip != NULL);
+ postgis_fuzzer_assert(second != NULL);
+ postgis_fuzzer_assert(first_size == LWSIZE_GET(first->size));
+ postgis_fuzzer_assert(second_size == LWSIZE_GET(second->size));
+ postgis_fuzzer_assert(first_size == second_size);
+ postgis_fuzzer_assert(memcmp(first, second, first_size) == 0);
+
+ lwgeom_free(roundtrip);
+ lwfree(first);
+ lwfree(second);
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len)
+{
+ if (len < offsetof(GSERIALIZED, data))
+ return 0;
+
+ if (POSTGIS_LWGEOM_FUZZER_SETJMP())
+ {
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ return 0;
+ }
+
+ /* Copy exactly the bytes supplied by the fuzzer and leave g->size
+ * untouched. The first four bytes are the PostgreSQL varlena size
+ * header read through LWSIZE_GET(), and they are attacker-controlled
+ * when GSERIALIZED arrives from a damaged page, binary COPY, bytea cast,
+ * or hostile dump. Rewriting the header to len would make every input
+ * self-consistent and hide the over-declared-size cases this target is
+ * meant to exercise.
+ */
+ GSERIALIZED *gserialized = static_cast<GSERIALIZED *>(postgis_lwgeom_fuzzer_malloc(len));
+ if (gserialized == NULL)
+ return 0;
+ memcpy(gserialized, buf, len);
+
+ LWGEOM *lwgeom = lwgeom_from_gserialized(gserialized);
+ if (lwgeom != NULL)
+ {
+ assert_idempotent_serialization(lwgeom);
+ lwgeom_free(lwgeom);
+ }
+
+ postgis_lwgeom_fuzzer_free(gserialized);
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ return 0;
+}
diff --git a/fuzzers/gserialized_from_bytea_fuzzer.dict b/fuzzers/gserialized_from_bytea_fuzzer.dict
new file mode 100644
index 0000000000..c9ba6bf1f3
--- /dev/null
+++ b/fuzzers/gserialized_from_bytea_fuzzer.dict
@@ -0,0 +1,6 @@
+"\x00\x00\x00\x00"
+"\x20\x00\x00\x00"
+"\x40"
+"\x44"
+"\x50"
+"\x54"
diff --git a/fuzzers/gserialized_from_bytea_fuzzer.options b/fuzzers/gserialized_from_bytea_fuzzer.options
new file mode 100644
index 0000000000..678d526b1e
--- /dev/null
+++ b/fuzzers/gserialized_from_bytea_fuzzer.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65536
diff --git a/fuzzers/gserialized_from_lwgeom_fuzzer.cpp b/fuzzers/gserialized_from_lwgeom_fuzzer.cpp
new file mode 100644
index 0000000000..c9074a35f8
--- /dev/null
+++ b/fuzzers/gserialized_from_lwgeom_fuzzer.cpp
@@ -0,0 +1,103 @@
+/******************************************************************************
+ *
+ * Project: PostGIS
+ * Purpose: GSERIALIZED output property fuzzer
+ *
+ ******************************************************************************
+ * Copyright (C) 2026 Darafei Praliaskouski <me at komzpa.net>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ ****************************************************************************/
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+extern "C" {
+#include "geos_stub.h"
+#include "proj_stub.h"
+}
+
+#include "liblwgeom_fuzzer.hpp"
+
+extern "C" size_t gserialized_from_lwgeom_size(const LWGEOM *geom);
+
+extern "C" int
+LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/)
+{
+ postgis_lwgeom_fuzzer_initialize();
+ return 0;
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len);
+
+static void
+postgis_fuzzer_assert(int condition)
+{
+ if (!condition)
+ abort();
+}
+
+static void
+assert_matching_gbox(const LWGEOM *input, const LWGEOM *roundtrip)
+{
+ if (input->bbox == NULL || roundtrip->bbox == NULL)
+ {
+ postgis_fuzzer_assert(input->bbox == NULL);
+ postgis_fuzzer_assert(roundtrip->bbox == NULL);
+ return;
+ }
+
+ postgis_fuzzer_assert(gbox_same(input->bbox, roundtrip->bbox));
+}
+
+static void
+assert_matching_metadata(const LWGEOM *input, const LWGEOM *roundtrip)
+{
+ postgis_fuzzer_assert(input->type == roundtrip->type);
+ postgis_fuzzer_assert(input->srid == roundtrip->srid);
+ postgis_fuzzer_assert(FLAGS_GET_Z(input->flags) == FLAGS_GET_Z(roundtrip->flags));
+ postgis_fuzzer_assert(FLAGS_GET_M(input->flags) == FLAGS_GET_M(roundtrip->flags));
+ postgis_fuzzer_assert(FLAGS_GET_GEODETIC(input->flags) == FLAGS_GET_GEODETIC(roundtrip->flags));
+}
+
+int
+LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len)
+{
+ if (POSTGIS_LWGEOM_FUZZER_SETJMP())
+ {
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ return 0;
+ }
+
+ LWGEOM *input = lwgeom_from_wkb(buf, len, LW_PARSER_CHECK_NONE);
+ if (input == NULL)
+ {
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ return 0;
+ }
+
+ size_t serialized_size = 0;
+ GSERIALIZED *serialized = gserialized_from_lwgeom(input, &serialized_size);
+ const size_t expected_size = gserialized_from_lwgeom_size(input);
+ postgis_fuzzer_assert(serialized != NULL);
+ postgis_fuzzer_assert(serialized_size == expected_size);
+ postgis_fuzzer_assert(serialized_size == LWSIZE_GET(serialized->size));
+ postgis_fuzzer_assert(gserialized_cmp(serialized, serialized) == 0);
+
+ LWGEOM *roundtrip = lwgeom_from_gserialized(serialized);
+ postgis_fuzzer_assert(roundtrip != NULL);
+ assert_matching_metadata(input, roundtrip);
+ assert_matching_gbox(input, roundtrip);
+ postgis_fuzzer_assert(lwgeom_same(input, roundtrip));
+
+ lwgeom_free(input);
+ lwgeom_free(roundtrip);
+ lwfree(serialized);
+ postgis_lwgeom_fuzzer_cleanup_allocations();
+ return 0;
+}
diff --git a/fuzzers/gserialized_from_lwgeom_fuzzer.dict b/fuzzers/gserialized_from_lwgeom_fuzzer.dict
new file mode 100644
index 0000000000..8f54328039
--- /dev/null
+++ b/fuzzers/gserialized_from_lwgeom_fuzzer.dict
@@ -0,0 +1,9 @@
+"\x00"
+"\x01"
+"\x01\x00\x00\x00"
+"\x02\x00\x00\x00"
+"\x03\x00\x00\x00"
+"\x07\x00\x00\x00"
+"\x08\x00\x00\x00"
+"\x11\x00\x00\x00"
+"\x01\x00\x00\x20"
diff --git a/fuzzers/gserialized_from_lwgeom_fuzzer.options b/fuzzers/gserialized_from_lwgeom_fuzzer.options
new file mode 100644
index 0000000000..678d526b1e
--- /dev/null
+++ b/fuzzers/gserialized_from_lwgeom_fuzzer.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65536
diff --git a/fuzzers/gserialized_seed_corpus.in b/fuzzers/gserialized_seed_corpus.in
new file mode 100644
index 0000000000..f401c855eb
--- /dev/null
+++ b/fuzzers/gserialized_seed_corpus.in
@@ -0,0 +1,20 @@
+# Regression-suite geometries used to seed the GSERIALIZED fuzzers.
+# Keep names filesystem-safe: they become seed file names.
+# regress/core/wkt.sql and regress/core/regress.sql
+point|POINT(0 0)
+point_zm_srid|SRID=4326;POINT ZM(1 2 3 4)
+linestring|LINESTRING(0 0,1 1,2 1)
+polygon|POLYGON((0 0,0 4,4 4,4 0,0 0),(1 1,2 1,2 2,1 1))
+multipoint|MULTIPOINT((0 0),(1 2),(3 4))
+multilinestring|MULTILINESTRING((0 0,1 1),(2 2,3 3,4 4))
+multipolygon|MULTIPOLYGON(((0 0,0 2,2 2,2 0,0 0)),((3 3,3 5,5 5,5 3,3 3)))
+collection|GEOMETRYCOLLECTION(POINT(1 2),LINESTRING(0 0,1 1))
+empty_collection|GEOMETRYCOLLECTION EMPTY
+# regress/core/sql-mm-circularstring.sql and related curve tests
+circularstring|CIRCULARSTRING(0 0,1 1,2 0)
+compoundcurve|COMPOUNDCURVE((0 0,1 1),CIRCULARSTRING(1 1,2 2,3 1))
+curvepolygon|CURVEPOLYGON(CIRCULARSTRING(0 0,1 1,2 0,1 -1,0 0))
+# regress/core/triangulatedsurface.sql and regress/core/polyhedralsurface.sql
+triangle|TRIANGLE((0 0,1 0,0 1,0 0))
+tin|TIN(((0 0,1 0,0 1,0 0)))
+polyhedralsurface|POLYHEDRALSURFACE(((0 0,0 1,1 1,1 0,0 0)))
-----------------------------------------------------------------------
Summary of changes:
.woodpecker/qa.yml | 1 +
fuzzers/Makefile | 37 +++--
fuzzers/build_google_oss_fuzzers.sh | 2 +-
fuzzers/build_gserialized_seed_corpus.cpp | 162 +++++++++++++++++++++
fuzzers/build_seed_corpus.sh | 41 +++++-
...zzer.cpp => encoded_polyline_import_fuzzer.cpp} | 25 ++--
fuzzers/encoded_polyline_import_fuzzer.options | 2 +
.../encoded_polyline_import_fuzzer_seed_corpus.zip | Bin 0 -> 422 bytes
...import_fuzzer.cpp => geohash_import_fuzzer.cpp} | 27 ++--
fuzzers/geohash_import_fuzzer.dict | 5 +
fuzzers/geohash_import_fuzzer.options | 2 +
fuzzers/geohash_import_fuzzer_seed_corpus.zip | Bin 0 -> 390 bytes
fuzzers/gserialized_from_bytea_fuzzer.cpp | 120 +++++++++++++++
fuzzers/gserialized_from_bytea_fuzzer.dict | 6 +
fuzzers/gserialized_from_bytea_fuzzer.options | 2 +
fuzzers/gserialized_from_lwgeom_fuzzer.cpp | 130 +++++++++++++++++
fuzzers/gserialized_from_lwgeom_fuzzer.dict | 9 ++
fuzzers/gserialized_from_lwgeom_fuzzer.options | 2 +
fuzzers/gserialized_seed_corpus.in | 20 +++
19 files changed, 554 insertions(+), 39 deletions(-)
create mode 100644 fuzzers/build_gserialized_seed_corpus.cpp
copy fuzzers/{twkb_import_fuzzer.cpp => encoded_polyline_import_fuzzer.cpp} (72%)
create mode 100644 fuzzers/encoded_polyline_import_fuzzer.options
create mode 100644 fuzzers/encoded_polyline_import_fuzzer_seed_corpus.zip
copy fuzzers/{twkb_import_fuzzer.cpp => geohash_import_fuzzer.cpp} (71%)
create mode 100644 fuzzers/geohash_import_fuzzer.dict
create mode 100644 fuzzers/geohash_import_fuzzer.options
create mode 100644 fuzzers/geohash_import_fuzzer_seed_corpus.zip
create mode 100644 fuzzers/gserialized_from_bytea_fuzzer.cpp
create mode 100644 fuzzers/gserialized_from_bytea_fuzzer.dict
create mode 100644 fuzzers/gserialized_from_bytea_fuzzer.options
create mode 100644 fuzzers/gserialized_from_lwgeom_fuzzer.cpp
create mode 100644 fuzzers/gserialized_from_lwgeom_fuzzer.dict
create mode 100644 fuzzers/gserialized_from_lwgeom_fuzzer.options
create mode 100644 fuzzers/gserialized_seed_corpus.in
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list