Back to Blog

What to reindex after PostgreSQL's 52-CVE batch

The front of a wooden library card catalogue with dozens of card drawers: an index somebody has to rebuild by hand

Yesterday PostgreSQL released 18.7, 17.12, 16.16, 15.20 and 14.25 with 52 CVEs fixed in one go. You update the package, restart the service and all 52 are closed: that is true, and it is the easy part. What the restart does not fix are the five index-corruption items riding in the same batch. The new code stops breaking things; what is already broken stays exactly where it is until somebody types REINDEX by hand. And if you take incremental backups, yesterday's chain stopped working as a base. None of that is in the headline, and none of it is done by your package manager.

What the restart does close

There is one figure among the 52 that decides whether this is urgent for you, and it is not the count: it is the vector. 32 of the 52 carry CVSS 8.8 with AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H —we checked three of them one by one: the ts_rank_cd() overflow (CVE-2026-116317), the min/max aggregate one and the plperl one— and that PR:L is what tempts people to close the tab: "it needs an authenticated database user, nobody from outside gets in here". The reading is understandable and it is weak. That low privilege it needs is held, in a software company, by your own application, which has been connected since boot and runs the queries your users write for it. The useful question is not whether anyone has an account on the server, but what reaches the planner with your application's permissions.

And part of the list is not fixed by patching the server, because it is not in the server: six of the 52 are in the command-line tools —pg_restore, pg_dump (two), reindexdb, vacuumdb and pg_basebackup— and those run wherever whoever runs them runs them. The clearest is CVE-2026-116314: pg_restore executes code because it does not validate a non-plain archive, and its vector is PR:N/UI:R, meaning no account is needed, just somebody restoring. It is the same failure shape we saw last week in another database's dumps: the file somebody sends you is in charge of the tool that opens it. If somebody at your company restores customer dumps on their laptop, that laptop is part of this update even though there is no server on it.

All of that part is handled the usual way: the minor release is cumulative, there is no dump or pg_upgrade, you stop the service, swap the binaries and start it again. If your server comes from distribution packages, it is an apt upgrade or a dnf update and a restart. And if you stop there, the server is secure and may at the same time be returning wrong data, which is what comes next.

The five items the restart does not touch

  1. GIN, incomplete cleanup in VACUUM. When a VACUUM needs several index scans because it cannot hold everything in memory, it could fail to remove some entries; there is also a race with leaf page splits with the same effect. The release note puts it plainly: "These bugs would eventually lead to a corrupt index with entries pointing at unrelated table rows". Remedy: REINDEX if you suspect it happened.
  2. GIN, page deletion after an incomplete split. The deletion step could disconnect sibling pages, "causing subsequent index searches to miss rows". Same remedy.
  3. pg_trgm GiST. Combining index entries could lose the flag identifying their format. Verbatim: "This fix does not repair existing indexes. Users should REINDEX existing gist_trgm_ops indexes after updating".
  4. B-tree on bpchar. De-duplication of entries is disabled because that type's notion of equality ignores trailing spaces, which let an index-only scan return subtly wrong results. "This patch disables de-duplication for new index entries, but does nothing about possible corruption of existing entries". With its nuance, which gets the next section.
  5. TOAST-table indexes. A REINDEX CONCURRENTLY changed pg_index fields the relation cache was not expecting, and index updates were missed. The symptom has a name you have probably seen in a ticket: "missing chunk number" errors. The good news is in the note itself: the TOAST table is fine, it is only the index, and a REINDEX recovers it.

Look at the pattern, because it is the one thing really worth taking away: three of the five produce the same symptom, and the symptom is not an error. A corrupt index pointing at unrelated rows, or skipping the right ones, does not crash and does not write anything to the log. It answers. And it answers fast, which is what stops anyone from looking. The query returns four invoices instead of five, the monthly report comes out with a total that is slightly off, and the one who ends up investigating is accounting, not IT. A server that is down shows up in thirty seconds; an index that lies can sit there for months without anyone connecting it to a patch batch.

The bpchar nuance worth reading twice

