Volver al Blog

Qué hay que reindexar después de la tanda de 52 CVE de PostgreSQL

Frente de un fichero de biblioteca de madera con decenas de cajones de fichas: un índice que alguien tiene que reconstruir a mano

Ayer PostgreSQL publicó 18.7, 17.12, 16.16, 15.20 y 14.25 con 52 CVE corregidos de una sentada. Actualizas el paquete, reinicias el servicio y los 52 quedan cerrados: eso es verdad y es la parte fácil. Lo que no se arregla con el reinicio son los cinco apuntes de corrupción de índice que viajan en la misma tanda. El código nuevo deja de estropear; lo que ya está estropeado sigue donde está hasta que alguien escriba REINDEX a mano. Y si haces copias incrementales, la cadena de ayer dejó de servir de base. Nada de eso aparece en el titular, y nada de eso lo hace tu gestor de paquetes.

Lo que sí cierra el reinicio

Hay un dato de los 52 que decide si esto te corre prisa o no, y no es el número: es el vector. 32 de los 52 llevan CVSS 8,8 con AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H —lo hemos comprobado uno a uno en tres de ellos, el desbordamiento de ts_rank_cd() (CVE-2026-116317), el de los agregados de mínimo y máximo y el de plperl—, y esa PR:L es la que invita a cerrar la pestaña: «hace falta usuario autenticado en la base, aquí no entra nadie de fuera». La lectura es comprensible y es floja. Ese privilegio bajo que hace falta lo tiene, en una empresa de software, tu propia aplicación, que lleva conectada desde que arrancó y ejecuta las consultas que le escriben tus usuarios. La pregunta útil no es si alguien tiene cuenta en el servidor, sino qué llega hasta el planificador con los permisos de tu aplicación.

Y hay una parte de la lista que no se arregla parcheando el servidor porque no está en el servidor: seis de los 52 son de las herramientas de línea de comandos —pg_restore, pg_dump (dos), reindexdb, vacuumdb y pg_basebackup—, y esas corren donde las lance quien las lance. El más claro es CVE-2026-116314: pg_restore ejecuta código porque no valida un archivo que no sea de texto plano, y su vector es PR:N/UI:R, o sea que no hace falta cuenta, hace falta que alguien restaure. Es la misma forma de fallo que vimos la semana pasada en los volcados de otra base de datos: el fichero que te mandan manda sobre la herramienta que lo abre. Si en tu empresa alguien restaura volcados de clientes en su portátil, ese portátil entra en esta actualización aunque allí no haya ningún servidor.

Toda esa parte se resuelve como siempre: la versión menor es acumulativa, no hay volcado ni pg_upgrade, se para el servicio, se cambian los binarios y se arranca. Si tu servidor está empaquetado por la distribución, es un apt upgrade o un dnf update y un reinicio. Y si acabas ahí, el servidor está seguro y al mismo tiempo puede estar devolviendo datos mal, que es lo que viene ahora.

Los cinco apuntes que el reinicio no toca

  1. GIN, limpieza incompleta en VACUUM. Cuando un VACUUM necesita varias pasadas sobre el índice porque no le cabe todo en memoria, podía dejarse entradas sin borrar; hay además una carrera con la división de páginas hoja con el mismo efecto. La nota de versión lo dice sin rodeos: «These bugs would eventually lead to a corrupt index with entries pointing at unrelated table rows». Remedio: REINDEX si sospechas que te ha pasado.
  2. GIN, borrado de página tras una división incompleta. El paso de borrado podía desconectar páginas hermanas, «causing subsequent index searches to miss rows». Mismo remedio.
  3. GiST de pg_trgm. La combinación de entradas podía perder la marca que identifica su formato. Literal: «This fix does not repair existing indexes. Users should REINDEX existing gist_trgm_ops indexes after updating».
  4. B-tree sobre bpchar. Se desactiva la deduplicación de entradas porque la igualdad de ese tipo ignora los espacios finales y permitía que un index-only scan devolviera resultados sutilmente equivocados. «This patch disables de-duplication for new index entries, but does nothing about possible corruption of existing entries». Con su matiz, que va en la sección siguiente.
  5. Índices de tablas TOAST. Un REINDEX CONCURRENTLY cambiaba campos de pg_index que la caché de relaciones no esperaba, y se perdían actualizaciones del índice. El síntoma tiene nombre y seguro que lo has visto en un ticket: errores de «missing chunk number». La buena noticia va en la propia nota: la tabla TOAST está bien, es solo el índice, y un REINDEX lo recupera.

