SevyDevy

Guide · AI and GenAI

vLLM's October 2026 RCE Vulnerability Explained: Request-Chosen Processor Code

19 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • The vLLM remote code execution vulnerability (GHSA-h3rc-6mm3-gc2m, CVE-2026-114053) let a client choose which version of a model's custom Python code the server imported. Remote code execution means an outsider gets their own code run on your machine.
  • On the server side it needed three things together: vLLM 0.7.3 up to 0.30.0, the --trust-remote-code flag switched on, and a model whose processor ships its own Python code. The processor is the part that turns images and text into model input.
  • A request field called mm_processor_kwargs, meant for per-request image, video or audio settings, could carry code_revision. Transformers then fetched processor code from that git revision instead of the one the operator pinned.
  • vLLM 0.31.0 (5 October 2026) rejects these per-request settings unless you start the server with --trust-request-mm-kwargs. Upgrade, and leave that flag off for untrusted clients.
  • Beginners can stop after the section on who was exposed. Engineers get the exact code path, why the API key never covered the /tokenize helper endpoint, and a source-level finding: turning the new flag on reopens the same path.

What goes wrong when a request can choose which code your server imports?

Your server ends up running code that the caller picked, not code you reviewed. That is the core of the vLLM remote code execution vulnerability disclosed in October 2026. One optional field in an API request could change which file of Python code the server loaded.

vLLM is an open-source server for running large language models. Teams use it to host a model on their own hardware and expose an HTTP API that looks like the common chat-completions style.

Picture a common setup. A team serves a vision-language model, one that reads images as well as text, behind an internal document-reading service. The setup is made up, but the settings are the real ones the advisory describes. The model's repository ships custom Python for its image processor, so the team starts vLLM with --trust-remote-code. They pin --revision to one commit they reviewed and set --api-key so only their app can call it.

The team believes three things. Only reviewed code runs. Only callers with the key get in. Requests carry data, not instructions about which code to load. Before 0.31.0, all three beliefs had a gap, and this guide walks through each one.

Which words do you need before reading this vLLM advisory?

You need six terms. Each one comes with a plain meaning and how it shows up in the running example.

  • Model repository: a git-style folder on the Hugging Face Hub holding a model's weights, settings and sometimes Python files. Every saved change has a revision, which can be a commit ID, a branch or a tag.
  • Processor: the part that prepares input for a model, such as resizing an image and turning text into tokens. In Transformers it is loaded with AutoProcessor.from_pretrained.
  • trust_remote_code: a switch, --trust-remote-code in vLLM, that lets Transformers download and import Python files from the model repository. It is off by default because importing a file runs it.
  • revision and code_revision: revision picks which commit of the model files to use. code_revision picks which commit of the custom Python code to use, and Transformers lets it differ.
  • mm_processor_kwargs: an optional field in vLLM requests, short for multimodal processor keyword arguments. It was meant for small per-request settings such as image size limits.
  • API key: a shared secret that vLLM checks when you start it with --api-key. A request without the right key gets a 401 Unauthorized reply, on the paths the key covers.

What is the vLLM remote code execution vulnerability fixed in 0.31.0?

It is a flaw where a client could set code_revision inside mm_processor_kwargs, and vLLM passed it on to the processor loader. With trust_remote_code on, that value chose which revision of the processor's Python file got imported. Importing it ran that code inside the vLLM API server process.

The operator's pinned --revision did not stop it, because Transformers uses code_revision for code whenever it is given. The default setup, with trust_remote_code off, was not affected, according to the advisory.

The advisory in one table (as of 28 October 2026). Scores differ because each source rated it separately.
ItemValueSource
Advisory IDGHSA-h3rc-6mm3-gc2mvLLM security advisory
CVE IDCVE-2026-114053CVE record (assigned by VulnCheck, 20 October 2026), GitHub Advisory Database via OSV
Affectedvllm 0.7.3 up to, but not including, 0.31.0Advisory; it says the range was not bisected
Fixed invllm 0.31.0, released 5 October 2026Advisory and PyPI
Severity from GitHub8.1 High, CVSS 3.1, attack complexity HighAdvisory sidebar and OSV
Severity from VulnCheck9.2 Critical, CVSS 4.0, attack requirements PresentVulnCheck advisory, dated 20 October 2026
Not affectedServers with trust_remote_code off, the defaultAdvisory