Any business schema is full of CHAR(2) for country codes, CHAR(4) for fiscal years and CHAR(24) for IBANs. Reading "B-tree indexes on bpchar columns must be rebuilt", the natural reaction is to hold your head and book a whole night of reindexing. You do not need to. The release note takes the trouble to clarify it: "this problem only applies to columns declared as bpchar, that is, lacking any length constraint. The more common usage of CHAR(n) is not at risk", because the length constraint forces otherwise-equal strings to be the same size and therefore bitwise equal.

The affected one is the type with no length: a column declared plain character or bpchar. Almost nobody writes that on purpose; it shows up through the back door of a CREATE TABLE AS, a cast with ::char or a table somebody generated from a tool. Knowing that they exist is not much use if you do not know which ones you have, and the type name will not tell you, because CHAR(24) is also bpchar in the catalogue. What separates them is atttypmod, which is why the third query below looks at that field and not at the name: it is the difference between rebuilding two indexes and rebuilding two hundred.

Order matters: binary first, REINDEX after

The two notes that matter carry the sequence written inside them and it is easy to skim past: "after updating" in the pg_trgm one, "after installing this update" in the bpchar one. That is not a politeness formula. Rebuilding an index with the old binaries means rebuilding it with the code that breaks it: you spend the lock window, the disk and the night, and end up with a brand-new index that can go corrupt again. Package first, then restart, then REINDEX, and the backup last. In that order.

The four census queries

None of the four modifies anything; you can run them right now against production and against every database on every server. What they produce is not the list of corrupt indexes —that is not visible from the catalogue— but the list of those that fall under the conditions in the release note, which is the decision to make today; and, with the size next to it, what it is going to cost.

-- 1) GIN indexes (jsonb, full text search, arrays) and their size
SELECT n.nspname, c.relname, pg_size_pretty(pg_relation_size(c.oid))
  FROM pg_class c JOIN pg_am a ON a.oid = c.relam
  JOIN pg_namespace n ON n.oid = c.relnamespace
 WHERE a.amname = 'gin' ORDER BY pg_relation_size(c.oid) DESC;

-- 2) GiST indexes using the gist_trgm_ops operator class
SELECT DISTINCT n.nspname, c.relname
  FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
  JOIN pg_namespace n ON n.oid = c.relnamespace
  JOIN pg_am a ON a.oid = c.relam
  JOIN pg_opclass o ON o.oid = ANY (i.indclass::oid[])
 WHERE a.amname = 'gist' AND o.opcname = 'gist_trgm_ops';

-- 3) bpchar columns with NO length; char(n) is absent here, and that is correct
SELECT n.nspname, c.relname, att.attname
  FROM pg_attribute att JOIN pg_class c ON c.oid = att.attrelid
  JOIN pg_namespace n ON n.oid = c.relnamespace
 WHERE att.atttypid = 'bpchar'::regtype AND att.atttypmod = -1
   AND att.attnum > 0 AND NOT att.attisdropped
   AND c.relkind IN ('r', 'p', 'm');

-- 4) invalid indexes: leftovers from a REINDEX or CREATE INDEX CONCURRENTLY
SELECT n.nspname, c.relname FROM pg_index i
  JOIN pg_class c ON c.oid = i.indexrelid
  JOIN pg_namespace n ON n.oid = c.relnamespace
 WHERE NOT i.indisvalid;

The third returns columns, not indexes: with that list in hand you check with \d which tables have a B-tree on those columns, usually zero or one. The fourth is not about this batch and comes for free: if it returns rows, you have invalid indexes from a REINDEX CONCURRENTLY or a CREATE INDEX CONCURRENTLY that failed halfway —the latter being the more common cause— taking up disk and doing nothing. Make the most of the trip.