Fíjate en el patrón, porque es lo único que de verdad hay que llevarse de aquí: tres de los cinco producen el mismo síntoma, y el síntoma no es un error. Un índice corrupto que apunta a filas que no son, o que se salta las que son, no revienta ni escribe nada en el log. Responde. Y responde rápido, que es lo que hace que nadie mire. La consulta devuelve cuatro facturas en vez de cinco, el informe del mes sale con un total que no cuadra por poco, y el que acaba investigando es contabilidad, no sistemas. Un servidor caído se nota en treinta segundos; un índice que miente puede estar meses sin que nadie lo relacione con una tanda de parches.

El matiz del bpchar que conviene leer dos veces

Cualquier esquema de gestión está lleno de CHAR(2) para códigos de país, CHAR(4) para ejercicios contables y CHAR(24) para IBAN. Al leer «los índices B-tree sobre columnas bpchar hay que reconstruirlos» lo lógico es echarse las manos a la cabeza y preparar una noche entera de reindexado. No hace falta. La nota de versión se toma la molestia de aclararlo: «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», porque la restricción de longitud obliga a que dos cadenas iguales tengan el mismo tamaño y sean, por tanto, idénticas bit a bit.

El afectado es el tipo sin longitud: una columna declarada character o bpchar a secas. Casi nadie lo escribe a propósito; aparece solo, por la puerta de atrás de un CREATE TABLE AS, de una conversión con ::char o de una tabla que alguien generó desde una herramienta. Saber que existen no sirve de mucho si no sabes cuáles tienes, y el nombre del tipo no te lo dice porque CHAR(24) también es bpchar en el catálogo. Lo que los separa es el atttypmod, y por eso la tercera consulta de abajo mira ese campo y no el nombre: es la diferencia entre reconstruir dos índices y reconstruir doscientos.

El orden importa: primero el binario, después el REINDEX

Las dos notas que mandan traen la secuencia escrita dentro y es fácil pasarla por encima: «after updating» en la de pg_trgm, «after installing this update» en la de bpchar. No es una fórmula de cortesía. Reconstruir un índice con los binarios viejos es reconstruirlo con el código que lo estropea: te comes la ventana de bloqueo, el consumo de disco y la noche, y acabas con un índice recién hecho que puede volver a corromperse. Primero el paquete, luego el reinicio, luego el REINDEX, y al final la copia. En ese orden.

Las cuatro consultas del censo

Ninguna de las cuatro modifica nada; se pueden lanzar ahora mismo contra producción y contra cada base de cada servidor. Lo que sacan no es la lista de índices corruptos —eso no se ve desde el catálogo— sino la lista de los que entran en los supuestos de la nota de versión, que es la decisión que hay que tomar hoy; y, con el tamaño al lado, cuánto va a costar.

-- 1) índices GIN (jsonb, búsqueda de texto, arrays) y lo que ocupan
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) índices GiST con la clase de operadores 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) columnas bpchar SIN longitud; char(n) no sale aquí y es lo correcto
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) índices inválidos: restos de 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 devuelve columnas, no índices: con esa lista en la mano se mira con \d qué tablas tienen un B-tree encima de esas columnas, que suelen ser cero o una. La cuarta no va de esta tanda y sale gratis: si devuelve filas, tienes índices inválidos de un REINDEX CONCURRENTLY o de un CREATE INDEX CONCURRENTLY que falló a medias —esta última es la causa más habitual—, ocupando disco y sin servir para nada. Aprovecha el viaje.

Falta una quinta comprobación que no es de índices y que el propio anuncio incluye entre los pasos manuales: las restricciones NOT NULL NO INHERIT sobre tablas particionadas, que esta versión pasa a rechazar. CREATE TABLE ya las rechazaba, pero caminos como CREATE TABLE LIKE no comprobaban las restricciones copiadas, «potentially allowing a partition to contain null values when it should not». Es decir: puede que tengas nulos donde declaraste que no los habría. La consulta la da el anuncio: SELECT conrelid::regclass, conname FROM pg_constraint WHERE contype = 'n' AND connoinherit AND conrelid IN (SELECT oid FROM pg_class WHERE relkind = 'p'). Si devuelve algo, se borra la restricción o se arregla con ALTER TABLE ... ALTER CONSTRAINT ... INHERIT. Si no, un resultado vacío más para el acta.

REINDEX a secas o CONCURRENTLY

El REINDEX INDEX clásico bloquea las escrituras de la tabla mientras dura. Es rápido y es honesto: sabes cuándo empieza, cuándo acaba y a quién molesta. El REINDEX INDEX CONCURRENTLY no bloquea escrituras, pero hace el trabajo en varias fases, necesita espacio para mantener el índice nuevo y el viejo a la vez, y si se interrumpe deja un índice inválido —el de la consulta 4— que hay que borrar a mano. En una tabla de cien megas, el primero. En el índice GIN de doscientos gigas de la tabla de documentos, el segundo, y con el disco mirado antes.

