pydantic-ai: SSRF blocklist bypass via IPv4-compatible, SIIT/IVI, and local NAT64 IPv6 addresses (incomplete fix of CVE-2026-46678)
Summary
When an application using Pydantic AI opts a URL into force_download='allow-local' (which disables the default block on private/internal IPs) and runs on a network that routes the affected IPv6 transition forms (NAT64- or ISATAP-configured networks), the cloud-metadata blocklist could be bypassed by encoding the metadata IP in an IPv6 transition form that the previous fix did not decode — IPv4-compatible IPv6 (::a.b.c.d), the NAT64 RFC 8215 local-use prefix (64:ff9b:1::/48), operator-chosen NAT64 prefixes, or ISATAP. The IPv6 wrapper is then delivered to the underlying IPv4 metadata endpoint, exposing cloud IAM short-term credentials.
The bypass is exploitable only in environments whose network actually routes these forms — NAT64-configured networks (IPv6-only or dual-stack-with-NAT64 deployments, including some Kubernetes setups) for the NAT64 variants, or networks with an ISATAP tunnel for ISATAP. A standard dual-stack cloud VM or container does not route them and is not affected in practice. The IPv4-compatible and Teredo variants are deprecated and addressed as defense-in-depth.
This is an incomplete fix of GHSA-cqp8-fcvh-x7r3 / CVE-2026-46678 (itself a follow-up to CVE-2026-25580). The prior remediation decoded only IPv4-mapped IPv6, 6to4, and the NAT64 well-known prefix; the metadata guarantee did not hold for the remaining transition forms.
Severity
MEDIUM — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:N/A:N = 6.8
Same impact metrics and narrow attack surface as the parent advisory (AC:H): exploitation requires the application to have opted into allow-local on a URL influenced by untrusted input, and the NAT64/ISATAP variants additionally require the deployment network to route those forms.
CWE-918: Server-Side Request Forgery (SSRF)
Affected Versions
| Package | Vulnerable | Patched |
|---|---|---|
| pydantic-ai | >= 1.56.0, < 1.102.0; >= 2.0.0b1, < 2.0.0b3 | 1.102.0; 2.0.0b3 |
| pydantic-ai-slim | >= 1.56.0, < 1.102.0; >= 2.0.0b1, < 2.0.0b3 | 1.102.0; 2.0.0b3 |
These transition forms have not been decoded since SSRF protection was introduced in 1.56.0.
Who Is Affected
Users are affected only if their application explicitly opts a FileUrl (ImageUrl, AudioUrl, VideoUrl, DocumentUrl) into force_download='allow-local' on a URL that is, or could be, influenced by untrusted input.
Beyond that precondition, the affected encodings only reach a metadata endpoint in environments whose network actually routes them. The broadly-routable IPv4-mapped form was addressed in 1.99.0 (CVE-2026-46678); the additional forms addressed here require a NAT64-configured network (IPv6-only or dual-stack-with-NAT64 deployments, including some Kubernetes setups) for the NAT64 variants, or an ISATAP tunnel for the ISATAP variant. The IPv4-compatible and Teredo forms are deprecated and not routed by modern stacks; they are addressed as defense-in-depth. Most deployments on a standard dual-stack cloud VM or container are therefore not exploitable in practice, but the fix restores the "always blocked" guarantee for the environments that are.
Users are not affected if they use any of the bundled integrations to ingest user input, because they do not propagate force_download from external data:
Agent.to_web / clai web
VercelAIAdapter
AGUIAdapter / Agent.to_ag_ui
Applications that only download from developer-controlled URLs are not affected.
Remediation
Upgrade to 1.102.0 or later (or 2.0.0b3 or later on the 2.0 pre-release line). The cloud-metadata and private-IP blocklists now decode the embedded IPv4 of every standardized IPv6 transition form before evaluating it — IPv4-mapped, IPv4-compatible, 6to4, NAT64 across all prefix lengths (including the RFC 8215 local-use prefix and operator-chosen prefixes), ISATAP, and Teredo. The set of always-blocked cloud metadata/credential endpoints has also been expanded across providers.
Workaround for Unpatched Versions
Avoid passing force_download='allow-local' on any URL that could be influenced by untrusted input. If developers must, resolve the hostname themselves and validate the result against their own metadata blocklist — including IPv6 transition forms — before constructing the FileUrl.
Credits
Reported by @SnailSploit.
golang.org/x/crypto: Invoking VerifiedPublicKeyCallback permissions skip enforcement
Previously, CVE-2024-45337 fixed an authorization bypass for misused ssh server configurations; if any other type of callback is passed other than public key, then the source-address validation would be skipped.
golang.org/x/crypto vulnerable to auth bypass via unenforced @revoked status
Previously, a revoked 'SignatureKey' belonging to a CA was not correctly checked for revocation. Now, both the 'key' and 'key.SignatureKey' are checked for @revoked.
golang.org/x/crypto vulnerable to infinite loop on large channel writes
When writing data larger than 4GB in a single Write call on an SSH channel, an integer overflow in the internal payload size calculation caused the write loop to spin indefinitely, sending empty packets without making progress. The size comparison now uses int64 to prevent truncation.
golang.org/x/crypto: FIDO/U2F security key physical presence check can be bypassed
The Verify() method for FIDO/U2F security key types (sk-ecdsa-sha2-nistp256@openssh.com, sk-ssh-ed25519@openssh.com) did not check the User Presence flag. Signatures generated without physical touch were accepted, allowing unattended use of a hardware security key. To restore the previous behavior, return a "no-touch-required" extension in Permissions.Extensions from PublicKeyCallback.
golang.org/x/crypto: Invoking pathological RSA/DSA parameters may cause DoS
The RSA and DSA public key parsers did not enforce size limits on key parameters. A crafted public key with an excessively large modulus or DSA parameter could cause several minutes of CPU consumption during signature verification. This could be triggered by unauthenticated clients during public key authentication. RSA moduli are now limited to 8192 bits, and DSA parameters are validated per FIPS 186-2.
golang.org/x/crypto: Invoking client can cause server deadlock on unexpected responses
A malicious SSH peer could send unsolicited global request responses to fill an internal buffer, blocking the connection's read loop. The blocked goroutine could not be released by calling Close(), resulting in a resource leak per connection. Unsolicited global responses are now discarded.
golang.org/x/crypto: Invoking memory leak when rejecting channels can lead to DoS
An authenticated SSH client that repeatedly opened channels which were rejected by the server caused unbounded memory growth, eventually crashing the server process and affecting all connected users. Rejected channels are now properly removed from the connection's internal state and released for garbage collection.
golang.org/x/crypto is vulnerable to invoking server panic during CheckHostKey/Authenticate flow
SSH servers which use CertChecker as a public key callback without setting IsUserAuthority or IsHostAuthority could be caused to panic by a client presenting a certificate. CertChecker now returns an error instead of panicking when these callbacks are nil.
golang.org/x/crypto vulnerable to invoking bypass of certificate restrictions
When an SSH server authentication callback returned PartialSuccessError with non-nil Permissions, those permissions were silently discarded, potentially dropping certificate restrictions such as force-command after a second factor succeeded. Returning non-nil Permissions with PartialSuccessError now results in a connection error.
golang.org/x/crypto: Invoking byte arithmetic causes underflow and panic
An incorrectly placed cast from bytes to int allowed for server-side panic in the AES-GCM packet decoder for well-crafted inputs.
golang.org/x/crypto doesn't drop invoking agent constraints when forwarding keys
When adding a key to a remote agent constraint extensions such as restrict-destination-v00@openssh.com were not serialized in the request. Destination restrictions were silently stripped when forwarding keys, allowing unrestricted use of the key on the remote host. The client now serializes all constraint extensions. Additionally, the in-memory keyring returned by NewKeyring() now rejects keys with unsupported constraint extensions instead of silently ignoring them.
golang.org/x/crypto doesn't enforce invoking key constraints
The in-memory keyring returned by NewKeyring() silently accepted keys with the ConfirmBeforeUse constraint but never enforced it. The key would sign without any confirmation prompt, with no indication to the caller that the constraint was not in effect. NewKeyring() now returns an error when unsupported constraints are requested.
golang.org/x/crypto: Invoking pathological inputs can lead to client panic
For certain crafted inputs, a 'ed25519.PrivateKey' was created by casting malformed wire bytes, leading to a panic when used.
Multer vulnerable to Denial of Service via incomplete cleanup of aborted uploads
Impact
A vulnerability in Multer allows an attacker to trigger a Denial of Service (DoS) by aborting or sending malformed multipart uploads, causing orphaned partial files to accumulate on disk when using diskStorage.
Patches
Users should upgrade to 2.2.0, 3.0.0-alpha.2 or higher
Workarounds
None
vLLM: OOM Denial of Service via Audio Decompression Bomb
Summary
vLLM's /v1/audio/transcriptions endpoint limits compressed upload size but not decoded PCM output. A 25MB OPUS file expands to ~14.9GB of float32 PCM at decode time. Tested on vLLM v0.19.0.
Details
SpeechToTextProcessor rejects uploads over VLLM_MAX_AUDIO_CLIP_FILESIZE_MB (default 25MB) based on compressed byte length, but the audio decoder in audio.py accumulates all decoded frames into memory with no size limit before returning:
``python
speech_to_text.py L184-189
if len(audio_data) / 1024 ** 2 > self.max_audio_filesize_mb:
raise VLLMValidationError(...)
y, sr = load_audio(buf, sr=self.asr_config.sample_rate) # decoded size unchecked
audio.py L77-107
chunks: list[npt.NDArray] = []
for frame in container.decode(stream):
chunks.append(frame.to_ndarray())
audio = np.concatenate(chunks, axis=-1).astype(np.float32) # single contiguous allocation
`
A 25MB OPUS file at 6kbps encodes ~8.7 hours of audio. Decoding produces ~5.7GB of float32 PCM (232x amplification), and np.concatenate then allocates a second contiguous array, bringing peak RSS to ~14.9GB from a single request. SpeechToTextConfig.max_audio_clip_s` (default 30s) applies only after the full decode and does not prevent the allocation.
Impact
An unauthenticated attacker can exhaust server memory with a small number of concurrent requests, each a valid upload within the documented size limit. Severity was assessed with reference to prior OOM vulnerability reports in vLLM.
Fix
A fix for this vulnerability was merged here: https://github.com/vllm-project/vllm/pull/44970
vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via Anthropic router
vLLM: incomplete CVE-2026-22778 fix leaks PIL repr addresses via the Anthropic API router
Researcher: Kai Aizen — SnailSploit (@SnailSploit), Adversarial & Offensive Security Research
Severity: CVSS 3.1 5.3 (Medium) AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
Target: https://github.com/vllm-project/vllm
---
Summary
The fix for CVE-2026-22778 / GHSA-4r2x-xpjr-7cvv (PRs #31987 and #32319) introduced sanitize_message and applied it at four FastAPI exception-handling sites in the OpenAI router. The sanitizer strips object-repr memory addresses (<_io.BytesIO object at 0x7a95e299e750> → <_io.BytesIO object>) before error messages reach the client, defeating the ASLR-bypass primitive that CVE-2026-22778 chained with a libopenjp2 heap overflow for RCE.
The fix is incomplete: response paths added to vLLM at or after the same time as the fix continue to echo str(exc) directly to clients without sanitize_message. The original Stage 1 primitive — sending malformed image bytes so PIL raises UnidentifiedImageError whose message contains the BytesIO object repr — reaches all of them unmodified and leaks the heap address verbatim in the response body.
All five lines below are present in main HEAD (771e1e48b, 2026-05-26).
Affected sites
Current main HEAD (771e1e48b, 2026-05-26):
| # | File | Line | Code |
|---|---|---|---|
| 1 | vllm/entrypoints/anthropic/api_router.py | 78 | message=str(e), (inside POST /v1/messages exception handler) |
| 2 | vllm/entrypoints/anthropic/api_router.py | 124 | message=str(e), (inside POST /v1/messages/count_tokens) |
| 3 | vllm/entrypoints/anthropic/serving.py | 808 | error=AnthropicError(type="internal_error", message=str(e)), (SSE streaming converter) |
| 4 | vllm/entrypoints/speech_to_text/realtime/connection.py | 75 | await self.send_error(str(e), "processing_error") (WebSocket event loop) |
| 5 | vllm/entrypoints/speech_to_text/realtime/connection.py | 265 | await self.send_error(str(e), "processing_error") (WebSocket generation loop) |
Why the global exception handler does not save these paths
api_server.py registers a catch-all app.exception_handler(Exception)(exception_handler) at line 262, and that handler calls create_error_response(exc) which DOES apply sanitize_message. However, FastAPI exception handlers fire only on unhandled exceptions that propagate out of a route function.
All affected HTTP paths catch Exception inside the route coroutine and construct the response themselves:
``python
vllm/entrypoints/anthropic/api_router.py:71-81 (POST /v1/messages)
try:
generator = await handler.create_messages(request, raw_request)
except Exception as e:
logger.exception("Error in create_messages: %s", e)
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
content=AnthropicErrorResponse(
error=AnthropicError(
type="internal_error",
message=str(e), # <-- unsanitized
)
).model_dump(),
)
`
Because the exception is caught and a JSONResponse is returned in-route, every registered FastAPI exception handler — including the sanitizing global one — is bypassed. The WebSocket path bypasses it for a different reason: WebSocket frames don't traverse FastAPI's HTTP exception handler chain at all.
Reachability — the same primitive as the parent CVE
The Anthropic Messages API accepts image content parts in the request body (type: "image" with base64 source.data or type: "image_url"). Image bytes are passed to the same multimodal loader used by the OpenAI router. Malformed bytes cause PIL.Image.open to raise:
`
UnidentifiedImageError: cannot identify image file <_io.BytesIO object at 0x7a95e299e750>
`
The exception propagates up through handler.create_messages into the except Exception as e: at api_router.py:75. str(e) returns the exception message verbatim, including the address. The address ends up in the error.message field of the JSON response body returned to the attacker. ASLR entropy on the affected process drops from ~4 billion to ~8 candidates, identically to CVE-2026-22778 Stage 1.
The same primitive is reachable on POST /v1/messages/count_tokens (route #2), inside the SSE streaming converter when an exception is raised mid-stream (route #3), and over the realtime speech-to-text WebSocket when audio decoder or generation paths raise an exception containing any object repr (routes #4, #5).
Chronology — these are scope misses, not legacy code
2026-01-09: PR #31987 (aa125ecf0) introduces sanitize_message and applies it to OpenAI router HTTP exception handlers.
2026-01-15 (six days later): PR #32369 (4c1c501a7) adds vllm/entrypoints/anthropic/api_router.py containing line 78's message=str(e). The fix was not applied to the new router.
2026-03-02 (~two months later): PR #35588 (9a87b0578) adds the Anthropic count_tokens endpoint, replicating the same message=str(e) pattern at line 124.
2026-05-12 (~four months later): PR #42370 (d37e25ffb) consolidates speech-to-text entrypoints and the realtime WebSocket uses send_error(str(e), ...) for both error paths.
2026-05-26: current main HEAD, all five lines still present.
CVSS v3.1
AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N — Base 5.3 (MEDIUM)
The parent CVE-2026-22778 was 9.8 (CRITICAL) because it chained the Stage 1 leak with a Stage 2 libopenjp2 heap overflow. Stage 2 is patched in OpenCV ≥ 4.13.0 (PR #32668 bumped the requirement), so Stage 1 alone here is a partial ASLR-bypass info-disclosure primitive rather than a complete RCE. Deployments that ship vLLM alongside an older OpenCV/libopenjp2 (system OpenCV on long-LTS distros, custom Docker images, downstream rebuilds) re-enable the full chain via the affected endpoints.
CWE
CWE-532 (Insertion of Sensitive Information into Log File / Error Message). Same CWE assigned to the parent CVE-2026-22778.
Remediation
1. Apply sanitize_message symmetrically to the five sites
`python
vllm/entrypoints/anthropic/api_router.py — add at top:
from vllm.entrypoints.utils import sanitize_message
Line 78 (POST /v1/messages) and Line 124 (count_tokens):
message=sanitize_message(str(e)),
`
`python
vllm/entrypoints/anthropic/serving.py — add at top:
from vllm.entrypoints.utils import sanitize_message
Line 808:
error=AnthropicError(type="internal_error", message=sanitize_message(str(e))),
`
`python
vllm/entrypoints/speech_to_text/realtime/connection.py — add at top:
from vllm.entrypoints.utils import sanitize_message
Lines 75 and 265:
await self.send_error(sanitize_message(str(e)), "processing_error")
`
2. Tighten the regex (defense in depth)
The current regex r" at 0x[0-9a-f]+>" is narrow — it only matches the exact CPython builtin object-repr suffix in lowercase hex with a trailing >. Future Python versions, C extensions, or custom __repr__ methods could produce non-matching formats that re-enable the leak:
`python
vllm/entrypoints/utils.py
def sanitize_message(message: str) -> str:
# Strip any standalone hex address; downstream observers don't need them.
return re.sub(r"\b0x[0-9a-fA-F]{6,}\b", "0x?", message)
`
3. Future-proofing: consider a response middleware
Both the route-local exception handling pattern (Anthropic router) and the WebSocket path bypass FastAPI's exception handler chain. A response-level middleware that always invokes sanitize_message on outgoing error bodies would prevent this class of regression entirely.
Affected versions
All vLLM versions containing vllm/entrypoints/anthropic/api_router.py (introduced 2026-01-15 in PR #32369).
All vLLM versions containing vllm/entrypoints/speech_to_text/realtime/connection.py (introduced 2026-05-12 in PR #42370).
Confirmed present in main HEAD 771e1e48b (2026-05-26).
References
Parent advisory: https://github.com/vllm-project/vllm/security/advisories/GHSA-4r2x-xpjr-7cvv (CVE-2026-22778)
Fix PRs to model the patch on: #31987, #32319, #32668
vllm/entrypoints/utils.py:sanitize_message: https://github.com/vllm-project/vllm/blob/771e1e48b/vllm/entrypoints/utils.py#L323-L326
vllm/entrypoints/anthropic/api_router.py leak site: https://github.com/vllm-project/vllm/blob/771e1e48b/vllm/entrypoints/anthropic/api_router.py#L78
Steps to reproduce
1. Clone the target: git clone --depth 1 https://github.com/vllm-project/vllm
2. Run the proof of concept (PoC.py`) against the cloned source.
3. Observe the result shown under Verified result below.
Credit
Kai Aizen — SnailSploit (@SnailSploit). Adversarial & Offensive Security Research.
Fix
A fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/45119
vLLM: GGUF dequantize kernel int truncation exposes uninitialized GPU memory in multi-tenant serving
Summary
Integer truncation of tensor dimensions in vLLM's GGUF dequantize kernels (csrc/quantization/gguf/gguf_kernel.cu) causes partial tensor processing. The output tensor is allocated at full size via torch::empty (uninitialized memory), but the dequantize CUDA kernel processes only a truncated number of elements. The unfilled portion of the output tensor retains whatever was previously in GPU memory. In multi-tenant inference deployments, this residual GPU memory may contain tensor data from other users' inference requests, constituting information disclosure.
Root Cause
The to_cuda_ggml_t function pointer type at ggml-common.h:1067 declares its element count parameter as int (32-bit):
``cpp
using to_cuda_ggml_t = void ()(const void __restrict__ x,
dst_t __restrict__ y,
int k, // 32-bit
cudaStream_t stream);
`
All dequantize kernel functions (dequantize_block_cuda, dequantize_row_q2_K_cuda, etc. in dequantize.cuh) inherit this int k parameter and use it as the kernel launch grid size:
`cpp
static void dequantize_block_cuda(..., const int k, cudaStream_t stream) {
const int num_blocks = (k + 2CUDA_DEQUANTIZE_BLOCK_SIZE - 1) / (2CUDA_DEQUANTIZE_BLOCK_SIZE);
dequantize_block<<<num_blocks, CUDA_DEQUANTIZE_BLOCK_SIZE, 0, stream>>>(vx, y, k);
}
`
In ggml_dequantize() at gguf_kernel.cu:85, the caller passes m n (an int64_t product) to this int k parameter:
`cpp
at::Tensor DW = torch::empty({m, n}, options); // line 80: full-size, UNINITIALIZED
// ...
to_cuda((void)W.data_ptr(), (scalar_t)DW.data_ptr(), m n, stream); // line 85: mn truncated to int
`
When m n > INT_MAX, the truncated k is smaller than the actual tensor size. The kernel processes k elements. The remaining (m n) - k elements in DW are never written and contain stale GPU memory.
This is a single root cause -- the int type on the k parameter in to_cuda_ggml_t -- with a single fix: change int k to int64_t k. All dequantize functions inherit this type through the same typedef.
Affected Functions
All in csrc/quantization/gguf/gguf_kernel.cu:
| Function | Line | Allocation | Info Disclosure? |
|----------|------|-----------|-----------------|
| ggml_dequantize | 74 | torch::empty({m, n}) at line 80 | Yes -- mn truncated to int k at line 85 |
| ggml_mul_mat_vec_a8 | 91 | torch::empty({vecs, row}) at line 99 | Yes -- int col = X.sizes()[1] at line 94 |
| ggml_mul_mat_a8 | 207 | torch::empty({batch, row}) at line 215 | Yes -- int col = X.sizes()[1] at line 210 |
| ggml_moe_a8 | 279 | torch::empty({tokenstop_k, row}) at line 289 | Yes -- int col = X.sizes()[1] at line 285 |
All four functions allocate output tensors with torch::empty (uninitialized) and then run CUDA kernels that use truncated dimension values as loop bounds. The unfilled portion of each output tensor retains stale GPU memory.
ggml_moe_a8_vec (line 382) uses torch::zeros instead of torch::empty, so it is not affected by the info disclosure variant.
Impact: Information Disclosure in Multi-Tenant Serving
vLLM is designed for multi-tenant inference serving. GPU memory is reused across requests from different users. When the dequantize kernel partially fills an output tensor:
1. The output tensor DW is allocated with torch::empty -- the buffer contains whatever was previously in that GPU memory region
2. The dequantize kernel fills only a truncated portion of the buffer
3. The unfilled portion retains residual data from prior GPU operations, which may include tensor data from other users' inference requests
4. The contaminated tensor proceeds through the model computation
5. No error or warning is generated -- the partial fill is silent
This is a confidentiality violation. In shared inference deployments (the primary vLLM use case), one user's inference data can leak into another user's model computation through residual GPU memory.
Attacker Control
The attacker crafts a GGUF model file with weight tensor dimensions whose product exceeds INT_MAX (e.g., a matrix with shape [65536, 65536] gives m * n = 4,294,967,296`). The model is hosted on HuggingFace or any model hub. The victim loads the model with vLLM for inference serving. The truncation happens automatically during model weight dequantization.
Fix
A fix for this vulnerability was added here: https://github.com/vllm-project/vllm/pull/44971
vLLM: image EXIF Rotation & PNG tRNS Transparency Not Normalized, Causing Mismatch Between Model Input and Expectations
Summary
Issue 1: EXIF orientation not normalized → The image orientation processed by the model differs from how humans view it, introducing interpretation bias.
Issue 2: PNG tRNS not explicitly flattened before converting to RGB → After conversion, transparent/semi-transparent pixels are rendered unexpectedly, making otherwise subtle overlay elements visible and distorting the input content. (This attack is similar to AlphaDog: RGBA handling is already correct in vLLM, but since tRNS permits RGB images, the correct processing path isn’t taken.)
Issue 3 : Pillow only loads the first frame when loading APNG or GIF files.
---
Root Cause
Rotation: After opening an image, ImageOps.exif_transpose is not called to normalize EXIF orientation.
Transparency: Only RGBA→RGB is flattened with a background; PNGs carrying tRNS in P/L/RGB + tRNS and other non-RGBA modes take the image.convert("RGB") path, which implicitly discards/remaps transparency semantics.
---
Affected Code
https://github.com/vllm-project/vllm/blob/16b37f3119918c1e5a39f303e0d0892c65c07a90/vllm/multimodal/image.py#L77-L84
https://github.com/vllm-project/vllm/blob/16b37f3119918c1e5a39f303e0d0892c65c07a90/vllm/multimodal/image.py#L37-L43
https://github.com/vllm-project/vllm/blob/16b37f3119918c1e5a39f303e0d0892c65c07a90/vllm/multimodal/image.py#L26-L34
> Current state: ImageOps.exif_transpose is not used. (Although the rescale_image_size function (https://github.com/vllm-project/vllm/blob/main/vllm/multimodal/image.py#L14) exists and includes a transpose parameter, I’ve found that it doesn’t seem to be called anywhere outside the test directory.)
> Call order: _convert_image_mode runs first; if the conditions are met, convert_image_mode is called.
>
> Issue: Only the “RGBA → RGB” path is explicitly flattened. P, L, or RGB with tRNS all fall back to image.convert("RGB"). For PNGs that include tRNS, convert("RGB") directly produces 24-bit RGB, leading to:
>
> P mode: The transparent index becomes an actual RGB color (often black, white, or an undefined background), so transparency is lost.
> L/LA and RGB + tRNS: convert("RGB") doesn’t composite against a chosen background first, so elements that relied on transparency to be hidden or softened become solid.
Impact & Scope
Impact: Pixels the model sees can diverge from operator expectations (due to orientation or transparency handling), potentially altering downstream reasoning.
Scope: The image I/O and mode-conversion paths in vllm/multimodal/image.py. The existing RGBA→RGB flattening is correct; the issues center on missing EXIF normalization and non-RGBA tRNS not being explicitly composited.
Case
EXIF: http://qiniu.funxingzuo.top/exif_orient_180.jpg
tRNS: http://qiniu.funxingzuo.top/hello.png
Fix
A fix for this vulnerability was merged here: https://github.com/vllm-project/vllm/pull/44974
vLLM: temperature=NaN and temperature=Infinity bypass validation and propagate to GPU kernels
Summary
All temperature validation gates use comparison operators (<, >), which silently evaluate to False for NaN and for positive Infinity in Python's IEEE 754 float semantics. Both values pass every guard and propagate to GPU sampling kernels, where they produce undefined behavior or CUDA errors that can crash the inference worker. Note: -Infinity is correctly caught.
Root Cause
sampling_params.py:384:
``python
if 0 < self.temperature < _MAX_TEMP: # NaN → False; +Inf → False
`
sampling_params.py:462:
`python
if self.temperature < 0.0: # NaN → False; +Inf → False
raise VLLMValidationError(...)
`
No math.isnan() or math.isinf() check exists anywhere in sampling_params.py.
Python semantics (verified): float('nan') < 0.0 → False, float('inf') < 0.0 → False.
Impact
Crash of inference worker on GPU kernel execution with NaN/Inf softmax input, degrading service for all concurrent users.
Remediation
Add math.isfinite(self.temperature) check in _verify_args()`. Reject non-finite float values with a 400 error.
Fix
A fix for this vulnerability was merged here: https://github.com/vllm-project/vllm/pull/45116