There is a fifth check that is not about indexes and that the announcement itself lists among the manual steps: NOT NULL NO INHERIT constraints on partitioned tables, which this release now rejects. Plain CREATE TABLE already rejected them, but paths such as CREATE TABLE LIKE did not check copied constraints, "potentially allowing a partition to contain null values when it should not". That is: you may have nulls where you declared there would be none. The announcement gives the query: SELECT conrelid::regclass, conname FROM pg_constraint WHERE contype = 'n' AND connoinherit AND conrelid IN (SELECT oid FROM pg_class WHERE relkind = 'p'). If it returns anything, you drop the constraint or fix it with ALTER TABLE ... ALTER CONSTRAINT ... INHERIT. If not, one more empty result for the record.

Plain REINDEX or CONCURRENTLY

The classic REINDEX INDEX blocks writes on the table while it runs. It is fast and it is honest: you know when it starts, when it ends and who it bothers. REINDEX INDEX CONCURRENTLY does not block writes, but it works in several phases, needs room to keep the new and the old index at once, and if it is interrupted it leaves an invalid index —the one from query 4— that has to be dropped by hand. On a hundred-megabyte table, the first. On the two-hundred-gigabyte GIN index of the documents table, the second, with the disk checked beforehand.

And here is the irony of the batch, which also reinforces the order above: two of the 52 CVEs are in the tool you are going to fix things with. CVE-2026-116336 is that REINDEX CONCURRENTLY did not block a concurrent DROP INDEX, and CVE-2026-116325 (CVSS 8.0) is an SQL injection through index-name quoting in reindexdb, which is exactly the shortcut used by whoever reindexes a whole database. On top of that, outside the CVE list, sits the TOAST index item caused by REINDEX CONCURRENTLY itself. Running the reindex before updating is not just wasted work: it is doing it with the version of the tool that still has all three problems.

With replicas you have to draw a distinction, and the distinction changes the amount of work. If your replica is physical —streaming replication, the usual case— the REINDEX travels over WAL like any other write and the standby rebuilds itself on replay: you plan the window once. If your replica is logical, it does not: the subscriber's indexes are its own, they do not travel in the replication stream and the subscriber has to be reindexed separately, with its own window. It is the kind of detail that decides whether the maintenance night is two hours or four, and it is worth checking before you promise an hour of downtime. While you are in the replication roles, review who holds the REPLICATION attribute, because it was never a read-only permission.

The incremental backup that stopped being a chain

There is a sixth item, this one outside the five about indexes, and it is the one that costs most if it slips by. An incremental backup taken after a VACUUM truncates a table could restore stale visibility map bits and cause data corruption. The sentence that matters is the remedy: "This fix does not correct existing WAL summaries, therefore incremental backups taken after updating can still be affected. It's recommended that users of incremental backups take a new full backup after updating and use it as the basis for subsequent incremental backups". That is: you update, and your incrementals stay contaminated until you take a new full backup to serve as the base.

Now the nuance, which also cuts the work down: the bug only occurs with data checksums and wal_log_hints both disabled, so having either one on is enough to be out of scope. Since 18, initdb turns checksums on by default —the documentation says it in so many words, "This is enabled by default"— but that default only applies to clusters created with 18: one that reached 18 via pg_upgrade from 14, 15, 16 or 17 carries its original setting, and so do those created by hand with --no-data-checksums. Add that pg_basebackup incremental backups only exist from 17 onwards and you get the exact profile this is urgent for: any 17 or 18 cluster with data_checksums off, without wal_log_hints, using --incremental. It takes one line to check, SHOW data_checksums;, and if that is you, the new full backup is not a textbook recommendation: it is the difference between having backups and believing you do. The only way to find out which of the two it is, besides, is to restore one; that is the work worth planning together with the infrastructure that stores them.

And 14 is left without a next one

On 1 November we wrote that PostgreSQL 14 was going end-of-life on 12 November and that nothing was going to tell your server. It happened with uncomfortable punctuality: 14.25 is its final release, and it came out on the very day the project shipped the largest batch of patches we have seen from it. The arithmetic is simple and it is what you take to Monday's meeting: today your 14.25 has all 52 fixes and is as up to date as an 18.7; the day CVE number 53 shows up, it will not have it, and there will be nothing to install. No notice, no package, no line in the log.