Y aquí está la ironía de la tanda, que además refuerza el orden de antes: dos de los 52 CVE están en la herramienta con la que vas a arreglar. CVE-2026-116336 es que REINDEX CONCURRENTLY no bloqueaba un DROP INDEX simultáneo, y CVE-2026-116325 (CVSS 8,0) es una inyección SQL por el entrecomillado de nombres de índice en reindexdb, que es justo el atajo que usa quien reindexa una base entera. A eso se suma, ya fuera de la lista de CVE, el apunte de los índices TOAST que causaba el propio REINDEX CONCURRENTLY. Lanzar el reindexado antes de actualizar no es solo trabajo tirado: es hacerlo con la versión de la herramienta que todavía tiene los tres problemas.

Con réplicas hay que distinguir, y la distinción cambia la cuenta del trabajo. Si tu réplica es física —replicación en continuo, el caso habitual—, el REINDEX va por WAL como cualquier otra escritura y la secundaria se reconstruye sola al reproducirlo: la ventana la planificas una vez. Si tu réplica es lógica, no: los índices del suscriptor son suyos, no viajan en el flujo de replicación y hay que reindexar el suscriptor aparte, con su propia ventana. Es el tipo de detalle que decide si la noche de mantenimiento son dos horas o cuatro, y conviene mirarlo antes de prometer una hora de corte. Ya que entras en los roles de replicación, repasa quién tiene el atributo REPLICATION, que nunca fue un permiso de solo lectura.

La copia incremental que ha dejado de encadenar

Hay un sexto apunte, este ya fuera de los cinco de índices, y es el que más caro sale si se pasa por alto. Una copia incremental tomada después de que un VACUUM trunque una tabla podía restaurar mapas de visibilidad caducados y provocar corrupción de datos. La frase que importa es la del remedio: «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». Es decir: actualizas, y tus incrementales siguen contaminados hasta que hagas un completo nuevo que sirva de base.

Ahora el matiz, que también reduce el trabajo: el fallo solo se da con las sumas de verificación de datos y wal_log_hints desactivados, de modo que basta con tener activa cualquiera de las dos para quedar fuera. Desde la 18, initdb activa las sumas por defecto —la documentación lo dice tal cual, «This is enabled by default»—, pero ese «por defecto» solo se aplica a clústeres creados con la 18: uno que haya llegado a la 18 por pg_upgrade desde la 14, la 15, la 16 o la 17 arrastra la configuración de origen, y los creados a mano con --no-data-checksums también. Añade que las copias incrementales de pg_basebackup solo existen desde la 17 y te queda el perfil exacto al que esto le corre prisa: cualquier clúster 17 o 18 con data_checksums desactivado, sin wal_log_hints y usando --incremental. Se comprueba en una línea, SHOW data_checksums;, y si eres ese perfil el completo nuevo no es una recomendación de manual: es la diferencia entre tener copias y creer que las tienes. La única forma de saber cuál de las dos es, además, restaurar una; ese es el trabajo que vale la pena planificar junto con la infraestructura que las guarda.

Y la 14 se queda sin la siguiente

El 1 de noviembre escribimos que PostgreSQL 14 caducaba el 12 de noviembre y que a tu servidor no se lo iba a decir nadie. Se cumplió con puntualidad incómoda: la 14.25 es su última versión, y ha salido el mismo día en que el proyecto publicaba la tanda de parches más grande que le hemos visto. La aritmética es fácil y es la que hay que llevar a la reunión del lunes: hoy tu 14.25 tiene los 52 arreglos y está tan al día como un 18.7; el día que aparezca el CVE número 53, no lo tendrá, y no habrá nada que instalar. Ni aviso, ni paquete, ni línea en el registro.

Hay un detalle práctico que une las dos cosas: la 19 sale el 19 de noviembre, dentro de seis días, y sus notas de migración vuelven a pedir reindexar al subir con pg_upgrade. Conviene separar los dos casos, porque no tienen la misma letra pequeña: el de búsqueda de texto y pg_trgm está condicionado a bases con un proveedor de locale distinto de libc cuyo comportamiento difiera del LC_CTYPE configurado, y ahí además hay que recalcular los tsvector almacenados; el de fuzzystrmatch no lleva esa condición y se recomienda en cualquier pg_upgrade, porque esas funciones cambian de comportamiento con caracteres no ASCII. Resultado: si estás en una versión vieja y tenías la migración mayor apuntada para el primer trimestre, el reindexado de esta semana y el de la migración se solapan en buena parte. Esa conversación es más barata tenerla hoy, con la lista de índices delante, que en enero.

