From 5c3b9a5e11a30eecb1ecd28ecf3c62f5d1b71990 Mon Sep 17 00:00:00 2001 From: Manuel Mendez Date: Fri, 21 Aug 2026 19:41:14 -0400 Subject: [PATCH 1/6] nix/checks: Minor refactors Sorted runtimeInputs and indented code in `if`s because they annoyed me. I moved the check for postgresql.conf to right after it was first created because it doesn't really make much sense to do it later than necessary. --- nix/checks.nix | 53 +++++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/nix/checks.nix b/nix/checks.nix index 518aac569..d4b1b6932 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -254,24 +254,23 @@ "pipefail" ]; runtimeInputs = with pkgs; [ - coreutils bash + coreutils + getkey-script + netcat perl - pgpkg + pg_isolation_regress pg_prove pg_regress - pg_isolation_regress + pgpkg procps + python3 start-postgres-server-bin - which - getkey-script supabase-groonga - python3 - netcat + which ]; text = '' - #shellcheck disable=SC1091 source ${bashlog} #shellcheck disable=SC1091 @@ -363,19 +362,25 @@ substitute ${./tests/postgresql.conf.in} "$PGTAP_CLUSTER"/postgresql.conf \ --subst-var-by PGSODIUM_GETKEY_SCRIPT "${getkey-script}/bin/pgsodium-getkey" \ --subst-var-by PRELOAD_LIBRARIES "$PRELOAD_LIBRARIES" + + # Check if postgresql.conf exists + if [ ! -f "$PGTAP_CLUSTER/postgresql.conf" ]; then + log error "postgresql.conf is missing!" + exit 1 + fi + { echo "listen_addresses = '127.0.0.1'" echo "port = ${pgPort}" - echo "session_preload_libraries = 'supautils'" + echo "dynamic_library_path = '${supautils}/lib:\$libdir'" + echo "session_preload_libraries = 'supautils'" } >> "$PGTAP_CLUSTER"/postgresql.conf - echo "host all all 127.0.0.1/32 trust" >> "$PGTAP_CLUSTER/pg_hba.conf" - log info "Checking shared_preload_libraries setting:" - log info "$(grep -rn "shared_preload_libraries" "$PGTAP_CLUSTER"/postgresql.conf)" # Configure OrioleDB if running orioledb-17 check if ${lib.boolToString isOrioleDB}; then log info "Configuring OrioleDB..." + # Add orioledb to shared_preload_libraries perl -pi -e "s/(shared_preload_libraries = ')/\$1orioledb, /" "$PGTAP_CLUSTER/postgresql.conf" log info "OrioleDB added to shared_preload_libraries" @@ -384,23 +389,23 @@ echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" >> "$PGTAP_CLUSTER/postgresql.conf" fi - # Check if postgresql.conf exists - if [ ! -f "$PGTAP_CLUSTER/postgresql.conf" ]; then - log error "postgresql.conf is missing!" - exit 1 - fi + log info "Checking shared_preload_libraries setting:" + log info "$(grep -rn "shared_preload_libraries" "$PGTAP_CLUSTER"/postgresql.conf)" + + log info "Configuring local auth" + echo "host all all 127.0.0.1/32 trust" >> "$PGTAP_CLUSTER/pg_hba.conf" # PostgreSQL startup if [[ "$(uname)" == "Darwin" ]]; then - log_cmd pg_ctl -D "$PGTAP_CLUSTER" -l "$PGTAP_CLUSTER/postgresql.log" -o "-k $PGTAP_CLUSTER -p ${pgPort} -d 5" start + log_cmd pg_ctl -D "$PGTAP_CLUSTER" -l "$PGTAP_CLUSTER/postgresql.log" -o "-k $PGTAP_CLUSTER -p ${pgPort} -d 5" start else - mkdir -p "$PGTAP_CLUSTER/sockets" - log_cmd pg_ctl -D "$PGTAP_CLUSTER" -l "$PGTAP_CLUSTER/postgresql.log" -o "-k $PGTAP_CLUSTER/sockets -p ${pgPort} -d 5" start + mkdir -p "$PGTAP_CLUSTER/sockets" + log_cmd pg_ctl -D "$PGTAP_CLUSTER" -l "$PGTAP_CLUSTER/postgresql.log" -o "-k $PGTAP_CLUSTER/sockets -p ${pgPort} -d 5" start fi || { - log error "pg_ctl failed to start PostgreSQL" - log error "Contents of postgresql.log:" - cat "$PGTAP_CLUSTER"/postgresql.log - exit 1 + log error "pg_ctl failed to start PostgreSQL" + log error "Contents of postgresql.log:" + cat "$PGTAP_CLUSTER"/postgresql.log + exit 1 } log info "Waiting for PostgreSQL to be ready..." From 0a37663e6a7ebb4b96804c72c147737c624e2b44 Mon Sep 17 00:00:00 2001 From: Utkarash Singh Date: Tue, 25 Aug 2026 12:13:42 +0100 Subject: [PATCH 2/6] regress: CVE & behavior-change tests for 15.19 / 17.11 pg_regress suite under nix/tests/sql pinning this release's CVE and behavior-change fixes. - btree_gist_nan (fix NaN handling for float4 & float8) - create_statistics_priv (CVE-2025-12817) - hstore_copy_binary (CVE-2026-6472) - intarray_ltree_query (CVE-2026-6473) - ltree_label_overflow (fix integer overflow in comparisons) - multirange_create_priv (CVE-2026-6472) - operator_breaking_change (CVE-2026-2004) - output_plugin_libraries (CVE-2026-6471) - pgcrypto_cipher_matrix (CVE-2026-14663) All skipped on orioledb-17 because its based on 17.9. CLI variant skips output_plugin_libraries and btree_gist_nan since logical-decoding infra / btree_gist not primed there. Refs: MPG-140, MPG-164 - merge_serialization (must raise serialization failure (40001) under REPEATABLE READ when its target row was updated+committed concurrently) Ref: MPG-163 Assisted-By: Claude Opus 4.8 --- nix/checks.nix | 7 +- nix/tests/expected/btree_gist_nan.out | 46 +++++++++++ nix/tests/expected/create_statistics_priv.out | 26 +++++++ nix/tests/expected/hstore_copy_binary.out | 47 ++++++++++++ nix/tests/expected/intarray_ltree_query.out | 59 ++++++++++++++ nix/tests/expected/ltree_label_overflow.out | 37 +++++++++ nix/tests/expected/multirange_create_priv.out | 31 ++++++++ .../expected/operator_breaking_change.out | 50 ++++++++++++ .../expected/output_plugin_libraries.out | 35 +++++++++ nix/tests/expected/pgcrypto_cipher_matrix.out | 76 +++++++++++++++++++ .../expected/replica_identity_upsert.out | 35 +++++++++ .../expected/z_15_intarray_ltree_query.out | 4 + .../expected/z_17_intarray_ltree_query.out | 4 + .../z_orioledb-17_intarray_ltree_query.out | 4 + .../expected/merge_serialization.out | 16 ++++ .../isolation/specs/merge_serialization.spec | 32 ++++++++ nix/tests/sql/btree_gist_nan.sql | 32 ++++++++ nix/tests/sql/create_statistics_priv.sql | 33 ++++++++ nix/tests/sql/hstore_copy_binary.sql | 38 ++++++++++ nix/tests/sql/intarray_ltree_query.sql | 39 ++++++++++ nix/tests/sql/ltree_label_overflow.sql | 35 +++++++++ nix/tests/sql/multirange_create_priv.sql | 37 +++++++++ nix/tests/sql/operator_breaking_change.sql | 57 ++++++++++++++ nix/tests/sql/output_plugin_libraries.sql | 27 +++++++ nix/tests/sql/pgcrypto_cipher_matrix.sql | 55 ++++++++++++++ nix/tests/sql/replica_identity_upsert.sql | 32 ++++++++ nix/tests/sql/z_15_intarray_ltree_query.sql | 2 + nix/tests/sql/z_17_intarray_ltree_query.sql | 2 + .../z_orioledb-17_intarray_ltree_query.sql | 2 + 29 files changed, 899 insertions(+), 1 deletion(-) create mode 100644 nix/tests/expected/btree_gist_nan.out create mode 100644 nix/tests/expected/create_statistics_priv.out create mode 100644 nix/tests/expected/hstore_copy_binary.out create mode 100644 nix/tests/expected/intarray_ltree_query.out create mode 100644 nix/tests/expected/ltree_label_overflow.out create mode 100644 nix/tests/expected/multirange_create_priv.out create mode 100644 nix/tests/expected/operator_breaking_change.out create mode 100644 nix/tests/expected/output_plugin_libraries.out create mode 100644 nix/tests/expected/pgcrypto_cipher_matrix.out create mode 100644 nix/tests/expected/replica_identity_upsert.out create mode 100644 nix/tests/expected/z_15_intarray_ltree_query.out create mode 100644 nix/tests/expected/z_17_intarray_ltree_query.out create mode 100644 nix/tests/expected/z_orioledb-17_intarray_ltree_query.out create mode 100644 nix/tests/isolation/expected/merge_serialization.out create mode 100644 nix/tests/isolation/specs/merge_serialization.spec create mode 100644 nix/tests/sql/btree_gist_nan.sql create mode 100644 nix/tests/sql/create_statistics_priv.sql create mode 100644 nix/tests/sql/hstore_copy_binary.sql create mode 100644 nix/tests/sql/intarray_ltree_query.sql create mode 100644 nix/tests/sql/ltree_label_overflow.sql create mode 100644 nix/tests/sql/multirange_create_priv.sql create mode 100644 nix/tests/sql/operator_breaking_change.sql create mode 100644 nix/tests/sql/output_plugin_libraries.sql create mode 100644 nix/tests/sql/pgcrypto_cipher_matrix.sql create mode 100644 nix/tests/sql/replica_identity_upsert.sql create mode 100644 nix/tests/sql/z_15_intarray_ltree_query.sql create mode 100644 nix/tests/sql/z_17_intarray_ltree_query.sql create mode 100644 nix/tests/sql/z_orioledb-17_intarray_ltree_query.sql diff --git a/nix/checks.nix b/nix/checks.nix index d4b1b6932..2f1cf2158 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -220,6 +220,8 @@ "pg_cron_trigger_privileges" # needs pg_cron + the postgres role and cron-schema grants from the full migrations, not in the CLI prime file "supautils_restrict_versions" # needs the postgres role + primed hstore from the full migrations/prime, not present in the CLI variant "amcheck" # needs the postgres/anon/authenticated/service_role roles and the default privileges from the full migrations, plus amcheck primed by prime.sql + "output_plugin_libraries" # needs wal_level=logical + logical-decoding infra, not exercised in the CLI variant + "btree_gist_nan" # needs btree_gist, not in the CLI prime file # Version-specific extension tests "z_17_ext_interface" "z_17_pg_stat_monitor" @@ -245,7 +247,10 @@ # Concurrency/isolation specs run via pg_isolation_regress (the stock # PostgreSQL isolation tester). Specs live in tests/isolation/specs/, # expected output in tests/isolation/expected/. Add new spec names here. - isolationSpecList = [ "sample_isolation" ]; + isolationSpecList = [ + "merge_serialization" + "sample_isolation" + ]; in pkgs.writeShellApplication rec { name = "postgres-${pgpkg.version}-check-harness"; diff --git a/nix/tests/expected/btree_gist_nan.out b/nix/tests/expected/btree_gist_nan.out new file mode 100644 index 000000000..3fadd5096 --- /dev/null +++ b/nix/tests/expected/btree_gist_nan.out @@ -0,0 +1,46 @@ +-- btree_gist NaN handling in the float4/float8 opclasses (comparisons and the +-- GiST penalty/distance functions) previously gave wrong answers when a NaN was +-- present; upstream recommends reindexing btree_gist float indexes that may hold +-- NaN after the update. This pins the post-fix within-version correctness of +-- index scans over a float8 column containing NaN. +-- +-- Upstream commit: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- (The cross-version pre-upgrade-build / post-upgrade-REINDEX leg is A3, +-- PSQL-1235.) +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +SET client_min_messages = warning; -- OrioleDB shows a NOTICE/DETAILS, lets avoid the diff from vanilla +CREATE EXTENSION IF NOT EXISTS btree_gist; +CREATE TABLE bg_nan (v float8); +INSERT INTO bg_nan VALUES (1), (2), (3), ('NaN'); +CREATE INDEX bg_nan_gist ON bg_nan USING gist (v); +SET enable_seqscan = off; -- Force index scans so we exercise the opclass, not a seqscan recheck. +-- NaN sorts as greater than every non-NaN value and equals only itself. +SELECT count(*) AS eq_nan FROM bg_nan WHERE v = 'NaN'::float8; + eq_nan +-------- + 1 +(1 row) + +SELECT count(*) AS gt_one FROM bg_nan WHERE v > 1; -- 2, 3, NaN + gt_one +-------- + 3 +(1 row) + +SELECT count(*) AS ne_two FROM bg_nan WHERE v <> 2; -- 1, 3, NaN + ne_two +-------- + 3 +(1 row) + +SELECT v FROM bg_nan WHERE v >= 3 ORDER BY v; -- 3, NaN + v +----- + 3 + NaN +(2 rows) + +RESET enable_seqscan; +ROLLBACK; diff --git a/nix/tests/expected/create_statistics_priv.out b/nix/tests/expected/create_statistics_priv.out new file mode 100644 index 000000000..983a754f0 --- /dev/null +++ b/nix/tests/expected/create_statistics_priv.out @@ -0,0 +1,26 @@ +-- CVE-2025-12817: CREATE STATISTICS did not check CREATE privilege on the +-- schema where the statistics object is created, letting a table owner create +-- statistics objects in any schema (naming-conflict / privilege concern). +-- +-- Upstream commits: 2393d374 + d202ec1f (PG 15.15), e2fb3dfa (PG 17.7). The fix +-- adds a pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE). +-- +-- Verified as a non-superuser table owner. Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE SCHEMA owned_ns; +CREATE SCHEMA forbidden_ns; +-- postgres can create in owned_ns only; it has no rights on forbidden_ns. +GRANT CREATE, USAGE ON SCHEMA owned_ns TO postgres; +SET ROLE postgres; +-- A table postgres owns, in a schema postgres controls. +CREATE TABLE owned_ns.stat_tbl (a int, b int); +INSERT INTO owned_ns.stat_tbl SELECT g % 10, g % 5 FROM generate_series(1, 100) g; +-- Positive control: stats object in owned_ns (postgres has CREATE) is allowed. +CREATE STATISTICS owned_ns.okstat (dependencies) ON a, b FROM owned_ns.stat_tbl; +-- The fix: a stats object targeting a schema where postgres lacks CREATE is denied. +SAVEPOINT no_priv; +CREATE STATISTICS forbidden_ns.badstat (dependencies) ON a, b FROM owned_ns.stat_tbl; +ERROR: permission denied for schema forbidden_ns +ROLLBACK TO SAVEPOINT no_priv; +RESET ROLE; +ROLLBACK; diff --git a/nix/tests/expected/hstore_copy_binary.out b/nix/tests/expected/hstore_copy_binary.out new file mode 100644 index 000000000..c96abb526 --- /dev/null +++ b/nix/tests/expected/hstore_copy_binary.out @@ -0,0 +1,47 @@ +-- Non-CVE behavior change: the hstore receive function had a NULL-pointer +-- dereference (backend crash) on COPY BINARY of an hstore whose binary form +-- contains a DUPLICATE key where the second occurrence's value is NULL. +-- +-- Upstream commits: 63c05e03 (PG 15.x), 0dfbe42d (PG 17.x). +-- +-- A normal INSERT cannot reproduce this: hstore de-duplicates on text input, so +-- a stored value never carries a duplicate key into the binary path. We instead +-- hand-craft a COPY-BINARY stream whose single hstore field contains the pair +-- sequence [ 'a' => '1', 'a' => NULL ] and feed it through hstore_recv via +-- COPY ... FROM. Pre-fix this crashed the backend; on the fixed builds the +-- duplicate is de-duplicated and the row loads cleanly. +-- +-- pg_regress runs as the superuser supabase_admin, so lo_export / server-side +-- COPY FROM a file are permitted. Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE TABLE hstore_dst (h hstore); +-- Materialise the crafted COPY-BINARY stream to a file (created and exported in +-- separate statements so the large object is visible to lo_export). +SELECT lo_from_bytea(81000, + '\x5047434f50590aff0d0a00'::bytea || -- COPY binary signature + '\x00000000'::bytea || '\x00000000'::bytea || -- flags + header-extension length + '\x0001'::bytea || '\x00000017'::bytea || -- one row, one field of length 23 + '\x00000002'::bytea || -- hstore: 2 pairs + '\x00000001'::bytea||'\x61'::bytea||'\x00000001'::bytea||'\x31'::bytea || -- 'a' => '1' + '\x00000001'::bytea||'\x61'::bytea||'\xffffffff'::bytea || -- 'a' => NULL + '\xffff'::bytea) AS loid; -- COPY trailer + loid +------- + 81000 +(1 row) + +SELECT lo_export(81000, '/tmp/pg_regress_hstore_dup.bin') AS exported; + exported +---------- + 1 +(1 row) + +-- Must not crash the backend; the duplicate key is de-duplicated on receive. +COPY hstore_dst FROM '/tmp/pg_regress_hstore_dup.bin' WITH (FORMAT binary); +SELECT h AS received, akeys(h) AS keys FROM hstore_dst; + received | keys +----------+------ + "a"=>"1" | {a} +(1 row) + +ROLLBACK; diff --git a/nix/tests/expected/intarray_ltree_query.out b/nix/tests/expected/intarray_ltree_query.out new file mode 100644 index 000000000..30531060e --- /dev/null +++ b/nix/tests/expected/intarray_ltree_query.out @@ -0,0 +1,59 @@ +-- CVE-2026-6473 +-- +-- Overflow of the internal "left" / length / variant-count fields used +-- when parsing contrib intarray query_int and contrib ltree ltxtquery / +-- lquery. Pre-fix (<= 15.18 / 17.9) a sufficiently large query was +-- accepted and silently built a corrupt parse tree; the fixed builds +-- (15.19 / 17.11) reject it with a clean error. +-- +-- Upstream fixes and their own regress tests (contrib/intarray/sql/_int.sql, +-- contrib/ltree/sql/ltree.sql): +-- query_int + ltxtquery "left" overflow: +-- 84a9f264 https://github.com/postgres/postgres/commit/84a9f264 +-- lquery totallen / numvar overflow: +-- 9c2fa5b6 https://github.com/postgres/postgres/commit/9c2fa5b6 +-- +-- The reproducers below are those upstream tests. They must use FLAT +-- operator chains (built with string_agg / repeat): the query grammar +-- parses a flat chain iteratively, so it reaches the overflow guard, +-- whereas a deeply nested expression would trip check_stack_depth() first +-- (identically on both builds). +-- +-- Refs: PSQL-1110, PSQL-1234. +-- Functional sanity: well-formed queries still parse and match on both builds. +SELECT '{1,2,3}'::int[] @@ '2&4'::query_int AS q_and; -- false + q_and +------- + f +(1 row) + +SELECT '{1,2,3}'::int[] @@ '2|4'::query_int AS q_or; -- true + q_or +------ + t +(1 row) + +SELECT 'Top.Science.Astronomy'::ltree ~ 'Top.*.Astronomy'::lquery AS lquery_match; -- true + lquery_match +-------------- + t +(1 row) + +SELECT 'Top.Science.Astronomy'::ltree @ 'Astronomy & Top'::ltxtquery AS ltxtquery_match; -- true + ltxtquery_match +----------------- + t +(1 row) + +-- query_int: 17000 AND'd terms overflow the int16 "left" offset field. +SELECT (SELECT '0 | ' || string_agg(i::text, ' & ') + FROM generate_series(1, 17000) AS i)::query_int IS NOT NULL AS q_int_overflow; +ERROR: query_int expression is too complex +-- ltxtquery: same flat-chain shape overflows the ltxtquery size field. +SELECT (SELECT 'a | ' || string_agg('b', ' & ') + FROM generate_series(1, 17000) AS i)::ltxtquery IS NOT NULL AS ltxt_overflow; +ERROR: ltxtquery is too large +-- lquery: too many OR-variants in a single level. +SELECT (repeat('a|', 65535) || 'a')::lquery IS NOT NULL AS lq_numvar; +ERROR: lquery level has too many variants +DETAIL: Number of variants exceeds the maximum allowed (65535). diff --git a/nix/tests/expected/ltree_label_overflow.out b/nix/tests/expected/ltree_label_overflow.out new file mode 100644 index 000000000..c3b34a354 --- /dev/null +++ b/nix/tests/expected/ltree_label_overflow.out @@ -0,0 +1,37 @@ +-- contrib/ltree: an integer overflow in ltree_compare() made comparisons return +-- the wrong sign once two values differed in depth by more than ~14,653 labels. +-- A btree index over such values could therefore be built in the wrong order and +-- needs a REINDEX after upgrade. Fixed in PG 15.19 / 17.11 (2026-08-13) by +-- dropping the overflowing "* 10 * (an + 1)" scaling from the comparator's +-- return values. +-- +-- This reproduces the bug index-free, straight through the btree ordering +-- operators: `a` is a deep value (20,001 labels) whose leading label equals the +-- shallow value `b`, so `b` is a proper prefix of `a` and therefore `a > b` must +-- hold. Pre-fix, the final "(a->numlevel - b->numlevel) * 10 * (an + 1)" term +-- overflows int32 for that depth gap and flips sign, so `a > b` wrongly returns +-- false and `a < b` wrongly returns true. `b < a` stays correct pre-fix (the +-- shorter operand exhausts the loop first, so no scaling overflow occurs), which +-- makes the pre-fix result internally contradictory. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE EXTENSION IF NOT EXISTS ltree; +NOTICE: extension "ltree" already exists, skipping +WITH v AS ( + SELECT (repeat('a.', 20000) || 'a')::ltree AS a, -- 20,001 labels + 'a'::ltree AS b -- 1 label, a prefix of a +) +SELECT nlevel(a) AS na, + nlevel(b) AS nb, + a > b AS a_gt_b, -- must be TRUE (a extends prefix b) + a < b AS a_lt_b, -- must be FALSE + b < a AS b_lt_a, -- must be TRUE + a = a AS a_eq_a -- must be TRUE +FROM v; + na | nb | a_gt_b | a_lt_b | b_lt_a | a_eq_a +-------+----+--------+--------+--------+-------- + 20001 | 1 | t | f | t | t +(1 row) + +ROLLBACK; diff --git a/nix/tests/expected/multirange_create_priv.out b/nix/tests/expected/multirange_create_priv.out new file mode 100644 index 000000000..ecbeba42d --- /dev/null +++ b/nix/tests/expected/multirange_create_priv.out @@ -0,0 +1,31 @@ +-- CVE-2026-6472: CREATE TYPE ... AS RANGE auto-creates a companion multirange +-- type. When the multirange type name was given EXPLICITLY, the schema CREATE +-- privilege for that name was not validated (the auto-generated-name path was +-- already checked), letting a role create a multirange type in any schema. +-- +-- Upstream commits: 08c397b0 (PG 15.18), c27ba08c (PG 17.10). The fix adds a +-- pg_namespace_aclcheck(multirangeNamespace, GetUserId(), ACL_CREATE). +-- +-- Verified as a non-superuser. Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE SCHEMA allowed_ns; +CREATE SCHEMA forbidden_ns; +-- postgres gets CREATE on allowed_ns only; it has no rights on forbidden_ns. +GRANT CREATE, USAGE ON SCHEMA allowed_ns TO postgres; +SET ROLE postgres; +-- Positive control: explicit multirange name in a schema postgres CAN create in. +CREATE TYPE allowed_ns.okrange AS RANGE ( + subtype = int4, + multirange_type_name = allowed_ns.okmultirange +); +-- The fix: an explicit multirange name targeting a schema where postgres lacks +-- CREATE must now be denied. +SAVEPOINT no_priv; +CREATE TYPE allowed_ns.badrange AS RANGE ( + subtype = int4, + multirange_type_name = forbidden_ns.badmultirange +); +ERROR: permission denied for schema forbidden_ns +ROLLBACK TO SAVEPOINT no_priv; +RESET ROLE; +ROLLBACK; diff --git a/nix/tests/expected/operator_breaking_change.out b/nix/tests/expected/operator_breaking_change.out new file mode 100644 index 000000000..93d1cb344 --- /dev/null +++ b/nix/tests/expected/operator_breaking_change.out @@ -0,0 +1,50 @@ +-- Pin CVE-2026-2004 behaviour: attaching a non-built-in selectivity estimator +-- to an operator requires superuser. Verified against both RESTRICT and JOIN. +-- +-- Upstream commits: b764b26f (PG 15.16), bbf5bcf5 (PG 17.8). The check fires in +-- both ValidateRestrictionEstimator() and ValidateJoinEstimator() in +-- src/backend/commands/operatorcmds.c. +-- +-- We use real non-built-in estimators shipped by intarray (_int_matchsel for +-- RESTRICT, _int_overlap_joinsel for JOIN) -- these are exactly the customer- +-- reachable estimators the CVE-2026-2004 fleet-scan query targets. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +-- A schema the non-superuser controls, so CREATE OPERATOR reaches the estimator +-- validation rather than failing an earlier schema-permission check. +CREATE SCHEMA op_ns; +GRANT CREATE, USAGE ON SCHEMA op_ns TO postgres; +-- Trivial boolean procedure for the operator (no internal args -> valid in SQL). +CREATE FUNCTION op_ns.fake_op_proc(_int4, _int4) + RETURNS bool LANGUAGE sql IMMUTABLE AS $$ SELECT true $$; +-- Switch to a non-superuser role. +SET ROLE postgres; +-- 1) RESTRICT = non-built-in estimator should be rejected. +SAVEPOINT before_restrict; +CREATE OPERATOR op_ns.@@@ ( + LEFTARG = _int4, RIGHTARG = _int4, + PROCEDURE = op_ns.fake_op_proc, + RESTRICT = _int_matchsel +); +ERROR: must be superuser to specify a non-built-in restriction estimator function +ROLLBACK TO SAVEPOINT before_restrict; +-- 2) JOIN = non-built-in estimator should be rejected. +SAVEPOINT before_join; +CREATE OPERATOR op_ns.@@@ ( + LEFTARG = _int4, RIGHTARG = _int4, + PROCEDURE = op_ns.fake_op_proc, + JOIN = _int_overlap_joinsel +); +ERROR: must be superuser to specify a non-built-in join estimator function +ROLLBACK TO SAVEPOINT before_join; +-- 3) Sanity check: built-in selectivity estimators still work for non-superusers. +CREATE OPERATOR op_ns.@@@ ( + LEFTARG = _int4, RIGHTARG = _int4, + PROCEDURE = op_ns.fake_op_proc, + RESTRICT = eqsel, + JOIN = eqjoinsel +); +DROP OPERATOR op_ns.@@@ (_int4, _int4); +RESET ROLE; +ROLLBACK; diff --git a/nix/tests/expected/output_plugin_libraries.out b/nix/tests/expected/output_plugin_libraries.out new file mode 100644 index 000000000..30e49b874 --- /dev/null +++ b/nix/tests/expected/output_plugin_libraries.out @@ -0,0 +1,35 @@ +-- CVE-2026-6471: logical decoding could load any library named as an output +-- plugin. PG 15.19 / 17.11 add the "output_plugin_libraries" allowlist GUC; +-- only libraries named there may be used as output plugins. +-- +-- Upstream commit: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- +-- This pins the allowlist enforcement, independent of the exact allowlist value: +-- * an allowlisted, always-shipped plugin (test_decoding) can back a slot; +-- * a non-allowlisted library is rejected at slot-creation time. +-- The image's own allowlist additionally includes wal2json (Realtime); that +-- positive case is covered by wal2json.sql. Requires wal_level = logical, which +-- the image config sets. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +-- 1) Allowlisted plugin: slot creation succeeds, then clean up. +SELECT slot_name FROM pg_create_logical_replication_slot('opl_ok', 'test_decoding'); + slot_name +----------- + opl_ok +(1 row) + +SELECT pg_drop_replication_slot('opl_ok'); + pg_drop_replication_slot +-------------------------- + +(1 row) + +-- 2) Non-allowlisted library: slot creation must be rejected. +SAVEPOINT s_bad; +SELECT pg_create_logical_replication_slot('opl_bad', 'nonesuch_plugin'); +ERROR: library "nonesuch_plugin" may not be used as an output plugin +HINT: If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration. +ROLLBACK TO SAVEPOINT s_bad; +ROLLBACK; diff --git a/nix/tests/expected/pgcrypto_cipher_matrix.out b/nix/tests/expected/pgcrypto_cipher_matrix.out new file mode 100644 index 000000000..5d703c6cd --- /dev/null +++ b/nix/tests/expected/pgcrypto_cipher_matrix.out @@ -0,0 +1,76 @@ +-- CVE-2026-14663: pgcrypto's PGP functions previously did not detect when the +-- requested cipher was unavailable in the server's OpenSSL build. Encryption +-- then silently produced output that was not actually encrypted, and decryption +-- would succeed even with the wrong key. The fix makes encryption fail loudly +-- when the cipher is unavailable, and makes decryption reject such messages. +-- +-- Upstream commits: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- +-- This pins the post-fix contract for the cipher options pgcrypto exposes: +-- * ciphers unavailable in this build (bf/blowfish/cast5) -> encrypt ERRORs +-- rather than silently emitting unencrypted output; +-- * available ciphers (aes*, 3des) round-trip correctly AND reject a wrong +-- passphrase with "Wrong key or corrupt data" (integrity protection intact). +-- The exact set of unavailable ciphers is a function of the OpenSSL build; this +-- suite runs against the image's own OpenSSL, so the assertions below track that +-- build's behavior. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE EXTENSION IF NOT EXISTS pgcrypto; +NOTICE: extension "pgcrypto" already exists, skipping +-- 1) Unavailable ciphers must fail at encrypt time, not silently pass through. +SAVEPOINT s_bf; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=bf') IS NOT NULL AS bf_encrypted; +ERROR: encrypt error: Cipher cannot be initialized +ROLLBACK TO SAVEPOINT s_bf; +SAVEPOINT s_blowfish; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=blowfish') IS NOT NULL AS blowfish_encrypted; +ERROR: encrypt error: Cipher cannot be initialized +ROLLBACK TO SAVEPOINT s_blowfish; +SAVEPOINT s_cast5; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=cast5') IS NOT NULL AS cast5_encrypted; +ERROR: encrypt error: Cipher cannot be initialized +ROLLBACK TO SAVEPOINT s_cast5; +-- 2) Available ciphers round-trip correctly with the right key. +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes128'), 'k') AS aes128_rt; + aes128_rt +----------- + secret +(1 row) + +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes192'), 'k') AS aes192_rt; + aes192_rt +----------- + secret +(1 row) + +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes256'), 'k') AS aes256_rt; + aes256_rt +----------- + secret +(1 row) + +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=3des'), 'k') AS des3_rt; + des3_rt +--------- + secret +(1 row) + +-- 3) The security property: a WRONG passphrase must be rejected, not accepted. +SAVEPOINT s_aes_wrong; +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes256'), 'WRONG'); +ERROR: Wrong key or corrupt data +ROLLBACK TO SAVEPOINT s_aes_wrong; +SAVEPOINT s_des3_wrong; +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=3des'), 'WRONG'); +ERROR: Wrong key or corrupt data +ROLLBACK TO SAVEPOINT s_des3_wrong; +-- 4) Backend still healthy after the expected errors. +SELECT pgp_sym_decrypt(pgp_sym_encrypt('still-here', 'k'), 'k') AS post_error_ok; + post_error_ok +--------------- + still-here +(1 row) + +ROLLBACK; diff --git a/nix/tests/expected/replica_identity_upsert.out b/nix/tests/expected/replica_identity_upsert.out new file mode 100644 index 000000000..72b84c19b --- /dev/null +++ b/nix/tests/expected/replica_identity_upsert.out @@ -0,0 +1,35 @@ +-- 17.7/15.15 tightened logical-replication checks: MERGE and +-- INSERT ... ON CONFLICT DO UPDATE now also require a REPLICA IDENTITY when the +-- target table is in a publication that publishes updates (previously these two +-- paths could slip through, unlike a plain UPDATE). Pins the post-fix behavior: +-- both error without a replica identity, and succeed once one is set. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE TABLE ri (id int UNIQUE, v int); +CREATE PUBLICATION pub_ri FOR TABLE ri; +INSERT INTO ri VALUES (1, 10); +-- No replica identity yet: both write paths must be rejected. +SAVEPOINT s_upsert; +INSERT INTO ri VALUES (1, 20) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v; +ERROR: cannot update table "ri" because it does not have a replica identity and publishes updates +HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE. +ROLLBACK TO SAVEPOINT s_upsert; +SAVEPOINT s_merge; +MERGE INTO ri t USING (SELECT 1 AS id, 30 AS v) s ON t.id = s.id + WHEN MATCHED THEN UPDATE SET v = s.v; +ERROR: cannot update table "ri" because it does not have a replica identity and publishes updates +HINT: To enable updating the table, set REPLICA IDENTITY using ALTER TABLE. +ROLLBACK TO SAVEPOINT s_merge; +-- With a replica identity, both succeed. +ALTER TABLE ri REPLICA IDENTITY FULL; +INSERT INTO ri VALUES (1, 20) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v; +MERGE INTO ri t USING (SELECT 1 AS id, 30 AS v) s ON t.id = s.id + WHEN MATCHED THEN UPDATE SET v = s.v; +SELECT v FROM ri WHERE id = 1; + v +---- + 30 +(1 row) + +ROLLBACK; diff --git a/nix/tests/expected/z_15_intarray_ltree_query.out b/nix/tests/expected/z_15_intarray_ltree_query.out new file mode 100644 index 000000000..7397f4ebe --- /dev/null +++ b/nix/tests/expected/z_15_intarray_ltree_query.out @@ -0,0 +1,4 @@ +-- lquery: total length of one level's OR-variants overflows. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery IS NOT NULL AS lq_totallen; +ERROR: label string is too long +DETAIL: Label length is 1000, must be at most 255, at character 1001. diff --git a/nix/tests/expected/z_17_intarray_ltree_query.out b/nix/tests/expected/z_17_intarray_ltree_query.out new file mode 100644 index 000000000..c996e20aa --- /dev/null +++ b/nix/tests/expected/z_17_intarray_ltree_query.out @@ -0,0 +1,4 @@ +-- lquery: total length of one level's OR-variants overflows. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery IS NOT NULL AS lq_totallen; +ERROR: lquery level is too large +DETAIL: Total size of level exceeds the maximum allowed (65535 bytes). diff --git a/nix/tests/expected/z_orioledb-17_intarray_ltree_query.out b/nix/tests/expected/z_orioledb-17_intarray_ltree_query.out new file mode 100644 index 000000000..c996e20aa --- /dev/null +++ b/nix/tests/expected/z_orioledb-17_intarray_ltree_query.out @@ -0,0 +1,4 @@ +-- lquery: total length of one level's OR-variants overflows. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery IS NOT NULL AS lq_totallen; +ERROR: lquery level is too large +DETAIL: Total size of level exceeds the maximum allowed (65535 bytes). diff --git a/nix/tests/isolation/expected/merge_serialization.out b/nix/tests/isolation/expected/merge_serialization.out new file mode 100644 index 000000000..13421ae42 --- /dev/null +++ b/nix/tests/isolation/expected/merge_serialization.out @@ -0,0 +1,16 @@ +Parsed test spec with 2 sessions + +starting permutation: s1_begin s1_snapshot s2_update s1_merge s1_commit +step s1_begin: BEGIN ISOLATION LEVEL REPEATABLE READ; +step s1_snapshot: SELECT v FROM merge_target WHERE k = 1; +v +- +0 +(1 row) + +step s2_update: UPDATE merge_target SET v = v + 100 WHERE k = 1; +step s1_merge: MERGE INTO merge_target t + USING (SELECT 1 AS k) s ON t.k = s.k + WHEN MATCHED THEN UPDATE SET v = t.v + 1; +ERROR: could not serialize access due to concurrent update +step s1_commit: COMMIT; diff --git a/nix/tests/isolation/specs/merge_serialization.spec b/nix/tests/isolation/specs/merge_serialization.spec new file mode 100644 index 000000000..e02b3621a --- /dev/null +++ b/nix/tests/isolation/specs/merge_serialization.spec @@ -0,0 +1,32 @@ +# Pins the upstream fix (PG 15.16 / 17.8) where MERGE must raise a +# serialization failure (SQLSTATE 40001) under REPEATABLE READ when its target +# row was updated and committed by a concurrent transaction after the MERGE +# transaction took its snapshot. Before the fix this concurrent-update check +# was silently skipped for MERGE (unlike a plain UPDATE). +# +# s1 opens a REPEATABLE READ transaction and takes its snapshot (s1_snapshot); +# s2 then updates+commits the row; s1's MERGE on that row must fail with 40001. + +setup +{ + CREATE TABLE merge_target (k int PRIMARY KEY, v int); + INSERT INTO merge_target VALUES (1, 0); +} + +teardown +{ + DROP TABLE merge_target; +} + +session s1 +step s1_begin { BEGIN ISOLATION LEVEL REPEATABLE READ; } +step s1_snapshot { SELECT v FROM merge_target WHERE k = 1; } +step s1_merge { MERGE INTO merge_target t + USING (SELECT 1 AS k) s ON t.k = s.k + WHEN MATCHED THEN UPDATE SET v = t.v + 1; } +step s1_commit { COMMIT; } + +session s2 +step s2_update { UPDATE merge_target SET v = v + 100 WHERE k = 1; } + +permutation s1_begin s1_snapshot s2_update s1_merge s1_commit diff --git a/nix/tests/sql/btree_gist_nan.sql b/nix/tests/sql/btree_gist_nan.sql new file mode 100644 index 000000000..69c8bd3ee --- /dev/null +++ b/nix/tests/sql/btree_gist_nan.sql @@ -0,0 +1,32 @@ +-- btree_gist NaN handling in the float4/float8 opclasses (comparisons and the +-- GiST penalty/distance functions) previously gave wrong answers when a NaN was +-- present; upstream recommends reindexing btree_gist float indexes that may hold +-- NaN after the update. This pins the post-fix within-version correctness of +-- index scans over a float8 column containing NaN. +-- +-- Upstream commit: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- (The cross-version pre-upgrade-build / post-upgrade-REINDEX leg is A3, +-- PSQL-1235.) +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +SET client_min_messages = warning; -- OrioleDB shows a NOTICE/DETAILS, lets avoid the diff from vanilla +CREATE EXTENSION IF NOT EXISTS btree_gist; + +CREATE TABLE bg_nan (v float8); +INSERT INTO bg_nan VALUES (1), (2), (3), ('NaN'); +CREATE INDEX bg_nan_gist ON bg_nan USING gist (v); + +SET enable_seqscan = off; -- Force index scans so we exercise the opclass, not a seqscan recheck. + +-- NaN sorts as greater than every non-NaN value and equals only itself. +SELECT count(*) AS eq_nan FROM bg_nan WHERE v = 'NaN'::float8; +SELECT count(*) AS gt_one FROM bg_nan WHERE v > 1; -- 2, 3, NaN +SELECT count(*) AS ne_two FROM bg_nan WHERE v <> 2; -- 1, 3, NaN +SELECT v FROM bg_nan WHERE v >= 3 ORDER BY v; -- 3, NaN + +RESET enable_seqscan; + +ROLLBACK; diff --git a/nix/tests/sql/create_statistics_priv.sql b/nix/tests/sql/create_statistics_priv.sql new file mode 100644 index 000000000..0db0775b1 --- /dev/null +++ b/nix/tests/sql/create_statistics_priv.sql @@ -0,0 +1,33 @@ +-- CVE-2025-12817: CREATE STATISTICS did not check CREATE privilege on the +-- schema where the statistics object is created, letting a table owner create +-- statistics objects in any schema (naming-conflict / privilege concern). +-- +-- Upstream commits: 2393d374 + d202ec1f (PG 15.15), e2fb3dfa (PG 17.7). The fix +-- adds a pg_namespace_aclcheck(namespaceId, GetUserId(), ACL_CREATE). +-- +-- Verified as a non-superuser table owner. Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE SCHEMA owned_ns; +CREATE SCHEMA forbidden_ns; +-- postgres can create in owned_ns only; it has no rights on forbidden_ns. +GRANT CREATE, USAGE ON SCHEMA owned_ns TO postgres; + +SET ROLE postgres; + +-- A table postgres owns, in a schema postgres controls. +CREATE TABLE owned_ns.stat_tbl (a int, b int); +INSERT INTO owned_ns.stat_tbl SELECT g % 10, g % 5 FROM generate_series(1, 100) g; + +-- Positive control: stats object in owned_ns (postgres has CREATE) is allowed. +CREATE STATISTICS owned_ns.okstat (dependencies) ON a, b FROM owned_ns.stat_tbl; + +-- The fix: a stats object targeting a schema where postgres lacks CREATE is denied. +SAVEPOINT no_priv; +CREATE STATISTICS forbidden_ns.badstat (dependencies) ON a, b FROM owned_ns.stat_tbl; +ROLLBACK TO SAVEPOINT no_priv; + +RESET ROLE; + +ROLLBACK; diff --git a/nix/tests/sql/hstore_copy_binary.sql b/nix/tests/sql/hstore_copy_binary.sql new file mode 100644 index 000000000..35da4f02c --- /dev/null +++ b/nix/tests/sql/hstore_copy_binary.sql @@ -0,0 +1,38 @@ +-- Non-CVE behavior change: the hstore receive function had a NULL-pointer +-- dereference (backend crash) on COPY BINARY of an hstore whose binary form +-- contains a DUPLICATE key where the second occurrence's value is NULL. +-- +-- Upstream commits: 63c05e03 (PG 15.x), 0dfbe42d (PG 17.x). +-- +-- A normal INSERT cannot reproduce this: hstore de-duplicates on text input, so +-- a stored value never carries a duplicate key into the binary path. We instead +-- hand-craft a COPY-BINARY stream whose single hstore field contains the pair +-- sequence [ 'a' => '1', 'a' => NULL ] and feed it through hstore_recv via +-- COPY ... FROM. Pre-fix this crashed the backend; on the fixed builds the +-- duplicate is de-duplicated and the row loads cleanly. +-- +-- pg_regress runs as the superuser supabase_admin, so lo_export / server-side +-- COPY FROM a file are permitted. Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE TABLE hstore_dst (h hstore); + +-- Materialise the crafted COPY-BINARY stream to a file (created and exported in +-- separate statements so the large object is visible to lo_export). +SELECT lo_from_bytea(81000, + '\x5047434f50590aff0d0a00'::bytea || -- COPY binary signature + '\x00000000'::bytea || '\x00000000'::bytea || -- flags + header-extension length + '\x0001'::bytea || '\x00000017'::bytea || -- one row, one field of length 23 + '\x00000002'::bytea || -- hstore: 2 pairs + '\x00000001'::bytea||'\x61'::bytea||'\x00000001'::bytea||'\x31'::bytea || -- 'a' => '1' + '\x00000001'::bytea||'\x61'::bytea||'\xffffffff'::bytea || -- 'a' => NULL + '\xffff'::bytea) AS loid; -- COPY trailer +SELECT lo_export(81000, '/tmp/pg_regress_hstore_dup.bin') AS exported; + +-- Must not crash the backend; the duplicate key is de-duplicated on receive. +COPY hstore_dst FROM '/tmp/pg_regress_hstore_dup.bin' WITH (FORMAT binary); + +SELECT h AS received, akeys(h) AS keys FROM hstore_dst; + +ROLLBACK; diff --git a/nix/tests/sql/intarray_ltree_query.sql b/nix/tests/sql/intarray_ltree_query.sql new file mode 100644 index 000000000..f7f14760b --- /dev/null +++ b/nix/tests/sql/intarray_ltree_query.sql @@ -0,0 +1,39 @@ +-- CVE-2026-6473 +-- +-- Overflow of the internal "left" / length / variant-count fields used +-- when parsing contrib intarray query_int and contrib ltree ltxtquery / +-- lquery. Pre-fix (<= 15.18 / 17.9) a sufficiently large query was +-- accepted and silently built a corrupt parse tree; the fixed builds +-- (15.19 / 17.11) reject it with a clean error. +-- +-- Upstream fixes and their own regress tests (contrib/intarray/sql/_int.sql, +-- contrib/ltree/sql/ltree.sql): +-- query_int + ltxtquery "left" overflow: +-- 84a9f264 https://github.com/postgres/postgres/commit/84a9f264 +-- lquery totallen / numvar overflow: +-- 9c2fa5b6 https://github.com/postgres/postgres/commit/9c2fa5b6 +-- +-- The reproducers below are those upstream tests. They must use FLAT +-- operator chains (built with string_agg / repeat): the query grammar +-- parses a flat chain iteratively, so it reaches the overflow guard, +-- whereas a deeply nested expression would trip check_stack_depth() first +-- (identically on both builds). +-- +-- Refs: PSQL-1110, PSQL-1234. + +-- Functional sanity: well-formed queries still parse and match on both builds. +SELECT '{1,2,3}'::int[] @@ '2&4'::query_int AS q_and; -- false +SELECT '{1,2,3}'::int[] @@ '2|4'::query_int AS q_or; -- true +SELECT 'Top.Science.Astronomy'::ltree ~ 'Top.*.Astronomy'::lquery AS lquery_match; -- true +SELECT 'Top.Science.Astronomy'::ltree @ 'Astronomy & Top'::ltxtquery AS ltxtquery_match; -- true + +-- query_int: 17000 AND'd terms overflow the int16 "left" offset field. +SELECT (SELECT '0 | ' || string_agg(i::text, ' & ') + FROM generate_series(1, 17000) AS i)::query_int IS NOT NULL AS q_int_overflow; + +-- ltxtquery: same flat-chain shape overflows the ltxtquery size field. +SELECT (SELECT 'a | ' || string_agg('b', ' & ') + FROM generate_series(1, 17000) AS i)::ltxtquery IS NOT NULL AS ltxt_overflow; + +-- lquery: too many OR-variants in a single level. +SELECT (repeat('a|', 65535) || 'a')::lquery IS NOT NULL AS lq_numvar; diff --git a/nix/tests/sql/ltree_label_overflow.sql b/nix/tests/sql/ltree_label_overflow.sql new file mode 100644 index 000000000..5201e09de --- /dev/null +++ b/nix/tests/sql/ltree_label_overflow.sql @@ -0,0 +1,35 @@ +-- contrib/ltree: an integer overflow in ltree_compare() made comparisons return +-- the wrong sign once two values differed in depth by more than ~14,653 labels. +-- A btree index over such values could therefore be built in the wrong order and +-- needs a REINDEX after upgrade. Fixed in PG 15.19 / 17.11 (2026-08-13) by +-- dropping the overflowing "* 10 * (an + 1)" scaling from the comparator's +-- return values. +-- +-- This reproduces the bug index-free, straight through the btree ordering +-- operators: `a` is a deep value (20,001 labels) whose leading label equals the +-- shallow value `b`, so `b` is a proper prefix of `a` and therefore `a > b` must +-- hold. Pre-fix, the final "(a->numlevel - b->numlevel) * 10 * (an + 1)" term +-- overflows int32 for that depth gap and flips sign, so `a > b` wrongly returns +-- false and `a < b` wrongly returns true. `b < a` stays correct pre-fix (the +-- shorter operand exhausts the loop first, so no scaling overflow occurs), which +-- makes the pre-fix result internally contradictory. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE EXTENSION IF NOT EXISTS ltree; + +WITH v AS ( + SELECT (repeat('a.', 20000) || 'a')::ltree AS a, -- 20,001 labels + 'a'::ltree AS b -- 1 label, a prefix of a +) +SELECT nlevel(a) AS na, + nlevel(b) AS nb, + a > b AS a_gt_b, -- must be TRUE (a extends prefix b) + a < b AS a_lt_b, -- must be FALSE + b < a AS b_lt_a, -- must be TRUE + a = a AS a_eq_a -- must be TRUE +FROM v; + +ROLLBACK; diff --git a/nix/tests/sql/multirange_create_priv.sql b/nix/tests/sql/multirange_create_priv.sql new file mode 100644 index 000000000..ee7b36c5a --- /dev/null +++ b/nix/tests/sql/multirange_create_priv.sql @@ -0,0 +1,37 @@ +-- CVE-2026-6472: CREATE TYPE ... AS RANGE auto-creates a companion multirange +-- type. When the multirange type name was given EXPLICITLY, the schema CREATE +-- privilege for that name was not validated (the auto-generated-name path was +-- already checked), letting a role create a multirange type in any schema. +-- +-- Upstream commits: 08c397b0 (PG 15.18), c27ba08c (PG 17.10). The fix adds a +-- pg_namespace_aclcheck(multirangeNamespace, GetUserId(), ACL_CREATE). +-- +-- Verified as a non-superuser. Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE SCHEMA allowed_ns; +CREATE SCHEMA forbidden_ns; +-- postgres gets CREATE on allowed_ns only; it has no rights on forbidden_ns. +GRANT CREATE, USAGE ON SCHEMA allowed_ns TO postgres; + +SET ROLE postgres; + +-- Positive control: explicit multirange name in a schema postgres CAN create in. +CREATE TYPE allowed_ns.okrange AS RANGE ( + subtype = int4, + multirange_type_name = allowed_ns.okmultirange +); + +-- The fix: an explicit multirange name targeting a schema where postgres lacks +-- CREATE must now be denied. +SAVEPOINT no_priv; +CREATE TYPE allowed_ns.badrange AS RANGE ( + subtype = int4, + multirange_type_name = forbidden_ns.badmultirange +); +ROLLBACK TO SAVEPOINT no_priv; + +RESET ROLE; + +ROLLBACK; diff --git a/nix/tests/sql/operator_breaking_change.sql b/nix/tests/sql/operator_breaking_change.sql new file mode 100644 index 000000000..c1abb3382 --- /dev/null +++ b/nix/tests/sql/operator_breaking_change.sql @@ -0,0 +1,57 @@ +-- Pin CVE-2026-2004 behaviour: attaching a non-built-in selectivity estimator +-- to an operator requires superuser. Verified against both RESTRICT and JOIN. +-- +-- Upstream commits: b764b26f (PG 15.16), bbf5bcf5 (PG 17.8). The check fires in +-- both ValidateRestrictionEstimator() and ValidateJoinEstimator() in +-- src/backend/commands/operatorcmds.c. +-- +-- We use real non-built-in estimators shipped by intarray (_int_matchsel for +-- RESTRICT, _int_overlap_joinsel for JOIN) -- these are exactly the customer- +-- reachable estimators the CVE-2026-2004 fleet-scan query targets. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +-- A schema the non-superuser controls, so CREATE OPERATOR reaches the estimator +-- validation rather than failing an earlier schema-permission check. +CREATE SCHEMA op_ns; +GRANT CREATE, USAGE ON SCHEMA op_ns TO postgres; + +-- Trivial boolean procedure for the operator (no internal args -> valid in SQL). +CREATE FUNCTION op_ns.fake_op_proc(_int4, _int4) + RETURNS bool LANGUAGE sql IMMUTABLE AS $$ SELECT true $$; + +-- Switch to a non-superuser role. +SET ROLE postgres; + +-- 1) RESTRICT = non-built-in estimator should be rejected. +SAVEPOINT before_restrict; +CREATE OPERATOR op_ns.@@@ ( + LEFTARG = _int4, RIGHTARG = _int4, + PROCEDURE = op_ns.fake_op_proc, + RESTRICT = _int_matchsel +); +ROLLBACK TO SAVEPOINT before_restrict; + +-- 2) JOIN = non-built-in estimator should be rejected. +SAVEPOINT before_join; +CREATE OPERATOR op_ns.@@@ ( + LEFTARG = _int4, RIGHTARG = _int4, + PROCEDURE = op_ns.fake_op_proc, + JOIN = _int_overlap_joinsel +); +ROLLBACK TO SAVEPOINT before_join; + +-- 3) Sanity check: built-in selectivity estimators still work for non-superusers. +CREATE OPERATOR op_ns.@@@ ( + LEFTARG = _int4, RIGHTARG = _int4, + PROCEDURE = op_ns.fake_op_proc, + RESTRICT = eqsel, + JOIN = eqjoinsel +); +DROP OPERATOR op_ns.@@@ (_int4, _int4); + +RESET ROLE; + +ROLLBACK; diff --git a/nix/tests/sql/output_plugin_libraries.sql b/nix/tests/sql/output_plugin_libraries.sql new file mode 100644 index 000000000..8042b2e41 --- /dev/null +++ b/nix/tests/sql/output_plugin_libraries.sql @@ -0,0 +1,27 @@ +-- CVE-2026-6471: logical decoding could load any library named as an output +-- plugin. PG 15.19 / 17.11 add the "output_plugin_libraries" allowlist GUC; +-- only libraries named there may be used as output plugins. +-- +-- Upstream commit: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- +-- This pins the allowlist enforcement, independent of the exact allowlist value: +-- * an allowlisted, always-shipped plugin (test_decoding) can back a slot; +-- * a non-allowlisted library is rejected at slot-creation time. +-- The image's own allowlist additionally includes wal2json (Realtime); that +-- positive case is covered by wal2json.sql. Requires wal_level = logical, which +-- the image config sets. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +-- 1) Allowlisted plugin: slot creation succeeds, then clean up. +SELECT slot_name FROM pg_create_logical_replication_slot('opl_ok', 'test_decoding'); +SELECT pg_drop_replication_slot('opl_ok'); + +-- 2) Non-allowlisted library: slot creation must be rejected. +SAVEPOINT s_bad; +SELECT pg_create_logical_replication_slot('opl_bad', 'nonesuch_plugin'); +ROLLBACK TO SAVEPOINT s_bad; + +ROLLBACK; diff --git a/nix/tests/sql/pgcrypto_cipher_matrix.sql b/nix/tests/sql/pgcrypto_cipher_matrix.sql new file mode 100644 index 000000000..830ac2571 --- /dev/null +++ b/nix/tests/sql/pgcrypto_cipher_matrix.sql @@ -0,0 +1,55 @@ +-- CVE-2026-14663: pgcrypto's PGP functions previously did not detect when the +-- requested cipher was unavailable in the server's OpenSSL build. Encryption +-- then silently produced output that was not actually encrypted, and decryption +-- would succeed even with the wrong key. The fix makes encryption fail loudly +-- when the cipher is unavailable, and makes decryption reject such messages. +-- +-- Upstream commits: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- +-- This pins the post-fix contract for the cipher options pgcrypto exposes: +-- * ciphers unavailable in this build (bf/blowfish/cast5) -> encrypt ERRORs +-- rather than silently emitting unencrypted output; +-- * available ciphers (aes*, 3des) round-trip correctly AND reject a wrong +-- passphrase with "Wrong key or corrupt data" (integrity protection intact). +-- The exact set of unavailable ciphers is a function of the OpenSSL build; this +-- suite runs against the image's own OpenSSL, so the assertions below track that +-- build's behavior. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- 1) Unavailable ciphers must fail at encrypt time, not silently pass through. +SAVEPOINT s_bf; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=bf') IS NOT NULL AS bf_encrypted; +ROLLBACK TO SAVEPOINT s_bf; + +SAVEPOINT s_blowfish; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=blowfish') IS NOT NULL AS blowfish_encrypted; +ROLLBACK TO SAVEPOINT s_blowfish; + +SAVEPOINT s_cast5; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=cast5') IS NOT NULL AS cast5_encrypted; +ROLLBACK TO SAVEPOINT s_cast5; + +-- 2) Available ciphers round-trip correctly with the right key. +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes128'), 'k') AS aes128_rt; +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes192'), 'k') AS aes192_rt; +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes256'), 'k') AS aes256_rt; +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=3des'), 'k') AS des3_rt; + +-- 3) The security property: a WRONG passphrase must be rejected, not accepted. +SAVEPOINT s_aes_wrong; +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=aes256'), 'WRONG'); +ROLLBACK TO SAVEPOINT s_aes_wrong; + +SAVEPOINT s_des3_wrong; +SELECT pgp_sym_decrypt(pgp_sym_encrypt('secret', 'k', 'cipher-algo=3des'), 'WRONG'); +ROLLBACK TO SAVEPOINT s_des3_wrong; + +-- 4) Backend still healthy after the expected errors. +SELECT pgp_sym_decrypt(pgp_sym_encrypt('still-here', 'k'), 'k') AS post_error_ok; + +ROLLBACK; diff --git a/nix/tests/sql/replica_identity_upsert.sql b/nix/tests/sql/replica_identity_upsert.sql new file mode 100644 index 000000000..92c5df4a9 --- /dev/null +++ b/nix/tests/sql/replica_identity_upsert.sql @@ -0,0 +1,32 @@ +-- 17.7/15.15 tightened logical-replication checks: MERGE and +-- INSERT ... ON CONFLICT DO UPDATE now also require a REPLICA IDENTITY when the +-- target table is in a publication that publishes updates (previously these two +-- paths could slip through, unlike a plain UPDATE). Pins the post-fix behavior: +-- both error without a replica identity, and succeed once one is set. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE TABLE ri (id int UNIQUE, v int); +CREATE PUBLICATION pub_ri FOR TABLE ri; +INSERT INTO ri VALUES (1, 10); + +-- No replica identity yet: both write paths must be rejected. +SAVEPOINT s_upsert; +INSERT INTO ri VALUES (1, 20) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v; +ROLLBACK TO SAVEPOINT s_upsert; + +SAVEPOINT s_merge; +MERGE INTO ri t USING (SELECT 1 AS id, 30 AS v) s ON t.id = s.id + WHEN MATCHED THEN UPDATE SET v = s.v; +ROLLBACK TO SAVEPOINT s_merge; + +-- With a replica identity, both succeed. +ALTER TABLE ri REPLICA IDENTITY FULL; +INSERT INTO ri VALUES (1, 20) ON CONFLICT (id) DO UPDATE SET v = EXCLUDED.v; +MERGE INTO ri t USING (SELECT 1 AS id, 30 AS v) s ON t.id = s.id + WHEN MATCHED THEN UPDATE SET v = s.v; +SELECT v FROM ri WHERE id = 1; + +ROLLBACK; diff --git a/nix/tests/sql/z_15_intarray_ltree_query.sql b/nix/tests/sql/z_15_intarray_ltree_query.sql new file mode 100644 index 000000000..9e07cdebb --- /dev/null +++ b/nix/tests/sql/z_15_intarray_ltree_query.sql @@ -0,0 +1,2 @@ +-- lquery: total length of one level's OR-variants overflows. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery IS NOT NULL AS lq_totallen; diff --git a/nix/tests/sql/z_17_intarray_ltree_query.sql b/nix/tests/sql/z_17_intarray_ltree_query.sql new file mode 100644 index 000000000..9e07cdebb --- /dev/null +++ b/nix/tests/sql/z_17_intarray_ltree_query.sql @@ -0,0 +1,2 @@ +-- lquery: total length of one level's OR-variants overflows. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery IS NOT NULL AS lq_totallen; diff --git a/nix/tests/sql/z_orioledb-17_intarray_ltree_query.sql b/nix/tests/sql/z_orioledb-17_intarray_ltree_query.sql new file mode 100644 index 000000000..9e07cdebb --- /dev/null +++ b/nix/tests/sql/z_orioledb-17_intarray_ltree_query.sql @@ -0,0 +1,2 @@ +-- lquery: total length of one level's OR-variants overflows. +SELECT (repeat('x', 1000) || repeat('|' || repeat('x', 1000), 65))::lquery IS NOT NULL AS lq_totallen; From 081eda482a29087f312e09ade8949768bf83c35e Mon Sep 17 00:00:00 2001 From: Utkarash Singh Date: Tue, 25 Aug 2026 12:13:42 +0100 Subject: [PATCH 3/6] regress: Temporarily skip hstore_copy_binary test for all variants This crashes postgres and we don't have automatic restarts in these tests (as seen in last commit's ci run). --- nix/checks.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/checks.nix b/nix/checks.nix index 2f1cf2158..6140f9a9e 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -538,7 +538,7 @@ --host=localhost \ --port=${pgPort} \ --user=supabase_admin \ - ${builtins.concatStringsSep " " sortedTestList} 2>&1; then + ${builtins.concatStringsSep " " (lib.lists.remove "hstore_copy_binary" sortedTestList)} 2>&1; then log error "pg_regress tests failed" cat "$out/regression_output/regression.diffs" exit 1 From e56350e9fac5a0beb8b00dc8f26d07ce7a2391e0 Mon Sep 17 00:00:00 2001 From: Manuel Mendez Date: Fri, 21 Aug 2026 19:41:14 -0400 Subject: [PATCH 4/6] postgres: 15.16 -> 15.19, 17.6 -> 17.11 PG 15.19/17.11 introduces output_plugin_libraries (CVE-2026-6471) defaulting to pgoutput and test_decoding only, which breaks wal2json slots (Realtime, CI wal2json tests). Allow wal2json in the shared config. --- Dockerfile-multigres | 10 ++++------ Dockerfile-orioledb-17 | 10 ++++------ README.md | 4 ++-- ansible/files/postgresql_config/postgresql.conf.j2 | 3 +++ ansible/tasks/stage2-setup-postgres.yml | 8 -------- docker/pgctld/postgresql.conf.tmpl | 2 ++ migrations/schema-15.sql | 4 ++-- migrations/schema-17.sql | 4 ++-- nix/checks.nix | 6 ++---- nix/config.nix | 8 ++++---- nix/ext/tests/lib.nix | 4 ++-- nix/ext/wal2json.nix | 2 ++ nix/tools/run-server.sh.in | 3 +-- 13 files changed, 30 insertions(+), 38 deletions(-) diff --git a/Dockerfile-multigres b/Dockerfile-multigres index 6fe3acdcf..da9ae1648 100644 --- a/Dockerfile-multigres +++ b/Dockerfile-multigres @@ -263,12 +263,10 @@ RUN chown -R postgres:postgres /usr/lib/postgresql && \ RUN sed -i 's/ timescaledb,//g; s/ plv8,//g; s/ postgis,//g; s/ pgrouting,//g' \ /etc/postgresql-custom/supautils.conf -# Add orioledb to shared_preload_libraries and configure as default table access method -# orioledb-17 is pinned to 17.11+, which added output_plugin_libraries -# as an allow-list for logical decoding output plugins. -RUN sed -i "s/\(shared_preload_libraries.*\)'/\1, orioledb'/" /etc/postgresql/postgresql.conf && \ - echo "default_table_access_method = 'orioledb'" >> /etc/postgresql/postgresql.conf && \ - echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" >> /etc/postgresql/postgresql.conf +# OrioleDB configuration +RUN sed -i /etc/postgresql/postgresql.conf \ + -e "/shared_preload_libraries/ s|'|'orioledb,|" \ + -e "$ a default_table_access_method = 'orioledb'" \ # Register orioledb before initdb migrations run RUN echo "CREATE EXTENSION orioledb;" > /docker-entrypoint-initdb.d/init-scripts/00-pre-init.sql && \ diff --git a/Dockerfile-orioledb-17 b/Dockerfile-orioledb-17 index 4405cc22b..baf24e0ed 100644 --- a/Dockerfile-orioledb-17 +++ b/Dockerfile-orioledb-17 @@ -155,12 +155,10 @@ RUN sed -i 's/ timescaledb,//g;' "/etc/postgresql/postgresql.conf" && \ sed -i 's/ timescaledb,//g; s/ plv8,//g; s/ postgis,//g; s/ pgrouting,//g' "/etc/postgresql-custom/supautils.conf" # OrioleDB configuration -RUN sed -i 's/\(shared_preload_libraries.*\)'\''\(.*\)$/\1, orioledb'\''\2/' "/etc/postgresql/postgresql.conf" && \ - echo "default_table_access_method = 'orioledb'" >> "/etc/postgresql/postgresql.conf" - -# orioledb-17 is pinned to 17.11+, which added output_plugin_libraries as an -# allow-list for logical decoding output plugins -RUN echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" >> "/etc/postgresql/postgresql.conf" +RUN sed -i /etc/postgresql/postgresql.conf \ + -e "/shared_preload_libraries/ s|'|'orioledb,|" \ + -e "$ a default_table_access_method = 'orioledb'" \ + -e "$ a output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" # Include schema migrations COPY migrations/db /docker-entrypoint-initdb.d/ diff --git a/README.md b/README.md index cf1670315..bf27974cd 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,8 @@ This is the same PostgreSQL build that powers [Supabase](https://supabase.io), b ## Primary Features -- ✅ Postgres [postgresql-15.14](https://www.postgresql.org/docs/15/index.html) -- ✅ Postgres [postgresql-17.6](https://www.postgresql.org/docs/17/index.html) +- ✅ Postgres [postgresql-15.19](https://www.postgresql.org/docs/15/index.html) +- ✅ Postgres [postgresql-17.11](https://www.postgresql.org/docs/17/index.html) - ✅ Postgres [orioledb-postgresql-17_11](https://github.com/orioledb/orioledb) - ✅ Ubuntu 24.04 (Noble Numbat). - ✅ [wal_level](https://www.postgresql.org/docs/current/runtime-config-wal.html) = logical and [max_replication_slots](https://www.postgresql.org/docs/current/runtime-config-replication.html) = 5. Ready for replication. diff --git a/ansible/files/postgresql_config/postgresql.conf.j2 b/ansible/files/postgresql_config/postgresql.conf.j2 index 154ec1341..37e50623e 100644 --- a/ansible/files/postgresql_config/postgresql.conf.j2 +++ b/ansible/files/postgresql_config/postgresql.conf.j2 @@ -204,6 +204,9 @@ shared_buffers = 128MB # min 128kB wal_level = logical # minimal, replica, or logical # (change requires restart) +output_plugin_libraries = 'pgoutput, test_decoding, wal2json' # allowlist of logical decoding + # output plugins (PG 15.19 / 17.11+); wal2json is + # required by Realtime and shipped in the image #fsync = on # flush data to disk for crash safety # (turning this off can cause # unrecoverable data corruption) diff --git a/ansible/tasks/stage2-setup-postgres.yml b/ansible/tasks/stage2-setup-postgres.yml index 7b230a8e6..d4823238a 100644 --- a/ansible/tasks/stage2-setup-postgres.yml +++ b/ansible/tasks/stage2-setup-postgres.yml @@ -48,14 +48,6 @@ path: '/etc/postgresql/postgresql.conf' state: 'present' - # orioledb-17 is pinned to 17.11+, which added output_plugin_libraries - # as an allow-list for logical decoding output plugins. - - name: Allow wal2json as a logical decoding output plugin - ansible.builtin.lineinfile: - line: "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" - path: '/etc/postgresql/postgresql.conf' - state: 'present' - - name: Add ORIOLEDB_ENABLED environment variable ansible.builtin.lineinfile: line: 'ORIOLEDB_ENABLED=true' diff --git a/docker/pgctld/postgresql.conf.tmpl b/docker/pgctld/postgresql.conf.tmpl index cb74a6d81..7039789df 100644 --- a/docker/pgctld/postgresql.conf.tmpl +++ b/docker/pgctld/postgresql.conf.tmpl @@ -83,6 +83,8 @@ max_parallel_maintenance_workers = {{.MaxParallelMaintenanceWorkers}} # taken fr wal_level = logical # minimal, replica, or logical # (change requires restart) +output_plugin_libraries = 'pgoutput, test_decoding, wal2json' # allowlist of logical decoding + # output plugins (PG 15.19 / 17.11+) wal_buffers = {{.WalBuffers}} # min 32kB, -1 sets based on shared_buffers # (change requires restart) min_wal_size = {{.MinWalSize}} diff --git a/migrations/schema-15.sql b/migrations/schema-15.sql index 322f47404..fcd89ac95 100644 --- a/migrations/schema-15.sql +++ b/migrations/schema-15.sql @@ -4,8 +4,8 @@ \restrict SupabaseTestDumpKey123 --- Dumped from database version 15.14 --- Dumped by pg_dump version 15.14 +-- Dumped from database version 15.19 +-- Dumped by pg_dump version 15.19 SET statement_timeout = 0; SET lock_timeout = 0; diff --git a/migrations/schema-17.sql b/migrations/schema-17.sql index 2cbfa1b14..9df111002 100644 --- a/migrations/schema-17.sql +++ b/migrations/schema-17.sql @@ -4,8 +4,8 @@ \restrict SupabaseTestDumpKey123 --- Dumped from database version 17.6 --- Dumped by pg_dump version 17.6 +-- Dumped from database version 17.11 +-- Dumped by pg_dump version 17.11 SET statement_timeout = 0; SET lock_timeout = 0; diff --git a/nix/checks.nix b/nix/checks.nix index 6140f9a9e..e9d2305f6 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -379,6 +379,7 @@ echo "port = ${pgPort}" echo "dynamic_library_path = '${supautils}/lib:\$libdir'" + echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" echo "session_preload_libraries = 'supautils'" } >> "$PGTAP_CLUSTER"/postgresql.conf @@ -389,9 +390,6 @@ # Add orioledb to shared_preload_libraries perl -pi -e "s/(shared_preload_libraries = ')/\$1orioledb, /" "$PGTAP_CLUSTER/postgresql.conf" log info "OrioleDB added to shared_preload_libraries" - # orioledb-17 is pinned to 17.11+, which added output_plugin_libraries - # as an allow-list for logical decoding output plugins. - echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" >> "$PGTAP_CLUSTER/postgresql.conf" fi log info "Checking shared_preload_libraries setting:" @@ -538,7 +536,7 @@ --host=localhost \ --port=${pgPort} \ --user=supabase_admin \ - ${builtins.concatStringsSep " " (lib.lists.remove "hstore_copy_binary" sortedTestList)} 2>&1; then + ${builtins.concatStringsSep " " sortedTestList} 2>&1; then log error "pg_regress tests failed" cat "$out/regression_output/regression.diffs" exit 1 diff --git a/nix/config.nix b/nix/config.nix index a70fb4d61..1a717674c 100644 --- a/nix/config.nix +++ b/nix/config.nix @@ -46,12 +46,12 @@ in supportedPostgresVersions = { postgres = { "15" = { - version = "15.14"; - hash = "sha256-Bt110wXNOHDuYrOTLmYcYkVD6vmuK6N83sCk+O3QUdI="; + version = "15.19"; + hash = "sha256-4aZKh6RrgluIwILkUYFhpHqrU8RWlJZPi6HfKPeFn4k="; }; "17" = { - version = "17.6"; - hash = "sha256-4GMKNgCuonURcVVjJZ7CERzV9DU6SwQOC+gn+UzXqLA="; + version = "17.11"; + hash = "sha256-3Sfys8Wec+0UqjMkkBJCv2mgMqY0eAXydOYmAyLUKXk="; }; }; orioledb = { diff --git a/nix/ext/tests/lib.nix b/nix/ext/tests/lib.nix index 9c268f35f..8522848e6 100644 --- a/nix/ext/tests/lib.nix +++ b/nix/ext/tests/lib.nix @@ -4,8 +4,8 @@ let system = pkgs.pkgsLinux.stdenv.hostPlatform.system; expectedVersions = { - "15" = "15.14"; - "17" = "17.6"; + "15" = "15.19"; + "17" = "17.11"; }; defaultPort = 5432; diff --git a/nix/ext/wal2json.nix b/nix/ext/wal2json.nix index b082301c8..170fc492f 100644 --- a/nix/ext/wal2json.nix +++ b/nix/ext/wal2json.nix @@ -116,6 +116,8 @@ pkgs.buildEnv { "multi-" + lib.concatStringsSep "-" (map (v: lib.replaceStrings [ "." ] [ "-" ] v) versions); defaultSettings = { wal_level = "logical"; + # PG 15.19 / 17.11+ only load output plugins named here (CVE-2026-6471); + output_plugin_libraries = "pgoutput, test_decoding, wal2json"; }; }; } diff --git a/nix/tools/run-server.sh.in b/nix/tools/run-server.sh.in index 6090cab8f..041304abd 100644 --- a/nix/tools/run-server.sh.in +++ b/nix/tools/run-server.sh.in @@ -243,6 +243,7 @@ vault.getkey_script = '$PGSODIUM_GETKEY_SCRIPT'" \ -e "s|include = '/etc/postgresql-custom/read-replica.conf'|include = '$DATDIR/read-replica.conf'|" \ -e "\$a\\ session_preload_libraries = 'supautils'" \ +-e "$ a output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" \ -e "s|include_dir = '/etc/postgresql-custom/conf.d'|include_dir = '$DATDIR/conf.d'|" \ "$PSQL_CONF_FILE" > "$DATDIR/postgresql.conf" @@ -256,7 +257,6 @@ orioledb_config_items() { sed -i 's/ timescaledb,//g; s/ plv8,//g; s/ pgjwt,//g;' "$DATDIR/supautils.conf" sed -i 's/\(shared_preload_libraries.*\)'\''\(.*\)$/\1, orioledb'\''\2/' "$DATDIR/postgresql.conf" echo "default_table_access_method = 'orioledb'" >> "$DATDIR/postgresql.conf" - echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" >> "$DATDIR/postgresql.conf" elif [[ "$1" = "orioledb-17" && "$CURRENT_SYSTEM" = "aarch64-darwin" ]]; then # macOS specific configuration echo "macOS detected, applying macOS specific configuration" @@ -272,7 +272,6 @@ orioledb_config_items() { perl -pi -e 's/(shared_preload_libraries\s*=\s*'\''.*?)'\''/\1, orioledb'\''/' "$DATDIR/postgresql.conf" echo "default_table_access_method = 'orioledb'" >> "$DATDIR/postgresql.conf" - echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" >> "$DATDIR/postgresql.conf" elif [[ "$VERSION" == "17" && "$CURRENT_SYSTEM" != "aarch64-darwin" ]]; then echo "non-macos pg 17 conf" sed -i 's/ timescaledb,//g;' "$DATDIR/postgresql.conf" From c6f2da7f597952d0dcabf84f8f8c898eccd538db Mon Sep 17 00:00:00 2001 From: Manuel Mendez Date: Mon, 28 Sep 2026 13:26:26 -0400 Subject: [PATCH 5/6] nix/tests: Drop sample_isolation Was only here as an example test and did nothing, now that we have *real* test this can be dropped. --- nix/checks.nix | 1 - .../isolation/expected/sample_isolation.out | 20 ------------ .../isolation/specs/sample_isolation.spec | 31 ------------------- 3 files changed, 52 deletions(-) delete mode 100644 nix/tests/isolation/expected/sample_isolation.out delete mode 100644 nix/tests/isolation/specs/sample_isolation.spec diff --git a/nix/checks.nix b/nix/checks.nix index e9d2305f6..8eb2f1665 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -249,7 +249,6 @@ # expected output in tests/isolation/expected/. Add new spec names here. isolationSpecList = [ "merge_serialization" - "sample_isolation" ]; in pkgs.writeShellApplication rec { diff --git a/nix/tests/isolation/expected/sample_isolation.out b/nix/tests/isolation/expected/sample_isolation.out deleted file mode 100644 index 28c72466c..000000000 --- a/nix/tests/isolation/expected/sample_isolation.out +++ /dev/null @@ -1,20 +0,0 @@ -Parsed test spec with 2 sessions - -starting permutation: s1_begin s1_update s2_begin s2_read s1_commit s2_read s2_commit -step s1_begin: BEGIN; -step s1_update: UPDATE iso_sample SET val = val + 1 WHERE id = 1; -step s2_begin: BEGIN ISOLATION LEVEL READ COMMITTED; -step s2_read: SELECT val FROM iso_sample WHERE id = 1; -val ---- -100 -(1 row) - -step s1_commit: COMMIT; -step s2_read: SELECT val FROM iso_sample WHERE id = 1; -val ---- -101 -(1 row) - -step s2_commit: COMMIT; diff --git a/nix/tests/isolation/specs/sample_isolation.spec b/nix/tests/isolation/specs/sample_isolation.spec deleted file mode 100644 index 4f4ff653d..000000000 --- a/nix/tests/isolation/specs/sample_isolation.spec +++ /dev/null @@ -1,31 +0,0 @@ -# Sample isolation spec -- proves the pg_isolation_regress harness runs in CI. -# This is not a regression test for any particular fix; it just exercises the -# stock PostgreSQL isolation tester so real concurrency specs (e.g. MERGE -# serialization, postgres_fdw EvalPlanQual) can be dropped in alongside it. -# -# Scenario: session s2 reads a row under READ COMMITTED before and after a -# concurrent UPDATE+COMMIT by session s1 -- the second read observes the -# committed change. No step blocks. - -setup -{ - CREATE TABLE iso_sample (id int PRIMARY KEY, val int); - INSERT INTO iso_sample VALUES (1, 100); -} - -teardown -{ - DROP TABLE iso_sample; -} - -session s1 -step s1_begin { BEGIN; } -step s1_update { UPDATE iso_sample SET val = val + 1 WHERE id = 1; } -step s1_commit { COMMIT; } - -session s2 -step s2_begin { BEGIN ISOLATION LEVEL READ COMMITTED; } -step s2_read { SELECT val FROM iso_sample WHERE id = 1; } -step s2_commit { COMMIT; } - -permutation s1_begin s1_update s2_begin s2_read s1_commit s2_read s2_commit From d06f87d3dd2413e594c599f4f598d41f7afea452 Mon Sep 17 00:00:00 2001 From: Manuel Mendez Date: Fri, 25 Sep 2026 16:22:44 -0400 Subject: [PATCH 6/6] Bump pg_release versions for final release --- ansible/vars.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/vars.yml b/ansible/vars.yml index 80f8ab5a6..74b3903a7 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -8,8 +8,8 @@ postgres_major: - orioledb-17 postgres_release: postgresorioledb-17: "17.11.0.002-orioledb" - postgres17: "17.6.1.178" - postgres15: "15.14.1.178" + postgres17: "17.11.0.001" + postgres15: "15.19.0.001" supabase_admin_agent_splay: 30s ############################################################################################################### # The following block of yaml is for get_url and co throughout the playbook #