diff --git a/nix/checks.nix b/nix/checks.nix index ca5fb99ec..06a9375ef 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -183,6 +183,21 @@ # Tests to skip for OrioleDB (not compatible with OrioleDB storage) orioledbSkipTests = [ "index_advisor" # index_advisor doesn't support OrioleDB tables + # The CVE / behavior-change regression tests below pin fixes that + # first landed in 15.16-15.18 / 17.7-17.10. orioledb-17 is built on + # a PG 17.6 base (config.nix: orioledb version "17_16"), which + # predates these fixes, so the post-fix behavior they assert is not + # present here. They run on psql_15 (15.18) and psql_17 (17.10). + # Refs: PSQL-1110, PSQL-1234. + "operator_breaking_change" # CVE-2026-2004 gate (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) ]; # Helper function to filter SQL files based on version 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..30084cbd7 --- /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_reindex.out b/nix/tests/expected/ltree_reindex.out new file mode 100644 index 000000000..fc4e9162b --- /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 000000000..12ea671bc --- /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 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/pg_trgm.out b/nix/tests/expected/pg_trgm.out new file mode 100644 index 000000000..0cead1ddd --- /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 000000000..40dce0f4c --- /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/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..89f35027d --- /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_reindex.sql b/nix/tests/sql/ltree_reindex.sql new file mode 100644 index 000000000..47917670a --- /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 000000000..18314ad5b --- /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 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/pg_trgm.sql b/nix/tests/sql/pg_trgm.sql new file mode 100644 index 000000000..4ca484922 --- /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 000000000..705cec2d6 --- /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;