Skip to content

Commit a21e718

Browse files
jiafatomCopilot
andauthored
Validate eos_token_id against vocab_size to prevent OOB write in ApplyMinLength (#2266)
## Summary Fixes an out-of-bounds write (CWE-787 / CWE-129) reachable from a malicious `genai_config.json`. `Search_Cpu::ApplyMinLength` suppresses EOS tokens while the sequence is shorter than `min_length` by writing `std::numeric_limits<float>::lowest()` into the per-beam score row at each configured `eos_token_id`: ```cpp std::span<float> const beam_token_scores = GetScores(i); // vocab_size elements for (auto token_id : params_->config.model.eos_token_id) beam_token_scores[token_id] = std::numeric_limits<float>::lowest(); // unchecked index ``` `eos_token_id` comes straight from config (`src/config.cpp`) with only a `static_cast<int>` and is never bounded against `vocab_size`. A model whose `eos_token_id >= vocab_size` (or `< 0`) drives the store past the end of the `vocab_size`-sized `GetScores()` subspan — a heap-buffer-overflow write on the first decode step (reached via `Generator::GenerateNextToken` -> `ApplyMinLength`). The same pattern exists in the CUDA backend (`Search_Cuda::ApplyMinLength`). ## Changes - **`src/generators.cpp`**: reject any `eos_token_id` outside `[0, vocab_size)` in the `Generator::Generator` validator, mirroring the existing `top_k <= vocab_size` check (added in #2224). - **`src/search.cpp`** and **`src/cuda/search_cuda.cpp`**: defense-in-depth — skip out-of-range ids at the sink in both backends. - **`test/sampling_tests.cpp`**: add `EosTokenIdExceedsVocabSizeThrowsCpu`, which overlays an out-of-range `eos_token_id` and asserts `OgaGenerator::Create` throws instead of proceeding to the OOB write. ## Testing New regression test follows the existing `SamplingTests` overlay pattern (`tiny-random-gpt2-fp32` + `vocab_size`/`eos_token_id` overlay). Local build isn't available in my environment; relying on CI to build and run the C++ unit tests. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b58789f commit a21e718

2 files changed

Lines changed: 37 additions & 8 deletions

File tree

src/generators.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,14 @@ Generator::Generator(const Model& model, const GeneratorParams& params) : model_
405405
if (params.search.num_beams > 1 && params.config.model.vocab_size < 2)
406406
throw std::runtime_error("vocab_size (" + std::to_string(params.config.model.vocab_size) + ") must be 2 or greater when using beam search (num_beams=" + std::to_string(params.search.num_beams) + ")");
407407

408+
// eos_token_id values are used directly as indices into the per-token score
409+
// row (of size vocab_size), e.g. in Search::ApplyMinLength. An out-of-range
410+
// value would cause an out-of-bounds write, so reject it here.
411+
for (auto eos_token_id : params.config.model.eos_token_id) {
412+
if (eos_token_id < 0 || eos_token_id >= params.config.model.vocab_size)
413+
throw std::runtime_error("eos_token_id (" + std::to_string(eos_token_id) + ") must be in range [0, " + std::to_string(params.config.model.vocab_size) + ") (vocab_size)");
414+
}
415+
408416
search_ = CreateSearch(params);
409417
state_ = model.CreateState(search_->GetSequenceLengths(), params); // Search sequence lengths set when creating state
410418
guidance_logits_processor_ = CreateGuidanceLogitsProcessor(*state_); // Could be nullptr if use_guidance (constrained decoding) is not used

test/sampling_tests.cpp

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ TEST(SamplingTests, BatchedSamplingTopPCpu) {
2828
0.1f, 0.1f, 0.1f, 0.1f, 0.6f};
2929

3030
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
31-
config->Overlay(R"({ "model": { "vocab_size" : 5 } })");
31+
config->Overlay(R"({ "model": { "vocab_size" : 5, "eos_token_id" : 0 } })");
3232

3333
auto model = OgaModel::Create(*config);
3434
auto params = OgaGeneratorParams::Create(*model);
@@ -56,7 +56,7 @@ TEST(SamplingTests, BatchedSamplingTopKCpu) {
5656
1.25f, 0.25f, 1.5f, 0.25f, 2.0f};
5757

5858
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
59-
config->Overlay(R"({ "model": { "vocab_size" : 5 } })");
59+
config->Overlay(R"({ "model": { "vocab_size" : 5, "eos_token_id" : 0 } })");
6060

6161
int batch_size = 4;
6262

@@ -102,6 +102,27 @@ TEST(SamplingTests, BeamSearchVocabSizeTooSmallThrowsCpu) {
102102
}
103103
}
104104