How did one request field choose the processor code?

The field travelled, untouched, from the HTTP request into the function that downloads and imports processor code. No step on the way removed it. The diagram follows a request to the /tokenize endpoint, which the advisory names because it needs no API key.

How code_revision reached the code loader before 0.31.0

Follow the code_revision value from the first box to the last. vLLM pinned revision at step 4 but still forwarded code_revision, so the caller's value reached the loader at step 6.

Diagram connections:

  • Caller to API key check (1. POST /tokenize)
  • API key check to Request renderer (2. Path not guarded)
  • Request renderer to Processor config step (3. Extras passed on)
  • Processor config step to AutoProcessor (4. kwargs incl. code_revision)
  • AutoProcessor to Dynamic module loader (5. Remote code allowed)
  • Dynamic module loader to Model repo at chosen ref (6. Fetch at code_revision)
  • Model repo at chosen ref to Import runs the file (7. Import the module)
  • Caller: Caller → /tokenize: JSON with mm_processor_kwargs
  • API key check: Guards /v1, /v2, /inference, /cohere only
  • Request renderer: Copies mm_processor_kwargs forward
  • Processor config step: Pins revision, forwards loader keys
  • AutoProcessor: Transformers: from_pretrained takes any keyword
  • Dynamic module loader: Uses code_revision for the code file
  • Model repo at chosen ref: Python file from the caller's revision
  • Import runs the file: Code runs inside the vLLM API process

Step 4 is where a safer design would have stopped it. vLLM's function cached_processor_from_config sets revision and trust_remote_code from the server's own settings. It then forwards the remaining keys after a filter that keeps any name the target function can accept.

AutoProcessor.from_pretrained accepts any keyword at all, because its signature ends in a kwargs catch-all, Python's way to take any named argument. So the filter kept code_revision. We confirmed both facts by reading the vLLM 0.30.0 source and inspecting Transformers 5.17.0. A later step removes only the keys the processor uses at call time, such as image sizes, and code_revision is not one of them.

Why pinning --revision did not help

Pause and predict. The operator pinned revision to one commit, and the caller sent a different code_revision. Which commit does Transformers fetch the processor's Python file from?

It fetches from the caller's code_revision. In Transformers 5.17.0, get_class_from_dynamic_module falls back to revision only when code_revision is not given and the code lives in the same repository. The script below shows this without downloading or importing anything, by swapping the two download and import functions for recorders. You can skip it; the next paragraph gives the result.

python
import transformers.dynamic_module_utils as dmu

seen = []
dmu.get_cached_module_file = lambda repo, f, **kw: seen.append(kw.get("revision")) or "x"
dmu.get_class_in_module = lambda name, path, **kw: name  # nothing is imported

REPO = "example-org/example-vision-model"  # placeholder, never contacted
dmu.get_class_from_dynamic_module("proc.MyProcessor", REPO, revision="a1b2c3d")
dmu.get_class_from_dynamic_module("proc.MyProcessor", REPO, revision="a1b2c3d",
                                  code_revision="some-other-ref")
print(seen)

On Transformers 5.17.0 this prints ['a1b2c3d', 'some-other-ref']. The first call uses the pin. The second ignores it for the code file. vLLM 0.31.0 allows Transformers from 5.10.4 up to, but not including, 5.18.0.

What just happened? Transformers treats code_revision as a deliberate choice by whoever loads the model. That is reasonable for the operator. The bug was letting an HTTP caller make that choice.

vLLM's own --code-revision flag would not have helped either. In 0.30.0, 0.31.0 and 0.32.0, the processor loader never passes it to Transformers. The advisory asks vLLM to pass the configured code revision explicitly, as the only one allowed.

