CVE-2026-33625

CVSS 8.8 · High Patched
Type
Vulnerability research
Published
Reading time
8 min read
Author
rootzero
Lire en français
Lire en français →

CVE-2026-33625: code execution in LMDeploy through eval() of a model’s quant_dtype

A crafted quantization_config in a Hugging Face model made LMDeploy pass quant_dtype to eval(), so loading the model ran the attacker’s Python code.

For your decision

Impact
A malicious model configuration can cause code execution on the inference host.
Conditions
An operator must load the affected model. Code runs with the LMDeploy process permissions.
Recommended action
Upgrade to LMDeploy 0.12.3 or later and review the provenance of loaded models.

Advisory facts

Product
LMDeploy
Package
pypi · lmdeploy
Affected versions
≥ 0.12.1, < 0.12.3
Fixed in
0.12.3
CVSS
8.8 High
Weakness
CWE-95
Access needed
No authentication
Advisory published
Credit
rootzero

Investigation summary

LMDeploy built a Python expression from the quant_dtype field of a model’s quantization_config and passed it to eval() while loading the model configuration. Anyone able to publish a model that an operator later loads, on Hugging Face Hub or any other repository, could run arbitrary code on the inference host with the privileges of the LMDeploy process. Fixed in 0.12.3.

Two eval() calls, one controlled by the model

We asked whether untrusted model configuration could reach a Python evaluator during loading. Searching the PyTorch engine for eval() gave us two leads. We ruled out the general dtype path: in automatic mode the value was reduced to a controlled set. The other call read quant_dtype directly from the model’s quantization_config. That was the trust boundary between a model repository and the inference process.

We first checked the branches: fp8 replaced the model value with a fixed dtype, while awq left it untouched. Our local PoC therefore built a mock Hugging Face configuration with AWQ fields and called the real ModelConfig.from_hf_config() path. A tuple expression resolved torch.float16 and ran a harmless marker command as its second element. The command output appeared before any model weights were read; we did not need to publish or load a complete malicious repository to prove this evaluator path. We reported that bounded result on 11 March 2026. The maintainers replaced eval() with explicit dtype resolution and then handled the AWQ fallback; 0.12.3 shipped on 8 April. The public advisory and CVE-2026-33625 followed in September. The scenario still requires someone or some service to load an untrusted model.

A configuration file before model loading

LMDeploy accepts a local model directory or a Hugging Face repository. Its PyTorch engine reads config.json before inference, so a configuration field is part of the model supply chain.

The model supply chain has spent several years learning which parts of a model repository are dangerous. Pickled weights run code when they are deserialised, which is why the field moved to safetensors. Python files shipped with a model only run when the operator passes trust_remote_code. The JSON configuration is the part everyone treats as inert data: it describes the architecture, the number of layers, the dtype and, for quantised models, a quantization_config block. An operator who refuses pickles and remote code still loads config.json from whoever published the model. That assumption is what this vulnerability breaks.

The eval() call that survived the branch checks

