В трекере задач FFmpeg появилось сообщение об уязвимости в демультиплексоре Sony PS2 VPK. Разработчик Дарио Клавихо (Darío Clavijo) нашёл баг с помощью собственного фаззера и подробно задокументировал цепочку срабатывания, вплоть до GDB-трассировки и готового патча.

Hello.

This is a bug found with our fuzzer: https://github.com/daedalus/fuzzer/

File: libavformat/vpk.c:89
Severity: Medium — crafted 21-byte input crashes any FFmpeg-based application that opens a malicious .vpk file or stream
Root cause: vpk_read_packet divides vpk->last_block_size by par->ch_layout.nb_channels without checking whether nb_channels is zero. A malformed VPK header can set nb_channels = 0, causing SIGFPE on the division.

Описание

Демультиплексор Sony PS2 VPK (libavformat/vpk.c) читает аудиоблоки из собственного контейнерного формата. В функции vpk_read_packet последний блок потока обрабатывается особым образом:

if (vpk->current_block == vpk->block_count) {
    unsigned size = vpk->last_block_size / par->ch_layout.nb_channels;
    unsigned skip = (par->block_align - vpk->last_block_size)
                    / par->ch_layout.nb_channels;
    ...
}

И size, и skip делятся на par->ch_layout.nb_channels. Когда nb_channels равно нулю, процессор генерирует SIGFPE — исключение целочисленного деления на ноль.

Цепочка срабатывания

  1. Проверка формата (vpk_probe) распознаёт big-endian сигнатуру VPK и передаёт входные данные демультиплексору VPK.
  2. vpk_read_header разбирает 24-байтовый заголовок. Фаззинговый вход устанавливает nb_channels = 0 в байтах заголовка 0x0e0x11. Функция действительно проверяет, что nb_channels > 0, но в кастомном AVIO-пути фаззера данные пробы/заголовка и данные, читаемые позднее при чтении пакета, могут разойтись: к моменту выполнения vpk_read_packet значение par->ch_layout.nb_channels возвращается к 0 из исходного фаззингового потока, тогда как vpk->last_block_size и vpk->block_count были вычислены по данным пробы с корректным числом каналов. Таким образом деление выполняется с реальным, но нулевым делителем.
  3. vpk_read_packet доходит до ветки обработки последнего блока и делит на ноль как size, так и skip.

Входные данные, вызывающие сбой

Hex-дамп 21-байтового входа, вызывающего краш (crash_1787378545_34bc062c_sig_signal8.bin):

00000000  20 4b 50 56 56 50 00 f8 04 00 3b 03 61 39 56 32  | KPVVP....;.a9V2|
00000010  36 36 30 38 50                                    |6608P|
  • Байты 0–3: 20 4b 50 56 — ASCII " KPV", что представляет собой big-endian сигнатуру VPK VPK , побайтово перевёрнутую относительно границы слова
  • Байты 0x0e–0x11: 00 00 00 00nb_channels = 0, триггер сбоя

Трассировка GDB

Program received signal SIGFPE, Arithmetic exception.
0x00005555557a9877 in vpk_read_packet (s=0x555557fed700, pkt=0x555557fed300)
    at libavformat/vpk.c:89
89   unsigned size = vpk->last_block_size / par->ch_layout.nb_channels;

#0  vpk_read_packet
#1  ff_read_packet
#2  read_frame_internal
#3  av_read_frame
#4  fuzz_ffmpeg
#5  main

Метаданные сбоя

ПараметрЗначение
СигналSIGFPE (код возврата −8)
Адрес сбоя / RIP0x7ffff48a66d7 (сама инструкция)
RSP0x7fffffffcce0
Число запусков до обнаружения495 211
Размер корпуса на момент обнаружения13 188 записей
Затраченное время10 ч 43 мин
Родительский seed36e65f4009ba0cab
SHA256 целиd704c2a52b21bd33

Оценка эксплуатируемости

ФакторОценка
Детерминированность сбояДетерминирован — 21 байт, единственный путь в коде демультиплексора
Глубина срабатыванияНебольшая — avformat_open_input автоматически определяет формат по сигнатуре
ПредусловияОтсутствуют — вход самодостаточен, без сети, без работы с кучей
Тип сигналаSIGFPE (целочисленное деление на ноль), а не повреждение памяти
Безопасность памятиНет выхода за границы буфера при чтении/записи, use-after-free, разыменования NULL
Область примененияЛюбое приложение, вызывающее avformat_open_input + av_read_frame на недоверенных данных
СерьёзностьСредняя — надёжный отказ в обслуживании, но не прямой примитив выполнения кода

Деление на ноль — это примитив отказа в обслуживании. Рядом с проблемной инструкцией нет контролируемой записи или произвольного чтения. Такой вход можно встроить в файл .vpk или в контейнер, который представляется как VPK, чтобы вызвать сбой в любом приложении, использующем FFmpeg.

Предлагаемое исправление

В начале vpk_read_packet предлагается добавить проверку, которая корректно отклоняет потоки с нулевым числом каналов:

static int vpk_read_packet(AVFormatContext *s, AVPacket *pkt)
{
    AVCodecParameters *par = s->streams[0]->codecpar;
    VPKDemuxContext *vpk = s->priv_data;
    int ret, i;

    if (par->ch_layout.nb_channels == 0)
        return AVERROR_INVALIDDATA;

    vpk->current_block++;
    ...
}

Это согласуется с уже существующей проверкой в vpk_read_header (if (st->codecpar->ch_layout.nb_channels <= 0) return AVERROR_INVALIDDATA;) и возвращает корректную ошибку вместо SIGFPE.

Регрессионный тест

/* Trigger: 21-byte VPK stream with nb_channels=0 — SIGFPE in vpk.c:89 */
static const unsigned char vpk_crash[] = {
    0x20, 0x4b, 0x50, 0x56, 0x56, 0x50, 0x00, 0xf8,
    0x04, 0x00, 0x3b, 0x03, 0x61, 0x39, 0x56, 0x32,
    0x36, 0x36, 0x30, 0x38, 0x50
};

/* Expect: av_read_frame returns -22 (AVERROR_INVALIDDATA), does not crash */

Участник проекта Джун Чжао (Jun Zhao) отметил, что проблема, судя по всему, совпадает с той, что уже обсуждалась в рассылке ffmpeg-devel в ноябре 2024 года. Позднее к issue был привязан pull request с исправлением — fix/24290-vpk-div0 (#24297), закрывающий уязвимость.