Think of a hospital pharmacy. The head pharmacist fixes which edition of the drug handbook staff must follow, like the pinned revision. A prescription slip has a notes box for small things such as the tablet size, like mm_processor_kwargs. Before the fix, writing "use the handbook from shelf 7" in that box made staff switch handbooks.

Where the picture breaks: a pharmacist still reads a handbook with judgement. Python import has no judgement. It runs every line in the file with the full permissions of the server.

Why did the API key not protect the /tokenize endpoint?

Because vLLM's API key check only looks at paths that start with /v1, /v2, /inference or /cohere. The /tokenize path starts with none of them, so it never asks for a key. vLLM's own security guide lists /tokenize under endpoints that need no authentication.

/tokenize turns text into token IDs, the numbers a model reads. It also accepts chat messages with images and an mm_processor_kwargs field, so it runs the same processor loading as a chat request.

Which paths need the key when vLLM starts with --api-key. Read from the middleware source of 0.30.0, 0.31.0 and 0.32.0; all three are identical here (as of 28 October 2026).
PathNeeds the API key?Why
/v1/chat/completionsYesStarts with /v1
/v1/modelsYesStarts with /v1
/tokenizeNoNo guarded prefix
/detokenizeNoNo guarded prefix
/poolingNoNo guarded prefix
/healthNoNo guarded prefix

In the running example, the team's key protected chat calls, but anyone who could reach the port could call /tokenize. The advisory notes this stays true unless a proxy in front protects it. The 0.31.0 fix did not change the key check; it closed the path from a different side.

Who was exposed to CVE-2026-114053, and how serious is it?

You were exposed only if all of these held: vLLM 0.7.3 through 0.30.0, trust_remote_code on, and a multimodal model whose processor loads custom code. The attacker also had to reach an endpoint that takes mm_processor_kwargs.

There is one more condition, and it is why scores differ. The advisory says the attacker must create or control a revision in the served model repository. Its example is a Hugging Face community pull-request ref, where the repository allows them.

Hugging Face pull requests are stored as refs such as refs/pr/42 directly on the source repository. Repository owners can turn pull requests off in settings. In our reading, the risk is highest for public repositories that accept community pull requests.

  • The GitHub advisory record rates it 8.1 High under CVSS 3.1, the common severity scale, with attack complexity High. That rating likely reflects the extra condition about controlling a revision, which is outside the attacker's normal reach.
  • The advisory's description carries a suggested score of 9.8 Critical with attack complexity Low, which does not match the vector in its own sidebar.
  • VulnCheck rates it 9.2 Critical under CVSS 4.0, the newer version of the scale. Its vector marks attack requirements as Present, which is how version 4.0 records preconditions like this one.

Beginners can stop here. You now know what went wrong, who was exposed and that 0.31.0 fixes it. The rest is for engineers who run vLLM or design similar APIs.

How does vLLM 0.31.0 block per-request multimodal kwargs?

vLLM 0.31.0 adds a check, validate_request_mm_kwargs, that rejects any non-empty mm_processor_kwargs or media_io_kwargs in a request. media_io_kwargs is the sibling field for how media files are loaded. The check runs before the request is rendered, in the chat, completion and pooling (embedding) paths, including /tokenize.

The new setting trust_request_mm_kwargs defaults to false. Starting the server with --trust-request-mm-kwargs makes the check return early and allows per-request values again. Settings given at server start with --mm-processor-kwargs, and offline use through the Python LLM class, are unchanged.

The 0.31.0 gate for per-request multimodal settings

Notice that the gate is all or nothing. With the flag on, the request takes the same path as before the fix, including any code_revision key.

Diagram connections:

  • Request arrives to Has mm kwargs?
  • Has mm kwargs? to Render normally (No)
  • Has mm kwargs? to Flag set at start? (Yes)
  • Flag set at start? to 400 Bad Request (No)
  • Flag set at start? to Forward as before (Yes)
  • Request arrives: Chat, completion, pooling or /tokenize
  • Has mm kwargs?: Non-empty mm_processor_kwargs or media_io_kwargs
  • Flag set at start?: --trust-request-mm-kwargs, default off
  • Render normally: Server-level settings only
  • 400 Bad Request: Error names the mm_processor_kwargs parameter
  • Forward as before: Keys reach the processor loader unfiltered