Searching the PyTorch engine of LMDeploy for eval( returned two hits in lmdeploy/pytorch/config.py. The first, in _update_torch_dtype(), converts the model dtype, but the value it evaluates is either the operator’s own dtype argument or, in auto mode, a value forced into float16 or bfloat16. A model cannot steer it. The second, in QuantizationConfig.from_config(), converts quant_dtype, and following that variable backwards shows that it comes straight from the model’s quantization_config with no check on the way.

Root cause

The vulnerable code, as it stands in 0.12.2 (permalink at 17ed9e5):

# lmdeploy/pytorch/config.py (0.12.2), QuantizationConfig.from_config()
quant_method = quant_config['quant_method']
quant_dtype = quant_config.get('quant_dtype', None)
...
if quant_method == 'awq':
    bits = quant_config.get('bits', 4)
    group_size = quant_config.get('group_size', 128)
elif quant_method == 'smooth_quant':
    if quant_dtype is None:
        quant_dtype = 'int8'
elif quant_method == 'fp8':
    fmt = quant_config.get('fmt', 'e4m3')
    if fmt == 'e4m3':
        quant_dtype = 'float8_e4m3fn'
    elif fmt == 'e5m2':
        quant_dtype = 'float8_e5m2'
    else:
        raise TypeError(f'Unsupported fp8 fmt: {fmt}')
else:
    raise TypeError(f'Unsupported quant method: {quant_method}')

if quant_dtype is not None:
    quant_dtype = eval(f'torch.{quant_dtype}')

quant_config is the quantization_config dictionary of the Hugging Face config (or of its llm_config or text_config sub-config for multimodal models). The fp8 branch overwrites quant_dtype with a fixed string, so it is safe. The awq branch never touches quant_dtype, and the smooth_quant branch only fills it in when it is missing. In both cases the string written by the model author reaches eval() unchanged.

This is CWE-95, improper neutralisation of directives in dynamically evaluated code (eval injection). The repository advisory carries CWE-95; the entry in the GitHub Advisory Database lists CWE-400 (uncontrolled resource consumption), which describes a different class of flaw and looks like a mislabel.

QuantizationConfig.from_config() is called unconditionally while LMDeploy builds its ModelConfig, before any weight is read and regardless of trust_remote_code. The flaw first appears in 0.12.1: the quant_dtype evaluation is absent from config.py in 0.12.0, which matches the affected range of the advisory.

Exploitation

The attacker publishes a model repository whose config.json declares an AWQ quantisation and a quant_dtype that is a Python expression:

{
  "quantization_config": {
    "quant_method": "awq",
    "quant_dtype": "float16, __import__('os').system('id')"
  }
}

LMDeploy then evaluates the following line:

eval("torch.float16, __import__('os').system('id')")

Python parses it as a tuple, a perfectly valid expression. The first element resolves to the expected dtype, and the second imports os and runs the shell command while the tuple is being built. Whatever happens to the model load afterwards, the command has already run. A real attacker would keep the rest of the configuration valid (architecture, head counts, weights) so that the model loads and serves normally after the payload has executed.

The operator only has to point LMDeploy at the repository:

lmdeploy serve api_server attacker/awesome-llm-awq

The proof of concept in the report drives the real code path in-process: it builds a mock Hugging Face config carrying the payload, with plausible values for the head counts, hidden size and vocabulary, and calls ModelConfig.from_hf_config(). The command output (uid=0(root) gid=0(root) groups=0(root)) appears before any model weight is involved.

Impact

The advisory scores the flaw 8.8 with AV:N/AC:L/PR:N/UI:R. The attacker needs no account on the target: publishing a model on a public hub is enough, which is why the vector is network and no privileges are required. User interaction is required because someone with access to the inference host has to choose to load that model. The original report describes this as “no user interaction beyond running lmdeploy serve”, which is the same fact seen from the operator’s side: loading the model is the only interaction, and it is the normal use of the tool.

The code runs with the privileges of the LMDeploy process, with full impact on confidentiality, integrity and availability. In practice an inference host is a valuable foothold: GPU capacity, the weights of the models it serves (sometimes proprietary fine-tunes), the Hugging Face token used to pull private models, and whatever cloud credentials the deployment gives it. The payload can also tamper with the model that is about to be served, which is harder to notice than a crashed server.

The interaction requirement weakens where model choice is delegated. A platform that lets its users request a model by repository id, or an evaluation pipeline that pulls new quantised checkpoints automatically, turns “an operator loads a model” into “an attacker submits a repository id”, and the attack then needs no human decision at all.

The fix

LMDeploy 0.12.3, released on 2026-04-08, fixes the flaw. The change is in pull request #4447 (“fix security issues”, merge commit 71d64a3), which replaces both eval() calls in config.py with an attribute lookup followed by a type check:

# lmdeploy/pytorch/config.py (0.12.3), QuantizationConfig.from_config()
resolved_quant_dtype = getattr(torch, quant_dtype, None)
if not isinstance(resolved_quant_dtype, torch.dtype):
    raise ValueError(f'Invalid quant dtype "{quant_dtype}" resolved from model config; '
                     'expected a torch.dtype attribute on torch.')
quant_dtype = resolved_quant_dtype

getattr() treats the string as an attribute name and never parses it as code, and the isinstance(..., torch.dtype) check rejects attributes of torch that exist but are not dtypes, such as functions or submodules. The same pattern now guards config.dtype in _update_torch_dtype(), so the second evaluation site is closed as well, even though a model could not reach it.

The rewrite dropped the old if quant_dtype is not None guard, so an ordinary AWQ model without a quant_dtype field would have passed None to getattr(). Pull request #4503 (“Fix torch awq”, commit 2539582), merged the day before the release, sets a bfloat16 placeholder in the AWQ branch when the field is missing. PR #4447 also adds a check that blocks non-global IP addresses when the vision pipeline fetches media from a URL. That change addresses server-side request forgery and is unrelated to this CVE.

Detection and mitigation

  1. Upgrade LMDeploy to 0.12.3 or later on every host and in every container image that loads models, including evaluation and quantisation jobs that do not serve traffic.

  2. Treat a model repository as code until it has been reviewed. Load models from an internal mirror or a pinned revision (a commit hash) that someone has inspected, and avoid letting end users or automated jobs pick arbitrary repository ids.

  3. Hunt for crafted configurations in the local Hugging Face cache and in model directories. A legitimate quant_dtype is a plain torch dtype name, so any value containing other characters deserves a look:

    find ~/.cache/huggingface/hub /models -name config.json -exec \
      jq -r '.. | objects | select(has("quant_dtype")) | "\(input_filename): \(.quant_dtype)"' {} + \
      | grep -Ev ': [a-z0-9_]+$'
  4. Run inference servers as an unprivileged user, with read-only model storage, no cloud credentials beyond what serving requires, and outbound network access limited to the model registry. Set HF_HUB_OFFLINE=1 on production servers that should never download a new model at start-up.

  5. Watch process telemetry for shells or system utilities (sh, bash, curl, wget) whose parent is the Python process running LMDeploy, for instance with an auditd or Falco rule on execve. A model load has no reason to spawn them.

Timeline

Date Event
2026-02-13 LMDeploy 0.12.1 released, first affected version
2026-03-11 Private report to the maintainers through a GitHub security advisory
2026-03-18 LMDeploy 0.12.2 released, still affected
2026-03-27 Pull request #4447 merged (71d64a3), eval() replaced by getattr()
2026-04-07 Pull request #4503 merged (2539582), AWQ placeholder dtype
2026-04-08 LMDeploy 0.12.3 released
2026-09-16 Advisory GHSA-3hmm-rh5q-gwwr published, CVE-2026-33625 assigned
2026-09-18 Advisory reviewed into the GitHub Advisory Database

References

  1. Vendor advisory · CVE-2026-33625 github.com/InternLM/lmdeploy/security/advisories/GHSA-3hmm-rh5q-gwwr
  2. NVD entry · CVE-2026-33625 nvd.nist.gov/vuln/detail/CVE-2026-33625
  3. CVE.org record · CVE-2026-33625 www.cve.org/CVERecord?id=CVE-2026-33625
  4. MITRE CWE-95 cwe.mitre.org/data/definitions/95.html
  5. Project repository github.com/InternLM/lmdeploy

Cite this advisory

rootzero. “CVE-2026-33625: code execution in LMDeploy through eval() of a model’s quant_dtype”. Vulnerability research, rootzero, 16 September 2026. https://rootzero.eu/en/research/cve/cve-2026-33625-lmdeploy-eval-rce/

#Vulnerability research

Have your code audited before someone else does.

Your team needs to decide what to fix before a release or assess a critical dependency. We scope an engagement around that decision, with evidence and explained remediation priorities.