From 872b2f16e564d1703bf86ea0857ee7bd33437ca0 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 28 Sep 2026 11:37:14 +0100 Subject: [PATCH 1/3] fix: raise on vector index/size errors instead of out-of-bounds access Signed-off-by: Arthit Suriyawongkul --- .../fasttext/tests/test_vector_bounds.py | 49 +++++++++++++++++++ src/fasttext.cc | 7 +++ src/vector.cc | 34 ++++++++----- 3 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 python/fasttext_module/fasttext/tests/test_vector_bounds.py diff --git a/python/fasttext_module/fasttext/tests/test_vector_bounds.py b/python/fasttext_module/fasttext/tests/test_vector_bounds.py new file mode 100644 index 0000000..5e6eba5 --- /dev/null +++ b/python/fasttext_module/fasttext/tests/test_vector_bounds.py @@ -0,0 +1,49 @@ +"""Vector index and size errors must raise, not access out of bounds.""" + +import pytest + +import fasttext_pybind + +from .helpers import build_supervised_model, get_random_data + + +def _model(data=None): + # thread=12: thread <= 10 leaves the input matrix partly uninitialized. + data = data or get_random_data(300, max_vocab_size=100) + return build_supervised_model(data, {"thread": 12, "dim": 16, "verbose": 0}) + + +def _shrink_output_dim(model): + output = model.get_output_matrix()[:, :8].copy() # input dim is 16 + model.set_matrices(model.get_input_matrix(), output) + + +@pytest.mark.parametrize("ind", [-1, 10**9]) +def test_get_input_vector_out_of_range_raises(ind): + with pytest.raises(ValueError, match="out of range"): + _model().get_input_vector(ind) + + +def test_get_word_vector_size_mismatch_raises(): + model = _model() + vec = fasttext_pybind.Vector(model.get_dimension() - 1) + with pytest.raises(ValueError, match="size mismatch"): + model.f.getWordVector(vec, model.words[1]) + + +def test_predict_dim_mismatch_raises(): + model = _model() + _shrink_output_dim(model) + with pytest.raises(ValueError, match="size mismatch"): + model.predict(model.words[1]) + + +def test_training_thread_error_raises(tmp_path): + """Used to call std::terminate: exception escaped a training thread.""" + data = get_random_data(3000, max_vocab_size=600) # cutoff needs >= 256 rows + model = _model(data) + _shrink_output_dim(model) + train_txt = tmp_path / "train.txt" + train_txt.write_text("".join(f"__label__{line}\n" for line in data)) + with pytest.raises(ValueError, match="size mismatch"): + model.quantize(input=str(train_txt), cutoff=300, retrain=True) diff --git a/src/fasttext.cc b/src/fasttext.cc index 6852aa4..9e15287 100644 --- a/src/fasttext.cc +++ b/src/fasttext.cc @@ -798,6 +798,13 @@ namespace fasttext { trainException_ = std::current_exception(); } + catch (const std::exception &) + { + // Any other exception (e.g. a size mismatch thrown by Vector::addRow) + // must not escape a std::thread's entry function: that calls + // std::terminate() instead of surfacing as a Python exception. + trainException_ = std::current_exception(); + } if (threadId == 0) loss_ = state.getLoss(); ifs.close(); diff --git a/src/vector.cc b/src/vector.cc index e5c288a..e877ebd 100644 --- a/src/vector.cc +++ b/src/vector.cc @@ -8,10 +8,9 @@ #include "vector.h" -#include - #include #include +#include #include "matrix.h" @@ -38,36 +37,47 @@ void Vector::mul(real a) { } void Vector::addVector(const Vector& source) { - assert(size() == source.size()); + if (size() != source.size()) { + throw std::invalid_argument("Vector::addVector: size mismatch"); + } for (int64_t i = 0; i < size(); i++) { data_[i] += source.data_[i]; } } void Vector::addVector(const Vector& source, real s) { - assert(size() == source.size()); + if (size() != source.size()) { + throw std::invalid_argument("Vector::addVector: size mismatch"); + } for (int64_t i = 0; i < size(); i++) { data_[i] += s * source.data_[i]; } } void Vector::addRow(const Matrix& A, int64_t i, real a) { - assert(i >= 0); - assert(i < A.size(0)); - assert(size() == A.size(1)); + if (i < 0 || i >= A.size(0)) { + throw std::invalid_argument("Vector::addRow: row index out of range"); + } + if (size() != A.size(1)) { + throw std::invalid_argument("Vector::addRow: size mismatch"); + } A.addRowToVector(*this, i, a); } void Vector::addRow(const Matrix& A, int64_t i) { - assert(i >= 0); - assert(i < A.size(0)); - assert(size() == A.size(1)); + if (i < 0 || i >= A.size(0)) { + throw std::invalid_argument("Vector::addRow: row index out of range"); + } + if (size() != A.size(1)) { + throw std::invalid_argument("Vector::addRow: size mismatch"); + } A.addRowToVector(*this, i); } void Vector::mul(const Matrix& A, const Vector& vec) { - assert(A.size(0) == size()); - assert(A.size(1) == vec.size()); + if (A.size(0) != size() || A.size(1) != vec.size()) { + throw std::invalid_argument("Vector::mul: size mismatch"); + } for (int64_t i = 0; i < size(); i++) { data_[i] = A.dotRow(vec, i); } From 9813360dda4fe1c6f2ef361a34dcb1202c9ad465 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 28 Sep 2026 11:38:05 +0100 Subject: [PATCH 2/3] Add SPDX header Signed-off-by: Arthit Suriyawongkul --- python/fasttext_module/fasttext/tests/test_vector_bounds.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/fasttext_module/fasttext/tests/test_vector_bounds.py b/python/fasttext_module/fasttext/tests/test_vector_bounds.py index 5e6eba5..7b085dd 100644 --- a/python/fasttext_module/fasttext/tests/test_vector_bounds.py +++ b/python/fasttext_module/fasttext/tests/test_vector_bounds.py @@ -1,3 +1,8 @@ +# SPDX-FileContributor: Arthit Suriyawongkul +# SPDX-FileCopyrightText: 2026-present, fasttext-community +# SPDX-FileType: SOURCE +# SPDX-License-Identifier: MIT + """Vector index and size errors must raise, not access out of bounds.""" import pytest From 9bd04c2fc3f93fcdf4ea31ee26845b12aa597917 Mon Sep 17 00:00:00 2001 From: Arthit Suriyawongkul Date: Mon, 28 Sep 2026 11:41:16 +0100 Subject: [PATCH 3/3] Generalize comment Signed-off-by: Arthit Suriyawongkul --- src/fasttext.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/fasttext.cc b/src/fasttext.cc index 9e15287..631cf38 100644 --- a/src/fasttext.cc +++ b/src/fasttext.cc @@ -800,9 +800,8 @@ namespace fasttext } catch (const std::exception &) { - // Any other exception (e.g. a size mismatch thrown by Vector::addRow) - // must not escape a std::thread's entry function: that calls - // std::terminate() instead of surfacing as a Python exception. + // E.g. a Vector size check. An exception escaping a std::thread + // calls std::terminate(); store it so startThreads() rethrows it. trainException_ = std::current_exception(); } if (threadId == 0)