Назад к блогу

pgvector и CVE-2026-103484: почему обновления пакета недостаточно

pgvector и CVE-2026-103484: почему обновления пакета недостаточно

В свежем релизе pgvector 0.8.7 закрыто переполнение буфера при построении индекса IVFFlat, которое в CVE-2026-103484 описано как потенциальное выполнение произвольного кода. Разбираем, почему предыдущее исправление в 0.8.6 затрагивало только 32-битные системы, где именно в k-means-кластеризации происходит запись за границу и почему привычное `ALTER EXTENSION vector UPDATE;` не нейтрализует уязвимость в уже открытых соединениях.

В версии 0.8.7 от 1 октября 2026 года pgvector закрывает переполнение буфера при построении индекса IVFFlat. Формулировка в CHANGELOG — «Fixed buffer overflow with IVFFlat index build» — короче текста CVE-2026-103484, где сказано, что пользователь базы данных, который может создавать индекс IVFFlat, способен писать за пределы буфера в бэкенде, что может привести к выполнению произвольного кода. Ни пользователь, ни запись за пределы буфера, ни выполнение кода в CHANGELOG не упомянуты.

Разбираем, где именно возникает запись за границу, почему проверок размерности оказалось недостаточно и почему ALTER EXTENSION vector UPDATE; не выключает уязвимый код в уже работающих соединениях.

Что чинили раньше и что осталось

Запись 0.8.6 (2026-07-29) сообщает об исправлении переполнения буфера при построении индекса IVFFlat на 32-битных системах. Запись 0.8.7 (2026-10-01) — о том же переполнении, но уже без указания платформы. То есть в 0.8.6 закрыт частный случай, ограниченный 32-битными системами, а в 0.8.7 — более общий случай того же переполнения. В исходниках ivfbuild.c нет кода, различающего эти два исправления, поэтому детали того, какое именно переполнение оставалось незакрытым до 0.8.7, из источников не видны.

Механика переполнения при построении IVFFlat

Построение индекса проходит через кластеризацию методом k-means. Кластеризация получает образцы, центры, информацию о типе и объём использованной памяти; после её завершения образцы освобождаются. Сначала выбираются начальные центры — эта функция задаёт стартовые позиции центров, от которых начинается итеративное уточнение. Затем каждый образец привязывается к ближайшему центру по минимальному расстоянию. Дальше идёт итеративный цикл до 500 итераций: на шаге 1 считаются попарные расстояния между центрами, на шаге 2 отбираются точки, которые можно не пересчитывать, на шаге 3 уточняются расстояния и при необходимости меняется ближайший центр, на шаге 4 центры пересчитываются заново — функция пересчёта сбрасывает суммы и счётчики, накапливает суммы координат привязанных точек через функцию суммирования, делит их на количество и при необходимости нормализует результат функцией нормализации.

Сколько образцов берётся

Число образцов вычисляется при подготовке центров. Для unlogged-таблицы образцов ровно один. Иначе берётся число блоков таблицы, умноженное на MaxHeapTuplesPerPage — это верхняя оценка числа кортежей, которую можно разместить. Целевое число образцов — 50 на список, но не меньше 10000:

numSamples = buildstate->lists * 50;
if (numSamples < 10000)
	numSamples = 10000;

Затем оно ограничивается сверху оценкой числа кортежей и снизу единицей. Это число задаёт длину массива образцов и учитывается в подсчёте использованной памяти. В k-means по числу образцов и числу центров выделяются рабочие массивы lowerBound, upperBound, closestCenters, agg, newCenters, centerCounts. Их размер пропорционален произведению числа образцов на число центров, поэтому рост числа образцов (до 10000 или lists * 50) увеличивает рабочие массивы. Запись за границу буфера происходит, когда индексация выходит за пределы выделенного размера.

Что ограничивают lowerBound и upperBound

Это границы расстояний в Elkan-варианте k-means.