There is a practical detail that ties both together: 19 ships on 19 November, six days from now, and its migration notes ask for reindexing again when you move across with pg_upgrade. The two cases are worth separating, because the small print differs: the full text search and pg_trgm one is conditional on databases with a locale provider other than libc whose behaviour differs from the configured LC_CTYPE, and there you also have to recompute stored tsvector values; the fuzzystrmatch one carries no such condition and is recommended on any pg_upgrade, because those functions now handle non-ASCII characters differently. The upshot: if you are on an old version and had the major upgrade pencilled in for the first quarter, this week's reindex and the upgrade's reindex overlap substantially. That conversation is cheaper to have today, with the index list in front of you, than in January.

When there is nothing to do tonight

If the four queries come back empty —no GIN, no gist_trgm_ops, no length-less bpchar column, no invalid index—, the partitioned-constraint one returns nothing either and you do not use pg_basebackup incremental backups, this batch is an apt upgrade, a restart and off to bed. Saying so costs us billable work and it is the truth: REINDEX is not free, and firing it off "just in case" on a two-hundred-gigabyte GIN index at eleven on a Tuesday morning does more damage than the bug you are running from.

Two warnings so that "nothing to do" is honest, because the queries have a blind spot. First: TOAST index corruption is invisible to all four. Nothing in the catalogue gives it away; it shows up the day a read returns missing chunk number, and by then you know which table to reindex. If you have run REINDEX CONCURRENTLY on large tables with text or bytea fields, keep it in the on-call playbook rather than on tonight's list. Second: these queries speak about one database, and servers hold several. They have to be run against all of them, including the ones nobody looks at.

The opposite case is the one we meet most often at software companies: nobody knows off the top of their head how many PostgreSQL servers are running or what version each one is on, because half of them arrived inside an application that brought them along. There the problem is not yesterday's batch, which has merely made it visible. The part we do in data and applications starts with that inventory and continues with the window: when it can be rebuilt without stopping anyone, and with how much free disk.

Sources (verified 13 Nov 2026, 13:30 CET). Official announcement "PostgreSQL 18.7, 17.12, 16.16, 15.20, 14.25, and 19 RC 2 Released!" (12 Nov 2026): the five versions, the 52 CVEs, the cumulative-release condition with no dump or pg_upgrade, 14 being final, and the planned date for 19 (19 Nov 2026). 18.7 release notes: the verbatim quotes for both GIN items, the pg_trgm GiST item, the bpchar item (including the paragraph that explicitly excludes CHAR(n)), the TOAST-table indexes with the "missing chunk number" error, and the stale visibility map in incremental backups. PostgreSQL security page: CVSS vectors read one by one on the records for CVE-2026-116317, CVE-2026-116331 and CVE-2026-116307 (all three 8.8 with AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) and for CVE-2026-116314 (8.8 with AV:N/AC:L/PR:N/UI:R), plus each one's affected versions. CVE-2026-116325 in reindexdb (8.0), CVE-2026-116313 in vacuumdb, CVE-2026-16240 in pg_basebackup and the two in pg_dump (116318 and 116330) come from the announcement's list; the query for NOT NULL NO INHERIT constraints on partitioned tables and the two sentences quoted about them are verbatim from the announcement and the 18.7 notes. Versioning policy: 12 Nov 2026 as 14's final date. Documentation for initdb ("This is enabled by default" for checksums) and pg_basebackup ("incremental backup only works with server version 17 and later"); migration notes for version 19 on reindexing full text search, pg_trgm and fuzzystrmatch. The four census queries, the order of operations and the reading of what fixes itself and what does not are ours. Honest note: none of the queries tells you whether an index is corrupt —that is not visible from the catalogue—; they tell you which indexes fall under the release note's conditions, which is the decision to make today.

Do you know how many PostgreSQL servers you have running?

We run the census against every database on every server, place the rebuild window where it does not hurt, and check that the backup really restores. With no licences to sell.

Talk to everyWAN

Tags:

Share:

Subscribe to our newsletter

To receive IT stories, everyWAN news and exclusive subscriber offers, sign up to our mailing list

Minorisa de Sistemas Informaticos y Gestión S.L. © 2026
everyWAN
everyWAN