diff --git a/Dockerfile-orioledb-17 b/Dockerfile-orioledb-17 index 7cd70a3710..68aef054d6 100644 --- a/Dockerfile-orioledb-17 +++ b/Dockerfile-orioledb-17 @@ -150,8 +150,10 @@ RUN sed -i \ chown -R postgres:postgres /etc/postgresql-custom # Remove timescaledb, plv8, postgis, pgrouting references (not available in orioledb build) +# and output_plugin_libraries (GUC does not exist before PG 17.11; orioledb is on an older base) RUN sed -i 's/ timescaledb,//g;' "/etc/postgresql/postgresql.conf" && \ sed -i 's/db_user_namespace = off/#db_user_namespace = off/g;' "/etc/postgresql/postgresql.conf" && \ + sed -i 's/^output_plugin_libraries/#output_plugin_libraries/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 diff --git a/README.md b/README.md index 6add67a24e..1faf0780a1 100644 --- a/README.md +++ b/README.md @@ -211,8 +211,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 154ec13416..37e50623e0 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 d4823238a7..c77ffd8fc7 100644 --- a/ansible/tasks/stage2-setup-postgres.yml +++ b/ansible/tasks/stage2-setup-postgres.yml @@ -36,6 +36,12 @@ when: stage2 and is_psql_oriole become: true block: + - name: Comment out output_plugin_libraries if orioledb build (GUC does not exist before PG 17.11) + ansible.builtin.replace: + path: '/etc/postgresql/postgresql.conf' + regexp: '^output_plugin_libraries' + replace: '#output_plugin_libraries' + - name: Append orioledb to shared_preload_libraries append within closing quote ansible.builtin.replace: path: '/etc/postgresql/postgresql.conf' diff --git a/docker/pgctld/postgresql.conf.tmpl b/docker/pgctld/postgresql.conf.tmpl index 4067e5ab80..e8fdfbec60 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 322f47404c..fcd89ac957 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 2cbfa1b146..9df1110027 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 3fdd5ba2c0..6e23ac2877 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -183,6 +183,22 @@ # Tests to skip for OrioleDB (not compatible with OrioleDB storage) orioledbSkipTests = [ "index_advisor" # index_advisor doesn't support OrioleDB tables + + # Checks CVE/behavior changes from 17.7 -> 17.11, drop when orioledb base is updated to/past these. + "operator_breaking_change" # CVE-2026-2004 (17.8) + "pgcrypto" # CVE-2026-2005 (17.8) + "pg_trgm" # CVE-2026-2006 multibyte (17.8) + "intarray_ltree_query" # CVE-2026-6473 (17.10) + "ltree_reindex" # ltree multibyte fix (17.8/17.10) + "hstore_copy_binary" # hstore recv crash fix (17.x > 17.6) + "merge_repeatable_read" # MERGE 40001 serialization fix + "multirange_create_priv" # CVE-2026-6472 (17.10) + "create_statistics_priv" # CVE-2025-12817 (17.7) + "pgcrypto_cipher_matrix" # CVE-2026-14663 (17.11) + "output_plugin_libraries" # CVE-2026-6471 (17.11) + "btree_gist_nan" # btree_gist NaN fix (17.11) + "ltree_label_overflow" # ltree comparison overflow fix (17.11) + "replica_identity_upsert" # MERGE/ON CONFLICT replica-identity check (17.7/15.15) ]; # Helper function to filter SQL files based on version @@ -256,6 +272,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" @@ -411,6 +429,10 @@ # 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" + else + # PG 15.19 / 17.11+: allowlist wal2json for logical decoding (GUC does not + # exist on the orioledb 17.9 base, hence the else branch) + echo "output_plugin_libraries = 'pgoutput, test_decoding, wal2json'" >> "$PGTAP_CLUSTER"/postgresql.conf fi # Check if postgresql.conf exists diff --git a/nix/config.nix b/nix/config.nix index 7ed5338e0a..a473c2bb6f 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/default.nix b/nix/ext/tests/default.nix index f0d8e37d46..e088ff6765 100644 --- a/nix/ext/tests/default.nix +++ b/nix/ext/tests/default.nix @@ -155,7 +155,11 @@ let self.packages.${pkgs.pkgsLinux.stdenv.hostPlatform.system}.postgresql_orioledb-17 ); settings = lib.mkForce ( - ((installedExtension "17").defaultSettings or { }) + # output_plugin_libraries does not exist before PG 17.11; orioledb is on an + # older base, so strip it from any extension's defaultSettings (e.g. wal2json) + (removeAttrs ((installedExtension "17").defaultSettings or { }) [ + "output_plugin_libraries" + ]) // { jit = "off"; shared_preload_libraries = [ diff --git a/nix/ext/tests/lib.nix b/nix/ext/tests/lib.nix index 9c268f35f0..3c2d1ba3c0 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; @@ -115,6 +115,9 @@ let ${ if majorVersion == "orioledb-17" then '' + # OrioleDB: comment out output_plugin_libraries (GUC does not exist before PG 17.11; + # orioledb line is on a 17.9 base and would fail to start on an unknown parameter) + sed -i 's/^output_plugin_libraries/#output_plugin_libraries/' $out/postgresql.conf # OrioleDB: also remove pgjwt from supautils privileged_extensions sed -i 's/ pgjwt,//g;' $out/supautils.conf # OrioleDB: append orioledb to shared_preload_libraries diff --git a/nix/ext/wal2json.nix b/nix/ext/wal2json.nix index b082301c82..8ea1e44a7a 100644 --- a/nix/ext/wal2json.nix +++ b/nix/ext/wal2json.nix @@ -116,6 +116,10 @@ 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); + # stripped for the orioledb specialisation in nix/ext/tests/default.nix + # (GUC does not exist on orioledb's older base) + output_plugin_libraries = "pgoutput, test_decoding, wal2json"; }; }; } diff --git a/nix/tests/expected/btree_gist_nan.out b/nix/tests/expected/btree_gist_nan.out new file mode 100644 index 0000000000..b585706847 --- /dev/null +++ b/nix/tests/expected/btree_gist_nan.out @@ -0,0 +1,47 @@ +-- 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; +CREATE EXTENSION IF NOT EXISTS btree_gist; +NOTICE: extension "btree_gist" already exists, skipping +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); +-- Force index scans so we exercise the opclass, not a seqscan recheck. +SET enable_seqscan = off; +-- 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 0000000000..983a754f02 --- /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 0000000000..c96abb526d --- /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 0000000000..30084cbd72 --- /dev/null +++ b/nix/tests/expected/intarray_ltree_query.out @@ -0,0 +1,46 @@ +-- CVE-2026-6473: memory-allocation overflow umbrella covering, among others, +-- contrib intarray query_int and contrib ltree ltxtquery / lquery parsing. +-- +-- Upstream key commits: 84a9f264 (intarray/ltree), 9c2fa5b6 (ltree lquery) on +-- PG 15.18; c4d04cc4 and siblings on PG 17.10. Full list: +-- git log REL_17_6..REL_17_10 --grep='CVE-2026-6473' +-- +-- Functional regression: well-formed queries parse and match correctly; a +-- malformed query raises a clean parse error instead of crashing. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +-- 1) intarray query_int matching. +SELECT '{1,2,3}'::int[] @@ '2&4'::query_int AS q_and; -- expect false + q_and +------- + f +(1 row) + +SELECT '{1,2,3}'::int[] @@ '2|4'::query_int AS q_or; -- expect true + q_or +------ + t +(1 row) + +-- 2) ltree lquery and ltxtquery matching. +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) + +-- 3) A malformed query_int must raise a clean parse error, not crash. +SAVEPOINT bad_query; +SELECT '{1}'::int[] @@ '2&&'::query_int; +ERROR: syntax error +LINE 1: SELECT '{1}'::int[] @@ '2&&'::query_int; + ^ +ROLLBACK TO SAVEPOINT bad_query; +ROLLBACK; diff --git a/nix/tests/expected/ltree_label_overflow.out b/nix/tests/expected/ltree_label_overflow.out new file mode 100644 index 0000000000..a01c2a777e --- /dev/null +++ b/nix/tests/expected/ltree_label_overflow.out @@ -0,0 +1,28 @@ +-- contrib/ltree: an integer overflow in ltree comparisons made values with more +-- than about 14,653 labels compare incorrectly; a btree index over such values +-- could become corrupt and need reindexing. This pins the post-fix comparison +-- correctness for very deep ltree values (no index needed: a btree entry that +-- size cannot exist, so this exercises the comparator directly). +-- +-- Upstream commit: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE EXTENSION IF NOT EXISTS ltree; +NOTICE: extension "ltree" already exists, skipping +-- Build two deep ltree values (~15,000 labels) differing only in the last label. +WITH v AS ( + SELECT (repeat('a.', 15000) || 'x')::ltree AS a, + (repeat('a.', 15000) || 'y')::ltree AS b +) +SELECT nlevel(a) > 14653 AS deep_enough, + a < b AS a_lt_b, + NOT (b < a) AS b_not_lt_a, + a = a AS a_eq_a +FROM v; + deep_enough | a_lt_b | b_not_lt_a | a_eq_a +-------------+--------+------------+-------- + t | t | t | t +(1 row) + +ROLLBACK; diff --git a/nix/tests/expected/ltree_reindex.out b/nix/tests/expected/ltree_reindex.out new file mode 100644 index 0000000000..fc4e9162be --- /dev/null +++ b/nix/tests/expected/ltree_reindex.out @@ -0,0 +1,53 @@ +-- Non-CVE behavior change (highest customer blast radius this release: ltree is +-- enabled on 2,245 projects). Two upstream commits fix multibyte handling in +-- ltree's case-insensitive label matching, so GiST indexes built under the old +-- logic must be REINDEXed after upgrade on multibyte / ICU databases: +-- +-- 335b2f30 (PG 15.16) + 2b993167 (PG 15.18) "Fix multibyte issues in ltree" +-- b8cfe9dc (PG 17.8) + d1bd9a7d (PG 17.10) +-- +-- This pins WITHIN-version GiST index correctness + REINDEX idempotence. The +-- cross-version pre-upgrade-build / post-upgrade-REINDEX leg is covered by A3 +-- (pg_upgrade migration tests, PSQL-1235). +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +CREATE TABLE ltree_mb (id int, path ltree); +INSERT INTO ltree_mb VALUES + (1, 'Top.Naïve.Café'), + (2, 'Top.Science.Astronomy'), + (3, 'Top.Résumé'); +CREATE INDEX ltree_mb_gist ON ltree_mb USING gist (path); +SET enable_seqscan = off; +-- Index search before REINDEX. +SELECT id, path FROM ltree_mb WHERE path ~ 'Top.*'::lquery ORDER BY id; + id | path +----+----------------------- + 1 | Top.Naïve.Café + 2 | Top.Science.Astronomy + 3 | Top.Résumé +(3 rows) + +-- REINDEX (plain, so it runs inside the transaction) must not corrupt the index. +REINDEX INDEX ltree_mb_gist; +-- Same search after REINDEX must return the identical set. +SELECT id, path FROM ltree_mb WHERE path ~ 'Top.*'::lquery ORDER BY id; + id | path +----+----------------------- + 1 | Top.Naïve.Café + 2 | Top.Science.Astronomy + 3 | Top.Résumé +(3 rows) + +-- The '@' label modifier makes the match case-insensitive, which is what +-- invokes the multibyte ltree_strncasecmp path the upstream commits fixed. +SELECT id FROM ltree_mb WHERE path ~ 'top@.*'::lquery ORDER BY id; + id +---- + 1 + 2 + 3 +(3 rows) + +RESET enable_seqscan; +ROLLBACK; diff --git a/nix/tests/expected/merge_repeatable_read.out b/nix/tests/expected/merge_repeatable_read.out new file mode 100644 index 0000000000..12ea671bcf --- /dev/null +++ b/nix/tests/expected/merge_repeatable_read.out @@ -0,0 +1,29 @@ +-- Non-CVE behavior change: MERGE now correctly raises a serialization failure +-- (SQLSTATE 40001) under REPEATABLE READ / SERIALIZABLE when it hits a +-- concurrently-updated tuple (previously this could be silently mishandled). +-- +-- This pins the single-session HAPPY PATH only: MERGE under REPEATABLE READ +-- still produces correct results. The actual concurrent-conflict (40001) case +-- needs two concurrent sessions via the isolation tester, tracked in (PSQL-1277) +-- since pg_isolation_regress is not wired into nix/checks.nix yet. +-- +-- Refs: PSQL-1110, PSQL-1234, PSQL-1277. +BEGIN ISOLATION LEVEL REPEATABLE READ; +CREATE TABLE merge_target (id int PRIMARY KEY, v int); +CREATE TABLE merge_source (id int, v int); +INSERT INTO merge_target VALUES (1, 10), (2, 20); +INSERT INTO merge_source VALUES (1, 100), (3, 300); +MERGE INTO merge_target t +USING merge_source s ON t.id = s.id +WHEN MATCHED THEN UPDATE SET v = s.v +WHEN NOT MATCHED THEN INSERT (id, v) VALUES (s.id, s.v); +-- Expect: id 1 updated to 100, id 2 untouched (20), id 3 inserted (300). +SELECT id, v FROM merge_target ORDER BY id; + id | v +----+----- + 1 | 100 + 2 | 20 + 3 | 300 +(3 rows) + +ROLLBACK; diff --git a/nix/tests/expected/multirange_create_priv.out b/nix/tests/expected/multirange_create_priv.out new file mode 100644 index 0000000000..ecbeba42db --- /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 0000000000..93d1cb3444 --- /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 0000000000..30e49b874f --- /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/pg_trgm.out b/nix/tests/expected/pg_trgm.out new file mode 100644 index 0000000000..0cead1ddd4 --- /dev/null +++ b/nix/tests/expected/pg_trgm.out @@ -0,0 +1,39 @@ +-- CVE-2026-2006: multibyte length validation via bounds-checked pg_mblen() +-- variants. Affects every multibyte text path, including pg_trgm. +-- +-- Upstream commits: fd82ddb6, 50863be0, b2c81ac8, 8f8b1ffa (PG 15.16); +-- 319e8a64, 7a522039, 838248b1, dc072a09 (PG 17.8). +-- +-- Functional regression: trigram generation, similarity, and GIN index search +-- all return correct results on multibyte (UTF-8) input on the fixed builds. +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +-- 1) show_trgm() on a multibyte (UTF-8) string returns well-formed trigrams. +SELECT show_trgm('café'); + show_trgm +------------------------------------- + {0xef5960," c"," ca",0x544980,caf} +(1 row) + +-- 2) similarity() of two multibyte strings is positive and symmetric. +SELECT similarity('café', 'café') AS self_sim, + similarity('café', 'cafe') = similarity('cafe', 'café') AS symmetric; + self_sim | symmetric +----------+----------- + 1 | t +(1 row) + +-- 3) A GIN trigram index on multibyte data returns the correct match set. +CREATE TABLE trgm_mb (id int, t text); +INSERT INTO trgm_mb VALUES (1, 'café'), (2, 'naïve'), (3, 'résumé'); +CREATE INDEX trgm_mb_idx ON trgm_mb USING gin (t gin_trgm_ops); +SET enable_seqscan = off; +SELECT id, t FROM trgm_mb WHERE t % 'café' ORDER BY id; + id | t +----+------ + 1 | café +(1 row) + +RESET enable_seqscan; +ROLLBACK; diff --git a/nix/tests/expected/pgcrypto.out b/nix/tests/expected/pgcrypto.out new file mode 100644 index 0000000000..40dce0f4ce --- /dev/null +++ b/nix/tests/expected/pgcrypto.out @@ -0,0 +1,37 @@ +-- CVE-2026-2005: heap buffer overflow in pgcrypto's pgp_*_decrypt_bytea() on an +-- oversized PGP session-key length. The fix hardens the PGP packet-length parser +-- shared by the symmetric and public-key bytea decrypt paths. +-- +-- Upstream commits: 9a9982ec (PG 15.16), 7a7d9693 (PG 17.8). +-- pgcrypto is default-enabled on Supabase, so this path is customer-reachable. +-- +-- This is a functional + crash-safety regression: a valid round-trip still works, +-- and a malformed PGP packet raises a clean SQL error instead of crashing the +-- backend. (The public-key variant needs externally-generated GPG keys, so we +-- exercise the shared packet parser via the symmetric bytea path.) +-- +-- Refs: PSQL-1110, PSQL-1234. +BEGIN; +-- 1) Happy path: symmetric PGP bytea round-trip returns the original plaintext. +SELECT pgp_sym_decrypt_bytea( + pgp_sym_encrypt_bytea('\xdeadbeef'::bytea, 'test-key'), + 'test-key') = '\xdeadbeef'::bytea AS roundtrip_ok; + roundtrip_ok +-------------- + t +(1 row) + +-- 2) A malformed PGP packet must raise a clean error, not crash the backend +-- (exercises the hardened packet-length parser). +SAVEPOINT malformed; +SELECT pgp_sym_decrypt_bytea('\xdeadbeefcafebabe'::bytea, 'test-key'); +ERROR: Wrong key or corrupt data +ROLLBACK TO SAVEPOINT malformed; +-- 3) Backend is still alive and pgcrypto still works after the error. +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/pgcrypto_cipher_matrix.out b/nix/tests/expected/pgcrypto_cipher_matrix.out new file mode 100644 index 0000000000..45a0628f90 --- /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'); +ERROR: encrypt error: Cipher cannot be initialized +ROLLBACK TO SAVEPOINT s_bf; +SAVEPOINT s_blowfish; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=blowfish'); +ERROR: encrypt error: Cipher cannot be initialized +ROLLBACK TO SAVEPOINT s_blowfish; +SAVEPOINT s_cast5; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=cast5'); +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 0000000000..72b84c19ba --- /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/sql/btree_gist_nan.sql b/nix/tests/sql/btree_gist_nan.sql new file mode 100644 index 0000000000..9f17787299 --- /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; + +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); + +-- Force index scans so we exercise the opclass, not a seqscan recheck. +SET enable_seqscan = off; + +-- 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 0000000000..0db0775b16 --- /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 0000000000..35da4f02cb --- /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 0000000000..89f35027d2 --- /dev/null +++ b/nix/tests/sql/intarray_ltree_query.sql @@ -0,0 +1,28 @@ +-- CVE-2026-6473: memory-allocation overflow umbrella covering, among others, +-- contrib intarray query_int and contrib ltree ltxtquery / lquery parsing. +-- +-- Upstream key commits: 84a9f264 (intarray/ltree), 9c2fa5b6 (ltree lquery) on +-- PG 15.18; c4d04cc4 and siblings on PG 17.10. Full list: +-- git log REL_17_6..REL_17_10 --grep='CVE-2026-6473' +-- +-- Functional regression: well-formed queries parse and match correctly; a +-- malformed query raises a clean parse error instead of crashing. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +-- 1) intarray query_int matching. +SELECT '{1,2,3}'::int[] @@ '2&4'::query_int AS q_and; -- expect false +SELECT '{1,2,3}'::int[] @@ '2|4'::query_int AS q_or; -- expect true + +-- 2) ltree lquery and ltxtquery matching. +SELECT 'Top.Science.Astronomy'::ltree ~ 'Top.*.Astronomy'::lquery AS lquery_match; -- true +SELECT 'Top.Science.Astronomy'::ltree @ 'Astronomy & Top'::ltxtquery AS ltxtquery_match; -- true + +-- 3) A malformed query_int must raise a clean parse error, not crash. +SAVEPOINT bad_query; +SELECT '{1}'::int[] @@ '2&&'::query_int; +ROLLBACK TO SAVEPOINT bad_query; + +ROLLBACK; diff --git a/nix/tests/sql/ltree_label_overflow.sql b/nix/tests/sql/ltree_label_overflow.sql new file mode 100644 index 0000000000..c631857446 --- /dev/null +++ b/nix/tests/sql/ltree_label_overflow.sql @@ -0,0 +1,26 @@ +-- contrib/ltree: an integer overflow in ltree comparisons made values with more +-- than about 14,653 labels compare incorrectly; a btree index over such values +-- could become corrupt and need reindexing. This pins the post-fix comparison +-- correctness for very deep ltree values (no index needed: a btree entry that +-- size cannot exist, so this exercises the comparator directly). +-- +-- Upstream commit: (PG 15.19) / (PG 17.11), fixed 2026-08-13. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE EXTENSION IF NOT EXISTS ltree; + +-- Build two deep ltree values (~15,000 labels) differing only in the last label. +WITH v AS ( + SELECT (repeat('a.', 15000) || 'x')::ltree AS a, + (repeat('a.', 15000) || 'y')::ltree AS b +) +SELECT nlevel(a) > 14653 AS deep_enough, + a < b AS a_lt_b, + NOT (b < a) AS b_not_lt_a, + a = a AS a_eq_a +FROM v; + +ROLLBACK; diff --git a/nix/tests/sql/ltree_reindex.sql b/nix/tests/sql/ltree_reindex.sql new file mode 100644 index 0000000000..47917670a8 --- /dev/null +++ b/nix/tests/sql/ltree_reindex.sql @@ -0,0 +1,41 @@ +-- Non-CVE behavior change (highest customer blast radius this release: ltree is +-- enabled on 2,245 projects). Two upstream commits fix multibyte handling in +-- ltree's case-insensitive label matching, so GiST indexes built under the old +-- logic must be REINDEXed after upgrade on multibyte / ICU databases: +-- +-- 335b2f30 (PG 15.16) + 2b993167 (PG 15.18) "Fix multibyte issues in ltree" +-- b8cfe9dc (PG 17.8) + d1bd9a7d (PG 17.10) +-- +-- This pins WITHIN-version GiST index correctness + REINDEX idempotence. The +-- cross-version pre-upgrade-build / post-upgrade-REINDEX leg is covered by A3 +-- (pg_upgrade migration tests, PSQL-1235). +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +CREATE TABLE ltree_mb (id int, path ltree); +INSERT INTO ltree_mb VALUES + (1, 'Top.Naïve.Café'), + (2, 'Top.Science.Astronomy'), + (3, 'Top.Résumé'); +CREATE INDEX ltree_mb_gist ON ltree_mb USING gist (path); + +SET enable_seqscan = off; + +-- Index search before REINDEX. +SELECT id, path FROM ltree_mb WHERE path ~ 'Top.*'::lquery ORDER BY id; + +-- REINDEX (plain, so it runs inside the transaction) must not corrupt the index. +REINDEX INDEX ltree_mb_gist; + +-- Same search after REINDEX must return the identical set. +SELECT id, path FROM ltree_mb WHERE path ~ 'Top.*'::lquery ORDER BY id; + +-- The '@' label modifier makes the match case-insensitive, which is what +-- invokes the multibyte ltree_strncasecmp path the upstream commits fixed. +SELECT id FROM ltree_mb WHERE path ~ 'top@.*'::lquery ORDER BY id; + +RESET enable_seqscan; + +ROLLBACK; diff --git a/nix/tests/sql/merge_repeatable_read.sql b/nix/tests/sql/merge_repeatable_read.sql new file mode 100644 index 0000000000..18314ad5b2 --- /dev/null +++ b/nix/tests/sql/merge_repeatable_read.sql @@ -0,0 +1,27 @@ +-- Non-CVE behavior change: MERGE now correctly raises a serialization failure +-- (SQLSTATE 40001) under REPEATABLE READ / SERIALIZABLE when it hits a +-- concurrently-updated tuple (previously this could be silently mishandled). +-- +-- This pins the single-session HAPPY PATH only: MERGE under REPEATABLE READ +-- still produces correct results. The actual concurrent-conflict (40001) case +-- needs two concurrent sessions via the isolation tester, tracked in (PSQL-1277) +-- since pg_isolation_regress is not wired into nix/checks.nix yet. +-- +-- Refs: PSQL-1110, PSQL-1234, PSQL-1277. + +BEGIN ISOLATION LEVEL REPEATABLE READ; + +CREATE TABLE merge_target (id int PRIMARY KEY, v int); +CREATE TABLE merge_source (id int, v int); +INSERT INTO merge_target VALUES (1, 10), (2, 20); +INSERT INTO merge_source VALUES (1, 100), (3, 300); + +MERGE INTO merge_target t +USING merge_source s ON t.id = s.id +WHEN MATCHED THEN UPDATE SET v = s.v +WHEN NOT MATCHED THEN INSERT (id, v) VALUES (s.id, s.v); + +-- Expect: id 1 updated to 100, id 2 untouched (20), id 3 inserted (300). +SELECT id, v FROM merge_target ORDER BY id; + +ROLLBACK; diff --git a/nix/tests/sql/multirange_create_priv.sql b/nix/tests/sql/multirange_create_priv.sql new file mode 100644 index 0000000000..ee7b36c5a8 --- /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 0000000000..c1abb33826 --- /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 0000000000..8042b2e413 --- /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/pg_trgm.sql b/nix/tests/sql/pg_trgm.sql new file mode 100644 index 0000000000..4ca484922c --- /dev/null +++ b/nix/tests/sql/pg_trgm.sql @@ -0,0 +1,30 @@ +-- CVE-2026-2006: multibyte length validation via bounds-checked pg_mblen() +-- variants. Affects every multibyte text path, including pg_trgm. +-- +-- Upstream commits: fd82ddb6, 50863be0, b2c81ac8, 8f8b1ffa (PG 15.16); +-- 319e8a64, 7a522039, 838248b1, dc072a09 (PG 17.8). +-- +-- Functional regression: trigram generation, similarity, and GIN index search +-- all return correct results on multibyte (UTF-8) input on the fixed builds. +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +-- 1) show_trgm() on a multibyte (UTF-8) string returns well-formed trigrams. +SELECT show_trgm('café'); + +-- 2) similarity() of two multibyte strings is positive and symmetric. +SELECT similarity('café', 'café') AS self_sim, + similarity('café', 'cafe') = similarity('cafe', 'café') AS symmetric; + +-- 3) A GIN trigram index on multibyte data returns the correct match set. +CREATE TABLE trgm_mb (id int, t text); +INSERT INTO trgm_mb VALUES (1, 'café'), (2, 'naïve'), (3, 'résumé'); +CREATE INDEX trgm_mb_idx ON trgm_mb USING gin (t gin_trgm_ops); + +SET enable_seqscan = off; +SELECT id, t FROM trgm_mb WHERE t % 'café' ORDER BY id; +RESET enable_seqscan; + +ROLLBACK; diff --git a/nix/tests/sql/pgcrypto.sql b/nix/tests/sql/pgcrypto.sql new file mode 100644 index 0000000000..705cec2d6f --- /dev/null +++ b/nix/tests/sql/pgcrypto.sql @@ -0,0 +1,31 @@ +-- CVE-2026-2005: heap buffer overflow in pgcrypto's pgp_*_decrypt_bytea() on an +-- oversized PGP session-key length. The fix hardens the PGP packet-length parser +-- shared by the symmetric and public-key bytea decrypt paths. +-- +-- Upstream commits: 9a9982ec (PG 15.16), 7a7d9693 (PG 17.8). +-- pgcrypto is default-enabled on Supabase, so this path is customer-reachable. +-- +-- This is a functional + crash-safety regression: a valid round-trip still works, +-- and a malformed PGP packet raises a clean SQL error instead of crashing the +-- backend. (The public-key variant needs externally-generated GPG keys, so we +-- exercise the shared packet parser via the symmetric bytea path.) +-- +-- Refs: PSQL-1110, PSQL-1234. + +BEGIN; + +-- 1) Happy path: symmetric PGP bytea round-trip returns the original plaintext. +SELECT pgp_sym_decrypt_bytea( + pgp_sym_encrypt_bytea('\xdeadbeef'::bytea, 'test-key'), + 'test-key') = '\xdeadbeef'::bytea AS roundtrip_ok; + +-- 2) A malformed PGP packet must raise a clean error, not crash the backend +-- (exercises the hardened packet-length parser). +SAVEPOINT malformed; +SELECT pgp_sym_decrypt_bytea('\xdeadbeefcafebabe'::bytea, 'test-key'); +ROLLBACK TO SAVEPOINT malformed; + +-- 3) Backend is still alive and pgcrypto still works after the error. +SELECT pgp_sym_decrypt(pgp_sym_encrypt('still-here', 'k'), 'k') AS post_error_ok; + +ROLLBACK; diff --git a/nix/tests/sql/pgcrypto_cipher_matrix.sql b/nix/tests/sql/pgcrypto_cipher_matrix.sql new file mode 100644 index 0000000000..89714e5bf6 --- /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'); +ROLLBACK TO SAVEPOINT s_bf; + +SAVEPOINT s_blowfish; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=blowfish'); +ROLLBACK TO SAVEPOINT s_blowfish; + +SAVEPOINT s_cast5; +SELECT pgp_sym_encrypt('secret', 'k', 'cipher-algo=cast5'); +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 0000000000..92c5df4a99 --- /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/tools/run-server.sh.in b/nix/tools/run-server.sh.in index 333f062bc7..19b980a67e 100644 --- a/nix/tools/run-server.sh.in +++ b/nix/tools/run-server.sh.in @@ -254,6 +254,8 @@ orioledb_config_items() { sed -i 's/ timescaledb,//g;' "$DATDIR/postgresql.conf" sed -i 's/db_user_namespace = off/#db_user_namespace = off/g;' "$DATDIR/postgresql.conf" sed -i 's/ timescaledb,//g; s/ plv8,//g; s/ pgjwt,//g;' "$DATDIR/supautils.conf" + # output_plugin_libraries does not exist before PG 17.11 (orioledb is on an older base) + sed -i 's/^output_plugin_libraries/#output_plugin_libraries/g;' "$DATDIR/postgresql.conf" sed -i 's/\(shared_preload_libraries.*\)'\''\(.*\)$/\1, orioledb'\''\2/' "$DATDIR/postgresql.conf" echo "default_table_access_method = 'orioledb'" >> "$DATDIR/postgresql.conf" elif [[ "$1" = "orioledb-17" && "$CURRENT_SYSTEM" = "aarch64-darwin" ]]; then @@ -264,6 +266,8 @@ orioledb_config_items() { # Use perl instead of sed for macOS perl -pi -e 's/ timescaledb,//g' "$DATDIR/postgresql.conf" perl -pi -e 's/db_user_namespace = off/#db_user_namespace = off/g' "$DATDIR/postgresql.conf" + # output_plugin_libraries does not exist before PG 17.11 (orioledb is on an older base) + perl -pi -e 's/^output_plugin_libraries/#output_plugin_libraries/' "$DATDIR/postgresql.conf" perl -pi -e 's/ timescaledb,//g' "$DATDIR/supautils.conf" perl -pi -e 's/ plv8,//g' "$DATDIR/supautils.conf"