Cuándo no hay que hacer nada esta noche

Si las cuatro consultas salen vacías —ningún GIN, ningún gist_trgm_ops, ninguna columna bpchar sin longitud, ningún índice inválido—, la de las restricciones particionadas tampoco devuelve nada y no usas copias incrementales de pg_basebackup, esta tanda es un apt upgrade, un reinicio y a dormir. Decirlo nos quita trabajo facturable y es la verdad: el REINDEX no es gratis, y lanzarlo «por si acaso» sobre un índice GIN de doscientos gigas un martes a las once de la mañana hace más daño que el fallo del que huyes.

Dos avisos para que ese «no hay nada que hacer» sea honesto, porque las consultas tienen un punto ciego. El primero: la corrupción de índices TOAST no la ve ninguna de las cuatro. No hay nada en el catálogo que la delate; aparece el día que una lectura devuelve missing chunk number, y entonces ya sabes qué tabla reindexar. Si has corrido REINDEX CONCURRENTLY sobre tablas grandes con campos de texto o bytea, tenlo en el guion de guardia en vez de en la lista de esta noche. El segundo: estas consultas hablan de una base, y los servidores tienen varias. Hay que pasarlas por todas, también por las que nadie mira.

El caso contrario es el que nos encontramos más a menudo en empresas de software: nadie sabe de memoria cuántos PostgreSQL hay encendidos ni en qué versión va cada uno, porque la mitad llegaron dentro de una aplicación que los trajo de serie. Ahí el problema no es la tanda de ayer, que solo lo ha hecho visible. La parte que hacemos nosotros en datos y aplicaciones empieza por ese inventario y sigue por la ventana: cuándo se puede reconstruir sin parar a nadie y con cuánto disco libre.

Fuentes (verificadas el 13-nov-2026, 13:30 CET). Anuncio oficial «PostgreSQL 18.7, 17.12, 16.16, 15.20, 14.25, and 19 RC 2 Released!» (12-nov-2026): las cinco versiones, los 52 CVE, la condición de versión acumulativa sin volcado ni pg_upgrade, el carácter de última versión de la rama 14 y la fecha prevista de la 19 (19-nov-2026). Notas de la versión 18.7: las citas literales de los dos apuntes GIN, del GiST de pg_trgm, del bpchar (incluido el párrafo que excluye expresamente CHAR(n)), de los índices de tablas TOAST con el error «missing chunk number» y del mapa de visibilidad caducado en copias incrementales. Página de seguridad de PostgreSQL: vectores CVSS leídos uno a uno en las fichas de CVE-2026-116317, CVE-2026-116331 y CVE-2026-116307 (los tres 8,8 con AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) y de CVE-2026-116314 (8,8 con AV:N/AC:L/PR:N/UI:R), mas las versiones afectadas de cada uno. El CVE-2026-116325 de reindexdb (8,0), el CVE-2026-116313 de vacuumdb, el CVE-2026-16240 de pg_basebackup y los dos de pg_dump (116318 y 116330) salen de la lista del anuncio; la consulta de las restricciones NOT NULL NO INHERIT en tablas particionadas y las dos frases citadas sobre ellas son literales del anuncio y de las notas de la 18.7. Política de versiones: 12-nov-2026 como última fecha de la 14. Documentación de initdb («This is enabled by default» para las sumas de verificación) y de pg_basebackup («incremental backup only works with server version 17 and later»); notas de migración de la versión 19 para el reindexado de búsqueda de texto, pg_trgm y fuzzystrmatch. Las cuatro consultas del censo, el orden de operaciones y la lectura de qué se arregla solo y qué no son nuestros. Nota honesta: ninguna de las consultas dice si un índice está corrupto —eso no se ve desde el catálogo—; dicen qué índices entran en los supuestos de la nota de versión, que es la decisión que hay que tomar hoy.

¿Sabes cuántos PostgreSQL tienes encendidos?

Pasamos el censo por cada base de cada servidor, ponemos la ventana de reconstrucción donde no moleste y comprobamos que la copia restaura de verdad. Sin vender licencias de nadie.

Hablar con everyWAN

Etiquetas:

Compartir:

Suscríbete a nuestra newsletter

Para recibir historias del mundo IT, novedades de everyWAN y ofertas exclusivas para suscriptores, date de alta a nuestra lista de correo

everyWAN
everyWAN