105+
// Regression test: an eos_token_id outside [0, vocab_size) must be rejected at
106+
// generator creation time instead of causing an out-of-bounds write in
107+
// Search_Cpu::ApplyMinLength (which uses eos_token_id as a score-row index).
108+
TEST(SamplingTests, EosTokenIdExceedsVocabSizeThrowsCpu) {
109+
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
110+
config->Overlay(R"({ "model": { "vocab_size" : 5, "eos_token_id" : 5 } })");
111+
112+
auto model = OgaModel::Create(*config);
113+
auto params = OgaGeneratorParams::Create(*model);
114+
params->SetSearchOption("max_length", 10);
115+
params->SetSearchOption("min_length", 5);
116+
117+
try {
118+
OgaGenerator::Create(*model, *params);
119+
FAIL() << "Expected std::runtime_error for eos_token_id >= vocab_size";
120+
} catch (const std::runtime_error& e) {
121+
EXPECT_NE(std::string(e.what()).find("eos_token_id"), std::string::npos)
122+
<< "Unexpected error message: " << e.what();
123+
}
124+
}
125+
105126
TEST(SamplingTests, BatchedSamplingTopPAndKCpu) {
106127
std::vector<int32_t> input_ids{0, 1, 2, 3};
107128
std::vector<float> logits_cpu{2.0f, 1.5f, 1.25f, 0.25f, 0.25f,
@@ -110,7 +131,7 @@ TEST(SamplingTests, BatchedSamplingTopPAndKCpu) {
110131
1.25f, 0.25f, 1.5f, 0.25f, 2.0f};
111132

112133
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
113-
config->Overlay(R"({ "model": { "vocab_size" : 5 } })");
134+
config->Overlay(R"({ "model": { "vocab_size" : 5, "eos_token_id" : 0 } })");
114135

115136
int batch_size = 4;
116137

@@ -244,7 +265,7 @@ void Softmax(std::span<float> scores, float temperature) {
244265
void RunSamplingTest(int batch_size, int k, float p, int vocab_size, int num_iter, float temperature, bool use_cuda) {
245266
// --- 1. Setup Model and Generator Parameters ---
246267
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
247-
std::string overlay_json = R"({ "model": { "vocab_size" : )" + std::to_string(vocab_size) + R"( } })";
268+
std::string overlay_json = R"({ "model": { "vocab_size" : )" + std::to_string(vocab_size) + R"(, "eos_token_id" : 0 } })";
248269
config->Overlay(overlay_json.c_str());
249270

250271
if (use_cuda) {
@@ -506,7 +527,7 @@ TEST(SamplingTests, BatchedSamplingTopPCuda) {
506527
int vocab_size = 5;
507528

508529
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
509-
config->Overlay(R"({ "model": { "vocab_size" : 5 } })");
530+
config->Overlay(R"({ "model": { "vocab_size" : 5, "eos_token_id" : 0 } })");
510531
config->ClearProviders();
511532
config->AppendProvider("cuda");
512533
auto model = OgaModel::Create(*config);
@@ -536,7 +557,7 @@ TEST(SamplingTests, BatchedSamplingTopKCuda) {
536557
int vocab_size = 5;
537558

538559
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
539-
config->Overlay(R"({ "model": { "vocab_size" : 5 } })");
560+
config->Overlay(R"({ "model": { "vocab_size" : 5, "eos_token_id" : 0 } })");
540561
config->ClearProviders();
541562
config->AppendProvider("cuda");
542563
auto model = OgaModel::Create(*config);
@@ -569,7 +590,7 @@ TEST(SamplingTests, BatchedSamplingTopPAndKCuda) {
569590
int vocab_size = 5;
570591

571592
auto config = OgaConfig::Create(MODEL_PATH "hf-internal-testing/tiny-random-gpt2-fp32");
572-
config->Overlay(R"({ "model": { "vocab_size" : 5 } })");
593+
config->Overlay(R"({ "model": { "vocab_size" : 5, "eos_token_id" : 0 } })");
573594
config->ClearProviders();
574595
config->AppendProvider("cuda");
575596
auto model = OgaModel::Create(*config);
@@ -728,7 +749,7 @@ struct NvTensorRtRtxTestSetup {
728749

729750
// Create config with vocab_size overlay
730751
auto config = OgaConfig::Create(resolved_path.c_str());
731-
std::string overlay = R"({ "model": { "vocab_size" : )" + std::to_string(vocab_size) + R"( } })";
752+
std::string overlay = R"({ "model": { "vocab_size" : )" + std::to_string(vocab_size) + R"(, "eos_token_id" : 0 } })";
732753
config->Overlay(overlay.c_str());
733754
config->ClearProviders();
734755
config->AppendProvider("NvTensorRtRtx");

0 commit comments

Comments
 (0)