Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions python/sglang/srt/managers/tokenizer_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,12 @@ def _validate_one_request(

# Validate generation-specific fields
if isinstance(obj, GenerateReqInput):
# Only user-supplied token ids can be out of range; ids produced by
# the tokenizer are always valid, so skip the scan for text requests.
if obj.input_ids is not None:
self._validate_input_ids_in_vocab(
input_ids, self.model_config.vocab_size
)
Comment on lines 1034 to +1041

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The vocabulary validation for user-supplied input_ids is currently restricted to GenerateReqInput requests. However, EmbeddingReqInput requests can also accept user-supplied input_ids (e.g., via the embedding endpoints). If an out-of-bounds token ID is supplied to an embedding request, it will also trigger a CUDA device-side assert or index out-of-bounds error, crashing the server or causing silent corruption.

Moving this validation outside of the isinstance(obj, GenerateReqInput) block ensures that both generation and embedding requests are properly validated.

Suggested change
# Validate generation-specific fields
if isinstance(obj, GenerateReqInput):
# Only user-supplied token ids can be out of range; ids produced by
# the tokenizer are always valid, so skip the scan for text requests.
if obj.input_ids is not None:
self._validate_input_ids_in_vocab(
input_ids, self.model_config.vocab_size
)
# Only user-supplied token ids can be out of range; ids produced by
# the tokenizer are always valid, so skip the scan for text requests.
if obj.input_ids is not None:
self._validate_input_ids_in_vocab(
input_ids, self.model_config.vocab_size
)
# Validate generation-specific fields
if isinstance(obj, GenerateReqInput):

self._validate_token_ids_logprob(obj)
if (
obj.return_hidden_states
Expand Down Expand Up @@ -1117,20 +1123,20 @@ def _validate_token_ids_logprob(self, obj: GenerateReqInput) -> None:
def _validate_input_ids_in_vocab(
self, input_ids: Union[List[int], List[List[int]]], vocab_size: int
) -> None:
# Handle both single sequence and batch of sequences
if isinstance(input_ids[0], list):
# Batch of sequences
for seq in input_ids:
if any(id >= vocab_size for id in seq):
# Out-of-range ids index the model's embedding out of bounds (on CUDA a
# device-side assert that crashes the whole server); an id < 0 wraps to
# the end of the embedding table (silently wrong). Both are checked.
if not input_ids:
return
# Handle both a single sequence and a batch of sequences.
seqs = input_ids if isinstance(input_ids[0], list) else [input_ids]
for seq in seqs:
for token_id in seq:
if not 0 <= token_id < vocab_size:
raise ValueError(
f"The input_ids {seq} contains values greater than the vocab size ({vocab_size})."
f"input_ids contains an out-of-vocabulary token id "
f"{token_id}; valid range is [0, {vocab_size})."
)
else:
# Single sequence
if any(id >= vocab_size for id in input_ids):
raise ValueError(
f"The input_ids {input_ids} contains values greater than the vocab size ({vocab_size})."
)

def _create_tokenized_object(
self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Unit test for TokenizerManager._validate_input_ids_in_vocab.

Bug: user-supplied input_ids (native /generate and OpenAI token-id prompts) were
never validated -- _validate_input_ids_in_vocab existed but had zero callers, and
even it only checked id >= vocab_size (missing id < 0). An out-of-range id indexes
the model's embedding out of bounds (on CUDA a device-side assert that crashes the
whole server); a negative id silently wraps to the end of the table.
"""

import unittest
from types import SimpleNamespace

from sglang.srt.managers.tokenizer_manager import TokenizerManager
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase

register_cpu_ci(est_time=1, suite="base-a-test-cpu")

VOCAB_SIZE = 32000


class TestValidateInputIds(CustomTestCase):
def setUp(self):
self.tm = TokenizerManager.__new__(TokenizerManager)
self.tm.model_config = SimpleNamespace(vocab_size=VOCAB_SIZE)

def _validate(self, ids):
self.tm._validate_input_ids_in_vocab(ids, VOCAB_SIZE)

def test_above_vocab_raises(self):
with self.assertRaises(ValueError):
self._validate([1, 2, VOCAB_SIZE])
with self.assertRaises(ValueError):
self._validate([1, 2, 2_000_000_000])

def test_negative_raises(self):
# The previous check only guarded id >= vocab_size, so this used to pass.
with self.assertRaises(ValueError):
self._validate([1, -1, 2])

def test_batch_out_of_range_raises(self):
with self.assertRaises(ValueError):
self._validate([[1, 2], [3, VOCAB_SIZE + 5]])
with self.assertRaises(ValueError):
self._validate([[1, 2], [-4, 5]])

def test_in_range_passes(self):
self._validate([0, 1, VOCAB_SIZE - 1])
self._validate([[0, 1], [2, VOCAB_SIZE - 1]])

def test_empty_passes(self):
self._validate([])


if __name__ == "__main__":
unittest.main()
Loading