mudler / mudler/vllm.cpp

clip mmproj: block_count from a user-supplied --mmproj reaches an unbounded resize on the production Qwen3-VL arm

Open
#2,995 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
423
Forks
53
Avg merge
20h 26m
Merged PRs (30d)
310

Description

Row: -

ClipMmprojVisionConfig reads the Qwen3-VL vision tower's block count from a user-supplied file and resizes on it without a bound.

src/vllm/model_executor/models/clip_mmproj_gguf.cpp:146

cfg.depth = ReqInt(gguf, kKvBlocks);   // clip.vision.block_count, straight from the file

src/vllm/model_executor/models/clip_mmproj_gguf.cpp:300

vw.blocks.resize(static_cast<size_t>(cfg.depth));

Nothing between them bounds cfg.depth, and nothing rejects a negative value before the cast to size_t.

This arm is production-reachable. src/vllm/entrypoints/model_loader.cpp:2992-2993 calls RefuseUnsupportedClipMmproj then ClipMmprojVisionConfig on params.mmproj_path, which is whatever the user passed to --mmproj. It is currently the ONLY clip mmproj arm with a production call site.

This is not hypothetical

The identical shape was executed on 2026-09-05 in a test that declared clip.vision.block_count = 4000000000. It reached an unguarded resize, allocated about 80 GB of anonymous memory on an 84 GB host, and tripped the GLOBAL Linux OOM killer, which killed unrelated processes twice:

Out of memory: Killed process 28130 (test_deepseek_v)
  total-vm:82911460kB  anon-rss:80197996kB

That test was for the deepseek4v arm, and #2411 bounded that arm's geometry in response. The bound was added to the arm nothing reaches, and the arm a user can actually feed was left alone. The reasoning in that commit — "this path runs on a user-supplied --mmproj" — is true of this arm and not of the one it was applied to.

Suggested shape, not a prescription

Reject on the PARSED value, before any allocation, and name the key and the value in the message. deepseek4v's RequireGeometry in the same file is the local precedent. A guard whose only failure mode is bad_alloc is not a gate: it is a crash, and it passes or dies depending on the free memory of whoever runs it.

Note that a red-first test for this class of defect PERFORMS the unbounded allocation unless the guard lands with it, which is how the incident above happened. Land the bound and its test together, choose a value that proves the guard without being survivable by accident, and run the suite under ( ulimit -v 6000000; ... ).

Every other clip.* field this function reads — kKvEmbd, kKvHeads, kKvFf, kKvProjDim, kKvPatch — is read the same way and deserves the same treatment; num_heads and patch_size reaching zero are division-by-zero rather than allocation faults.

Found by a fresh review of #2411's repair waves, which observed that the OOM lesson had been recorded against the unreachable arm.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/vllm/model_executor/models/clip_mmproj_gguf.cpp, tracing ClipMmprojVisionConfig from the parsed fields through the block resize, and compare the local RequireGeometry precedent. Check the production call in src/vllm/entrypoints/model_loader.cpp:2992-2993, then add a regression test for invalid geometry and run the suite under ulimit -v 6000000. Done means malformed clip values are rejected before allocation with an informative error.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.