Tornar al Blog

Què cal reindexar després de la tanda de 52 CVE de PostgreSQL

Front d'un fitxer de biblioteca de fusta amb desenes de calaixos de fitxes: un índex que algú ha de reconstruir a mà

Ahir PostgreSQL va publicar 18.7, 17.12, 16.16, 15.20 i 14.25 amb 52 CVE corregits d'una tongada. Actualitzes el paquet, reinicies el servei i els 52 queden tancats: això és cert i és la part fàcil. El que no s'arregla amb el reinici són els cinc apunts de corrupció d'índex que viatgen a la mateixa tanda. El codi nou deixa d'espatllar; el que ja està espatllat continua on és fins que algú escrigui REINDEX a mà. I si fas còpies incrementals, la cadena d'ahir ha deixat de servir de base. Res d'això surt al titular, i res d'això ho fa el teu gestor de paquets.

El que sí que tanca el reinici

Hi ha una dada dels 52 que decideix si això et corre pressa o no, i no és el número: és el vector. 32 dels 52 porten CVSS 8,8 amb AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H —ho hem comprovat un a un en tres d'ells, el desbordament de ts_rank_cd() (CVE-2026-116317), el dels agregats de mínim i màxim i el de plperl—, i aquella PR:L és la que convida a tancar la pestanya: «cal usuari autenticat a la base, aquí no hi entra ningú de fora». La lectura és comprensible i és fluixa. Aquell privilegi baix que cal el té, en una empresa de programari, la teva pròpia aplicació, que fa estona que està connectada i executa les consultes que li escriuen els teus usuaris. La pregunta útil no és si algú té compte al servidor, sinó què arriba fins al planificador amb els permisos de la teva aplicació.

I hi ha una part de la llista que no s'arregla pedaçant el servidor perquè no és al servidor: sis dels 52 són de les eines de línia d'ordres —pg_restore, pg_dump (dos), reindexdb, vacuumdb i pg_basebackup—, i aquelles corren on les llanci qui les llanci. El més clar és CVE-2026-116314: pg_restore executa codi perquè no valida un arxiu que no sigui de text pla, i el seu vector és PR:N/UI:R, és a dir que no cal compte, cal que algú restauri. És la mateixa forma d'error que vam veure la setmana passada als bolcats d'una altra base de dades: el fitxer que t'envien mana sobre l'eina que l'obre. Si a la teva empresa algú restaura bolcats de clients al seu portàtil, aquell portàtil entra en aquesta actualització encara que allà no hi hagi cap servidor.

Tota aquesta part es resol com sempre: la versió menor és acumulativa, no hi ha bolcat ni pg_upgrade, s'atura el servei, es canvien els binaris i s'arrenca. Si el teu servidor està empaquetat per la distribució, és un apt upgrade o un dnf update i un reinici. I si acabes aquí, el servidor està segur i alhora pot estar tornant dades malament, que és el que ve ara.

Els cinc apunts que el reinici no toca

  1. GIN, neteja incompleta al VACUUM. Quan un VACUUM necessita diverses passades sobre l'índex perquè no li cap tot a memòria, podia deixar-se entrades sense esborrar; hi ha a més una cursa amb la divisió de pàgines fulla amb el mateix efecte. La nota de versió ho diu sense embuts: «These bugs would eventually lead to a corrupt index with entries pointing at unrelated table rows». Remei: REINDEX si sospites que t'ha passat.
  2. GIN, esborrat de pàgina després d'una divisió incompleta. El pas d'esborrat podia desconnectar pàgines germanes, «causing subsequent index searches to miss rows». Mateix remei.
  3. GiST de pg_trgm. La combinació d'entrades podia perdre la marca que identifica el seu format. Literal: «This fix does not repair existing indexes. Users should REINDEX existing gist_trgm_ops indexes after updating».
  4. B-tree sobre bpchar. Es desactiva la deduplicació d'entrades perquè la igualtat d'aquell tipus ignora els espais finals i permetia que un index-only scan tornés resultats subtilment equivocats. «This patch disables de-duplication for new index entries, but does nothing about possible corruption of existing entries». Amb el seu matís, que va a la secció següent.
  5. Índexs de taules TOAST. Un REINDEX CONCURRENTLY canviava camps de pg_index que la memòria cau de relacions no esperava, i es perdien actualitzacions de l'índex. El símptoma té nom i segur que l'has vist en un tiquet: errors de «missing chunk number». La bona notícia va a la mateixa nota: la taula TOAST està bé, és només l'índex, i un REINDEX el recupera.

