Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions nix/checks.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions nix/tests/expected/create_statistics_priv.out
Original file line number Diff line number Diff line change
@@ -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;
47 changes: 47 additions & 0 deletions nix/tests/expected/hstore_copy_binary.out
Original file line number Diff line number Diff line change
@@ -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;
46 changes: 46 additions & 0 deletions nix/tests/expected/intarray_ltree_query.out
Original file line number Diff line number Diff line change
@@ -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;
53 changes: 53 additions & 0 deletions nix/tests/expected/ltree_reindex.out
Original file line number Diff line number Diff line change
@@ -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;
29 changes: 29 additions & 0 deletions nix/tests/expected/merge_repeatable_read.out
Original file line number Diff line number Diff line change
@@ -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;
31 changes: 31 additions & 0 deletions nix/tests/expected/multirange_create_priv.out
Original file line number Diff line number Diff line change
@@ -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;
50 changes: 50 additions & 0 deletions nix/tests/expected/operator_breaking_change.out
Original file line number Diff line number Diff line change
@@ -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;
39 changes: 39 additions & 0 deletions nix/tests/expected/pg_trgm.out
Original file line number Diff line number Diff line change
@@ -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;
37 changes: 37 additions & 0 deletions nix/tests/expected/pgcrypto.out
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading