PostgreSQL 15.19 / 17.11 minor release — action may be required for ltree, pgcrypto, btree_gist, and custom operators

Sep 25, 2026

What changed#

We are rolling out the PostgreSQL 15.19 / 17.11 minor release (from 15.14 / 17.6). It bundles five upstream security cycles and closes 44 CVEs. Four changes may require action, depending on how you use your database:

  1. ltree indexes may need reindexing — a case-folding fix on multibyte/ICU databases, plus an integer-overflow fix for very deep ltree values (any encoding).
  2. pgcrypto stops decrypting legacy-cipher PGP data (CVE-2026-14663) — data encrypted with cipher-algo=bf/blowfish/cast5 was effectively stored unencrypted and fails to decrypt by default after the upgrade.
  3. btree_gist indexes on float columns that may contain NaN need reindexing.
  4. Custom operators with non-built-in selectivity estimators (CVE-2026-2004) — recreating them (dump/restore, branching) now requires superuser.

If none of the detection queries below return rows for your project, no action is needed.

Why we made this change#

This is an upstream PostgreSQL minor release. Staying current closes 44 CVEs (including several rated High) and picks up correctness fixes in the ltree and btree_gist extensions.

Who is affected#

1. ltree indexes#

Case-folding (multibyte/ICU)#

After upgrading, indexes on ltree columns built under the previous version can silently return incomplete results on databases using a multibyte encoding (such as UTF-8) or a non-libc collation provider (ICU or builtin). Check whether your database is affected:


_10
select
_10
pg_encoding_to_char(encoding) as encoding,
_10
pg_encoding_max_length(encoding) as max_bytes_per_char, -- 1 = single-byte, >1 = multibyte
_10
datlocprovider as collation_provider, -- 'c' libc, 'i' icu, 'b' builtin
_10
(pg_encoding_max_length(encoding) > 1 or datlocprovider != 'c') as reindex_required
_10
from pg_database
_10
where datname = current_database();

If reindex_required is true, find the affected indexes:


_14
select distinct
_14
n.nspname as schema_name,
_14
cls.relname as table_name,
_14
ic.relname as index_name
_14
from pg_index idx
_14
join pg_class ic on idx.indexrelid = ic.oid
_14
join pg_class cls on idx.indrelid = cls.oid
_14
join pg_namespace n on ic.relnamespace = n.oid
_14
join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true
_14
join pg_opclass oc on oc.oid = k.opclass
_14
join pg_type ty on ty.oid = oc.opcintype
_14
where
_14
k.pos <= idx.indnkeyatts -- key columns only, excludes INCLUDE
_14
and ty.typname in ('ltree', '_ltree');

Integer overflow (any encoding)#

This release also fixes an integer overflow in ltree comparisons: values with more than about 14,653 labels could compare incorrectly, which can corrupt B-tree indexes built over them, regardless of your database encoding. This query lists only the B-tree indexes that actually contain such values (an empty result means no action is needed):


_35
select s.schema_name || '.' || s.index_name as index_to_reindex
_35
from (
_35
select
_35
n.nspname as schema_name,
_35
c.relname as table_name,
_35
ic.relname as index_name,
_35
min(pg_get_expr(i.indpred, i.indrelid)) as pred, -- partial-index predicate, if any
_35
string_agg('nlevel(' || pg_get_indexdef(i.indexrelid, k.pos::int, true) || ') > 14653', ' or ') as keys_cond
_35
from pg_index i
_35
join pg_class ic on ic.oid = i.indexrelid
_35
join pg_class c on c.oid = i.indrelid
_35
join pg_namespace n on n.oid = c.relnamespace
_35
join pg_am am on am.oid = ic.relam
_35
join lateral generate_series(1, i.indnkeyatts) as k(pos) on true
_35
join pg_attribute ia on ia.attrelid = i.indexrelid and ia.attnum = k.pos
_35
join pg_type t on t.oid = ia.atttypid
_35
where am.amname = 'btree'
_35
and t.typname = 'ltree'
_35
and n.nspname not in ('pg_catalog', 'information_schema')
_35
group by n.nspname, c.relname, ic.relname
_35
) s
_35
where (xpath(
_35
'/row/cnt/text()',
_35
query_to_xml(
_35
format('select count(*) as cnt from %I.%I where %s(%s)',
_35
s.schema_name,
_35
s.table_name,
_35
case when s.pred is not null then '(' || s.pred || ') and ' else '' end,
_35
s.keys_cond),
_35
false,
_35
true,
_35
''
_35
)
_35
))[1]::text::bigint > 0
_35
order by 1;

2. pgcrypto legacy ciphers (CVE-2026-14663)#