Fixa't en el patró, perquè és l'única cosa que de debò cal endur-se d'aquí: tres dels cinc produeixen el mateix símptoma, i el símptoma no és un error. Un índex corrupte que apunta a files que no són, o que es salta les que són, no peta ni escriu res al registre. Respon. I respon ràpid, que és el que fa que ningú no ho miri. La consulta torna quatre factures en lloc de cinc, l'informe del mes surt amb un total que no quadra per poc, i qui acaba investigant és comptabilitat, no sistemes. Un servidor caigut es nota en trenta segons; un índex que menteix pot estar mesos sense que ningú el relacioni amb una tanda de pedaços.

El matís del bpchar que convé llegir dues vegades

Qualsevol esquema de gestió és ple de CHAR(2) per a codis de país, CHAR(4) per a exercicis comptables i CHAR(24) per a IBAN. En llegir «els índexs B-tree sobre columnes bpchar cal reconstruir-los» el lògic és posar-se les mans al cap i preparar una nit sencera de reindexat. No cal. La nota de versió es pren la molèstia d'aclarir-ho: «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», perquè la restricció de longitud obliga que dues cadenes iguals tinguin la mateixa mida i siguin, per tant, idèntiques bit a bit.

L'afectat és el tipus sense longitud: una columna declarada character o bpchar a seques. Gairebé ningú no ho escriu expressament; apareix sol, per la porta del darrere d'un CREATE TABLE AS, d'una conversió amb ::char o d'una taula que algú va generar des d'una eina. Saber que existeixen no serveix de gaire si no saps quines tens, i el nom del tipus no t'ho diu perquè CHAR(24) també és bpchar al catàleg. El que els separa és l'atttypmod, i per això la tercera consulta de sota mira aquell camp i no el nom: és la diferència entre reconstruir dos índexs i reconstruir-ne dos-cents.

L'ordre importa: primer el binari, després el REINDEX

Les dues notes que manen porten la seqüència escrita a dins i és fàcil passar-hi per sobre: «after updating» a la de pg_trgm, «after installing this update» a la de bpchar. No és una fórmula de cortesia. Reconstruir un índex amb els binaris vells és reconstruir-lo amb el codi que l'espatlla: et menges la finestra de bloqueig, el consum de disc i la nit, i acabes amb un índex acabat de fer que pot tornar-se a corrompre. Primer el paquet, després el reinici, després el REINDEX, i al final la còpia. En aquest ordre.

Les quatre consultes del cens

Cap de les quatre no modifica res; es poden llançar ara mateix contra producció i contra cada base de cada servidor. El que treuen no és la llista d'índexs corruptes —això no es veu des del catàleg— sinó la llista dels que entren en els supòsits de la nota de versió, que és la decisió que cal prendre avui; i, amb la mida al costat, quant costarà.

-- 1) índexs GIN (jsonb, cerca de text, arrays) i el que ocupen
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) índexs GiST amb la classe d'operadors gist_trgm_ops
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) columnes bpchar SENSE longitud; char(n) no surt aquí i és el correcte
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) índexs invàlids: restes d'un REINDEX o 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;

La tercera torna columnes, no índexs: amb aquella llista a la mà es mira amb \d quines taules tenen un B-tree damunt d'aquelles columnes, que acostumen a ser zero o una. La quarta no va d'aquesta tanda i surt de franc: si torna files, tens índexs invàlids d'un REINDEX CONCURRENTLY o d'un CREATE INDEX CONCURRENTLY que va fallar a mitges —aquesta última és la causa més habitual—, ocupant disc i sense servir de res. Aprofita el viatge.

Falta una cinquena comprovació que no és d'índexs i que el mateix anunci inclou entre els passos manuals: les restriccions NOT NULL NO INHERIT sobre taules particionades, que aquesta versió passa a rebutjar. CREATE TABLE ja les rebutjava, però camins com CREATE TABLE LIKE no comprovaven les restriccions copiades, «potentially allowing a partition to contain null values when it should not». És a dir: pot ser que tinguis nuls on vas declarar que no n'hi hauria. La consulta la dóna l'anunci: SELECT conrelid::regclass, conname FROM pg_constraint WHERE contype = 'n' AND connoinherit AND conrelid IN (SELECT oid FROM pg_class WHERE relkind = 'p'). Si torna alguna cosa, s'esborra la restricció o s'arregla amb ALTER TABLE ... ALTER CONSTRAINT ... INHERIT. Si no, un resultat buit més per a l'acta.

REINDEX a seques o CONCURRENTLY

