From 94239f59c5d693723b61dcdf8d786872ec230574 Mon Sep 17 00:00:00 2001 From: Jordi Mas Date: Sat, 29 Aug 2026 18:34:24 +0200 Subject: [PATCH 1/5] Validate model variable sizes before allocation --- src/models/model.cc | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/models/model.cc b/src/models/model.cc index 945fc5b15..2ec71142b 100644 --- a/src/models/model.cc +++ b/src/models/model.cc @@ -2,6 +2,8 @@ #include +#include + #include "ctranslate2/models/model_factory.h" #include "ctranslate2/ops/ops.h" #include "ctranslate2/utils.h" @@ -619,7 +621,6 @@ namespace ctranslate2 { // Load the variables. const auto num_variables = consume(model_file); - model->_variable_index.reserve(num_variables); // check config for tensor parallel bool multi_query_attention = false; @@ -655,9 +656,30 @@ namespace ctranslate2 { num_bytes = consume(model_file) * item_size; } - StorageView variable(std::move(shape), dtype); - if (num_bytes != variable.size() * variable.item_size()) + const dim_t item_size = StorageView(dtype).item_size(); + // Check that the claimed payload fits before allocating the StorageView. + const auto payload_position = model_file.tellg(); + if (payload_position == std::streampos(-1)) throw std::runtime_error("Variable '" + name + "' has an invalid payload size"); + model_file.seekg(0, std::ios::end); + const auto model_end = model_file.tellg(); + model_file.clear(); + model_file.seekg(payload_position); + if (model_end == std::streampos(-1) || model_end < payload_position) + throw std::runtime_error("Variable '" + name + "' has an invalid payload size"); + dim_t variable_size = 1; + for (const dim_t dim : shape) { + if (dim == 0 || variable_size > std::numeric_limits::max() / dim) + throw std::runtime_error("Variable '" + name + "' has an invalid shape"); + variable_size *= dim; + } + if (item_size == 0 + || variable_size > std::numeric_limits::max() / item_size + || num_bytes != variable_size * item_size + || static_cast(num_bytes) > static_cast(model_end - payload_position)) + throw std::runtime_error("Variable '" + name + "' has an invalid payload size"); + + StorageView variable(std::move(shape), dtype); consume(model_file, num_bytes, static_cast(variable.buffer())); if (tensor_parallel) { int outer_dim = 0; From 55d4138cfdf18c92fc500f4aab1c5c2dbdf71f14 Mon Sep 17 00:00:00 2001 From: Jordi Mas Date: Sat, 29 Aug 2026 18:46:29 +0200 Subject: [PATCH 2/5] Payload test --- src/models/model.cc | 3 ++- tests/model_test.cc | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/models/model.cc b/src/models/model.cc index 2ec71142b..9f5822bd7 100644 --- a/src/models/model.cc +++ b/src/models/model.cc @@ -657,7 +657,8 @@ namespace ctranslate2 { } const dim_t item_size = StorageView(dtype).item_size(); - // Check that the claimed payload fits before allocating the StorageView. + // The payload is the raw tensor data that follows this variable header. + // Check that it fits in the file before allocating the StorageView. const auto payload_position = model_file.tellg(); if (payload_position == std::streampos(-1)) throw std::runtime_error("Variable '" + name + "' has an invalid payload size"); diff --git a/tests/model_test.cc b/tests/model_test.cc index 718befd13..938ffd974 100644 --- a/tests/model_test.cc +++ b/tests/model_test.cc @@ -1,4 +1,5 @@ #include +#include #include @@ -8,6 +9,22 @@ TEST(ModelTest, ContainsModel) { ASSERT_TRUE(models::contains_model(default_model_dir())); } +TEST(ModelTest, RejectsSerializedVariableZeroDimension) { + const std::string model_bin( + "\x06\x00\x00\x00\x0c\x00WhisperSpec\0\x03\x00\x00\x00" + "\x01\x00\x00\x00\x07\x00weight\0\x02\x01\x00\x00\x00" + "\x00\x00\x00\x00\x01\x00\x00\x00\x00", 49); + + models::ModelMemoryReader reader("test_model"); + reader.register_file("model.bin", model_bin); + try { + models::Model::load(reader); + FAIL() << "Expected runtime_error"; + } catch (const std::runtime_error& e) { + EXPECT_NE(std::string(e.what()).find("invalid payload size"), std::string::npos); + } +} + TEST(ModelTest, UpdateDecoderOutputLayer) { auto model = models::Model::load(default_model_dir())->as_sequence_to_sequence(); auto& decoder = dynamic_cast(*model).decoder(); From 38dcd35e2cce5046ea95e943f0de0e043432403a Mon Sep 17 00:00:00 2001 From: Jordi Mas Date: Sun, 30 Aug 2026 10:42:52 +0200 Subject: [PATCH 3/5] Fix in-memory model loading after payload validation --- src/models/model_reader.cc | 30 ++++++++++++++++++++++++++++++ tests/model_test.cc | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/models/model_reader.cc b/src/models/model_reader.cc index 628f9f318..d9f15b461 100644 --- a/src/models/model_reader.cc +++ b/src/models/model_reader.cc @@ -40,6 +40,36 @@ namespace ctranslate2 { char* p = const_cast(base); setg(p, p, p + size); } + + std::streampos seekoff(std::streamoff off, + std::ios_base::seekdir dir, + std::ios_base::openmode which) override { + if (!(which & std::ios_base::in)) + return std::streampos(-1); + + char* begin = eback(); + char* end = egptr(); + std::streamoff base = 0; + if (dir == std::ios_base::beg) + base = 0; + else if (dir == std::ios_base::cur) + base = gptr() - begin; + else if (dir == std::ios_base::end) + base = end - begin; + else + return std::streampos(-1); + + const std::streamoff next = base + off; + if (next < 0 || next > end - begin) + return std::streampos(-1); + + setg(begin, begin + next, end); + return next; + } + + std::streampos seekpos(std::streampos pos, std::ios_base::openmode which) override { + return seekoff(static_cast(pos), std::ios_base::beg, which); + } }; struct imemstream : virtual membuf, std::istream { diff --git a/tests/model_test.cc b/tests/model_test.cc index 938ffd974..0c0ce32f8 100644 --- a/tests/model_test.cc +++ b/tests/model_test.cc @@ -21,7 +21,7 @@ TEST(ModelTest, RejectsSerializedVariableZeroDimension) { models::Model::load(reader); FAIL() << "Expected runtime_error"; } catch (const std::runtime_error& e) { - EXPECT_NE(std::string(e.what()).find("invalid payload size"), std::string::npos); + EXPECT_NE(std::string(e.what()).find("invalid shape"), std::string::npos); } } From 3540a2596606a67eb42c98d5d9a990168c4b6b8f Mon Sep 17 00:00:00 2001 From: Jordi Mas Date: Sun, 30 Aug 2026 11:48:17 +0200 Subject: [PATCH 4/5] Disable Hugging Face Xet downloads in wheel tests --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1611e2e79..c8a0b6229 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -325,6 +325,8 @@ jobs: pip install ${{ matrix.wheel_pattern }} - name: Test Python wheel + env: + HF_HUB_DISABLE_XET: "1" run: | pytest -v python/tests/ --ignore=python/tests/test_fairseq.py --ignore=python/tests/test_opennmt_py.py --ignore=python/tests/test_opennmt_tf.py From 0028a4a8452fd89a799090e2886341c52814259f Mon Sep 17 00:00:00 2001 From: Jordi Mas Date: Sun, 30 Aug 2026 13:09:42 +0200 Subject: [PATCH 5/5] Revert "Disable Hugging Face Xet downloads in wheel tests" This reverts commit 3540a2596606a67eb42c98d5d9a990168c4b6b8f. --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8a0b6229..1611e2e79 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -325,8 +325,6 @@ jobs: pip install ${{ matrix.wheel_pattern }} - name: Test Python wheel - env: - HF_HUB_DISABLE_XET: "1" run: | pytest -v python/tests/ --ignore=python/tests/test_fairseq.py --ignore=python/tests/test_opennmt_py.py --ignore=python/tests/test_opennmt_tf.py