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