El REINDEX INDEX clàssic bloqueja les escriptures de la taula mentre dura. És ràpid i és honest: saps quan comença, quan acaba i a qui molesta. El REINDEX INDEX CONCURRENTLY no bloqueja escriptures, però fa la feina en diverses fases, necessita espai per mantenir l'índex nou i el vell alhora, i si s'interromp deixa un índex invàlid —el de la consulta 4— que cal esborrar a mà. En una taula de cent megues, el primer. A l'índex GIN de dos-cents gigues de la taula de documents, el segon, i amb el disc mirat abans.

I aquí hi ha la ironia de la tanda, que a més reforça l'ordre d'abans: dos dels 52 CVE són a l'eina amb la qual arreglaràs. CVE-2026-116336 és que REINDEX CONCURRENTLY no bloquejava un DROP INDEX simultani, i CVE-2026-116325 (CVSS 8,0) és una injecció SQL pel cometejat de noms d'índex a reindexdb, que és just la drecera que fa servir qui reindexa una base sencera. A això s'hi suma, ja fora de la llista de CVE, l'apunt dels índexs TOAST que causava el mateix REINDEX CONCURRENTLY. Llançar el reindexat abans d'actualitzar no és només feina llençada: és fer-ho amb la versió de l'eina que encara té els tres problemes.

Amb rèpliques cal distingir, i la distinció canvia el compte de la feina. Si la teva rèplica és física —replicació en continu, el cas habitual—, el REINDEX va per WAL com qualsevol altra escriptura i la secundària es reconstrueix sola en reproduir-lo: la finestra la planifiques un cop. Si la teva rèplica és lògica, no: els índexs del subscriptor són seus, no viatgen en el flux de replicació i cal reindexar el subscriptor a part, amb la seva pròpia finestra. És el tipus de detall que decideix si la nit de manteniment són dues hores o quatre, i convé mirar-ho abans de prometre una hora de tall. Ja que entres als rols de replicació, repassa qui té l'atribut REPLICATION, que mai no va ser un permís de només lectura.

La còpia incremental que ha deixat d'encadenar

Hi ha un sisè apunt, aquest ja fora dels cinc d'índexs, i és el que surt més car si es passa per alt. Una còpia incremental feta després que un VACUUM trunqui una taula podia restaurar mapes de visibilitat caducats i provocar corrupció de dades. La frase que importa és la del remei: «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». És a dir: actualitzes, i els teus incrementals continuen contaminats fins que facis un complet nou que serveixi de base.

Ara el matís, que també redueix la feina: l'error només es dóna amb les sumes de verificació de dades i wal_log_hints desactivats, de manera que n'hi ha prou amb tenir-ne activa qualsevol de les dues per quedar fora. Des de la 18, initdb activa les sumes per defecte —la documentació ho diu tal qual, «This is enabled by default»—, però aquell «per defecte» només s'aplica a clústers creats amb la 18: un que hagi arribat a la 18 per pg_upgrade des de la 14, la 15, la 16 o la 17 arrossega la configuració d'origen, i els creats a mà amb --no-data-checksums també. Afegeix-hi que les còpies incrementals de pg_basebackup només existeixen des de la 17 i et queda el perfil exacte a qui això li corre pressa: qualsevol clúster 17 o 18 amb data_checksums desactivat, sense wal_log_hints i fent servir --incremental. Es comprova en una línia, SHOW data_checksums;, i si ets aquell perfil el complet nou no és una recomanació de manual: és la diferència entre tenir còpies i creure que les tens. L'única manera de saber quina de les dues és, a més, restaurar-ne una; aquella és la feina que val la pena planificar juntament amb la infraestructura que les guarda.

I la 14 es queda sense la següent

L'1 de novembre vam escriure que PostgreSQL 14 caducava el 12 de novembre i que al teu servidor no li ho diria ningú. Es va complir amb puntualitat incòmoda: la 14.25 és la seva última versió, i ha sortit el mateix dia en què el projecte publicava la tanda de pedaços més gran que li hem vist. L'aritmètica és fàcil i és la que cal portar a la reunió de dilluns: avui el teu 14.25 té els 52 arranjaments i està tan al dia com un 18.7; el dia que aparegui el CVE número 53, no el tindrà, i no hi haurà res per instal·lar. Ni avís, ni paquet, ni línia al registre.

Hi ha un detall pràctic que uneix les dues coses: la 19 surt el 19 de novembre, d'aquí a sis dies, i les seves notes de migració tornen a demanar reindexar en pujar amb pg_upgrade. Convé separar els dos casos, perquè no tenen la mateixa lletra petita: el de cerca de text i pg_trgm està condicionat a bases amb un proveïdor de locale diferent de libc el comportament del qual difereixi del LC_CTYPE configurat, i allà a més cal recalcular els tsvector emmagatzemats; el de fuzzystrmatch no porta aquella condició i es recomana en qualsevol pg_upgrade, perquè aquelles funcions canvien de comportament amb caràcters no ASCII. Resultat: si ets en una versió vella i tenies la migració major apuntada per al primer trimestre, el reindexat d'aquesta setmana i el de la migració se solapen en bona part. Aquella conversa és més barata tenir-la avui, amb la llista d'índexs al davant, que al gener.