If you call pgp_sym_encrypt/pgp_pub_encrypt (or their _bytea variants) with cipher-algo=bf, cipher-algo=blowfish, or cipher-algo=cast5, that data was effectively stored unencrypted — decryption succeeds even with the wrong key. The default cipher (AES) and 3des are not affected. If you never passed a cipher-algo option, no action is needed.

To find affected rows, scan each stored value with a deliberately wrong passphrase: properly encrypted values raise an error, while affected values decrypt successfully even with the wrong key. Run the helper and the scan in the same session (pg_temp functions are session-scoped):


_14
create function pg_temp.affected_by_cve_2026_14663(msg bytea)
_14
returns boolean
_14
language plpgsql as $$
_14
begin
_14
perform pgp_sym_decrypt_bytea(msg, 'deliberately-wrong-key');
_14
return true;
_14
exception when others then
_14
return false;
_14
end $$;
_14
_14
select <id_column>
_14
from <your_table>
_14
where <your_encrypted_column> is not null
_14
and pg_temp.affected_by_cve_2026_14663(<your_encrypted_column>);

Any rows returned hold affected values. The scan covers symmetric (pgp_sym_*) messages. The wrong-key probe does not apply to public-key (pgp_pub_*) messages. If pgp_pub_encrypt was used with an affected cipher-algo, treat those values as affected and re-encrypt them the same way using pgp_pub_decrypt and pgp_pub_encrypt with the key pair.

3. btree_gist indexes on float columns#

Indexes built under the previous version can return wrong results for rows containing NaN until reindexed. Find btree_gist indexes on float columns:


_16
select distinct
_16
n.nspname as schema_name,
_16
cls.relname as table_name,
_16
ic.relname as index_name
_16
from pg_index idx
_16
join pg_class ic on idx.indexrelid = ic.oid
_16
join pg_am am on ic.relam = am.oid
_16
join pg_class cls on idx.indrelid = cls.oid
_16
join pg_namespace n on ic.relnamespace = n.oid
_16
join lateral unnest(idx.indclass::oid[]) with ordinality as k(opclass, pos) on true
_16
join pg_opclass oc on oc.oid = k.opclass
_16
join pg_type ty on ty.oid = oc.opcintype
_16
where
_16
am.amname = 'gist'
_16
and k.pos <= idx.indnkeyatts
_16
and ty.typname in ('float4', 'float8');

You are affected only if indexes are returned and those columns may contain NaN values.

4. Custom operators (CVE-2026-2004)#

Only superusers may now attach a non-built-in selectivity estimator to an operator. Existing operators keep working. Only re-creation (dump/restore, branching, major-version upgrade) fails.

Operators installed by extensions (PostGIS, intarray, etc.) are not affected. Check for affected operators:


_13
select
_13
n.nspname as schema, o.oprname as operator,
_13
o.oprrest::regproc as restrict_estimator,
_13
o.oprjoin::regproc as join_estimator
_13
from pg_operator o
_13
join pg_namespace n on o.oprnamespace = n.oid
_13
where n.nspname not in ('pg_catalog', 'information_schema')
_13
and ((o.oprrest <> 0 and o.oprrest::oid >= 10000)
_13
or (o.oprjoin <> 0 and o.oprjoin::oid >= 10000))
_13
and not exists (
_13
select 1 from pg_depend d
_13
where d.classid = 'pg_operator'::regclass and d.objid = o.oid and d.deptype = 'e'
_13
);

What happens if you take no action#

  • ltree / btree_gist: affected index searches may silently return wrong or incomplete results - no error is raised.
  • pgcrypto: decryption of affected messages fails by default after the upgrade (recoverable with the ignore-cipher-failure=1 decrypt option). The underlying data remains effectively unencrypted at rest until re-encrypted.
  • Custom operators: a future dump/restore, branch, or major-version upgrade fails to recreate the operator.

Migration steps#

  1. Run the detection queries above for each database.
  2. After upgrading, reindex each affected ltree/btree_gist index using its schema-qualified name — REINDEX INDEX CONCURRENTLY <schema_name>.<index_name>; runs online with no downtime, but cannot run inside a transaction block.
  3. For pgcrypto: re-encrypt affected values with a modern cipher (for example cipher-algo=aes256), before the upgrade or after it using ignore-cipher-failure=1, and consider rotating secrets stored this way.
  4. For custom operators: recreate without the RESTRICT/JOIN clause (or with a built-in estimator), or contact support and we'll assist.

Rollout timeline#

DateMilestoneUser action
2026-09-25Release announce (this entry)Run detection queries
2026-09-28New projects created on 15.19 / 17.11—
2026-09-28Upgrade available in dashboard for existing projectsUpgrade, then reindex if affected

Build in a weekend, scale to millions