diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d537d70..458fc6d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -70,6 +70,7 @@ jobs: file: ./Dockerfile platforms: linux/amd64 push: false + load: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | @@ -77,6 +78,19 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + # Gate: a build whose PostGIS is vulnerable to CVE-2026-73514 / + # CVE-2026-73515 must never reach a tag. The PR job runs it on the loaded + # image; the push job below runs it on the pushed digest before the + # manifest job can move `latest`/`18` onto it. + - name: Verify PostGIS security (PR, amd64) + if: github.event_name == 'pull_request' && matrix.platform == 'linux/amd64' + run: | + set -euo pipefail + tag="$(echo '${{ steps.meta.outputs.tags }}' | head -1)" + docker run -d --name postgis-verify -e POSTGRES_PASSWORD=test "$tag" + until docker exec postgis-verify pg_isready -U postgres -q; do sleep 1; done + ./scripts/verify-postgis-security.sh postgis-verify + - name: Build & push by digest if: github.event_name != 'pull_request' id: build @@ -92,6 +106,15 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + - name: Verify PostGIS security (pushed digest) + if: github.event_name != 'pull_request' && matrix.platform == 'linux/amd64' + run: | + set -euo pipefail + image="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" + docker run -d --name postgis-verify -e POSTGRES_PASSWORD=test "$image" + until docker exec postgis-verify pg_isready -U postgres -q; do sleep 1; done + ./scripts/verify-postgis-security.sh postgis-verify + - name: Export digest if: github.event_name != 'pull_request' run: | diff --git a/Dockerfile b/Dockerfile index b298656..097fd25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ARG PG_VERSION=18 ARG PGVECTOR_VERSION=0.8.2 -ARG POSTGIS_VERSION=3.6.2 +ARG POSTGIS_VERSION=3.6.4 ARG PG_TEXTSEARCH_VERSION=1.3.1 ARG PG_PARTMAN_VERSION=5.4.3 @@ -48,12 +48,26 @@ RUN git clone --branch v${PGVECTOR_VERSION} --depth 1 https://github.com/pgvecto make OPTFLAGS="" -j$(nproc) && \ make install -# PostGIS with Tiger geocoder and address standardizer +# PostGIS with Tiger geocoder and address standardizer. +# patches/ carries upstream security fixes released after the 3.6.4 tarball +# (CVE-2026-73515 FlatGeobuf, CVE-2026-73514 address_standardizer) — `patch` +# exits non-zero on a reject, so a patch that stops applying fails the build. +COPY patches/ /build/patches/ + RUN curl -L https://download.osgeo.org/postgis/source/postgis-${POSTGIS_VERSION}.tar.gz | tar xz && \ cd postgis-${POSTGIS_VERSION} && \ + for p in /build/patches/*.patch; do \ + echo "Applying $(basename "$p")" && patch -p1 --batch --forward < "$p"; \ + done && \ ./configure --without-raster --without-topology && \ make && \ - make install + make install && \ + mkdir -p /usr/local/share/postgresql/security && \ + { echo "postgis_source_version=${POSTGIS_VERSION}"; \ + for p in /build/patches/*.patch; do echo "patch=$(basename "$p")"; done; \ + echo "cve_fixed=CVE-2026-73514"; \ + echo "cve_fixed=CVE-2026-73515"; \ + } > /usr/local/share/postgresql/security/postgis-patches.txt # pg_textsearch (BM25) RUN git clone --branch v${PG_TEXTSEARCH_VERSION} --depth 1 https://github.com/timescale/pg_textsearch.git && \ diff --git a/Makefile b/Makefile index 68d3bb4..5ead4b2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build run stop clean test shell push +.PHONY: build run stop clean test verify-security shell push IMAGE_NAME ?= constructiveio/postgres-plus IMAGE_TAG ?= latest @@ -41,9 +41,15 @@ test: build CREATE EXTENSION pg_textsearch; \ CREATE EXTENSION pg_partman; \ SELECT 'all extensions OK';" + @./scripts/verify-postgis-security.sh $(CONTAINER_NAME)-test @docker stop $(CONTAINER_NAME)-test > /dev/null @docker rm $(CONTAINER_NAME)-test > /dev/null +# Assert the running $(CONTAINER_NAME) is not a PostGIS build vulnerable to +# CVE-2026-73514 / CVE-2026-73515. +verify-security: + @./scripts/verify-postgis-security.sh $(CONTAINER_NAME) + clean: stop docker rmi $(IMAGE_NAME):$(IMAGE_TAG) || true diff --git a/README.md b/README.md index 7076c06..9765a43 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Lean PostgreSQL 18 image with essential extensions for modern applications. | Extension | Version | Description | |-----------|---------|-------------| | [pgvector](https://github.com/pgvector/pgvector) | 0.8.2 | Vector similarity search for embeddings | -| [PostGIS](https://postgis.net/) | 3.6.2 | Spatial and geographic data | +| [PostGIS](https://postgis.net/) | 3.6.4 (+ security patches) | Spatial and geographic data | | [pg_textsearch](https://github.com/timescale/pg_textsearch) | 1.3.1 | BM25 full-text search | | [pg_partman](https://github.com/pgpartman/pg_partman) | 5.4.3 | Partition management | | [pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html) | built-in | Query performance statistics | @@ -49,13 +49,40 @@ CREATE EXTENSION pg_stat_statements; ## Build ```bash -make build # Build image -make test # Build and verify extensions -make run # Run container -make shell # psql into container -make clean # Remove image +make build # Build image +make test # Build, verify extensions, and run the PostGIS security gate +make verify-security # Run the security gate against an already-running container +make run # Run container +make shell # psql into container +make clean # Remove image ``` +## PostGIS security patches + +PostGIS is built from the `3.6.4` tarball with the upstream security fixes in +[`patches/`](./patches) applied on top — no released tarball carries them yet: + +| Patch | Fixes | +|-------|-------| +| `0001-flatgeobuf-validate-input-buffers-before-decoding` | CVE-2026-73515 — out-of-bounds read decoding a FlatGeobuf buffer (`ST_FromFlatGeobuf`). `postgis/stable-3.6` `53e273fae`, landed after 3.6.4. | +| `0002-address_standardizer-harden-scanner-and-rule-parsing` | CVE-2026-73514 — equivalent to `423570b` in the split-out [`postgis/address_standardizer`](https://github.com/postgis/address_standardizer) repo. | +| `0003-address_standardizer-clean-up-partial-2D-allocations` | leak/partial-allocation cleanup accompanying the above. | +| `0004-Avoid-out-of-bounds-write-uninitialized-memory` | the `parse_rule()` off-by-one write past `rule_arr[MAX_RULE_LENGTH]` plus an uninitialized `RULE_PARAM`. | + +Each is a `git format-patch` of the upstream commit cherry-picked onto the +`3.6.4` tag, so the provenance stays greppable and a patch that stops applying +fails the build rather than being silently skipped. + +`scripts/verify-postgis-security.sh ` is the gate that keeps a +vulnerable build from reaching a tag. It asserts against the *installed* +extension, not the Dockerfile args: release floor, the patch manifest baked into +the image, that a truncated FlatGeobuf buffer is rejected with the backend still +alive, and that `standardize_address()` still works. CI runs it on every PR and +on the pushed digest before the `latest`/`18` manifests move. + +Drop the patches when a PostGIS release contains all four fixes; the gate's +`POSTGIS_MIN_VERSION` floor and manifest check are what to update then. + ## Building manually ```bash diff --git a/patches/0001-flatgeobuf-validate-input-buffers-before-decoding.patch b/patches/0001-flatgeobuf-validate-input-buffers-before-decoding.patch new file mode 100644 index 0000000..92b1d1a --- /dev/null +++ b/patches/0001-flatgeobuf-validate-input-buffers-before-decoding.patch @@ -0,0 +1,1629 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Darafei Praliaskouski +Date: Sat, 8 Aug 2026 15:23:44 +0400 +Subject: [PATCH 1/4] flatgeobuf: validate input buffers before decoding + +Validate FlatGeobuf size-prefixed header and feature buffers against the remaining bytea before FlatBuffers decodes them. Reject malformed variable-length property values and adjacent malformed geometry/index metadata before copying or advancing through decoder state. + +Also handle NULL bytea input to ST_FromFlatGeobuf explicitly, preserving its existing empty-set behavior for NULL input. + +(cherry picked from commit 0a7c72a9102033c8b6ddb4f3875cc0741d3baa0a) +(cherry picked from commit 53e273faeb440176eee83f76319b48afb0b86834) +--- + deps/flatgeobuf/flatgeobuf_c.cpp | 177 +++++++++++----- + deps/flatgeobuf/geometryreader.cpp | 174 +++++++++------ + postgis/flatgeobuf.c | 326 ++++++++++++++++++++++------- + postgis/flatgeobuf.h | 8 +- + postgis/lwgeom_in_flatgeobuf.c | 85 +++++--- + regress/core/flatgeobuf.sql | 32 +++ + regress/core/flatgeobuf_expected | 7 + + 7 files changed, 585 insertions(+), 224 deletions(-) + +diff --git a/deps/flatgeobuf/flatgeobuf_c.cpp b/deps/flatgeobuf/flatgeobuf_c.cpp +index 16a259375..a5b121f94 100644 +--- a/deps/flatgeobuf/flatgeobuf_c.cpp ++++ b/deps/flatgeobuf/flatgeobuf_c.cpp +@@ -28,12 +28,14 @@ + #include "geometryreader.h" + #include "packedrtree.h" + ++#include ++ + using namespace flatbuffers; + using namespace FlatGeobuf; + + typedef flatgeobuf_ctx ctx; + +-uint8_t flatgeobuf_magicbytes[] = { 0x66, 0x67, 0x62, 0x03, 0x66, 0x67, 0x62, 0x01 }; ++uint8_t flatgeobuf_magicbytes[] = {0x66, 0x67, 0x62, 0x03, 0x66, 0x67, 0x62, 0x01}; + uint8_t FLATGEOBUF_MAGICBYTES_SIZE = sizeof(flatgeobuf_magicbytes); + + struct FeatureItem : FlatGeobuf::Item { +@@ -41,20 +43,30 @@ struct FeatureItem : FlatGeobuf::Item { + uint64_t offset; + }; + +-int flatgeobuf_encode_header(ctx *ctx) ++static size_t ++flatgeobuf_size_prefixed_verifier_length(uoffset_t size) ++{ ++ return (size_t)size + sizeof(uoffset_t); ++} ++ ++int ++flatgeobuf_encode_header(ctx *ctx) + { + FlatBufferBuilder fbb; + fbb.TrackMinAlign(8); + + // inspect first geometry +- if (ctx->lwgeom != NULL) { ++ if (ctx->lwgeom != NULL) ++ { + if (lwgeom_has_srid(ctx->lwgeom)) + ctx->srid = ctx->lwgeom->srid; + ctx->has_z = lwgeom_has_z(ctx->lwgeom); + ctx->has_m = lwgeom_has_m(ctx->lwgeom); + ctx->lwgeom_type = ctx->lwgeom->type; +- ctx->geometry_type = (uint8_t) GeometryWriter::get_geometrytype(ctx->lwgeom); +- } else { ++ ctx->geometry_type = (uint8_t)GeometryWriter::get_geometrytype(ctx->lwgeom); ++ } ++ else ++ { + LWDEBUG(2, "ctx->lwgeom is null"); + ctx->geometry_type = 0; + } +@@ -64,10 +76,12 @@ int flatgeobuf_encode_header(ctx *ctx) + std::vector> columns; + std::vector> *pColumns = nullptr; + +- if (ctx->columns_size > 0) { +- for (uint16_t i = 0; i < ctx->columns_size; i++) { ++ if (ctx->columns_size > 0) ++ { ++ for (uint16_t i = 0; i < ctx->columns_size; i++) ++ { + auto c = ctx->columns[i]; +- columns.push_back(CreateColumnDirect(fbb, c->name, (ColumnType) c->type)); ++ columns.push_back(CreateColumnDirect(fbb, c->name, (ColumnType)c->type)); + } + } + if (columns.size() > 0) +@@ -79,7 +93,8 @@ int flatgeobuf_encode_header(ctx *ctx) + + std::vector envelope; + std::vector *pEnvelope = nullptr; +- if (ctx->has_extent) { ++ if (ctx->has_extent) ++ { + envelope.push_back(ctx->xmin); + envelope.push_back(ctx->ymin); + envelope.push_back(ctx->xmax); +@@ -88,21 +103,32 @@ int flatgeobuf_encode_header(ctx *ctx) + if (envelope.size() > 0) + pEnvelope = &envelope; + +- const auto header = CreateHeaderDirect( +- fbb, ctx->name, pEnvelope, (GeometryType) ctx->geometry_type, ctx->has_z, ctx->has_m, ctx->has_t, ctx->has_tm, pColumns, ctx->features_count, ctx->index_node_size, crs); ++ const auto header = CreateHeaderDirect(fbb, ++ ctx->name, ++ pEnvelope, ++ (GeometryType)ctx->geometry_type, ++ ctx->has_z, ++ ctx->has_m, ++ ctx->has_t, ++ ctx->has_tm, ++ pColumns, ++ ctx->features_count, ++ ctx->index_node_size, ++ crs); + fbb.FinishSizePrefixed(header); + const auto buffer = fbb.GetBufferPointer(); + const auto size = fbb.GetSize(); + + LWDEBUGF(2, "header size %d (with size prefix)", size); + +- Verifier verifier(buffer, size - sizeof(uoffset_t)); +- if (VerifySizePrefixedHeaderBuffer(verifier)) { ++ Verifier verifier(buffer, size); ++ if (!VerifySizePrefixedHeaderBuffer(verifier)) ++ { + lwerror("buffer did not pass verification"); + return -1; + } + +- ctx->buf = (uint8_t *) lwrealloc(ctx->buf, ctx->offset + size); ++ ctx->buf = (uint8_t *)lwrealloc(ctx->buf, ctx->offset + size); + LWDEBUGF(2, "copying to ctx->buf at offset %llu", ctx->offset); + memcpy(ctx->buf + ctx->offset, buffer, size); + +@@ -111,7 +137,8 @@ int flatgeobuf_encode_header(ctx *ctx) + return 0; + } + +-int flatgeobuf_encode_feature(ctx *ctx) ++int ++flatgeobuf_encode_feature(ctx *ctx) + { + FlatBufferBuilder fbb; + Offset geometry = 0; +@@ -119,13 +146,15 @@ int flatgeobuf_encode_feature(ctx *ctx) + + fbb.TrackMinAlign(8); + +- if (ctx->lwgeom != NULL && !lwgeom_is_empty(ctx->lwgeom)) { ++ if (ctx->lwgeom != NULL && !lwgeom_is_empty(ctx->lwgeom)) ++ { + LWDEBUGG(3, ctx->lwgeom, "GeometryWriter input LWGEOM"); +- if (ctx->lwgeom_type != ctx->lwgeom->type) { ++ if (ctx->lwgeom_type != ctx->lwgeom->type) ++ { + lwerror("mixed geometry type is not supported"); + return -1; + } +- GeometryWriter writer(fbb, ctx->lwgeom, (GeometryType) ctx->geometry_type, ctx->has_z, ctx->has_m); ++ GeometryWriter writer(fbb, ctx->lwgeom, (GeometryType)ctx->geometry_type, ctx->has_z, ctx->has_m); + geometry = writer.write(0); + } + if (ctx->properties_len > 0) +@@ -140,21 +169,24 @@ int flatgeobuf_encode_feature(ctx *ctx) + + LWDEBUGF(3, "encode_feature size %u", size); + +- Verifier verifier(buffer, size - sizeof(uoffset_t)); +- if (VerifySizePrefixedFeatureBuffer(verifier)) { ++ Verifier verifier(buffer, size); ++ if (!VerifySizePrefixedFeatureBuffer(verifier)) ++ { + lwerror("buffer did not pass verification"); + return -1; + } + + LWDEBUGF(3, "reallocating ctx->buf to size %llu", ctx->offset + size); +- ctx->buf = (uint8_t * ) lwrealloc(ctx->buf, ctx->offset + size); ++ ctx->buf = (uint8_t *)lwrealloc(ctx->buf, ctx->offset + size); + LWDEBUGF(3, "copying feature to ctx->buf at offset %llu", ctx->offset); + memcpy(ctx->buf + ctx->offset, buffer, size); + +- if (ctx->create_index) { +- auto item = (flatgeobuf_item *) lwalloc(sizeof(flatgeobuf_item)); ++ if (ctx->create_index) ++ { ++ auto item = (flatgeobuf_item *)lwalloc(sizeof(flatgeobuf_item)); + memset(item, 0, sizeof(flatgeobuf_item)); +- if (ctx->lwgeom != NULL && !lwgeom_is_empty(ctx->lwgeom)) { ++ if (ctx->lwgeom != NULL && !lwgeom_is_empty(ctx->lwgeom)) ++ { + auto gbox = lwgeom_get_bbox(ctx->lwgeom); + item->xmin = gbox->xmin; + item->xmax = gbox->xmax; +@@ -171,15 +203,15 @@ int flatgeobuf_encode_feature(ctx *ctx) + return 0; + } + +-void flatgeobuf_create_index(ctx *ctx) ++void ++flatgeobuf_create_index(ctx *ctx) + { + // convert to structure expected by packedrtree + std::vector> items; +- for (uint64_t i = 0; i < ctx->features_count; i++) { ++ for (uint64_t i = 0; i < ctx->features_count; i++) ++ { + const auto item = std::make_shared(); +- item->nodeItem = { +- ctx->items[i]->xmin, ctx->items[i]->ymin, ctx->items[i]->xmax, ctx->items[i]->ymax +- }; ++ item->nodeItem = {ctx->items[i]->xmin, ctx->items[i]->ymin, ctx->items[i]->xmax, ctx->items[i]->ymax}; + item->offset = ctx->items[i]->offset; + item->size = ctx->items[i]->size; + items.push_back(item); +@@ -196,30 +228,32 @@ void flatgeobuf_create_index(ctx *ctx) + // allocate new buffer and write magicbytes + auto oldbuf = ctx->buf; + auto oldoffset = ctx->offset; +- ctx->buf = (uint8_t *) lwalloc(sizeof(signed int) + FLATGEOBUF_MAGICBYTES_SIZE); ++ ctx->buf = (uint8_t *)lwalloc(sizeof(signed int) + FLATGEOBUF_MAGICBYTES_SIZE); + memcpy(ctx->buf + sizeof(signed int), flatgeobuf_magicbytes, FLATGEOBUF_MAGICBYTES_SIZE); + ctx->offset = sizeof(signed int) + FLATGEOBUF_MAGICBYTES_SIZE; + // write new header + flatgeobuf_encode_header(ctx); + // calculate new offsets + uint64_t featureOffset = 0; +- for (auto item : items) { ++ for (auto item : items) ++ { + auto featureItem = std::static_pointer_cast(item); + featureItem->nodeItem.offset = featureOffset; + featureOffset += featureItem->size; + } + // create and write index + PackedRTree tree(items, extent, ctx->index_node_size); +- const auto writeData = [&ctx] (const void *data, const size_t size) { +- ctx->buf = (uint8_t *) lwrealloc(ctx->buf, ctx->offset + size); ++ const auto writeData = [&ctx](const void *data, const size_t size) { ++ ctx->buf = (uint8_t *)lwrealloc(ctx->buf, ctx->offset + size); + memcpy(ctx->buf + ctx->offset, data, size); + ctx->offset += size; + }; + tree.streamWrite(writeData); + // read items and write in sorted order +- for (auto item : items) { ++ for (auto item : items) ++ { + auto featureItem = std::static_pointer_cast(item); +- ctx->buf = (uint8_t *) lwrealloc(ctx->buf, ctx->offset + featureItem->size); ++ ctx->buf = (uint8_t *)lwrealloc(ctx->buf, ctx->offset + featureItem->size); + LWDEBUGF(2, "copy from offset %llu", featureItem->offset); + memcpy(ctx->buf + ctx->offset, oldbuf + featureItem->offset, featureItem->size); + ctx->offset += featureItem->size; +@@ -227,14 +261,16 @@ void flatgeobuf_create_index(ctx *ctx) + lwfree(oldbuf); + } + +-int flatgeobuf_decode_feature(ctx *ctx) ++int ++flatgeobuf_decode_feature(ctx *ctx) + { + LWDEBUGF(2, "reading size prefix at %llu", ctx->offset); + auto size = flatbuffers::GetPrefixedSize(ctx->buf + ctx->offset); + LWDEBUGF(2, "size is %u (without size prefix)", size); + +- Verifier verifier(ctx->buf + ctx->offset, size); +- if (VerifySizePrefixedFeatureBuffer(verifier)) { ++ Verifier verifier(ctx->buf + ctx->offset, flatgeobuf_size_prefixed_verifier_length(size)); ++ if (!VerifySizePrefixedFeatureBuffer(verifier)) ++ { + lwerror("buffer did not pass verification"); + return -1; + } +@@ -245,34 +281,46 @@ int flatgeobuf_decode_feature(ctx *ctx) + ctx->offset += size; + + const auto geometry = feature->geometry(); +- if (geometry != nullptr) { +- LWDEBUGF(3, "Constructing GeometryReader with geometry_type %d has_z %d haz_m %d", ctx->geometry_type, ctx->has_z, ctx->has_m); +- GeometryReader reader(geometry, (GeometryType) ctx->geometry_type, ctx->has_z, ctx->has_m); ++ if (geometry != nullptr) ++ { ++ LWDEBUGF(3, ++ "Constructing GeometryReader with geometry_type %d has_z %d haz_m %d", ++ ctx->geometry_type, ++ ctx->has_z, ++ ctx->has_m); ++ GeometryReader reader(geometry, (GeometryType)ctx->geometry_type, ctx->has_z, ctx->has_m); + ctx->lwgeom = reader.read(); + if (ctx->srid > 0) + lwgeom_set_srid(ctx->lwgeom, ctx->srid); + LWDEBUGG(3, ctx->lwgeom, "GeometryReader output LWGEOM"); +- } else { ++ } ++ else ++ { + ctx->lwgeom = NULL; + } +- if (feature->properties() != nullptr && feature->properties()->size() != 0) { +- ctx->properties = (uint8_t *) feature->properties()->data(); ++ if (feature->properties() != nullptr && feature->properties()->size() != 0) ++ { ++ ctx->properties = (uint8_t *)feature->properties()->data(); + ctx->properties_len = feature->properties()->size(); +- } else { ++ } ++ else ++ { + ctx->properties_len = 0; + } + + return 0; + } + +-int flatgeobuf_decode_header(ctx *ctx) ++int ++flatgeobuf_decode_header(ctx *ctx) + { + LWDEBUGF(2, "reading size prefix at %llu", ctx->offset); + auto size = flatbuffers::GetPrefixedSize(ctx->buf + ctx->offset); + LWDEBUGF(2, "size is %u (without size prefix)", size); + +- Verifier verifier(ctx->buf + ctx->offset, size); +- if (VerifySizePrefixedHeaderBuffer(verifier)) { ++ Verifier verifier(ctx->buf + ctx->offset, flatgeobuf_size_prefixed_verifier_length(size)); ++ if (!VerifySizePrefixedHeaderBuffer(verifier)) ++ { + lwerror("buffer did not pass verification"); + return -1; + } +@@ -283,7 +331,7 @@ int flatgeobuf_decode_header(ctx *ctx) + auto header = GetHeader(ctx->buf + ctx->offset); + ctx->offset += size; + +- ctx->geometry_type = (uint8_t) header->geometry_type(); ++ ctx->geometry_type = (uint8_t)header->geometry_type(); + ctx->features_count = header->features_count(); + ctx->has_z = header->has_z(); + ctx->has_m = header->has_m(); +@@ -294,24 +342,41 @@ int flatgeobuf_decode_header(ctx *ctx) + if (crs != nullptr) + ctx->srid = crs->code(); + auto columns = header->columns(); +- if (columns != nullptr) { ++ if (columns != nullptr) ++ { + auto size = columns->size(); +- ctx->columns = (flatgeobuf_column **) lwalloc(sizeof(flatgeobuf_column *) * size); ++ ctx->columns = (flatgeobuf_column **)lwalloc(sizeof(flatgeobuf_column *) * size); + ctx->columns_size = size; +- for (uint32_t i = 0; i < size; i++) { ++ for (uint32_t i = 0; i < size; i++) ++ { + auto column = columns->Get(i); +- ctx->columns[i] = (flatgeobuf_column *) lwalloc(sizeof(flatgeobuf_column)); ++ ctx->columns[i] = (flatgeobuf_column *)lwalloc(sizeof(flatgeobuf_column)); + memset(ctx->columns[i], 0, sizeof(flatgeobuf_column)); + ctx->columns[i]->name = column->name()->c_str(); +- ctx->columns[i]->type = (uint8_t) column->type(); ++ ctx->columns[i]->type = (uint8_t)column->type(); + } + } + + LWDEBUGF(2, "ctx->geometry_type: %d", ctx->geometry_type); + LWDEBUGF(2, "ctx->columns_len: %d", ctx->columns_size); + +- if (ctx->index_node_size > 0 && ctx->features_count > 0) { +- auto treeSize = PackedRTree::size(ctx->features_count, ctx->index_node_size); ++ if (ctx->index_node_size > 0 && ctx->features_count > 0) ++ { ++ uint64_t treeSize; ++ try ++ { ++ treeSize = PackedRTree::size(ctx->features_count, ctx->index_node_size); ++ } ++ catch (const std::exception &e) ++ { ++ lwerror("flatgeobuf: invalid packed rtree metadata: %s", e.what()); ++ return -1; ++ } ++ if (treeSize > ctx->size - ctx->offset) ++ { ++ lwerror("flatgeobuf: packed rtree exceeds remaining input"); ++ return -1; ++ } + LWDEBUGF(2, "Adding tree size %llu to offset", treeSize); + ctx->offset += treeSize; + } +diff --git a/deps/flatgeobuf/geometryreader.cpp b/deps/flatgeobuf/geometryreader.cpp +index 06f30b1f1..413e56257 100644 +--- a/deps/flatgeobuf/geometryreader.cpp ++++ b/deps/flatgeobuf/geometryreader.cpp +@@ -27,92 +27,105 @@ + using namespace flatbuffers; + using namespace FlatGeobuf; + +-LWPOINT *GeometryReader::readPoint() ++LWPOINT * ++GeometryReader::readPoint() + { + POINTARRAY *pa; + POINT4D pt; + + pa = ptarray_construct_empty(m_has_z, m_has_m, 1); + +- if (m_geometry->xy() == nullptr || m_geometry->xy()->size() == 0) { ++ if (m_geometry->xy() == nullptr || m_geometry->xy()->size() == 0) ++ { + return lwpoint_construct(0, NULL, pa); + } + +- const auto xy = m_geometry->xy()->data(); ++ const auto xy = m_geometry->xy(); + +- double x = xy[m_offset + 0]; +- double y = xy[m_offset + 1]; ++ double x = xy->Get(m_offset + 0); ++ double y = xy->Get(m_offset + 1); + double z = 0; + double m = 0; + + if (m_has_z) +- z = m_geometry->z()->data()[m_offset]; ++ z = m_geometry->z()->Get(m_offset); + if (m_has_m) +- m = m_geometry->m()->data()[m_offset]; ++ m = m_geometry->m()->Get(m_offset); + +- pt = (POINT4D) { x, y, z, m }; ++ pt = (POINT4D){x, y, z, m}; + ptarray_append_point(pa, &pt, LW_TRUE); + return lwpoint_construct(0, NULL, pa); + } + +-POINTARRAY *GeometryReader::readPA() ++POINTARRAY * ++GeometryReader::readPA() + { + POINTARRAY *pa; + POINT4D pt; +- uint32_t npoints; + +- const double *xy = m_geometry->xy()->data(); +- const double *z = m_has_z ? m_geometry->z()->data() : nullptr; +- const double *m = m_has_m ? m_geometry->m()->data() : nullptr; ++ const auto xy = m_geometry->xy(); ++ const auto z = m_has_z ? m_geometry->z() : nullptr; ++ const auto m = m_has_m ? m_geometry->m() : nullptr; + + pa = ptarray_construct_empty(m_has_z, m_has_m, m_length); + +- for (uint32_t i = m_offset; i < m_offset + m_length; i++) { +- double xv = xy[i * 2 + 0]; +- double yv = xy[i * 2 + 1]; ++ for (uint32_t i = m_offset; i < m_offset + m_length; i++) ++ { ++ double xv = xy->Get(i * 2 + 0); ++ double yv = xy->Get(i * 2 + 1); + double zv = 0; + double mv = 0; + if (m_has_z) +- zv = z[i]; ++ zv = z->Get(i); + if (m_has_m) +- mv = m[i]; +- pt = (POINT4D) { xv, yv, zv, mv }; ++ mv = m->Get(i); ++ pt = (POINT4D){xv, yv, zv, mv}; + ptarray_append_point(pa, &pt, LW_TRUE); + } + + return pa; + } + +-LWMPOINT *GeometryReader::readMultiPoint() ++LWMPOINT * ++GeometryReader::readMultiPoint() + { + POINTARRAY *pa = readPA(); + return lwmpoint_construct(0, pa); + } + +-LWLINE *GeometryReader::readLineString() ++LWLINE * ++GeometryReader::readLineString() + { + POINTARRAY *pa = readPA(); + return lwline_construct(0, NULL, pa); + } + +-LWMLINE *GeometryReader::readMultiLineString() ++LWMLINE * ++GeometryReader::readMultiLineString() + { + auto ends = m_geometry->ends(); ++ const uint32_t totalPoints = m_length; + + uint32_t ngeoms = 1; + if (ends != nullptr && ends->size() > 1) + ngeoms = ends->size(); + + auto *lwmline = lwmline_construct_empty(0, m_has_z, m_has_m); +- if (ngeoms > 1) { +- for (uint32_t i = 0; i < ngeoms; i++) { ++ if (ngeoms > 1) ++ { ++ for (uint32_t i = 0; i < ngeoms; i++) ++ { + const auto e = ends->Get(i); ++ if (e < m_offset || e > totalPoints) ++ lwerror("flatgeobuf: invalid geometry ends"); + m_length = e - m_offset; + POINTARRAY *pa = readPA(); + lwmline_add_lwline(lwmline, lwline_construct(0, NULL, pa)); + m_offset = e; + } +- } else { ++ } ++ else ++ { + POINTARRAY *pa = readPA(); + lwmline_add_lwline(lwmline, lwline_construct(0, NULL, pa)); + } +@@ -120,86 +133,125 @@ LWMLINE *GeometryReader::readMultiLineString() + return lwmline; + } + +-LWPOLY *GeometryReader::readPolygon() ++LWPOLY * ++GeometryReader::readPolygon() + { + const auto ends = m_geometry->ends(); ++ const uint32_t totalPoints = m_length; + + uint32_t nrings = 1; + if (ends != nullptr && ends->size() > 1) + nrings = ends->size(); + +- auto **ppa = (POINTARRAY **) lwalloc(sizeof(POINTARRAY *) * nrings); +- if (nrings > 1) { +- for (uint32_t i = 0; i < nrings; i++) { ++ auto **ppa = (POINTARRAY **)lwalloc(sizeof(POINTARRAY *) * nrings); ++ if (nrings > 1) ++ { ++ for (uint32_t i = 0; i < nrings; i++) ++ { + const auto e = ends->Get(i); ++ if (e < m_offset || e > totalPoints) ++ lwerror("flatgeobuf: invalid geometry ends"); + m_length = e - m_offset; + ppa[i] = readPA(); + m_offset = e; + } +- } else { ++ } ++ else ++ { + ppa[0] = readPA(); + } + + return lwpoly_construct(0, NULL, nrings, ppa); + } + +-LWMPOLY *GeometryReader::readMultiPolygon() ++LWMPOLY * ++GeometryReader::readMultiPolygon() + { + auto parts = m_geometry->parts(); ++ if (parts == nullptr) ++ lwerror("flatgeobuf: missing geometry parts"); ++ + auto *mp = lwmpoly_construct_empty(0, m_has_z, m_has_m); +- for (uoffset_t i = 0; i < parts->size(); i++) { +- GeometryReader reader { parts->Get(i), GeometryType::Polygon, m_has_z, m_has_m }; +- const auto p = (LWPOLY *) reader.read(); ++ for (uoffset_t i = 0; i < parts->size(); i++) ++ { ++ GeometryReader reader{parts->Get(i), GeometryType::Polygon, m_has_z, m_has_m}; ++ const auto p = (LWPOLY *)reader.read(); + lwmpoly_add_lwpoly(mp, p); + } + return mp; + } + +-LWCOLLECTION *GeometryReader::readGeometryCollection() ++LWCOLLECTION * ++GeometryReader::readGeometryCollection() + { + auto parts = m_geometry->parts(); ++ if (parts == nullptr) ++ lwerror("flatgeobuf: missing geometry parts"); ++ + auto *gc = lwcollection_construct_empty(COLLECTIONTYPE, 0, m_has_z, m_has_m); +- for (uoffset_t i = 0; i < parts->size(); i++) { ++ for (uoffset_t i = 0; i < parts->size(); i++) ++ { + auto part = parts->Get(i); +- GeometryReader reader { part, part->type(), m_has_z, m_has_m }; ++ GeometryReader reader{part, part->type(), m_has_z, m_has_m}; + const auto g = reader.read(); + lwcollection_add_lwgeom(gc, g); + } + return gc; + } + +-LWGEOM *GeometryReader::read() ++LWGEOM * ++GeometryReader::read() + { + // nested types +- switch (m_geometry_type) { +- case GeometryType::GeometryCollection: return (LWGEOM *) readGeometryCollection(); +- case GeometryType::MultiPolygon: return (LWGEOM *) readMultiPolygon(); +- /*case GeometryType::CompoundCurve: return readCompoundCurve(); +- case GeometryType::CurvePolygon: return readCurvePolygon(); +- case GeometryType::MultiCurve: return readMultiCurve(); +- case GeometryType::MultiSurface: return readMultiSurface(); +- case GeometryType::PolyhedralSurface: return readPolyhedralSurface();*/ +- default: break; ++ switch (m_geometry_type) ++ { ++ case GeometryType::GeometryCollection: ++ return (LWGEOM *)readGeometryCollection(); ++ case GeometryType::MultiPolygon: ++ return (LWGEOM *)readMultiPolygon(); ++ /*case GeometryType::CompoundCurve: return readCompoundCurve(); ++ case GeometryType::CurvePolygon: return readCurvePolygon(); ++ case GeometryType::MultiCurve: return readMultiCurve(); ++ case GeometryType::MultiSurface: return readMultiSurface(); ++ case GeometryType::PolyhedralSurface: return readPolyhedralSurface();*/ ++ default: ++ break; + } + + // if not nested must have geometry data + const auto pXy = m_geometry->xy(); ++ if (pXy == nullptr) ++ lwerror("flatgeobuf: missing geometry coordinates"); ++ + const auto xySize = pXy->size(); ++ if (xySize % 2 != 0) ++ lwerror("flatgeobuf: invalid xy coordinate count"); ++ + m_length = xySize / 2; ++ if (m_has_z && (m_geometry->z() == nullptr || m_geometry->z()->size() < m_length)) ++ lwerror("flatgeobuf: invalid z coordinate count"); ++ if (m_has_m && (m_geometry->m() == nullptr || m_geometry->m()->size() < m_length)) ++ lwerror("flatgeobuf: invalid m coordinate count"); + +- switch (m_geometry_type) { +- case GeometryType::Point: return (LWGEOM *) readPoint(); +- case GeometryType::MultiPoint: return (LWGEOM *) readMultiPoint(); +- case GeometryType::LineString: return (LWGEOM *) readLineString(); +- case GeometryType::MultiLineString: return (LWGEOM *) readMultiLineString(); +- case GeometryType::Polygon: return (LWGEOM *) readPolygon(); +- /* +- case GeometryType::CircularString: return readSimpleCurve(true); +- case GeometryType::Triangle: return readTriangle(); +- case GeometryType::TIN: return readTIN(); +- */ +- default: +- lwerror("flatgeobuf: GeometryReader::read: Unknown type %d", (int) m_geometry_type); ++ switch (m_geometry_type) ++ { ++ case GeometryType::Point: ++ return (LWGEOM *)readPoint(); ++ case GeometryType::MultiPoint: ++ return (LWGEOM *)readMultiPoint(); ++ case GeometryType::LineString: ++ return (LWGEOM *)readLineString(); ++ case GeometryType::MultiLineString: ++ return (LWGEOM *)readMultiLineString(); ++ case GeometryType::Polygon: ++ return (LWGEOM *)readPolygon(); ++ /* ++ case GeometryType::CircularString: return readSimpleCurve(true); ++ case GeometryType::Triangle: return readTriangle(); ++ case GeometryType::TIN: return readTIN(); ++ */ ++ default: ++ lwerror("flatgeobuf: GeometryReader::read: Unknown type %d", (int)m_geometry_type); + } + return nullptr; +-} +\ No newline at end of file ++} +diff --git a/postgis/flatgeobuf.c b/postgis/flatgeobuf.c +index c984557dd..41f069241 100644 +--- a/postgis/flatgeobuf.c ++++ b/postgis/flatgeobuf.c +@@ -33,7 +33,122 @@ + #include "utils/datetime.h" + #include "utils/jsonb.h" + +-static uint8_t get_column_type(Oid typoid) { ++static uint16_t ++flatgeobuf_le16(uint16_t v) ++{ ++#if IS_BIG_ENDIAN ++ return (uint16_t)((v << 8) | (v >> 8)); ++#else ++ return v; ++#endif ++} ++ ++static uint32_t ++flatgeobuf_le32(uint32_t v) ++{ ++#if IS_BIG_ENDIAN ++ return ((v & UINT32_C(0x000000ff)) << 24) | ((v & UINT32_C(0x0000ff00)) << 8) | ++ ((v & UINT32_C(0x00ff0000)) >> 8) | ((v & UINT32_C(0xff000000)) >> 24); ++#else ++ return v; ++#endif ++} ++ ++static uint64_t ++flatgeobuf_le64(uint64_t v) ++{ ++#if IS_BIG_ENDIAN ++ return ((v & UINT64_C(0x00000000000000ff)) << 56) | ((v & UINT64_C(0x000000000000ff00)) << 40) | ++ ((v & UINT64_C(0x0000000000ff0000)) << 24) | ((v & UINT64_C(0x00000000ff000000)) << 8) | ++ ((v & UINT64_C(0x000000ff00000000)) >> 8) | ((v & UINT64_C(0x0000ff0000000000)) >> 24) | ++ ((v & UINT64_C(0x00ff000000000000)) >> 40) | ((v & UINT64_C(0xff00000000000000)) >> 56); ++#else ++ return v; ++#endif ++} ++ ++static void ++flatgeobuf_write_le16(uint8_t *dst, uint16_t v) ++{ ++ v = flatgeobuf_le16(v); ++ memcpy(dst, &v, sizeof(v)); ++} ++ ++static void ++flatgeobuf_write_le32(uint8_t *dst, uint32_t v) ++{ ++ v = flatgeobuf_le32(v); ++ memcpy(dst, &v, sizeof(v)); ++} ++ ++static void ++flatgeobuf_write_le64(uint8_t *dst, uint64_t v) ++{ ++ v = flatgeobuf_le64(v); ++ memcpy(dst, &v, sizeof(v)); ++} ++ ++static uint16_t ++flatgeobuf_read_le16(const uint8_t *src) ++{ ++ uint16_t v; ++ memcpy(&v, src, sizeof(v)); ++ return flatgeobuf_le16(v); ++} ++ ++static uint32_t ++flatgeobuf_read_le32(const uint8_t *src) ++{ ++ uint32_t v; ++ memcpy(&v, src, sizeof(v)); ++ return flatgeobuf_le32(v); ++} ++ ++static uint64_t ++flatgeobuf_read_le64(const uint8_t *src) ++{ ++ uint64_t v; ++ memcpy(&v, src, sizeof(v)); ++ return flatgeobuf_le64(v); ++} ++ ++static float ++flatgeobuf_read_float_le(const uint8_t *src) ++{ ++ uint32_t bits = flatgeobuf_read_le32(src); ++ float value; ++ memcpy(&value, &bits, sizeof(value)); ++ return value; ++} ++ ++static double ++flatgeobuf_read_double_le(const uint8_t *src) ++{ ++ uint64_t bits = flatgeobuf_read_le64(src); ++ double value; ++ memcpy(&value, &bits, sizeof(value)); ++ return value; ++} ++ ++static void ++flatgeobuf_write_float_le(uint8_t *dst, float value) ++{ ++ uint32_t bits; ++ memcpy(&bits, &value, sizeof(bits)); ++ flatgeobuf_write_le32(dst, bits); ++} ++ ++static void ++flatgeobuf_write_double_le(uint8_t *dst, double value) ++{ ++ uint64_t bits; ++ memcpy(&bits, &value, sizeof(bits)); ++ flatgeobuf_write_le64(dst, bits); ++} ++ ++static uint8_t ++get_column_type(Oid typoid) ++{ + switch (typoid) + { + case BOOLOID: +@@ -61,11 +176,11 @@ static uint8_t get_column_type(Oid typoid) { + case TIMESTAMPTZOID: + return flatgeobuf_column_type_datetime; + } +- elog(ERROR, "flatgeobuf: get_column_type: '%d' column type not supported", +- typoid); ++ elog(ERROR, "flatgeobuf: get_column_type: '%d' column type not supported", typoid); + } + +-static void inspect_table(struct flatgeobuf_agg_ctx *ctx) ++static void ++inspect_table(struct flatgeobuf_agg_ctx *ctx) + { + flatgeobuf_column *c; + flatgeobuf_column **columns; +@@ -83,18 +198,24 @@ static void inspect_table(struct flatgeobuf_agg_ctx *ctx) + + // inspect columns + // NOTE: last element will be unused if geom attr is found +- for (int i = 0; i < natts; i++) { ++ for (int i = 0; i < natts; i++) ++ { + Oid typoid = getBaseType(TupleDescAttr(tupdesc, i)->atttypid); + const char *key = TupleDescAttr(tupdesc, i)->attname.data; + POSTGIS_DEBUGF(2, "inspecting column definition for %s with oid %d", key, typoid); +- if (ctx->geom_name == NULL) { +- if (!geom_found && typoid == postgis_oid(GEOMETRYOID)) { ++ if (ctx->geom_name == NULL) ++ { ++ if (!geom_found && typoid == postgis_oid(GEOMETRYOID)) ++ { + ctx->geom_index = i; + geom_found = true; + continue; + } +- } else { +- if (!geom_found && strcmp(key, ctx->geom_name) == 0) { ++ } ++ else ++ { ++ if (!geom_found && strcmp(key, ctx->geom_name) == 0) ++ { + ctx->geom_index = i; + geom_found = true; + continue; +@@ -102,7 +223,7 @@ static void inspect_table(struct flatgeobuf_agg_ctx *ctx) + } + POSTGIS_DEBUGF(2, "creating column definition for %s with oid %d", key, typoid); + +- c = (flatgeobuf_column *) palloc0(sizeof(flatgeobuf_column)); ++ c = (flatgeobuf_column *)palloc0(sizeof(flatgeobuf_column)); + c->name = pstrdup(key); + c->type = get_column_type(typoid); + columns[columns_size] = c; +@@ -112,21 +233,25 @@ static void inspect_table(struct flatgeobuf_agg_ctx *ctx) + if (!geom_found) + elog(ERROR, "no geom column found"); + +- if (columns_size > 0) { ++ if (columns_size > 0) ++ { + ctx->ctx->columns = columns; + ctx->ctx->columns_size = columns_size; + } + } + + // ensure properties has room for at least size +-static void ensure_properties_size(struct flatgeobuf_agg_ctx *ctx, size_t size) ++static void ++ensure_properties_size(struct flatgeobuf_agg_ctx *ctx, size_t size) + { +- if (ctx->ctx->properties_size == 0) { ++ if (ctx->ctx->properties_size == 0) ++ { + ctx->ctx->properties_size = 1024 * 4; + POSTGIS_DEBUGF(2, "flatgeobuf: properties buffer to size %d", ctx->ctx->properties_size); + ctx->ctx->properties = palloc(ctx->ctx->properties_size); + } +- if (ctx->ctx->properties_size < size) { ++ if (ctx->ctx->properties_size < size) ++ { + ctx->ctx->properties_size = ctx->ctx->properties_size * 2; + POSTGIS_DEBUGF(2, "flatgeobuf: reallocating properties buffer to size %d", ctx->ctx->properties_size); + ctx->ctx->properties = repalloc(ctx->ctx->properties, ctx->ctx->properties_size); +@@ -135,13 +260,16 @@ static void ensure_properties_size(struct flatgeobuf_agg_ctx *ctx, size_t size) + } + + // ensure items have room for at least ctx->ctx->features_count + 1 +-static void ensure_items_len(struct flatgeobuf_agg_ctx *ctx) ++static void ++ensure_items_len(struct flatgeobuf_agg_ctx *ctx) + { +- if (ctx->ctx->features_count == 0) { ++ if (ctx->ctx->features_count == 0) ++ { + ctx->ctx->items_len = 32; + ctx->ctx->items = palloc(sizeof(flatgeobuf_item *) * ctx->ctx->items_len); + } +- if (ctx->ctx->items_len < (ctx->ctx->features_count + 1)) { ++ if (ctx->ctx->items_len < (ctx->ctx->features_count + 1)) ++ { + ctx->ctx->items_len = ctx->ctx->items_len * 2; + POSTGIS_DEBUGF(2, "flatgeobuf: reallocating items to len %lld", ctx->ctx->items_len); + ctx->ctx->items = repalloc(ctx->ctx->items, sizeof(flatgeobuf_item *) * ctx->ctx->items_len); +@@ -149,7 +277,8 @@ static void ensure_items_len(struct flatgeobuf_agg_ctx *ctx) + } + } + +-static void encode_properties(flatgeobuf_agg_ctx *ctx) ++static void ++encode_properties(flatgeobuf_agg_ctx *ctx) + { + uint16_t ci = 0; + size_t offset = 0; +@@ -167,19 +296,21 @@ static void encode_properties(flatgeobuf_agg_ctx *ctx) + double double_value; + char *string_value; + +- //Jsonb *jb; ++ // Jsonb *jb; + +- for (i = 0; i < (uint32_t) ctx->tupdesc->natts; i++) { ++ for (i = 0; i < (uint32_t)ctx->tupdesc->natts; i++) ++ { + if (ctx->geom_index == i) + continue; + datum = GetAttributeByNum(ctx->row, i + 1, &isnull); + if (isnull) + continue; + ensure_properties_size(ctx, offset + sizeof(ci)); +- memcpy(ctx->ctx->properties + offset, &ci, sizeof(ci)); ++ flatgeobuf_write_le16(ctx->ctx->properties + offset, ci); + offset += sizeof(ci); + typoid = getBaseType(TupleDescAttr(ctx->tupdesc, i)->atttypid); +- switch (typoid) { ++ switch (typoid) ++ { + case BOOLOID: + byte_value = DatumGetBool(datum) ? 1 : 0; + ensure_properties_size(ctx, offset + sizeof(byte_value)); +@@ -189,38 +320,38 @@ static void encode_properties(flatgeobuf_agg_ctx *ctx) + case INT2OID: + short_value = DatumGetInt16(datum); + ensure_properties_size(ctx, offset + sizeof(short_value)); +- memcpy(ctx->ctx->properties + offset, &short_value, sizeof(short_value)); ++ flatgeobuf_write_le16(ctx->ctx->properties + offset, (uint16_t)short_value); + offset += sizeof(short_value); + break; + case INT4OID: + int_value = DatumGetInt32(datum); + ensure_properties_size(ctx, offset + sizeof(int_value)); +- memcpy(ctx->ctx->properties + offset, &int_value, sizeof(int_value)); ++ flatgeobuf_write_le32(ctx->ctx->properties + offset, (uint32_t)int_value); + offset += sizeof(int_value); + break; + case INT8OID: + long_value = DatumGetInt64(datum); + ensure_properties_size(ctx, offset + sizeof(long_value)); +- memcpy(ctx->ctx->properties + offset, &long_value, sizeof(long_value)); ++ flatgeobuf_write_le64(ctx->ctx->properties + offset, (uint64_t)long_value); + offset += sizeof(long_value); + break; + case FLOAT4OID: + float_value = DatumGetFloat4(datum); + ensure_properties_size(ctx, offset + sizeof(float_value)); +- memcpy(ctx->ctx->properties + offset, &float_value, sizeof(float_value)); ++ flatgeobuf_write_float_le(ctx->ctx->properties + offset, float_value); + offset += sizeof(float_value); + break; + case FLOAT8OID: + double_value = DatumGetFloat8(datum); + ensure_properties_size(ctx, offset + sizeof(double_value)); +- memcpy(ctx->ctx->properties + offset, &double_value, sizeof(double_value)); ++ flatgeobuf_write_double_le(ctx->ctx->properties + offset, double_value); + offset += sizeof(double_value); + break; + case TEXTOID: + string_value = text_to_cstring(DatumGetTextP(datum)); + len = strlen(string_value); + ensure_properties_size(ctx, offset + sizeof(len)); +- memcpy(ctx->ctx->properties + offset, &len, sizeof(len)); ++ flatgeobuf_write_le32(ctx->ctx->properties + offset, len); + offset += sizeof(len); + ensure_properties_size(ctx, offset + len); + memcpy(ctx->ctx->properties + offset, string_value, len); +@@ -238,45 +369,70 @@ static void encode_properties(flatgeobuf_agg_ctx *ctx) + EncodeDateTime(&tm, fsec, true, tz, tzn, USE_ISO_DATES, string_value); + len = strlen(string_value); + ensure_properties_size(ctx, offset + sizeof(len)); +- memcpy(ctx->ctx->properties + offset, &len, sizeof(len)); ++ flatgeobuf_write_le32(ctx->ctx->properties + offset, len); + offset += sizeof(len); + ensure_properties_size(ctx, offset + len); + memcpy(ctx->ctx->properties + offset, string_value, len); + offset += len; + break; + } +- // TODO: handle date/time types +- // case JSONBOID: +- // jb = DatumGetJsonbP(datum); +- // string_value = JsonbToCString(NULL, &jb->root, VARSIZE(jb)); +- // len = strlen(string_value); +- // memcpy(data + offset, &len, sizeof(len)); +- // offset += sizeof(len); +- // memcpy(data + offset, string_value, len); +- // offset += len; +- // break; ++ // TODO: handle date/time types ++ // case JSONBOID: ++ // jb = DatumGetJsonbP(datum); ++ // string_value = JsonbToCString(NULL, &jb->root, VARSIZE(jb)); ++ // len = strlen(string_value); ++ // memcpy(data + offset, &len, sizeof(len)); ++ // offset += sizeof(len); ++ // memcpy(data + offset, string_value, len); ++ // offset += len; ++ // break; + } + ci++; + } + +- if (offset > 0) { ++ if (offset > 0) ++ { + POSTGIS_DEBUGF(3, "offset %ld", offset); + ctx->ctx->properties_len = offset; + } + } + +-void flatgeobuf_check_magicbytes(struct flatgeobuf_decode_ctx *ctx) ++void ++flatgeobuf_check_magicbytes(struct flatgeobuf_decode_ctx *ctx) + { +- uint8_t *buf = ctx->ctx->buf + ctx->ctx->offset; ++ uint8_t *buf; + uint32_t i; + ++ if (ctx->ctx->offset > ctx->ctx->size || FLATGEOBUF_MAGICBYTES_SIZE > ctx->ctx->size - ctx->ctx->offset) ++ elog(ERROR, "Data is not FlatGeobuf"); ++ ++ buf = ctx->ctx->buf + ctx->ctx->offset; + for (i = 0; i < FLATGEOBUF_MAGICBYTES_SIZE / 2; i++) + if (buf[i] != flatgeobuf_magicbytes[i]) + elog(ERROR, "Data is not FlatGeobuf"); + ctx->ctx->offset += FLATGEOBUF_MAGICBYTES_SIZE; + } + +-static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, bool *isnull) ++void ++flatgeobuf_check_sizeprefix(flatgeobuf_ctx *ctx) ++{ ++ uint64_t remaining; ++ uint32_t size; ++ ++ if (ctx->offset > ctx->size) ++ elog(ERROR, "flatgeobuf: read past end of input"); ++ ++ remaining = ctx->size - ctx->offset; ++ if (remaining < sizeof(uint32_t)) ++ elog(ERROR, "flatgeobuf: truncated size prefix"); ++ ++ size = flatgeobuf_read_le32(ctx->buf + ctx->offset); ++ if ((uint64_t)size > remaining - sizeof(uint32_t)) ++ elog(ERROR, "flatgeobuf: size prefix exceeds remaining input"); ++} ++ ++static void ++decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, bool *isnull) + { + uint16_t i, ci; + flatgeobuf_column *column; +@@ -289,10 +445,11 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + + if (size > 0 && size < (sizeof(uint16_t) + sizeof(uint8_t))) + elog(ERROR, "flatgeobuf: decode_properties: Unexpected properties data size %d", size); +- while (offset + 1 < size) { ++ while (offset + 1 < size) ++ { + if (offset + sizeof(uint16_t) > size) + elog(ERROR, "flatgeobuf: decode_properties: Unexpected offset %d", offset); +- memcpy(&i, data + offset, sizeof(uint16_t)); ++ i = flatgeobuf_read_le16(data + offset); + ci = i + 2; + offset += sizeof(uint16_t); + if (i >= ctx->ctx->columns_size) +@@ -300,7 +457,8 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + column = ctx->ctx->columns[i]; + type = column->type; + isnull[ci] = false; +- switch (type) { ++ switch (type) ++ { + case flatgeobuf_column_type_bool: { + uint8_t value; + if (offset + sizeof(uint8_t) > size) +@@ -332,7 +490,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + int16_t value; + if (offset + sizeof(int16_t) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for short value"); +- memcpy(&value, data + offset, sizeof(int16_t)); ++ value = (int16_t)flatgeobuf_read_le16(data + offset); + values[ci] = Int16GetDatum(value); + offset += sizeof(int16_t); + break; +@@ -341,7 +499,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + uint16_t value; + if (offset + sizeof(uint16_t) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for ushort value"); +- memcpy(&value, data + offset, sizeof(uint16_t)); ++ value = flatgeobuf_read_le16(data + offset); + values[ci] = UInt16GetDatum(value); + offset += sizeof(uint16_t); + break; +@@ -350,7 +508,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + int32_t value; + if (offset + sizeof(int32_t) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for int value"); +- memcpy(&value, data + offset, sizeof(int32_t)); ++ value = (int32_t)flatgeobuf_read_le32(data + offset); + values[ci] = Int32GetDatum(value); + offset += sizeof(int32_t); + break; +@@ -359,7 +517,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + uint32_t value; + if (offset + sizeof(uint32_t) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for uint value"); +- memcpy(&value, data + offset, sizeof(uint32_t)); ++ value = flatgeobuf_read_le32(data + offset); + values[ci] = UInt32GetDatum(value); + offset += sizeof(uint32_t); + break; +@@ -368,7 +526,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + int64_t value; + if (offset + sizeof(int64_t) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for long value"); +- memcpy(&value, data + offset, sizeof(int64_t)); ++ value = (int64_t)flatgeobuf_read_le64(data + offset); + values[ci] = Int64GetDatum(value); + offset += sizeof(int64_t); + break; +@@ -377,7 +535,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + uint64_t value; + if (offset + sizeof(uint64_t) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for ulong value"); +- memcpy(&value, data + offset, sizeof(uint64_t)); ++ value = flatgeobuf_read_le64(data + offset); + values[ci] = UInt64GetDatum(value); + offset += sizeof(uint64_t); + break; +@@ -386,7 +544,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + float value; + if (offset + sizeof(float) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for float value"); +- memcpy(&value, data + offset, sizeof(float)); ++ value = flatgeobuf_read_float_le(data + offset); + if (getBaseType(TupleDescAttr(ctx->tupdesc, ci)->atttypid) == FLOAT8OID) + values[ci] = Float8GetDatum((double) value); + else +@@ -398,7 +556,7 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + double value; + if (offset + sizeof(double) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for double value"); +- memcpy(&value, data + offset, sizeof(double)); ++ value = flatgeobuf_read_double_le(data + offset); + values[ci] = Float8GetDatum(value); + offset += sizeof(double); + break; +@@ -407,9 +565,11 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + uint32_t len; + if (offset + sizeof(len) > size) + elog(ERROR, "flatgeobuf: decode_properties: Invalid size for string value"); +- memcpy(&len, data + offset, sizeof(uint32_t)); ++ len = flatgeobuf_read_le32(data + offset); + offset += sizeof(len); +- values[ci] = PointerGetDatum(cstring_to_text_with_len((const char *) data + offset, len)); ++ if (len > size - offset) ++ elog(ERROR, "flatgeobuf: decode_properties: string length exceeds buffer"); ++ values[ci] = PointerGetDatum(cstring_to_text_with_len((const char *)data + offset, len)); + offset += len; + break; + } +@@ -429,12 +589,14 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + DateTimeErrorExtra extra; + #endif + if (offset + sizeof(len) > size) +- elog(ERROR, "flatgeobuf: decode_properties: Invalid size for string value"); +- memcpy(&len, data + offset, sizeof(uint32_t)); ++ elog(ERROR, "flatgeobuf: decode_properties: Invalid size for datetime value"); ++ len = flatgeobuf_read_le32(data + offset); + offset += sizeof(len); +- buf = palloc0(len + 1); +- memcpy(buf, (const char *) data + offset, len); +- ParseDateTime((const char *) buf, workbuf, sizeof(workbuf), field, ftype, MAXDATEFIELDS, &nf); ++ if (len > size - offset) ++ elog(ERROR, "flatgeobuf: decode_properties: datetime length exceeds buffer"); ++ buf = palloc0((Size)len + 1); ++ memcpy(buf, (const char *)data + offset, len); ++ ParseDateTime((const char *)buf, workbuf, sizeof(workbuf), field, ftype, MAXDATEFIELDS, &nf); + + #if POSTGIS_PGSQL_VERSION >= 160 + DecodeDateTime(field, ftype, nf, &dtype, tm, &fsec, &tzp, &extra); +@@ -459,28 +621,33 @@ static void decode_properties(struct flatgeobuf_decode_ctx *ctx, Datum *values, + elog(ERROR, "flatgeobuf: decode_properties: Unknown type %d", type); + } + } +- + } + +-void flatgeobuf_decode_row(struct flatgeobuf_decode_ctx *ctx) ++void ++flatgeobuf_decode_row(struct flatgeobuf_decode_ctx *ctx) + { + HeapTuple heapTuple; + uint32_t natts = ctx->tupdesc->natts; + + Datum *values = palloc0(natts * sizeof(Datum)); + bool *isnull = palloc(natts * sizeof(bool)); +- for (uint32_t j = 0; j < natts; j++) isnull[j] = true; ++ for (uint32_t j = 0; j < natts; j++) ++ isnull[j] = true; + isnull[0] = false; + + values[0] = Int32GetDatum(ctx->fid); + ++ flatgeobuf_check_sizeprefix(ctx->ctx); + if (flatgeobuf_decode_feature(ctx->ctx)) + elog(ERROR, "flatgeobuf_decode_feature: unsuccessful"); + +- if (ctx->ctx->lwgeom != NULL) { ++ if (ctx->ctx->lwgeom != NULL) ++ { + values[1] = PointerGetDatum(geometry_serialize(ctx->ctx->lwgeom)); + isnull[1] = false; +- } else { ++ } ++ else ++ { + POSTGIS_DEBUG(3, "geometry is null"); + isnull[1] = true; + } +@@ -494,7 +661,11 @@ void flatgeobuf_decode_row(struct flatgeobuf_decode_ctx *ctx) + + POSTGIS_DEBUGF(3, "fid now %d", ctx->fid); + +- if (ctx->ctx->offset == ctx->ctx->size) { ++ if (ctx->ctx->offset > ctx->ctx->size) ++ elog(ERROR, "flatgeobuf_decode_row: read past end of input"); ++ ++ if (ctx->ctx->offset == ctx->ctx->size) ++ { + POSTGIS_DEBUGF(3, "reached end at %lld", ctx->ctx->offset); + ctx->done = true; + } +@@ -503,7 +674,8 @@ void flatgeobuf_decode_row(struct flatgeobuf_decode_ctx *ctx) + /** + * Initialize aggregation context. + */ +-struct flatgeobuf_agg_ctx *flatgeobuf_agg_ctx_init(const char *geom_name, const bool create_index) ++struct flatgeobuf_agg_ctx * ++flatgeobuf_agg_ctx_init(const char *geom_name, const bool create_index) + { + struct flatgeobuf_agg_ctx *ctx; + size_t size = VARHDRSZ + FLATGEOBUF_MAGICBYTES_SIZE; +@@ -527,7 +699,8 @@ struct flatgeobuf_agg_ctx *flatgeobuf_agg_ctx_init(const char *geom_name, const + * Allocates a new feature, increment feature counter and + * encode properties into it. +  */ +-void flatgeobuf_agg_transfn(struct flatgeobuf_agg_ctx *ctx) ++void ++flatgeobuf_agg_transfn(struct flatgeobuf_agg_ctx *ctx) + { + LWGEOM *lwgeom = NULL; + bool isnull = false; +@@ -538,8 +711,9 @@ void flatgeobuf_agg_transfn(struct flatgeobuf_agg_ctx *ctx) + inspect_table(ctx); + + datum = GetAttributeByNum(ctx->row, ctx->geom_index + 1, &isnull); +- if (!isnull) { +- gs = (GSERIALIZED *) PG_DETOAST_DATUM_COPY(datum); ++ if (!isnull) ++ { ++ gs = (GSERIALIZED *)PG_DETOAST_DATUM_COPY(datum); + lwgeom = lwgeom_from_gserialized(gs); + } + ctx->ctx->lwgeom = lwgeom; +@@ -558,15 +732,19 @@ void flatgeobuf_agg_transfn(struct flatgeobuf_agg_ctx *ctx) + * + * Encode into Data message and return it packed as a bytea. + */ +-uint8_t *flatgeobuf_agg_finalfn(struct flatgeobuf_agg_ctx *ctx) ++uint8_t * ++flatgeobuf_agg_finalfn(struct flatgeobuf_agg_ctx *ctx) + { + POSTGIS_DEBUGF(3, "called at offset %lld", ctx->ctx->offset); + if (ctx == NULL) + flatgeobuf_agg_ctx_init(NULL, false); + // header only result +- if (ctx->ctx->features_count == 0) { ++ if (ctx->ctx->features_count == 0) ++ { + flatgeobuf_encode_header(ctx->ctx); +- } else if (ctx->ctx->create_index) { ++ } ++ else if (ctx->ctx->create_index) ++ { + ctx->ctx->index_node_size = 16; + flatgeobuf_create_index(ctx->ctx); + } +diff --git a/postgis/flatgeobuf.h b/postgis/flatgeobuf.h +index c3b13cb6c..ddad54855 100644 +--- a/postgis/flatgeobuf.h ++++ b/postgis/flatgeobuf.h +@@ -43,8 +43,7 @@ + #include "lwgeom_log.h" + #include "flatgeobuf_c.h" + +-typedef struct flatgeobuf_agg_ctx +-{ ++typedef struct flatgeobuf_agg_ctx { + flatgeobuf_ctx *ctx; + const char *geom_name; + uint32_t geom_index; +@@ -52,13 +51,11 @@ typedef struct flatgeobuf_agg_ctx + HeapTupleHeader row; + } flatgeobuf_agg_ctx; + +- + flatgeobuf_agg_ctx *flatgeobuf_agg_ctx_init(const char *geom_name, const bool create_index); + void flatgeobuf_agg_transfn(flatgeobuf_agg_ctx *ctx); + uint8_t *flatgeobuf_agg_finalfn(flatgeobuf_agg_ctx *ctx); + +-typedef struct flatgeobuf_decode_ctx +-{ ++typedef struct flatgeobuf_decode_ctx { + flatgeobuf_ctx *ctx; + TupleDesc tupdesc; + Datum result; +@@ -68,6 +65,7 @@ typedef struct flatgeobuf_decode_ctx + } flatgeobuf_decode_ctx; + + void flatgeobuf_check_magicbytes(struct flatgeobuf_decode_ctx *ctx); ++void flatgeobuf_check_sizeprefix(flatgeobuf_ctx *ctx); + void flatgeobuf_decode_row(struct flatgeobuf_decode_ctx *ctx); + + #endif +diff --git a/postgis/lwgeom_in_flatgeobuf.c b/postgis/lwgeom_in_flatgeobuf.c +index 8fc2e6efb..a9da50624 100644 +--- a/postgis/lwgeom_in_flatgeobuf.c ++++ b/postgis/lwgeom_in_flatgeobuf.c +@@ -22,7 +22,6 @@ + * + **********************************************************************/ + +- + #include + + #include "postgres.h" +@@ -36,8 +35,11 @@ + #include + #include "flatgeobuf.h" + +-static char *get_pgtype(uint8_t column_type) { +- switch (column_type) { ++static char * ++get_pgtype(uint8_t column_type) ++{ ++ switch (column_type) ++ { + case flatgeobuf_column_type_bool: + return "boolean"; + case flatgeobuf_column_type_byte: +@@ -71,11 +73,21 @@ static const char * + flatgeobuf_type_name(uint8_t fgb_type) + { + /* Names match FlatGeobuf::EnumNamesColumnType() in header_generated.h */ +- static const char * const names[] = { +- "Byte", "UByte", "Bool", "Short", "UShort", +- "Int", "UInt", "Long", "ULong", +- "Float", "Double", "String", "Json", "DateTime", "Binary" +- }; ++ static const char *const names[] = {"Byte", ++ "UByte", ++ "Bool", ++ "Short", ++ "UShort", ++ "Int", ++ "UInt", ++ "Long", ++ "ULong", ++ "Float", ++ "Double", ++ "String", ++ "Json", ++ "DateTime", ++ "Binary"}; + if (fgb_type >= sizeof(names) / sizeof(names[0])) + return "unknown"; + return names[fgb_type]; +@@ -111,8 +123,7 @@ flatgeobuf_type_compatible(uint8_t fgb_type, Oid pgtype) + case flatgeobuf_column_type_string: + return pgtype == TEXTOID || pgtype == VARCHAROID; + case flatgeobuf_column_type_datetime: +- return pgtype == DATEOID || pgtype == TIMEOID || +- pgtype == TIMESTAMPOID || pgtype == TIMESTAMPTZOID; ++ return pgtype == DATEOID || pgtype == TIMEOID || pgtype == TIMESTAMPOID || pgtype == TIMESTAMPTZOID; + case flatgeobuf_column_type_json: + return pgtype == JSONBOID; + case flatgeobuf_column_type_binary: +@@ -122,7 +133,8 @@ flatgeobuf_type_compatible(uint8_t fgb_type, Oid pgtype) + } + + PG_FUNCTION_INFO_V1(pgis_tablefromflatgeobuf); +-Datum pgis_tablefromflatgeobuf(PG_FUNCTION_ARGS) ++Datum ++pgis_tablefromflatgeobuf(PG_FUNCTION_ARGS) + { + struct flatgeobuf_decode_ctx *ctx; + text *schema_input; +@@ -155,14 +167,16 @@ Datum pgis_tablefromflatgeobuf(PG_FUNCTION_ARGS) + ctx->ctx->offset = 0; + + flatgeobuf_check_magicbytes(ctx); ++ flatgeobuf_check_sizeprefix(ctx->ctx); + flatgeobuf_decode_header(ctx->ctx); + + initStringInfo(&sql); +- appendStringInfo(&sql, "create table %s.%s (id int, geom geometry", +- quote_identifier(schema), quote_identifier(table)); ++ appendStringInfo( ++ &sql, "create table %s.%s (id int, geom geometry", quote_identifier(schema), quote_identifier(table)); + + POSTGIS_DEBUGF(2, "found %d columns", ctx->ctx->columns_size); +- for (i = 0; i < ctx->ctx->columns_size; i++) { ++ for (i = 0; i < ctx->ctx->columns_size; i++) ++ { + flatgeobuf_column *column = ctx->ctx->columns[i]; + const char *name = column->name; + uint8_t column_type = column->type; +@@ -192,7 +206,8 @@ Datum pgis_tablefromflatgeobuf(PG_FUNCTION_ARGS) + + // https://stackoverflow.com/questions/11740256/refactor-a-pl-pgsql-function-to-return-the-output-of-various-select-queries + PG_FUNCTION_INFO_V1(pgis_fromflatgeobuf); +-Datum pgis_fromflatgeobuf(PG_FUNCTION_ARGS) ++Datum ++pgis_fromflatgeobuf(PG_FUNCTION_ARGS) + { + FuncCallContext *funcctx; + +@@ -202,7 +217,8 @@ Datum pgis_fromflatgeobuf(PG_FUNCTION_ARGS) + + struct flatgeobuf_decode_ctx *ctx; + +- if (SRF_IS_FIRSTCALL()) { ++ if (SRF_IS_FIRSTCALL()) ++ { + funcctx = SRF_FIRSTCALL_INIT(); + oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); + +@@ -210,8 +226,14 @@ Datum pgis_fromflatgeobuf(PG_FUNCTION_ARGS) + + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + ereport(ERROR, +- (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), +- errmsg("first argument of function must be composite type"))); ++ (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), ++ errmsg("first argument of function must be composite type"))); ++ ++ if (PG_ARGISNULL(1)) ++ { ++ MemoryContextSwitchTo(oldcontext); ++ SRF_RETURN_DONE(funcctx); ++ } + + data = PG_GETARG_BYTEA_PP(1); + +@@ -228,18 +250,21 @@ Datum pgis_fromflatgeobuf(PG_FUNCTION_ARGS) + + funcctx->user_fctx = ctx; + +- if (ctx->ctx->size == 0) { ++ if (ctx->ctx->size == 0) ++ { + POSTGIS_DEBUG(2, "no data"); + MemoryContextSwitchTo(oldcontext); + SRF_RETURN_DONE(funcctx); + } + + flatgeobuf_check_magicbytes(ctx); ++ flatgeobuf_check_sizeprefix(ctx->ctx); + flatgeobuf_decode_header(ctx->ctx); + + POSTGIS_DEBUGF(2, "header decoded now at offset %lld", ctx->ctx->offset); + +- if (ctx->ctx->size == ctx->ctx->offset) { ++ if (ctx->ctx->size == ctx->ctx->offset) ++ { + POSTGIS_DEBUGF(2, "no feature data offset %lld", ctx->ctx->offset); + MemoryContextSwitchTo(oldcontext); + SRF_RETURN_DONE(funcctx); +@@ -251,8 +276,9 @@ Datum pgis_fromflatgeobuf(PG_FUNCTION_ARGS) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("flatgeobuf: column count mismatch: " +- "file has %u columns, target type has %d", +- ctx->ctx->columns_size, tupdesc->natts - 2))); ++ "file has %u columns, target type has %d", ++ ctx->ctx->columns_size, ++ tupdesc->natts - 2))); + + for (uint16_t col_i = 0; col_i < ctx->ctx->columns_size; col_i++) + { +@@ -262,10 +288,10 @@ Datum pgis_fromflatgeobuf(PG_FUNCTION_ARGS) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("flatgeobuf: column \"%s\" type mismatch: " +- "file type \"%s\" is not compatible with PostgreSQL type %s", +- col->name, +- flatgeobuf_type_name(col->type), +- format_type_be(pgtype)))); ++ "file type \"%s\" is not compatible with PostgreSQL type %s", ++ col->name, ++ flatgeobuf_type_name(col->type), ++ format_type_be(pgtype)))); + } + + MemoryContextSwitchTo(oldcontext); +@@ -274,11 +300,14 @@ Datum pgis_fromflatgeobuf(PG_FUNCTION_ARGS) + funcctx = SRF_PERCALL_SETUP(); + ctx = funcctx->user_fctx; + +- if (!ctx->done) { ++ if (!ctx->done) ++ { + flatgeobuf_decode_row(ctx); + POSTGIS_DEBUG(2, "Calling SRF_RETURN_NEXT"); + SRF_RETURN_NEXT(funcctx, ctx->result); +- } else { ++ } ++ else ++ { + POSTGIS_DEBUG(2, "Calling SRF_RETURN_DONE"); + SRF_RETURN_DONE(funcctx); + } +diff --git a/regress/core/flatgeobuf.sql b/regress/core/flatgeobuf.sql +index df46eca1e..6d8ba9148 100644 +--- a/regress/core/flatgeobuf.sql ++++ b/regress/core/flatgeobuf.sql +@@ -158,6 +158,35 @@ select 'E1', id, bool_1, ST_AsText(geom), bool_2 from ST_FromFlatGeobuf(null::fl + ) q) + ); + ++select '--- Type mismatch detection ---'; ++ ++-- Setup: a long (bigint) column and a text column in separate tables ++select ST_FromFlatGeobufToTable('public', 'flatgeobuf_mm_long', (select ST_AsFlatGeobuf(q) fgb from (select ++ null::geometry, null::bigint as val) q)); ++select ST_FromFlatGeobufToTable('public', 'flatgeobuf_mm_text', (select ST_AsFlatGeobuf(q) fgb from (select ++ null::geometry, null::text as val) q)); ++select ST_FromFlatGeobufToTable('public', 'flatgeobuf_mm_twocols', (select ST_AsFlatGeobuf(q) fgb from (select ++ null::geometry, null::bigint as val1, null::bigint as val2) q)); ++ ++-- Type mismatch: file has bigint (long), target expects text ++select 'MM1' from ST_FromFlatGeobuf(null::flatgeobuf_mm_text, ( ++ select ST_AsFlatGeobuf(q) fgb from (select null::geometry, 42::bigint as val) q)); ++ ++-- Count mismatch: file has 2 property columns, target type has 1 ++select 'MM2' from ST_FromFlatGeobuf(null::flatgeobuf_mm_long, ( ++ select ST_AsFlatGeobuf(q) fgb from (select null::geometry, 42::bigint as val1, 43::bigint as val2) q)); ++ ++select '--- Malformed input detection ---'; ++ ++-- Magic bytes plus a truncated size prefix. ++select 'MI1' from ST_FromFlatGeobuf(null::flatgeobuf_t1, '\x6667620366676201010203'::bytea); ++ ++-- Valid magic bytes plus a header size prefix that exceeds the remaining input. ++select 'MI2' from ST_FromFlatGeobuf(null::flatgeobuf_t1, '\x6667620366676201ffffffff'::bytea); ++ ++-- NULL bytea input should be handled by STRICT rather than reaching the C decoder. ++select 'MI3', count(*) from ST_FromFlatGeobuf(null::flatgeobuf_t1, null::bytea); ++ + select '--- Quoted identifiers ---'; + + -- Verify that special characters in column names are properly quoted +@@ -181,3 +210,6 @@ drop table if exists public.flatgeobuf_t1; + drop table if exists public.flatgeobuf_a1; + drop table if exists public.flatgeobuf_e1; + drop table if exists public.flatgeobuf_qi; ++drop table if exists public.flatgeobuf_mm_long; ++drop table if exists public.flatgeobuf_mm_text; ++drop table if exists public.flatgeobuf_mm_twocols; +diff --git a/regress/core/flatgeobuf_expected b/regress/core/flatgeobuf_expected +index c93738ff9..b9ef9de1b 100644 +--- a/regress/core/flatgeobuf_expected ++++ b/regress/core/flatgeobuf_expected +@@ -24,6 +24,13 @@ ERROR: mixed geometry type is not supported + A1|0||t|1|2|3|4|1.2|1.3|2016-06-23 03:44:52.134125+00|hello + --- Exotic roundtrips --- + E1|0|t|POINT(1.1 2.1)|f ++--- Type mismatch detection --- ++ERROR: flatgeobuf: column "val" type mismatch: file type "Long" is not compatible with PostgreSQL type text ++ERROR: flatgeobuf: column count mismatch: file has 2 columns, target type has 1 ++--- Malformed input detection --- ++ERROR: flatgeobuf: truncated size prefix ++ERROR: flatgeobuf: size prefix exceeds remaining input ++MI3|0 + --- Quoted identifiers --- + QI1 + QI2 diff --git a/patches/0002-address_standardizer-harden-scanner-and-rule-parsing.patch b/patches/0002-address_standardizer-harden-scanner-and-rule-parsing.patch new file mode 100644 index 0000000..ff2622e --- /dev/null +++ b/patches/0002-address_standardizer-harden-scanner-and-rule-parsing.patch @@ -0,0 +1,303 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Darafei Praliaskouski +Date: Sun, 9 Aug 2026 14:55:47 +0400 +Subject: [PATCH 2/4] address_standardizer: harden scanner and rule parsing + bounds + +Ported-from: https://github.com/postgis/address_standardizer/commit/eda92cebef677bb604d1574b85328e3571c1590e +(cherry picked from commit 1ce0012b76ff0da4ba31ad0b6e9c768faa23a78a) +--- + extensions/address_standardizer/gamma.c | 95 +++++++++++++++++++--- + extensions/address_standardizer/standard.c | 46 ++++++++--- + 2 files changed, 121 insertions(+), 20 deletions(-) + +diff --git a/extensions/address_standardizer/gamma.c b/extensions/address_standardizer/gamma.c +index 0824c6535..091115d8d 100644 +--- a/extensions/address_standardizer/gamma.c ++++ b/extensions/address_standardizer/gamma.c +@@ -35,6 +35,10 @@ static int initialize_link( ERR_PARAM *, KW *** , NODE ) ; + static void classify_link( RULE_PARAM * , KW ***, KW *, NODE , SYMB , SYMB ) ; + static void add_failure_linkage( KW ***, NODE , NODE ) ; + static NODE **precompute_gamma_function( ERR_PARAM *, NODE ** , KW ***, NODE ) ; ++static void set_memory_error( ERR_PARAM * ) ; ++#ifdef BUILD_API ++static int rules_allocated_node_count( const RULES * ) ; ++#endif + + static double load_value[ NUMBER_OF_WEIGHTS ] = { + 0.00, 0.325, 0.35 , 0.375 , 0.4 , +@@ -42,6 +46,29 @@ static double load_value[ NUMBER_OF_WEIGHTS ] = { + 0.7 , 0.75 , 0.8 , 0.825 , 0.85 , + 0.9 , 0.95 , 1.00 } ; + ++static void set_memory_error( ERR_PARAM *err_p ) { ++ LOG_MESS( "Insufficient Memory", err_p ) ; ++} ++ ++#ifdef BUILD_API ++static int rules_allocated_node_count( const RULES *rules ) { ++ int node_count; ++ ++ if ( rules == NULL ) { ++ return 0; ++ } ++ ++ node_count = rules -> last_node + 1; ++ if ( node_count < 0 ) { ++ return 0; ++ } ++ if ( node_count > MAXNODES ) { ++ return MAXNODES; ++ } ++ return node_count; ++} ++#endif ++ + /*--------------------------------------------------------------------------- + gamma.c (refresh_transducer) + called by analyze.c (prepare_target_pattern) +@@ -135,6 +162,7 @@ RULES *rules_init( ERR_PARAM *err_p ) { + + /* -- storage for temporary trie for rules -- */ + PAGC_CALLOC_STRUC(Trie,NODE *,MAXNODES,err_p,NULL); ++ rules -> Trie = Trie ; + + /* -- initialize the first( EPSILON ) node of the trie -- */ + PAGC_CALLOC_STRUC(Trie[EPSILON],NODE,MAXINSYM,err_p,NULL); +@@ -156,9 +184,11 @@ RULES *rules_init( ERR_PARAM *err_p ) { + /* Cleanup allocated resources */ + FREE_AND_NULL(o_l); + FREE_AND_NULL(k_s); ++ FREE_AND_NULL(r_s); + FREE_AND_NULL(r_p); ++ rules -> r_p = NULL; + +- PAGC_DESTROY_2D_ARRAY(rules -> Trie,NODE,MAXINSYM); ++ PAGC_DESTROY_2D_ARRAY(rules -> Trie,NODE,rules_allocated_node_count(rules)); + rules -> Trie = NULL; + + rules_free(rules); +@@ -170,7 +200,6 @@ RULES *rules_init( ERR_PARAM *err_p ) { + rules -> r_p -> key_space = k_s ; + rules -> r_p -> output_link = o_l ; + +- rules -> Trie = Trie ; + rules -> rule_end = r_s + RULESPACESIZE ; + + rules -> r = r_s ; +@@ -298,6 +327,13 @@ int rules_add_rule(RULES *rules, int num, int *rule) { + t = rule[i] ; + i++ ; + w = rule[i] ; ++ if ( t < 0 || t >= MAX_CL ) { ++ RET_ERR2( "rules_add_rule: Rule File: Bad Type %d in Rule #%d\n", ++ t , ++ rules -> rule_number , ++ rules -> err_p, ++ 7 ) ; ++ } + + classify_link( rules -> r_p , + o_l , +@@ -335,12 +371,16 @@ int rules_ready(RULES *rules) { + } + } + ++ rules -> r_p -> num_nodes = rules -> last_node ; ++ + /* -- create the global Gamma function matrix -- */ + if ( ( rules -> r_p -> gamma_matrix = + precompute_gamma_function( rules -> err_p, + rules -> Trie , + rules -> r_p -> output_link , + rules -> last_node ) ) == NULL ) { ++ PAGC_DESTROY_2D_ARRAY(rules -> Trie,NODE,rules -> last_node) ; ++ rules -> Trie = NULL ; + return 5 ; + } + +@@ -348,8 +388,6 @@ int rules_ready(RULES *rules) { + PAGC_DESTROY_2D_ARRAY(rules -> Trie,NODE,rules -> last_node) ; + rules -> Trie = NULL ; + +- rules -> r_p -> num_nodes = rules -> last_node ; +- + /* + if ( glo_p -> log_init ) { + CLIENT_ERR( err_p ) ; +@@ -368,7 +406,15 @@ int rules_ready(RULES *rules) { + void rules_free(RULES *rules) { + + if (!rules) return; +- if (rules->r_p) destroy_rules(rules->r_p); ++ if (rules->r_p) { ++ if (!rules->ready && rules->r_p->num_nodes == 0) { ++ rules -> r_p -> num_nodes = rules_allocated_node_count(rules) ; ++ } ++ destroy_rules(rules->r_p); ++ } ++ if (rules->Trie) { ++ PAGC_DESTROY_2D_ARRAY(rules -> Trie,NODE,rules_allocated_node_count(rules)); ++ } + free(rules); + rules = NULL; + } +@@ -623,9 +669,13 @@ void destroy_rules( RULE_PARAM * r_p ) { + DBG("destroy_rules 2"); + FREE_AND_NULL( r_p -> key_space ) ; + DBG("destroy_rules 3"); +- PAGC_DESTROY_2D_ARRAY(r_p->output_link,KW*,r_p->num_nodes) ; ++ if (r_p->output_link) { ++ PAGC_DESTROY_2D_ARRAY(r_p->output_link,KW*,r_p->num_nodes) ; ++ } + DBG("destroy_rules 4"); +- PAGC_DESTROY_2D_ARRAY(r_p->gamma_matrix,NODE,r_p->num_nodes) ; ++ if (r_p->gamma_matrix) { ++ PAGC_DESTROY_2D_ARRAY(r_p->gamma_matrix,NODE,r_p->num_nodes) ; ++ } + DBG(" destroy_rules 5"); + FREE_AND_NULL( r_p ) ; + } +@@ -746,11 +796,36 @@ static NODE **precompute_gamma_function( ERR_PARAM *err_p , + *Queue ; + + /* -- Storage for Failure Function -- */ +- PAGC_CALLOC_STRUC(Failure,NODE,n,err_p,NULL) ; ++ Failure = calloc(n, sizeof(NODE)); ++ if (!Failure) { ++ set_memory_error(err_p); ++ return NULL; ++ } + /* -- Storage for Breadth First Search Queue -- */ +- PAGC_CALLOC_STRUC(Queue,NODE,n,err_p,NULL) ; ++ Queue = calloc(n, sizeof(NODE)); ++ if (!Queue) { ++ set_memory_error(err_p); ++ FREE_AND_NULL(Failure); ++ return NULL; ++ } + +- PAGC_CALLOC_2D_ARRAY(Gamma,NODE,n,MAXINSYM,err_p,NULL) ; ++ Gamma = calloc(n, sizeof(NODE *)); ++ if (!Gamma) { ++ set_memory_error(err_p); ++ FREE_AND_NULL(Failure); ++ FREE_AND_NULL(Queue); ++ return NULL; ++ } ++ for (i = 0; i < n; i++) { ++ Gamma[i] = calloc(MAXINSYM, sizeof(NODE)); ++ if (!Gamma[i]) { ++ set_memory_error(err_p); ++ PAGC_DESTROY_2D_ARRAY(Gamma,NODE,i); ++ FREE_AND_NULL(Failure); ++ FREE_AND_NULL(Queue); ++ return NULL; ++ } ++ } + + u = EPSILON ; + i = 0 ; +diff --git a/extensions/address_standardizer/standard.c b/extensions/address_standardizer/standard.c +index 07c3348c4..70b0e011b 100644 +--- a/extensions/address_standardizer/standard.c ++++ b/extensions/address_standardizer/standard.c +@@ -41,8 +41,26 @@ static char * _Scan_Next_(STAND_PARAM *, char *) ; + + static char __spacer__[] = " \\-.)}>_" ; + ++#define NO_STANDARDIZATION_PREFIX "std_standardize_mm: No standardization of " ++#define MAX_STANDARDIZATION_ERROR_INPUT \ ++ ((int) (MAXSTRLEN - sizeof(NO_STANDARDIZATION_PREFIX) - 1)) ++ ++#define ENSURE_SCAN_ROOM(CHARS) \ ++ do { \ ++ if ((size_t) (__dest__ - __scan_buf__) + (CHARS) >= sizeof(__scan_buf__)) \ ++ { \ ++ CLIENT_ERR(__stand_param__->errors) ; \ ++ RET_ERR("_Scan_Next_: Token exceeds maximum length", \ ++ __stand_param__->errors, NULL) ; \ ++ } \ ++ } while (0) ++ ++#define TERMINATE_SCAN_BUFFER \ ++ ENSURE_SCAN_ROOM(0) ; \ ++ *__dest__ = SENTINEL ++ + #define TERM_AND_LENGTH \ +- *__dest__ = SENTINEL ; \ ++ TERMINATE_SCAN_BUFFER ; \ + n = strlen(__scan_buf__) + + #define RETURN_NEW_MORPH(TOKEN_ARG) \ +@@ -53,13 +71,17 @@ static char __spacer__[] = " \\-.)}>_" ; + return __src__ + + #define COLLECT_LOOKAHEAD \ ++ ENSURE_SCAN_ROOM(2) ; \ + *__dest__++ = a ; __src__++ ; *__dest__++ = b ; __src__++ + + #define COLLECT_WHILE(COND) \ +- do { *__dest__++ = a ; __src__++ ; a = *__src__ ; } while (COND) ++ do { \ ++ ENSURE_SCAN_ROOM(1) ; \ ++ *__dest__++ = a ; __src__++ ; a = *__src__ ; \ ++ } while (COND) + + #define NO_COLLECT_WHILE(COND) \ +- do { __dest__++ ; __src__++ ; a = *__src__ ; } while (COND) ++ do { __src__++ ; a = *__src__ ; } while (COND) + + #define TEST_FOR_ORD_DIGIT(N,NEXT_LOW,NEXT_UP) \ + if ((b == NEXT_LOW) || (b == NEXT_UP)) \ +@@ -150,13 +172,14 @@ static char * _Scan_Next_( STAND_PARAM *__stand_param__,char * __in_ptr__) + char *__src__ = __in_ptr__ ; + char a = *__src__ ; + char *__dest__ = __scan_buf__ ; +- *__dest__ = SENTINEL ; ++ TERMINATE_SCAN_BUFFER ; + + /*-- Type one terminators --*/ + if ((a == ',') || (a == '\t') || (a == ';')) + { ++ ENSURE_SCAN_ROOM(1) ; + *__dest__++ = a ; +- *__dest__ = SENTINEL; ++ TERMINATE_SCAN_BUFFER ; + set_term(__stand_param__,1,__scan_buf__) ; + /*-- Point to next input char --*/ + return (__src__ + 1) ; +@@ -279,7 +302,8 @@ static char * _Scan_Next_( STAND_PARAM *__stand_param__,char * __in_ptr__) + /*-- Type 2 terminators ( spacing ) --*/ + if (strchr(__spacer__,a) != NULL) + { +- NO_COLLECT_WHILE(strchr(__spacer__,a) != NULL) ; ++ NO_COLLECT_WHILE(a != SENTINEL && ++ strchr(__spacer__,a) != NULL) ; + set_term(__stand_param__,2,__scan_buf__) ; + /*-- Retain position --*/ + return (__src__) ; +@@ -470,8 +494,9 @@ STDADDR *std_standardize_mm(STANDARDIZER *std, char *micro, char *macro, int opt + if (macro && macro[0] != '\0') { + err = standardize_field( stand_address, macro, MACRO ); + if (!err) { +- RET_ERR1("std_standardize_mm: No standardization of %s!", +- macro, std -> err_p, NULL); ++ RET_ERR2(NO_STANDARDIZATION_PREFIX "%.*s!", ++ MAX_STANDARDIZATION_ERROR_INPUT, macro, ++ std -> err_p, NULL); + } + + if (options & 1) { +@@ -483,8 +508,9 @@ STDADDR *std_standardize_mm(STANDARDIZER *std, char *micro, char *macro, int opt + + err = standardize_field( stand_address, micro, MICRO_M ); + if (!err) { +- RET_ERR1("std_standardize_mm: No standardization of %s!", +- micro, std -> err_p, NULL); ++ RET_ERR2(NO_STANDARDIZATION_PREFIX "%.*s!", ++ MAX_STANDARDIZATION_ERROR_INPUT, micro, ++ std -> err_p, NULL); + } + + if (options & 1) { diff --git a/patches/0003-address_standardizer-clean-up-partial-2D-allocations.patch b/patches/0003-address_standardizer-clean-up-partial-2D-allocations.patch new file mode 100644 index 0000000..c9783e6 --- /dev/null +++ b/patches/0003-address_standardizer-clean-up-partial-2D-allocations.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Maksim Korotkov +Date: Mon, 13 Jul 2026 13:16:40 +0300 +Subject: [PATCH 3/4] address_standardizer: clean up partial 2D allocations + +Ported-from: https://github.com/postgis/address_standardizer/commit/330d63a4723f9506c6a57ae10c595626db7f991d +(cherry picked from commit c38c8731915bda57a5baf1feb3a24ac393eade7e) +--- + extensions/address_standardizer/pagc_api.h | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/extensions/address_standardizer/pagc_api.h b/extensions/address_standardizer/pagc_api.h +index 7267db047..d26bee482 100644 +--- a/extensions/address_standardizer/pagc_api.h ++++ b/extensions/address_standardizer/pagc_api.h +@@ -1095,7 +1095,16 @@ int tokenize_landmark_words(char *, char **) ; + int row_num ; \ + PAGC_CALLOC_STRUC(temp_ptr,TYP*,ROWS,WHERE,EXIT_TYPE) ; \ + for ( row_num = 0 ; row_num < ROWS ; row_num++ ) { \ +- PAGC_CALLOC_STRUC(temp_ptr[row_num],TYP,COLS,WHERE,EXIT_TYPE) ; \ ++ /* Manual calloc: PAGC_CALLOC_STRUC can't clean up prior rows */ \ ++ temp_ptr[row_num] = (TYP*)calloc(COLS, sizeof(TYP)); \ ++ if (temp_ptr[row_num] == NULL) { \ ++ while (row_num) { \ ++ row_num--; \ ++ free(temp_ptr[row_num]); \ ++ } \ ++ FREE_AND_NULL(temp_ptr); \ ++ RET_ERR("Insufficient Memory", WHERE, EXIT_TYPE); \ ++ } \ + } \ + PTR = temp_ptr ; \ + } diff --git a/patches/0004-Avoid-out-of-bounds-write-uninitialized-memory.patch b/patches/0004-Avoid-out-of-bounds-write-uninitialized-memory.patch new file mode 100644 index 0000000..2ea10f4 --- /dev/null +++ b/patches/0004-Avoid-out-of-bounds-write-uninitialized-memory.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Paul Ramsey +Date: Mon, 22 Jun 2026 14:34:08 -0700 +Subject: [PATCH 4/4] Avoid out-of-bounds write, uninitialized memory. + +(cherry picked from commit 5964d6f1ae4d2ea69f2a8a76f260674bab2e93b4) +--- + extensions/address_standardizer/gamma.c | 2 +- + extensions/address_standardizer/std_pg_hash.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/extensions/address_standardizer/gamma.c b/extensions/address_standardizer/gamma.c +index 091115d8d..f56025d68 100644 +--- a/extensions/address_standardizer/gamma.c ++++ b/extensions/address_standardizer/gamma.c +@@ -149,7 +149,7 @@ RULES *rules_init( ERR_PARAM *err_p ) { + rules->rule_number = 0; + rules->last_node = EPSILON; + +- PAGC_ALLOC_STRUC(r_p,RULE_PARAM,err_p,NULL) ; ++ PAGC_CALLOC_STRUC(r_p,RULE_PARAM,1,err_p,NULL) ; + rules->r_p = r_p; + + /* -- initialize the statistics record -- */ +diff --git a/extensions/address_standardizer/std_pg_hash.c b/extensions/address_standardizer/std_pg_hash.c +index 63f490415..a83be783a 100644 +--- a/extensions/address_standardizer/std_pg_hash.c ++++ b/extensions/address_standardizer/std_pg_hash.c +@@ -534,12 +534,12 @@ static int parse_rule(char *buf, int *rule) + + + while (1) { ++ if (nr >= MAX_RULE_LENGTH) return -1; + *r = strtol( p, &q, 10 ); + if (p == q) break; + p = q; + nr++; + r++; +- if (nr > MAX_RULE_LENGTH) return -1; + } + + return nr; diff --git a/scripts/verify-postgis-security.sh b/scripts/verify-postgis-security.sh new file mode 100755 index 0000000..5c4e6a6 --- /dev/null +++ b/scripts/verify-postgis-security.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Fails if the image ships a PostGIS build that is vulnerable to +# CVE-2026-73515 (FlatGeobuf out-of-bounds read) or CVE-2026-73514 +# (address_standardizer out-of-bounds write). +# +# Everything is asserted against the *installed* extension, never against a +# Dockerfile ARG or a tag: an image can be rebuilt from a moved tag, and the +# upstream 3.6.x tarballs carry neither fix, so the build applies patches/ on +# top and records them in the manifest this script reads back. +# +# Usage: verify-postgis-security.sh +set -euo pipefail + +container="${1:?usage: verify-postgis-security.sh }" +psql() { docker exec -i "$container" psql -U postgres -X -q -At "$@"; } + +# Lowest release carrying the fixes we backport on top of. A tarball below this +# is vulnerable even before considering the patches. +POSTGIS_MIN_VERSION="${POSTGIS_MIN_VERSION:-3.6.4}" +MANIFEST=/usr/local/share/postgresql/security/postgis-patches.txt + +fail() { echo "FAIL: $*" >&2; exit 1; } +ok() { echo "ok: $*"; } + +psql -c 'CREATE EXTENSION IF NOT EXISTS postgis' >/dev/null +psql -c 'CREATE EXTENSION IF NOT EXISTS address_standardizer' >/dev/null +psql -c 'CREATE EXTENSION IF NOT EXISTS address_standardizer_data_us' >/dev/null + +# 1. installed PostGIS release floor +installed="$(psql -c "SELECT extversion FROM pg_extension WHERE extname = 'postgis'")" +[ -n "$installed" ] || fail "postgis extension is not installed" +if [ "$(printf '%s\n%s\n' "$POSTGIS_MIN_VERSION" "$installed" | sort -V | head -1)" != "$POSTGIS_MIN_VERSION" ]; then + fail "postgis $installed is below the patched floor $POSTGIS_MIN_VERSION" +fi +ok "postgis $installed >= $POSTGIS_MIN_VERSION ($(psql -c 'SELECT postgis_lib_version()'))" + +# 2. the security patches are recorded in the image +manifest="$(docker exec "$container" cat "$MANIFEST" 2>/dev/null || true)" +[ -n "$manifest" ] || fail "$MANIFEST missing — image was not built with patches/" +for cve in CVE-2026-73514 CVE-2026-73515; do + grep -qx "cve_fixed=$cve" <<<"$manifest" || fail "$MANIFEST does not record a fix for $cve" +done +ok "patch manifest records $(grep -c '^patch=' <<<"$manifest") patches for both CVEs" + +# 3. CVE-2026-73515: a truncated FlatGeobuf buffer must be rejected, and the +# backend must survive it (an unpatched build reads past the buffer). +fgb_error="$(docker exec -i "$container" psql -U postgres -X -q -At -v ON_ERROR_STOP=0 <<'SQL' 2>&1 || true +CREATE TEMP TABLE fgb_probe (geom geometry, name text); +SELECT ST_FromFlatGeobuf(NULL::fgb_probe, decode('6667620366676201', 'hex')); +SQL +)" +grep -qi 'flatgeobuf' <<<"$fgb_error" || fail "truncated FlatGeobuf input was not rejected: $fgb_error" +[ "$(psql -c 'SELECT 1')" = "1" ] || fail "backend did not survive truncated FlatGeobuf input" +ok "truncated FlatGeobuf input rejected, backend alive" + +# 4. CVE-2026-73514: the off-by-one that let parse_rule() write past +# rule_arr[MAX_RULE_LENGTH] is gone, and the standardizer still works. +psql -c "SELECT house_num, name, city, state FROM standardize_address( + 'us_lex', 'us_gaz', 'us_rules', '1 Devonshire Place, Boston, MA 02109')" >/dev/null \ + || fail "standardize_address() failed on patched address_standardizer" +ok "address_standardizer $(psql -c "SELECT extversion FROM pg_extension WHERE extname='address_standardizer'") functional" + +echo "PostGIS security verification passed"