The fix does not mention code_revision at all. Pull request 58830 is titled as a security change and explains it as stopping clients from forcing extreme image and video settings that exhaust memory. The error message gives the same kind of reason: resource usage. The advisory names this pull request as the patch, because blocking the whole field also blocks code_revision.

Turning the new flag on reopens the old path

We read the processor code in 0.30.0, 0.31.0 and 0.32.0. The function that forwards the keys, cached_processor_from_config, did not change in 0.31.0, and nothing in the gate or the filter mentions code_revision. vLLM 0.32.0 even adds a check in get_processor that skips its local snapshot when code_revision differs from revision, so the code still comes from that revision.

So with --trust-request-mm-kwargs on, a request's code_revision still reaches Transformers, by our reading. This is undocumented as of 28 October 2026; pin your version and recheck. The docs only say to enable the flag when API clients are trusted, and with trust_remote_code on that warning is literal.

How do you upgrade and check your vLLM server in 2026?

Upgrade to vLLM 0.31.0 or newer, then confirm two things: the version, and that a request carrying mm_processor_kwargs is refused. The steps below assume pip; adapt them for your container image.

  1. Find what you run

    Run pip show vllm inside the server's environment or image. Note the Version line. Anything from 0.7.3 to 0.30.0 is in the affected range.

  2. Check your start flags

    Look for --trust-remote-code and --trust-request-mm-kwargs in your launch command, manifest or vLLM config file. If --trust-remote-code is absent, the advisory says you were not affected.

  3. Upgrade

    Install vllm 0.31.0 or newer, then rebuild the image and regenerate any lock file. vLLM 0.32.0 also carries the fix but changes other defaults, so read its upgrade notes first.

  4. Confirm the version

    Run pip show vllm again. Expect Version: 0.31.0 or higher.

  5. Send a harmless test request

    POST to your own /tokenize with a messages list holding one short text message, and mm_processor_kwargs set to a harmless key such as max_pixels. Use a test server you own.

  6. Read the reply

    Per the 0.31.0 source, expect HTTP 400 with error type BadRequestError and param mm_processor_kwargs. An HTTP 200 with token IDs suggests the gate is off or the version is old.

  7. Fix your clients

    Move any per-request multimodal settings your app sends into the server's --mm-processor-kwargs. Otherwise those calls now fail with the same 400.

The 400 reply is what the source shows: VLLMValidationError maps to status 400 with the parameter name. We read this in the 0.31.0 code but did not start a GPU server, so treat the exact text as something to confirm on yours.

What if you cannot upgrade to vLLM 0.31.0 today?

Turn off trust_remote_code if your model works without it, because the advisory says that setting removes the exposure. If you need it, block the field and the open paths in front of vLLM until you can upgrade.

  • Remove --trust-remote-code where you can. Many popular model families are built into Transformers and load without any custom code.
  • Put vLLM behind a reverse proxy that allowlists only the paths your app uses. vLLM's security guide recommends this, and it covers /tokenize and the other unguarded paths.
  • Reject request bodies that contain mm_processor_kwargs at the proxy or API gateway. This is our suggestion, and it mirrors what 0.31.0 does by default.
  • If you host the model repository, turn off community pull requests in its settings, so outsiders cannot add a revision.
  • Never expose the vLLM port to the internet directly. Keep it on a private network that only your app servers can reach.

What should engineers learn about trust_remote_code and per-request options?

The lesson is to keep load-time settings and request-time settings in separate lists. Anything that decides which code or files get loaded belongs to the operator, set once at start. Requests should only tune values on an explicit allowlist.

