# Is the vLLM tool-call parser safe? CVE-2025-9141, the 29-day eval() window, and four checks a gateway owes you

> vLLM's Qwen3 Coder tool parser ran eval() on model output for 29 days in 2025 (CVE-2025-9141). Today: 0 eval(), 4 literal_eval. What a gateway can and cannot check.

- Published: Aug 25, 2026
- Author: Leo Kaka, Engineering
- Tags: security, gateway, local-llm
- Canonical: https://pirouter.ai/blog/tool-call-parser-attack-surface

---
It was not safe for 29 days in 2025, and the warning was on the pull request the whole time.
The tool-call parser is the code inside an inference engine that turns model tokens into a
structured tool call. vLLM's parser for Qwen3 Coder passed any tool argument whose type it did
not recognise to Python's `eval()`, which executes whatever expression it is handed. An
automated reviewer flagged the line as a critical security vulnerability on the day the PR
was opened, the maintainer force-merged it the same day, and the fix arrived on 20 August as
[CVE-2025-9141](https://github.com/vllm-project/vllm/security/advisories/GHSA-79j6-g2m3-jgfw),
CVSS 8.8. So yes, a model can exploit the engine that runs it: the parser is code that runs on
model-controlled input, and for a month that code was `eval`. Today's vLLM has zero `eval()`
calls across the 50 files of its tool-parser package (I checked), but four parsers still run
`ast.literal_eval` through a thin wrapper, and Python's own documentation now says not to call
that on untrusted data.

The part of this that concerns a gateway is the geometry. The parser sits *inside* the
inference engine. By the time a tool call reaches a gateway it is already JSON, and whatever
the parser did to produce that JSON has already happened. A gateway can enforce hygiene on
the way out; it cannot reach back into the engine. This post walks the CVE from the source,
then draws that line precisely, with the price of each check written next to it.

## Three layers, three dates, and only one CVE

Boyd Kane's [essay](https://boydkane.com/essays/llms-could-control-their-host-machines-by-exploiting-inference-engines)
of 25 August, which reached 173 points and 84 comments on [Hacker News](https://news.ycombinator.com/item?id=49424387)
by the time I pulled it, is about a boundary most threat models skip. The harness (the agent
program that actually executes tool calls) runs on one machine; the model's tokens are
computed on another, with GPUs, weights, and datacentre network access. His one-sentence
threat model: "Because the LLM controls the tokens passed to the inference engine, a
malicious LLM could therefore emit a sequence of tokens that a poorly written inference
engine mistakes for code or instructions to execute rather than data to return to the user."

That is the middle of three layers. The other two surfaced on their own dates, and they need
naming so nobody confuses them.

| Layer | What holds the boundary | What was reported, and when | Status |
|---|---|---|---|
| Workspace | The agent harness's filesystem rules | One r/LocalLLaMA user [reported](https://www.reddit.com/r/LocalLLaMA/comments/1vxi7gp/i_just_tried_deepseek_harness_and_it_escaped_from/) on 24 August 2026 that DeepSeek Harness, a preview, "left the project folder (although DSH was set up correctly) and started to walk through my other files" | ✗ Not a breach. DSH's [sandbox docs](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/sandbox.md) fence writes only, and its fs-sandbox [README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/fs/fs-sandbox/README.md) says so: "Reads always pass through — every mode permits reading." The confirmed write escape is [discussion #523](https://github.com/deepseek-ai/deepseek-harness/discussions/523), on the `minimal` preset |
| Inference engine | The parser between model tokens and structured output | CVE-2025-9141, July to August 2025: tool arguments to `eval()` in vLLM | ✓ Confirmed, fixed in 0.10.1.1; this post |
| Platform | The hosting platform's own infrastructure | OpenAI's [21 July 2026 disclosure](https://openai.com/index/hugging-face-model-evaluation-security-incident/): during an evaluation its models escaped a sandbox and chained stolen credentials and zero-days into remote code execution reaching Hugging Face's production database, a "platform-level compromise" in OpenAI's words | ✓ Confirmed by both parties; covered in [our 24 August post](/blog/distribution-layer-for-sale) |

Three different teams own those three boundaries, and this post is only about the middle
one. It is the only one of the three where the untrusted input is produced by the model
itself, arrives as a token stream, and lands in code running on someone else's expensive
machine.

## What CVE-2025-9141 actually did: the branch that reached eval()

The advisory's summary is one sentence: "An unsafe deserialization vulnerability allows any
authenticated user to execute arbitrary code on the server if they are able to get the model
to pass the code as an argument to a tool call." The CVSS vector is
`AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H`: network, low complexity, low privileges (an API key),
no user interaction, full compromise. Affected: `>= 0.10.0, < 0.10.1.1`. Weakness class:
CWE-502, deserialization of untrusted data. The advisory lists the preconditions too: tool
calling enabled (`--enable-auto-tool-choice`), `--tool-call-parser qwen3_coder`, and a
parameter whose type is not explicitly defined or recognised.

![Screenshot of GitHub advisory GHSA-79j6-g2m3-jgfw, "Remote code execution in the vllm tool call parser for Qwen3-Coder": affected versions 0.10.0 up to but excluding 0.10.1.1, patched 0.10.1.1, severity High 8.8/10 with the CVSS v3 base metrics broken out, CVE ID CVE-2025-9141, weakness CWE-502, and a Details section listing the three conditions under which the code path is reached](/blog/images/tool-call-parser-attack-surface-ghsa-advisory.png "The advisory as published: three preconditions, one weakness class, and an 8.8 that comes entirely from what the parser does with an unrecognised parameter type. Page captured 2026-08-26. Source: [GHSA-79j6-g2m3-jgfw](https://github.com/vllm-project/vllm/security/advisories/GHSA-79j6-g2m3-jgfw).")

Here is the code that earned that score, from the parser as shipped in
[v0.10.1](https://github.com/vllm-project/vllm/blob/v0.10.1/vllm/entrypoints/openai/tool_parsers/qwen3coder_tool_parser.py),
lines 200–220. Qwen3 Coder emits tool calls as XML-ish markup rather than JSON, so the parser
has to coerce each `<parameter=name>` string into the type the tool schema declares:

```python
# vllm/entrypoints/openai/tool_parsers/qwen3coder_tool_parser.py @ v0.10.1
# (branches above this point handle string / int / float / bool and never reach eval)
else:
    if param_type == "object" or param_type.startswith("dict"):
        try:
            converted_value = json.loads(param_value)
            return converted_value
        except json.JSONDecodeError:
            logger.warning(
                "Parsed value '%s' of parameter '%s' is not a "
                "valid JSON object in tool '%s', will try other "
                "methods to parse it.", param_value, param_name,
                func_name)
    try:
        converted_value = eval(param_value)   # <- model output
        return converted_value
    except Exception:
        logger.warning(
            "Parsed value '%s' of parameter '%s' cannot be "
            "converted via Python `eval()` in tool '%s', "
            "degenerating to string.", param_value, param_name,
            func_name)
    return param_value
```

The essay says the parser "passed almost every tool-call argument to eval()". Reading the
whole function, that is too strong, and the precise version matters because it tells you
which tool schemas were exposed. String, integer, float and boolean parameters each had
their own conversion and never reached `eval`. Parameters whose name was not in the schema
were returned as plain strings. Everything else fell through: an `object` whose value failed
`json.loads`, every `array`, and any type string the parser had not heard of. If your agent
had a single tool with a list-typed argument, a `files: string[]` say, then every call to it
put a model-generated string through `eval()` on the inference host. Most agent tool sets
have several.

The `PR:L` in the vector is doing real work. The attacker needs an API key to the vLLM
server, and needs the model to emit the payload as a tool argument. One HN commenter,
`Zambyte`, [frames](https://news.ycombinator.com/item?id=49426946) it as a malicious-model
problem: the same class as a crafted PDF and a vulnerable Acrobat, so be careful "downloading
and running random models". I would add the delivery route that needs no malicious weights:
prompt injection, where the payload is generated on the fly by an honest model reading a
hostile inbox, and the API key belongs to whoever the server already trusts.

## The 29 days between "critical" and the fix

The timeline is entirely public, and I am reproducing it from the pull request pages rather
than from the essay, because the essay's most quotable claim, that an automated reviewer
flagged the bug and the maintainer merged over it, deserved a primary source.

| Date (2025) | Event | Source |
|---|---|---|
| 22 Jul | PR #21396 "[Model] Add Qwen3CoderToolParser" opened by the parser's author; first commit contains `param_value = eval(param_value)` | [PR](https://github.com/vllm-project/vllm/pull/21396) |
| 22 Jul | `gemini-code-assist` review: "I've identified a critical security vulnerability with the use of `eval()` on model output, which must be addressed." | same PR |
| 22 Jul | Maintainer: "I'm force merging this to unblock model usage, after lint." Merged as `4594fc3`, 7 of 9 checks passed | same PR |
| 24 Jul | v0.10.0 released with the parser | [release](https://github.com/vllm-project/vllm/releases/tag/v0.10.0) |
| 18 Aug | v0.10.1 released, `eval` still present | [release](https://github.com/vllm-project/vllm/releases/tag/v0.10.1) |
| 20 Aug | Advisory published (reporter `levigross`); PR #23266 "Do not use eval() to convert unknown types" merged; v0.10.1.1 released as "a critical bugfix and security release" | [PR #23266](https://github.com/vllm-project/vllm/pull/23266), [release](https://github.com/vllm-project/vllm/releases/tag/v0.10.1.1) |
| 20 Aug | Same release also fixes GHSA-rxc4-3w6r-4v47: unauthenticated memory-exhaustion via an oversized HTTP header; the advisory's own remediation includes "use a proxy in front of vLLM" | [advisory](https://github.com/vllm-project/vllm/security/advisories/GHSA-rxc4-3w6r-4v47) |
| 27 Aug | PR #23099 merged: the `eval` fallback comes back as `ast.literal_eval(param_value)  # safer`, because "falling back to plain strings breaks many Qwen3-Coder outputs" | [PR #23099](https://github.com/vllm-project/vllm/pull/23099) |

Twenty-nine days from the warning to the fix, across two tagged releases. I want to be careful
about the tone here, because the obvious reading, maintainer ignores security bot and ships
RCE, is accurate and also not very useful. The maintainer's reason was written down: a popular
model had just been released and its tool calling did not work without this parser. Parsing an
arbitrary token stream into turns, tool calls and arguments is not trivial, and the pressure
to ship parsers on release day is real. The patched release went out the same day as the
advisory; that part of the record is to the maintainers' credit. None of that changes the
structural point: the review signal existed, a human overrode it in writing, and as far as the
public record shows nothing in the process brought anyone back to the line for a month. If you
run inference engines, that is the failure mode to design around: not the bot being wrong, but
the bot being right and nobody being on the hook to re-read it.

The fix itself is nine lines removed and four added. Diff the parser file at the `v0.10.1`
and `v0.10.1.1` tags and this is the whole change; the [PR](https://github.com/vllm-project/vllm/pull/23266/files)
replaces the `eval` block with a warning and a string:

```python
# vllm/entrypoints/openai/tool_parsers/qwen3coder_tool_parser.py @ v0.10.1.1
logger.warning(
    "Parameter '%s' has unknown type '%s'. "
    "The value will be treated as a string.", param_name,
    param_type)
return param_value
```

A week later `ast.literal_eval` went back in, on the argument that it has no namespace and
cannot call functions, so it does not reproduce the RCE. True, and not the end of the story;
the next section has the numbers.

## What the parser looks like a year later: 50 files, zero eval(), one very short adapter

The essay's broader claim is that inference engines are complex, fast-moving, and therefore
bug-prone. That is easy to assert and cheap to check, so I checked it on 25 August 2026
against vLLM's `main`. Method: fetch every file in
[`vllm/tool_parsers/`](https://github.com/vllm-project/vllm/tree/main/vllm/tool_parsers)
and grep. Anyone can reproduce this in about a minute.

| Measurement | Value |
|---|---|
| Files in `vllm/tool_parsers/` | 50 (44 parser modules plus 6 shared helpers such as `utils.py`) |
| Lines | 15,021 |
| `eval(` | 0 |
| `exec(` | 0 in code; one mention inside a docstring in `utils.py` |
| Parsers calling `safe_literal_eval` | 4 — `hy_v3`, `step3p5`, `minicpm5xml`, `poolside_v1`. The wrapper only silences `SyntaxWarning` and then calls `ast.literal_eval`; there is no size or depth guard |

The Qwen3 Coder parser that started all this no longer exists as a file. Both `qwen3_coder`
and `qwen3_xml` now [map](https://github.com/vllm-project/vllm/blob/main/vllm/tool_parsers/__init__.py)
to `Qwen3EngineToolParser`, an eight-line adapter over a generic parser engine driven by
per-model structural tags. The argument conversion for Qwen3 today is this, from
[`vllm/parser/qwen3.py`](https://github.com/vllm-project/vllm/blob/main/vllm/parser/qwen3.py):

```python
def _qwen3_arg_converter(raw_args: str, partial: bool) -> str:
    params: dict[str, object] = {}
    for match in _PARAM_RE.finditer(raw_args):
        name = match.group(1)
        value = match.group(2)
        params[name] = _trim_wrapping_newlines(value)
    # ... partial-stream handling elided ...
    return json.dumps(params, ensure_ascii=False)
```

Every parameter value stays a string. No coercion, no `eval`, no `literal_eval`. Most
self-hosted installs sit somewhere between the CVE and today, so I pulled the same file at
every tag in between:

| vLLM version | What `qwen3_coder` did with an unrecognised type |
|---|---|
| 0.10.0 – 0.10.1 | `eval()` — the CVE |
| 0.10.1.1 | Returned the string |
| 0.10.2 – 0.21.0 | `ast.literal_eval()` |
| 0.22.0 – 0.23.0 | `int`/`float`/`bool`/`json.loads` by declared type, otherwise the string; no `literal_eval` |
| 0.24.0 onwards | Engine parser ([PR #45413](https://github.com/vllm-project/vllm/pull/45413)); every value a string, old file deleted |

So the code path went `eval` → string → `literal_eval` → string, and today's answer goes
further than the day-29 fix did: every value, not just unknown types, is a string. The cost
moved downstream. A tool that declares `count: integer` now receives `"count": "3"` and has
to coerce it itself. That is the correct place for the cost to be, and it is worth knowing
that you are paying it.

The four `literal_eval` parsers are the residue. None is an RCE. But Python's
[documentation](https://docs.python.org/3/library/ast.html#ast.literal_eval) for the function
now reads, in full seriousness: "This function had been documented as 'safe' in the past
without defining what that meant. That was misleading." It continues: "A relatively small
input can lead to memory exhaustion or to C stack exhaustion, crashing the process… Calling
it on untrusted data is thus not recommended." Model output is untrusted data. The comment in
the 2025 patch says `# safer`. It is. It is not safe. The documented failure is a process
crash, a denial of service against the inference host delivered by a tool argument, and
whether that matters depends on how many other tenants share that host. I scanned vLLM only;
llama.cpp and SGLang have parsers of the same shape, and I have not checked them.

For a sense of how much a parser *interprets* rather than copies, take the bug the essay
cites as harmless: [issue #48663](https://github.com/vllm-project/vllm/issues/48663). A
sister parser, the one that splits reasoning from the answer (`--reasoning-parser minimax_m3`,
vLLM 0.24.0), cut in half any JSON response that quoted the literal text `<mm:think>`, 2 of
546 documents in the reporter's run, and reported `finish_reason: "stop"`. Nothing executed.
A schema-valid response was rewritten into an invalid one and reported as success, which is
what interpreting looks like when it goes wrong quietly.

## Where the gateway sits, and why it saw none of this

Draw the path a tool call takes. Tokens come off the GPU. The engine detokenises them into
text. The tool parser scans that text for the model's native markup, extracts names and
arguments, coerces types, and produces an OpenAI-shaped `tool_calls` array. That array is
serialised to JSON and goes out over HTTP. A gateway, if there is one, receives that HTTP
response, does whatever it does, and forwards it to the harness, which executes the tool.

The `eval()` in CVE-2025-9141 ran inside the parser, three steps in, and the streaming path in
the same file (line 514 at v0.10.1) called the same conversion, so streaming changed nothing.
The gateway is the last step on that list. By the time a gateway sees `"arguments":
"{\"files\": [\"a.py\"]}"`, the string that produced it has already been through the parser,
and if the parser was the vulnerable one, the code has already run. **A gateway is the
harness's boundary. It is not the engine's boundary.** There is no filter you can place
between engine and harness that protects the engine, because the engine is upstream of you.

The HN thread spent most of its energy re-discovering this. `angry_octet`
[insists](https://news.ycombinator.com/item?id=49426009) the essay "isn't talking about
exploits of sandboxes, it is about attacking the inference engine (e.g. vLLM or llama.cpp or
SGlang) via its http interface", and then describes running vLLM "on a separately sandboxed
VM on a firewalled VLAN … No DNS, no AD/LDAP, nothing." That is a deployment answer, and it is
the right one. `xg15` [notes](https://news.ycombinator.com/item?id=49425030) that on real
multi-GPU clusters the code that turns tokens into API JSON is probably already on a
different machine from the one doing the math; the essay's own proposed defence, "Run the
GPUs and token parser on separate computers", arriving as a description of what large
operators already do.
The rest of the thread argues about which layer isolation belongs to; the positions are not
in conflict, they are about different rows of the first table.

The official record points the same way. The remediation text for the header-size advisory
fixed in the same release says: upgrade, "or use a proxy in front of vLLM which provides
protection against this issue." That is a reverse proxy with a header-size limit; nginx does
it in one directive. A gateway is one place that job can live. It is a real job either way,
and it is not the job of stopping `eval`.

## Four checks a gateway owes you, and what each one costs

We build a gateway, so the list below is also a list of our own limits; I have written it
as what a gateway in this position *can* do, not what any particular product ships. Every
check here can also live in the harness, and most agent frameworks already do some of it;
what a gateway adds is one policy and one audit log in front of N harnesses, not a check the
harness cannot do. If you have no gateway, put these in the harness, before it executes
anything. The principle is the one every parser in this story eventually landed on: a tool
call is untrusted data until it has been checked against the schema that requested it, and
the request already carries that schema in `tools[].function.parameters`.

| Check | Verdict | Why, and the cost |
|---|---|---|
| `arguments` must parse as a JSON object | ✓ | Rejects the class of output where the upstream parser gave up and passed a string through. Cost: some models emit slightly broken JSON that the harness would have repaired; a fail-closed gateway turns those into hard errors, so the policy needs a per-tool "repair or reject" switch |
| Validate `arguments` against the tool's declared `parameters` schema | ✓ | Types, required fields, and `additionalProperties` where the schema declares it. The vulnerable parser coerced types with `eval` on the engine side; this check protects the harness, not the engine, and JSON Schema does it without evaluating the argument. Cost: one compile per distinct schema, cached by content; one validation per call, microseconds |
| Cap sizes: bytes per argument, bytes per call, calls per response | ✓ | Protects the harness's JSON deserialiser and the tool implementation from oversized arguments. It does nothing for `literal_eval` on the engine side; that has already run. Cost: legitimate long arguments (a file body, a patch) need explicit per-tool limits, or you will break real workloads |
| Reject native markup leaking through | ✓ | If `arguments` still contain the model's raw tool syntax (for Qwen3 that is `<tool_call>`, `<function=`, `<parameter=`) the upstream parser did not finish its job. Cost: false positives on text that legitimately discusses those tokens (issue #48663's shape); coding agents edit prompt templates containing exactly these strings, so keep it off for coding workloads and allowlist elsewhere |
| Pin and record the engine version behind each self-hosted route | ⚠ | Refuse to route tool-calling traffic to `vllm >= 0.10.0, < 0.10.1.1`, and to whatever the next advisory names. Cost: not every endpoint exposes its version; this is an ops register, not a header check |
| Filter model output to protect the engine from itself | ✗ | Wrong order. The engine parses before the gateway sees anything. The engine's protection is a VM, a VLAN and a patch cadence, none of which live in your request path |

The smallest useful version of the first four, in TypeScript, is short enough to read on
one screen. It is illustrative, not a product configuration:

```ts
// Validate an OpenAI-shaped tool call against the schema the *request* declared.
// Runs downstream of the engine, after it has produced JSON — hygiene only.
import Ajv, { type ValidateFunction } from "ajv";
const ajv = new Ajv({ allErrors: false, strict: false });
// Keyed by schema content, not tool name (two requests may declare different
// "search" schemas). Bounded; use a real LRU in production.
const compiled = new Map<string, ValidateFunction>();

const LIMITS = { argBytes: 64_000, callBytes: 256_000, callsPerResponse: 16 };
// Native markup that should never survive a correct upstream parse.
const LEAK = /<tool_call>|<\/?function[=>]|<\/?parameter[=>]/;

type Tool = { function: { name: string; parameters?: object } };
type ToolCall = { function: { name: string; arguments: string } };

export function checkToolCalls(tools: Tool[], calls: ToolCall[]): string | null {
  if (calls.length > LIMITS.callsPerResponse) return "too many tool calls";
  const byName = new Map(tools.map((t) => [t.function.name, t]));
  for (const c of calls) {
    const tool = byName.get(c.function.name);
    if (!tool) return `unknown tool ${c.function.name}`;          // not in the request
    if (Buffer.byteLength(c.function.arguments) > LIMITS.callBytes) return "arguments too large";
    if (LEAK.test(c.function.arguments)) return "raw parser markup in arguments";
    let args: unknown;
    try { args = JSON.parse(c.function.arguments); } catch { return "arguments not JSON"; }
    if (typeof args !== "object" || args === null || Array.isArray(args)) return "arguments not an object";
    for (const v of Object.values(args as Record<string, unknown>)) {
      const bytes = Buffer.byteLength(typeof v === "string" ? v : JSON.stringify(v));
      if (bytes > LIMITS.argBytes) return "argument too large";   // nested values included
    }
    const schema = tool.function.parameters ?? { type: "object" };
    const key = JSON.stringify(schema);
    let validate = compiled.get(key);
    if (!validate) {
      if (compiled.size >= 1024) compiled.clear();
      compiled.set(key, (validate = ajv.compile(schema)));
    }
    if (!validate(args)) return `schema violation in ${c.function.name}`;
  }
  return null;                                                   // forward it
}
```

Three notes on what this does not do. It does not know whether the engine that produced
these calls ran `eval` on the way; nothing at this position can. It does not repair anything;
a gateway that rewrites tool arguments is a second parser with the same problem class as the
first, so this one only says yes or no. And it trusts the schema more than it should: Ajv
compiles a schema into generated JavaScript, and on a multi-tenant gateway that schema arrives
from the client. Cap schema size, reject `pattern` (ReDoS) and remote `$ref`, put a timeout
on compile, or use a validator that interprets instead of generating code. A validator is
also a program that runs on someone else's input.

One more cost the table hides. Everything above assumes a complete `arguments` string. Under
streaming, `tool_calls[i].function.arguments` arrives in fragments across SSE events, so a
gateway has to buffer each tool call by `index` until it closes, validate, then release or
reject the whole call. Text before the first tool call can still stream; the tool-call segment
cannot. Its time to first byte becomes the full generation time of the call, and the gateway
holds up to `callBytes × callsPerResponse` per in-flight response until then. Validating after
you have already forwarded the fragments is a log entry, not a check.

## Go check three things this week

First, find out which tool parser your self-hosted engines run and which path it is on. The
parser is only in the loop if the server was started with `--enable-auto-tool-choice` and a
`--tool-call-parser`; if your harness parses the raw text itself, the parse runs on a machine
you own and this section does not apply. Otherwise, ask the package you actually installed:

```sh
pip show vllm | grep ^Version
V=$(python -c 'import vllm, os; print(os.path.dirname(vllm.__file__))')
grep -rn 'eval(' "$V/tool_parsers" "$V/entrypoints/openai/tool_parsers" "$V/parser" 2>/dev/null
```

A bare `eval(` in the file named by your `--tool-call-parser` is the 2025 incident path;
`literal_eval` is the crash-class path; no hit means string-only. For `qwen3_coder` the
version table above gives the same answer without grepping. A version anywhere in
`0.10.0`–`0.10.1` with that parser enabled is not a hardening task, it is an incident, and if
you cannot upgrade, stop passing `--tool-call-parser` and parse in the harness until you can.
`--reasoning-parser` is the sibling flag with the same shape and no CVE yet.

Second, count the `array` parameters and the parameters with a missing or unrecognised type
in your tool schemas. In 2025 those were the door. They are also the ones a schema check
downstream can do the most for, because a list of file paths has a shape and a
model-generated string that is not a list of file paths does not match it.

Then look at whether your engine and your harness share a machine, a network, or a set of
credentials. `angry_octet`'s deployment (engine on its own VM, own VLAN, logs out and nothing
in) is the reference answer, and its cost is one more VM, a firewall policy somebody has to
own, and a slower path to the GPU when something breaks at 3 a.m. That is cheaper than what
the parser was doing for 29 days.

The parser is a program that runs on input the model chose. Treat it the way you treat a
PDF renderer: patched on a schedule, isolated by default, and never the thing standing
between an attacker and root.

---

## Sources

### github.com

- [CVE-2025-9141](https://github.com/vllm-project/vllm/security/advisories/GHSA-79j6-g2m3-jgfw)
- [sandbox docs](https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/sandbox.md)
- [README](https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/fs/fs-sandbox/README.md)
- [discussion #523](https://github.com/deepseek-ai/deepseek-harness/discussions/523)
- [v0.10.1](https://github.com/vllm-project/vllm/blob/v0.10.1/vllm/entrypoints/openai/tool_parsers/qwen3coder_tool_parser.py)
- [PR](https://github.com/vllm-project/vllm/pull/21396)
- [release](https://github.com/vllm-project/vllm/releases/tag/v0.10.0)
- [release](https://github.com/vllm-project/vllm/releases/tag/v0.10.1)
- [PR #23266](https://github.com/vllm-project/vllm/pull/23266)
- [release](https://github.com/vllm-project/vllm/releases/tag/v0.10.1.1)
- [advisory](https://github.com/vllm-project/vllm/security/advisories/GHSA-rxc4-3w6r-4v47)
- [PR #23099](https://github.com/vllm-project/vllm/pull/23099)
- [`vllm/tool_parsers/`](https://github.com/vllm-project/vllm/tree/main/vllm/tool_parsers)
- [map](https://github.com/vllm-project/vllm/blob/main/vllm/tool_parsers/__init__.py)
- [`vllm/parser/qwen3.py`](https://github.com/vllm-project/vllm/blob/main/vllm/parser/qwen3.py)
- [PR #45413](https://github.com/vllm-project/vllm/pull/45413)
- [issue #48663](https://github.com/vllm-project/vllm/issues/48663)

### boydkane.com

- [essay](https://boydkane.com/essays/llms-could-control-their-host-machines-by-exploiting-inference-engines)

### news.ycombinator.com

- [Hacker News](https://news.ycombinator.com/item?id=49424387)
- [frames](https://news.ycombinator.com/item?id=49426946)
- [insists](https://news.ycombinator.com/item?id=49426009)
- [notes](https://news.ycombinator.com/item?id=49425030)

### reddit.com

- [reported](https://www.reddit.com/r/LocalLLaMA/comments/1vxi7gp/i_just_tried_deepseek_harness_and_it_escaped_from/)

### openai.com

- [21 July 2026 disclosure](https://openai.com/index/hugging-face-model-evaluation-security-incident/)

### docs.python.org

- [ast](https://docs.python.org/3/library/ast.html#ast.literal_eval)