lowerBound — массив размером «число образцов × число центров», хранящий нижние оценки расстояния от каждой точки до каждого центра. upperBound — массив размером «число образцов», хранящий верхнюю оценку расстояния от точки до её текущего ближайшего центра. Третий массив, halfcdist, размером «число центров × число центров», хранит половину расстояния между парами центров; он заполняется симметрично для обеих ячеек пары. Из него вычисляется s(c) — минимальное значение halfcdist от центра c до любого другого центра.

На шаге 2 точка пропускается, если её верхняя оценка не превосходит s текущего ближайшего центра. На шаге 3 центр k пропускается для точки, если её верхняя оценка не превосходит нижней оценки для этой пары или halfcdist между текущим ближайшим центром и k. Эти проверки корректны именно потому, что расстояние удовлетворяет неравенству треугольника: тогда d(x,k) ≥ d(x,c(x)) − d(c(x),k), и если верхняя оценка d(x,c(x)) не больше нижней оценки d(x,k) или половины расстояния между центрами, то k заведомо не ближе. На шаге 5 нижняя оценка уменьшается на newcdist[k] с ограничением снизу нулём, на шаге 6 верхняя оценка увеличивается на newcdist ближайшего центра.

Метастраница и страницы списков

Метастраница IVFFlat описывается структурой с четырьмя полями. magicNumber служит для проверки валидности индекса: при чтении метастраницы он сравнивается с константой, и при несовпадении выдаётся ошибка «ivfflat index is not valid». version не используется, и его назначение не видно. dimensions задаёт число измерений векторов: оно читается из метастраницы, возвращается наружу и используется при проверке размерности вставляемых значений. lists задаёт число списков (кластеров): оно читается из метастраницы и передаётся вместе с центрами при создании страниц списков, по которым затем раскладываются элементы.

Где именно рвётся буфер

Когда PageAddItem возвращает InvalidOffsetNumber, обе точки вставки — и обычная вставка в InsertTuple, и построение индекса в ivfbuild.c — вызывают elog(ERROR, ...), то есть аварийно прерывают операцию. В InsertTuple перед этим идёт цикл поиска страницы: если на странице достаточно свободного места, цикл прерывается, иначе берётся следующая страница по nextblkno, а если её нет — создаётся новая. Проверка свободного места и последующий вызов PageAddItem выполняются на одной и той же странице, зарегистрированной в GenericXLog. Запись за границу буфера возникает именно в этой точке: между проверкой свободного места и добавлением элемента страница не перечитывается и не перепроверяется, а размер элемента вычисляется заранее как выровненный размер индексного кортежа.

Почему проверок размерности оказалось недостаточно

Перед циклом суммирования в трёх функциях — для vector, halfvec, и bit — стоит однотипная проверка соответствия размерности. В первых двух она сравнивает переданное число измерений с полем dim вектора, в третьей — с длиной битового вектора. Эти проверки стоят непосредственно перед циклом, который обращается к координатам вектора или к байтам битового вектора по индексу «номер бита / 8», и цикл идёт до числа измерений. Если бы фактическая размерность вектора была меньше, обращения вышли бы за пределы выделенной памяти, поэтому проверка должна аварийно завершить работу до входа в цикл.

Проверка NaN и бесконечности

CheckCenters вызывается в конце IvfflatKmeans сразу после того, как центры получены либо случайным выбором (когда образцов нет), либо через ElkanKmeans, и до возврата управления в ComputeCenters, откуда затем BuildIndex вызывает CreateListPages. Он отсекает три вида некорректных центров: неполный набор центров, значения с NaN или бесконечностью в сумме координат центра и нулевую норму центра при наличии процедуры нормировки (что важно для косинусного расстояния). Проверка NaN и бесконечности идёт через суммирование координат: для vector складываются координаты, для halfvec — преобразованные значения, для bit — биты как 0 или 1. Проверка нормы пропускается, если процедура нормировки не задана. Все эти ошибки приводят к аварийному завершению до того, как CreateListPages скопирует центры в страницы списков.

