@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / b997071ec4
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

(security) Enforce audio upload size limit before full file materialization (#45510)

Signed-off-by: jperezde <jperezde@redhat.com>

Juan Pérez de Algaba committed Jun 15, 2026 at 12:25 UTC b997071ec493765abbed990c65843ed05e4708a8
4 files changed +210 -2
tests/entrypoints/speech_to_text/test_upload_size_limit.py new
+142
@@ -0,0 +1,142 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Regression tests for the speech-to-text upload size pre-check.
4 +
5 +These tests verify that over-limit audio uploads are rejected *before*
6 +the full file is materialized into memory, closing the vulnerability
7 +where vLLM would allocate memory proportional to an oversized upload
8 +before enforcing the VLLM_MAX_AUDIO_CLIP_FILESIZE_MB limit.
9 +"""
10 +
11 +from unittest.mock import AsyncMock, patch
12 +
13 +import pytest
14 +
15 +from vllm.entrypoints.speech_to_text.base.utils import read_upload_with_limit
16 +from vllm.exceptions import VLLMValidationError
17 +
18 +
19 +def _make_upload_file(data: bytes, *, size: int | None = None) -> AsyncMock:
20 + """Create a mock UploadFile that yields data in chunks."""
21 + mock = AsyncMock()
22 + mock.size = size
23 +
24 + offset = 0
25 +
26 + async def _read(n: int = -1):
27 + nonlocal offset
28 + if n <= 0:
29 + chunk = data[offset:]
30 + offset = len(data)
31 + return chunk
32 + chunk = data[offset : offset + n]
33 + offset += len(chunk)
34 + return chunk
35 +
36 + mock.read = AsyncMock(side_effect=_read)
37 + return mock
38 +
39 +
40 +@pytest.mark.asyncio
41 +async def test_rejects_oversized_upload_via_content_length():
42 + """File is rejected early when file.size exceeds the limit."""
43 + max_mb = 1
44 + oversized_bytes = max_mb * 1024 * 1024 + 1
45 +
46 + upload = _make_upload_file(b"", size=oversized_bytes)
47 +
48 + with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"):
49 + await read_upload_with_limit(upload, max_size_mb=max_mb)
50 +
51 + upload.read.assert_not_called()
52 +
53 +
54 +@pytest.mark.asyncio
55 +async def test_rejects_oversized_upload_via_chunked_read():
56 + """File is rejected mid-read without materializing the full content."""
57 + max_mb = 1
58 + max_bytes = max_mb * 1024 * 1024
59 + oversized_data = b"\x00" * (max_bytes + 1024)
60 +
61 + upload = _make_upload_file(oversized_data, size=None)
62 +
63 + with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"):
64 + await read_upload_with_limit(upload, max_size_mb=max_mb)
65 +
66 +
67 +@pytest.mark.asyncio
68 +async def test_accepts_file_within_limit():
69 + """File within the limit is read successfully."""
70 + max_mb = 1
71 + data = b"\x00" * (512 * 1024) # 512 KiB, well under 1 MB
72 +
73 + upload = _make_upload_file(data, size=len(data))
74 + result = await read_upload_with_limit(upload, max_size_mb=max_mb)
75 +
76 + assert result == data
77 +
78 +
79 +@pytest.mark.asyncio
80 +async def test_accepts_file_at_exact_limit():
81 + """File exactly at the limit boundary is accepted."""
82 + max_mb = 1
83 + max_bytes = max_mb * 1024 * 1024
84 + data = b"\x00" * max_bytes
85 +
86 + upload = _make_upload_file(data, size=len(data))
87 + result = await read_upload_with_limit(upload, max_size_mb=max_mb)
88 +
89 + assert result == data
90 +
91 +
92 +@pytest.mark.asyncio
93 +async def test_rejects_at_one_byte_over_limit():
94 + """File one byte over the limit is rejected."""
95 + max_mb = 1
96 + max_bytes = max_mb * 1024 * 1024
97 + data = b"\x00" * (max_bytes + 1)
98 +
99 + upload = _make_upload_file(data, size=None)
100 +
101 + with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"):
102 + await read_upload_with_limit(upload, max_size_mb=max_mb)
103 +
104 +
105 +@pytest.mark.asyncio
106 +async def test_uses_env_default_when_no_limit_specified():
107 + """Uses VLLM_MAX_AUDIO_CLIP_FILESIZE_MB when max_size_mb is not given."""
108 + with patch("vllm.entrypoints.speech_to_text.base.utils.envs") as mock_envs:
109 + mock_envs.VLLM_MAX_AUDIO_CLIP_FILESIZE_MB = 2
110 + max_bytes = 2 * 1024 * 1024
111 + oversized_data = b"\x00" * (max_bytes + 1)
112 +
113 + upload = _make_upload_file(oversized_data, size=None)
114 +
115 + with pytest.raises(VLLMValidationError, match="Maximum file size exceeded"):
116 + await read_upload_with_limit(upload)
117 +
118 +
119 +@pytest.mark.asyncio
120 +async def test_chunked_read_does_not_fully_materialize():
121 + """Verify that for large oversized files, we stop reading early.
122 +
123 + The function reads in 64 KiB chunks and aborts once the accumulated
124 + size exceeds the limit. We confirm that far fewer read calls were made
125 + than would be required to fully materialize the file.
126 + """
127 + max_mb = 1
128 + max_bytes = max_mb * 1024 * 1024
129 + large_size = max_bytes * 10 # 10x the limit
130 + data = b"\x00" * large_size
131 +
132 + upload = _make_upload_file(data, size=None)
133 +
134 + with pytest.raises(VLLMValidationError):
135 + await read_upload_with_limit(upload, max_size_mb=max_mb)
136 +
137 + chunk_size = 64 * 1024
138 + calls_for_full_read = large_size // chunk_size + 1
139 + calls_to_exceed_limit = max_bytes // chunk_size + 1
140 + actual_calls = upload.read.call_count
141 + assert actual_calls <= calls_to_exceed_limit + 1
142 + assert actual_calls < calls_for_full_read
vllm/entrypoints/speech_to_text/base/utils.py new
+64
@@ -0,0 +1,64 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""Shared utilities for speech-to-text API routes."""
4 +
5 +from fastapi import UploadFile
6 +
7 +import vllm.envs as envs
8 +from vllm.exceptions import VLLMValidationError
9 +from vllm.utils.mem_constants import KiB_bytes, MiB_bytes
10 +
11 +_READ_CHUNK_SIZE = 64 * KiB_bytes
12 +
13 +
14 +async def read_upload_with_limit(
15 + file: UploadFile,
16 + max_size_mb: float | None = None,
17 +) -> bytes:
18 + """Read an uploaded file enforcing a size limit *before* full
19 + materialization.
20 +
21 + The function first checks the Content-Length header (``file.size``) when
22 + available. Regardless, it then performs a chunked read that stops as soon
23 + as the accumulated bytes exceed the limit, ensuring that an oversized
24 + upload never fully materializes in memory.
25 +
26 + Args:
27 + file: The FastAPI/Starlette ``UploadFile`` object.
28 + max_size_mb: Maximum allowed compressed file size in megabytes.
29 + Defaults to ``envs.VLLM_MAX_AUDIO_CLIP_FILESIZE_MB``.
30 +
31 + Returns:
32 + The file content as ``bytes``.
33 +
34 + Raises:
35 + VLLMValidationError: If the file exceeds the configured size limit.
36 + """
37 + if max_size_mb is None:
38 + max_size_mb = envs.VLLM_MAX_AUDIO_CLIP_FILESIZE_MB
39 +
40 + max_bytes = int(max_size_mb * MiB_bytes)
41 +
42 + if file.size is not None and file.size > max_bytes:
43 + raise VLLMValidationError(
44 + "Maximum file size exceeded",
45 + parameter="audio_filesize_mb",
46 + value=file.size / MiB_bytes,
47 + )
48 +
49 + chunks: list[bytes] = []
50 + total = 0
51 + while True:
52 + chunk = await file.read(_READ_CHUNK_SIZE)
53 + if not chunk:
54 + break
55 + total += len(chunk)
56 + if total > max_bytes:
57 + raise VLLMValidationError(
58 + "Maximum file size exceeded",
59 + parameter="audio_filesize_mb",
60 + value=total / MiB_bytes,
61 + )
62 + chunks.append(chunk)
63 +
64 + return b"".join(chunks)
vllm/entrypoints/speech_to_text/transcription/api_router.py
+2 -1
@@ -13,6 +13,7 @@ from vllm.entrypoints.serve.utils.api_utils import (
13 load_aware_call,
14 with_cancellation,
15 )
16 +from vllm.entrypoints.speech_to_text.base.utils import read_upload_with_limit
17 from vllm.logger import init_logger
18
19 from .protocol import TranscriptionRequest, TranscriptionResponseVariant
@@ -45,7 +46,7 @@ async def create_transcriptions(
46 if handler is None:
47 raise NotImplementedError("The model does not support Transcriptions API")
48
48 - audio_data = await request.file.read()
49 + audio_data = await read_upload_with_limit(request.file)
50
51 generator = await handler.create_transcription(audio_data, request, raw_request)
52
vllm/entrypoints/speech_to_text/translation/api_router.py
+2 -1
@@ -13,6 +13,7 @@ from vllm.entrypoints.serve.utils.api_utils import (
13 load_aware_call,
14 with_cancellation,
15 )
16 +from vllm.entrypoints.speech_to_text.base.utils import read_upload_with_limit
17 from vllm.logger import init_logger
18
19 from .protocol import TranslationRequest, TranslationResponseVariant
@@ -45,7 +46,7 @@ async def create_translations(
46 if handler is None:
47 raise NotImplementedError("The model does not support Translations API")
48
48 - audio_data = await request.file.read()
49 + audio_data = await read_upload_with_limit(request.file)
50
51 generator = await handler.create_translation(audio_data, request, raw_request)
52