[Security] Accept zero-sum DeepSeek-OCR pixel tensors (#59417)

Signed-off-by: Juan Pérez de Algaba <jperezde@redhat.com>

Juan Pérez de Algaba committed Oct 2, 2026 at 11:17 UTC c15672cfb49152ebf65cfd188e71b84593ec5686
3 files changed +90 -5
tests/models/multimodal/processing/test_deepseek_ocr.py
+89 -2
@@ -12,12 +12,22 @@ Run with:
12 pytest tests/models/multimodal/processing/test_deepseek_ocr.py -v
13 """
14
15 +from types import SimpleNamespace
16 +
17 import pytest
18 +import torch
19 from PIL import Image
20 from transformers import AutoTokenizer
21
19 -from vllm.model_executor.models.deepseek_ocr import DeepseekOCRImagePixelInputs
20 -from vllm.transformers_utils.processors.deepseek_ocr import DeepseekOCRProcessor
22 +from vllm.model_executor.models.deepseek_ocr import (
23 + DeepseekOCRForCausalLM,
24 + DeepseekOCRImagePixelInputs,
25 +)
26 +from vllm.model_executor.models.deepseek_ocr2 import DeepseekOCR2ForCausalLM
27 +from vllm.transformers_utils.processors.deepseek_ocr import (
28 + DeepseekOCRProcessor,
29 + ImageTransform,
30 +)
31
32 MODEL_ID = "deepseek-ai/DeepSeek-OCR"
33
@@ -155,3 +165,80 @@ class TestDeepseekOCRInputValidation:
165 def test_missing_images_raises_value_error(self, processor):
166 with pytest.raises(ValueError, match="prompt and images"):
167 processor(prompt="<image>\nDescribe this image.", images=None)
168 +
169 +
170 +def _balanced_normalized_pixels(
171 + batch: int = 1, channels: int = 3, height: int = 64, width: int = 64
172 +) -> torch.Tensor:
173 + """Build a pixel tensor whose values cancel to an exact sum of 0.
174 +
175 + Matches the normalized half-black / half-white case from the DeepSeek-OCR
176 + processor (Normalize(0.5, 0.5) maps 0→-1 and 255→+1).
177 + """
178 + assert width % 2 == 0
179 + pixel_values = torch.ones(batch, channels, height, width)
180 + pixel_values[..., : width // 2] = -1.0
181 + assert pixel_values.sum().item() == 0.0
182 + return pixel_values
183 +
184 +
185 +class TestDeepseekOCRZeroSumPixelsAccepted:
186 + """Zero-sum normalized pixels must still produce validated image inputs.
187 +
188 + Returning None from ``_parse_and_validate_image_input`` makes
189 + ``embed_multimodal`` return None and trips the worker encoder-output
190 + sanity check, killing EngineCore. The zero-sum shortcut is therefore
191 + removed; balanced images must parse like any other valid tensor.
192 + """
193 +
194 + def test_ocr_accepts_balanced_normalized_pixels(self):
195 + pixel_values = _balanced_normalized_pixels()
196 + images_crop = torch.zeros(0, 3, 40, 40)
197 + images_spatial_crop = torch.tensor([[1, 1]])
198 +
199 + image_input = DeepseekOCRForCausalLM._parse_and_validate_image_input(
200 + None,
201 + pixel_values=pixel_values,
202 + images_crop=images_crop,
203 + images_spatial_crop=images_spatial_crop,
204 + )
205 +
206 + assert image_input is not None
207 + assert image_input.data is pixel_values
208 +
209 + def test_ocr2_accepts_balanced_normalized_pixels(self):
210 + pixel_values = _balanced_normalized_pixels()
211 + images_crop = torch.zeros(0, 3, 40, 40)
212 + images_spatial_crop = torch.tensor([[1, 1]])
213 + model = SimpleNamespace(vision_config=SimpleNamespace(image_size=64))
214 +
215 + image_input = DeepseekOCR2ForCausalLM._parse_and_validate_image_input(
216 + model,
217 + pixel_values=pixel_values,
218 + images_crop=images_crop,
219 + images_spatial_crop=images_spatial_crop,
220 + )
221 +
222 + assert image_input is not None
223 + assert image_input.data is pixel_values
224 +
225 + def test_image_transform_half_black_half_white_sums_to_zero(self):
226 + """A 1024×1024 half-black/half-white PNG through ImageTransform
227 + (ToTensor + Normalize(0.5, 0.5)) yields an exact zero sum, and parse
228 + must still accept it.
229 + """
230 + image = Image.new("RGB", (1024, 1024), (0, 0, 0))
231 + image.paste(Image.new("RGB", (512, 1024), (255, 255, 255)), (512, 0))
232 + pixel_values = ImageTransform()(image).unsqueeze(0)
233 + assert pixel_values.sum().item() == 0.0
234 +
235 + images_crop = torch.zeros(0, 3, 640, 640)
236 + images_spatial_crop = torch.tensor([[1, 1]])
237 + image_input = DeepseekOCRForCausalLM._parse_and_validate_image_input(
238 + None,
239 + pixel_values=pixel_values,
240 + images_crop=images_crop,
241 + images_spatial_crop=images_spatial_crop,
242 + )
243 + assert image_input is not None
244 + assert torch.equal(image_input.data, pixel_values)
vllm/model_executor/models/deepseek_ocr.py
-2
@@ -457,8 +457,6 @@ class DeepseekOCRForCausalLM(
457 return None
458 assert isinstance(pixel_values, torch.Tensor)
459 assert images_crop is None or isinstance(images_crop, torch.Tensor)
460 - if torch.sum(pixel_values).item() == 0:
461 - return None
460
461 # Use actual tensor spatial dim instead of hardcoded
462 # vision_config.image_size (1024). The vision encoders (SAM & CLIP)
vllm/model_executor/models/deepseek_ocr2.py
+1 -1
@@ -318,7 +318,7 @@ class DeepseekOCR2ForCausalLM(nn.Module, SupportsMultiModal, SupportsPP, Support
318 images_spatial_crop = kwargs.pop("images_spatial_crop", None)
319 images_crop = kwargs.pop("images_crop", None)
320
321 - if pixel_values is None or torch.sum(pixel_values).item() == 0:
321 + if pixel_values is None:
322 return None
323
324 base_size = self.vision_config.image_size