Что видит клиент

При вставке элемента в индекс ivfflat клиент получает сообщение об ошибке только в том случае, если PageAddItem не смог разместить кортеж на странице: тогда выполняется elog(ERROR, "failed to add index item to \"%s\"", ...). Это сообщение формируется в двух местах — при обычной вставке и при построении индекса. Размер кортежа заранее ограничен только утверждением Assert(...), которое в неотладочной сборке не проверяется, поэтому реальное переполнение буфера может остаться незамеченным, а клиент увидит только общее сообщение об ошибке добавления элемента.

Почему обновления пакета недостаточно

Бэкенд продолжает держать старую копию разделяемой библиотеки, потому что pg_extension.extversion показывает лишь то, какой SQL-скрипт был выполнен, а не то, какую библиотеку отобразил конкретный бэкенд: «don’t treat pg_extension.extversion as proof of anything: it reports which SQL script ran, not which library a given backend has mapped». Поэтому после ALTER EXTENSION vector UPDATE; уже работающие соединения могут продолжать исполнять уязвимый код.

Чтобы уязвимый код перестал исполняться в уже работающих соединениях, нужно закрыть эти соединения. Для этого есть pg_terminate_backend(). При использовании PgBouncer команда RECONNECT в админ-консоли закрывает каждое серверное соединение по мере его освобождения, иначе server_lifetime — параметр PgBouncer, задающий предельное время жизни серверного соединения, по истечении которого оно закрывается, — по умолчанию 3600 секунд сделает это сам. Если vector помещён в shared_preload_libraries, требуется перезапуск сервера.

Найти соединения, всё ещё держащие старую библиотеку, можно через pg_stat_activity: выбрать идентификатор процесса, пользователя, имя приложения и время старта бэкенда, отфильтровав по типу бэкенда клиентских соединений и по времени старта раньше момента установки пакета.

Что из этого следует на практике

  • Обновляйтесь до 0.8.7, но проверяйте тег, а не страницу релизов. Changelog на теге v0.8.7 датирует релиз 1 октября; changelog на master всё ещё указывает 0.8.7 как невыпущенную и не упоминает переполнение. pgvector публикует git-теги, а не GitHub releases, поэтому если ваше оповещение следит за страницей Releases, оно ничего не увидело.
  • ALTER EXTENSION vector UPDATE; недостаточно. Он выполняет SQL-скрипт, но не выгружает старую библиотеку из уже работающих бэкендов. Закрывайте соединения: pg_terminate_backend(), RECONNECT в PgBouncer или ожидание server_lifetime (3600 секунд по умолчанию). Если vector в shared_preload_libraries — перезапуск сервера.
  • pg_extension.extversion не доказывает, какая библиотека отображена в конкретном бэкенде. Не полагайтесь на него как на подтверждение, что уязвимый код выключен.
  • Уязвимость срабатывает при построении индекса IVFFlat. Пользователь базы данных, который может создавать такой индекс, способен писать за пределы буфера в бэкенде, вплоть до выполнения произвольного кода. Ограничьте право на создание индексов IVFFlat до завершения обновления.
  • Клиент может не увидеть признаков эксплуатации. Реальное переполнение в неотладочной сборке не проверяется утверждением, поэтому единственное, что увидит клиент, — общее сообщение об ошибке добавления элемента в индекс.
  • Прогресс построения индекса отслеживается через pg_stat_progress_create_index. Запрос выбирает phase, tuples_done и tuples_total, а процент выполнения вычисляется как round(100.0 * tuples_done / nullif(tuples_total, 0), 1). На ход построения влияет maintenance_work_mem (можно поднять, например, до '8GB') и число параллельных рабочих max_parallel_maintenance_workers (по умолчанию 2; можно поставить 7 плюс лидер, а при большом числе рабочих может потребоваться увеличить и max_parallel_workers, по умолчанию 8).

Где смотреть в коде

Источники

Похожее