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 GitHubAuthorization: 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:
- 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.
- 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.
| Product | vLLM (video-serving multimodal deployments) |
| Affected | 0.8.3 – 0.14.0 |
| Fixed in | 0.14.1 |
| CVSS 3.1 | 9.8 Critical — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Underlying overflow | CVE-2025-9951 (FFmpeg JPEG2000 decoder) |
Impact
| Precondition | Required? |
|---|---|
| Valid credentials | No |
| User interaction | No |
| Network reachability to the vLLM HTTP port | Yes |
| A video-capable multimodal model served | Yes |
| Outbound fetch or inline media accepted | Yes — 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:
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:
150x64 yuv420p frame
component 0 (150x64 = 9600 bytes) -> plane 1 (75x32 = 2400 bytes)
7200-byte overflow into the next heap chunkThe 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:
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 firesRun 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 OK2. 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.1or later. The fix strips memory addresses from error messages and upgradesopencv-python-headlessto>= 4.13.0, whose FFmpeg rejects non-permutationcdefmappings. - 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:
- 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.
- 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.
| Produto | vLLM (deploys multimodais com vídeo) |
| Afetadas | 0.8.3 – 0.14.0 |
| Corrigido em | 0.14.1 |
| CVSS 3.1 | 9.8 Crítica — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Overflow subjacente | CVE-2025-9951 (decodificador JPEG2000 do FFmpeg) |
Impacto
| Pré-condição | Necessária? |
|---|---|
| Credenciais válidas | Não |
| Interação do usuário | Não |
| Alcance de rede até a porta HTTP do vLLM | Sim |
| Um modelo multimodal com suporte a vídeo servido | Sim |
| Fetch externo ou mídia inline aceita | Sim — é 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:
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:
frame 150x64 yuv420p
componente 0 (150x64 = 9600 bytes) -> plano 1 (75x32 = 2400 bytes)
overflow de 7200 bytes no próximo chunk do heapO 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:
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 disparaRodando 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 OK2. 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.1ou superior. A correção remove endereços de memória das mensagens de erro e atualiza oopencv-python-headlesspara>= 4.13.0, cujo FFmpeg rejeita mapeamentoscdefque 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.