[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