clip mmproj: block_count from a user-supplied --mmproj reaches an unbounded resize on the production Qwen3-VL arm
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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