The naive design passes a request's dictionary of options straight into a library call that accepts any keyword. It looks harmless when the options are image sizes. It stops being harmless the day the library adds a keyword such as code_revision, token or cache_dir.

  • Allowlist, do not blocklist. The advisory recommends an allowlist for processor arguments. A blocklist of code_revision alone would miss the next loader argument.
  • Pass loader settings explicitly. Set revision and code_revision from your config in the call itself, after the request's values, so a request can never override them.
  • Treat trust_remote_code as running a stranger's script. Review the exact commit, pin it and keep the process with the least access you can.
  • Authenticate every path, not prefixes. A prefix check is easy to outgrow when new endpoints such as /tokenize get added later.
  • Add a regression test that sends a loader argument in a request and asserts it is refused. The advisory asks for exactly this test.

This fits a wider pattern in vLLM's recent releases. Version 0.32 also tightened defaults, for example blocking all browser origins until you allow them. Both changes point towards secure defaults, which can break clients that relied on lenient ones.

How do you explain the vLLM remote code execution vulnerability in a system design interview?

Frame it as a trust-boundary bug: a value from an untrusted caller crossed into a code-loading decision. Walk through it in this order.

  1. Name the boundary

    Requests come from callers you do not fully trust. Code loading is an operator decision. The bug let one cross into the other.

  2. Trace the value

    mm_processor_kwargs went from the request into AutoProcessor.from_pretrained, which accepts any keyword, so code_revision chose the Python file to import.

  3. Explain why defences missed it

    The pinned revision did not cover code_revision, and the API key did not cover /tokenize. Both were partial controls.

  4. State the fix and its trade-off

    vLLM 0.31.0 rejects per-request multimodal settings by default. That breaks clients that sent them, and with remote code on, the opt-in flag restores the old risk.

  5. Propose the durable design

    Allowlist request options, set loader arguments only from config, authenticate every route and keep remote code pinned and reviewed.

Questions people ask

Am I affected if I never use --trust-remote-code?
No, according to the advisory. The default trust_remote_code=False setup is not affected, because Transformers will not download and import Python files from the model repository. You should still upgrade to 0.31.0 or newer, since the release also fixes other issues and blocks per-request multimodal settings that can be used to exhaust memory.
Does pinning --revision protect against CVE-2026-114053?
No. Transformers uses code_revision for the custom code file whenever it is given, and only falls back to revision when it is not given. Before 0.31.0, a request could supply code_revision, so the pin covered the model files but not the processor code. vLLM's own --code-revision flag would not help either, because the processor loader never passes it on. We showed this behaviour on Transformers 5.17.0 without downloading anything.
Does the --api-key flag protect the /tokenize endpoint in vLLM?
No. As of vLLM 0.32.0, the key check only covers paths starting with /v1, /v2, /inference or /cohere. vLLM's security guide lists /tokenize, /detokenize, /pooling, /health and others as reachable without a key. Put a reverse proxy in front that allowlists only the paths your application needs.
What does --trust-request-mm-kwargs do in vLLM 0.31.0?
It switches off the new check that rejects per-request mm_processor_kwargs and media_io_kwargs. The default is off. By our reading of the 0.31.0 and 0.32.0 source, turning it on lets a request's code_revision reach the processor loader again. Enable it only when every client that can reach the server is trusted.
Why do GitHub and VulnCheck give different severity scores?
They used different versions of the CVSS scale and different readings of the precondition. GitHub gives 8.1 High under CVSS 3.1 with attack complexity High. VulnCheck gives 9.2 Critical under CVSS 4.0, recording the precondition as attack requirements Present. The advisory text itself suggests 9.8. Your own exposure depends on your flags and model.
Will upgrading to vLLM 0.31.0 break my application?
This fix breaks only clients that send non-empty mm_processor_kwargs or media_io_kwargs in requests. Those calls now get a 400 error. Read the release notes for the other changes in 0.31.0. Move those settings to the server's --mm-processor-kwargs or --media-io-kwargs options instead. Offline use through the Python LLM class is unchanged.

Sources and further reading

Practise this with Lala

Practise