Quan no cal fer res aquesta nit

Si les quatre consultes surten buides —cap GIN, cap gist_trgm_ops, cap columna bpchar sense longitud, cap índex invàlid—, la de les restriccions particionades tampoc no torna res i no fas servir còpies incrementals de pg_basebackup, aquesta tanda és un apt upgrade, un reinici i a dormir. Dir-ho ens treu feina facturable i és la veritat: el REINDEX no és de franc, i llançar-lo «per si de cas» damunt d'un índex GIN de dos-cents gigues un dimarts a les onze del matí fa més mal que l'error del qual fuges.

Dos avisos perquè aquell «no hi ha res a fer» sigui honest, perquè les consultes tenen un punt cec. El primer: la corrupció d'índexs TOAST no la veu cap de les quatre. No hi ha res al catàleg que la delati; apareix el dia que una lectura torna missing chunk number, i aleshores ja saps quina taula reindexar. Si has corregut REINDEX CONCURRENTLY damunt de taules grans amb camps de text o bytea, tingues-ho al guió de guàrdia en lloc de a la llista d'aquesta nit. El segon: aquestes consultes parlen d'una base, i els servidors en tenen diverses. Cal passar-les per totes, també per les que ningú no mira.

El cas contrari és el que ens trobem més sovint a empreses de programari: ningú no sap de memòria quants PostgreSQL hi ha encesos ni en quina versió va cadascun, perquè la meitat van arribar dins d'una aplicació que els va portar de sèrie. Allà el problema no és la tanda d'ahir, que només ho ha fet visible. La part que fem nosaltres a dades i aplicacions comença per aquell inventari i continua per la finestra: quan es pot reconstruir sense aturar ningú i amb quant de disc lliure.

Fonts (verificades el 13-nov-2026, 13:30 CET). Anunci oficial «PostgreSQL 18.7, 17.12, 16.16, 15.20, 14.25, and 19 RC 2 Released!» (12-nov-2026): les cinc versions, els 52 CVE, la condició de versió acumulativa sense bolcat ni pg_upgrade, el caràcter d'última versió de la branca 14 i la data prevista de la 19 (19-nov-2026). Notes de la versió 18.7: les cites literals dels dos apunts GIN, del GiST de pg_trgm, del bpchar (inclòs el paràgraf que exclou expressament CHAR(n)), dels índexs de taules TOAST amb l'error «missing chunk number» i del mapa de visibilitat caducat en còpies incrementals. Pàgina de seguretat de PostgreSQL: vectors CVSS llegits un a un a les fitxes de CVE-2026-116317, CVE-2026-116331 i CVE-2026-116307 (els tres 8,8 amb AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) i de CVE-2026-116314 (8,8 amb AV:N/AC:L/PR:N/UI:R), més les versions afectades de cadascun. El CVE-2026-116325 de reindexdb (8,0), el CVE-2026-116313 de vacuumdb, el CVE-2026-16240 de pg_basebackup i els dos de pg_dump (116318 i 116330) surten de la llista de l'anunci; la consulta de les restriccions NOT NULL NO INHERIT en taules particionades i les dues frases citades sobre elles són literals de l'anunci i de les notes de la 18.7. Política de versions: 12-nov-2026 com a última data de la 14. Documentació d'initdb («This is enabled by default» per a les sumes de verificació) i de pg_basebackup («incremental backup only works with server version 17 and later»); notes de migració de la versió 19 per al reindexat de cerca de text, pg_trgm i fuzzystrmatch. Les quatre consultes del cens, l'ordre d'operacions i la lectura de què s'arregla sol i què no són nostres. Nota honesta: cap de les consultes no diu si un índex està corrupte —això no es veu des del catàleg—; diuen quins índexs entren en els supòsits de la nota de versió, que és la decisió que cal prendre avui.

Saps quants PostgreSQL tens encesos?

Passem el cens per cada base de cada servidor, posem la finestra de reconstrucció on no molesti i comprovem que la còpia restaura de debò. Sense vendre llicències de ningú.

Parlar amb everyWAN

Etiquetes:

Compartir:

Subscriu-te al nostre butlletí

Per rebre històries del món IT, novetats d'everyWAN i ofertes exclusives per a subscriptors, dona't d'alta a la nostra llista de correu

everyWAN
everyWAN