Commit 56e09ca1 for tesseract

commit 56e09ca12e751623fe796ce1554ce704bffd2ef0
Author: Stefan Weil <sw@weilnetz.de>
Date:   Fri Aug 21 20:29:37 2026 +0200

    Validate vector counts in GenericVector::read

    The callback form of GenericVector::read read two independent int32
    fields from the file: reserved sized the allocation via reserve(),
    while size_used_ drove the element loop. Neither was capped and no
    size_used_ <= reserved invariant was checked, so a crafted
    .traineddata (e.g. the fontinfo table of a version >= 4 inttemp
    component) performed a heap out-of-bounds write during legacy
    engine initialization.

    Key changes:
    - genericvector.h: reject negative or over-limit reserved
      (matching the 50000000 cap of the DeSerialize overloads) and
      reject size_used_ < 0 or size_used_ > reserved before entering
      the read loop. Legit files always satisfy size_used_ <= reserved,
      as write() persists size_reserved_ first.
    - unittest: add genericvector_test covering size_used_ beyond
      reserved (on unpatched code the ASan build dies on a
      heap-buffer-overflow in the read loop), negative counts, and a
      consistent vector that must still load.

    Reported-by: Zhixi "Jace" Sun <g.mygenie@gmail.com>
    Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/Makefile.am b/Makefile.am
index cb206915..b392066d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1159,6 +1159,7 @@ check_PROGRAMS += equationdetect_test
 endif # !DISABLED_LEGACY_ENGINE
 check_PROGRAMS += fileio_test
 check_PROGRAMS += fullyconnected_test
+check_PROGRAMS += genericvector_test
 check_PROGRAMS += heap_test
 check_PROGRAMS += imagedata_test
 if !DISABLED_LEGACY_ENGINE
@@ -1299,6 +1300,10 @@ fullyconnected_test_SOURCES = unittest/fullyconnected_test.cc
 fullyconnected_test_CPPFLAGS = $(unittest_CPPFLAGS)
 fullyconnected_test_LDADD = $(TESS_LIBS)

+genericvector_test_SOURCES = unittest/genericvector_test.cc
+genericvector_test_CPPFLAGS = $(unittest_CPPFLAGS)
+genericvector_test_LDADD = $(TESS_LIBS)
+
 heap_test_SOURCES = unittest/heap_test.cc
 heap_test_CPPFLAGS = $(unittest_CPPFLAGS)
 heap_test_LDADD = $(TESS_LIBS)
diff --git a/src/ccutil/genericvector.h b/src/ccutil/genericvector.h
index 4a5bbe12..cbe1e203 100644
--- a/src/ccutil/genericvector.h
+++ b/src/ccutil/genericvector.h
@@ -654,10 +654,20 @@ bool GenericVector<T>::read(TFile *f, const std::function<bool(TFile *, T *)> &c
   if (f->FReadEndian(&reserved, sizeof(reserved), 1) != 1) {
     return false;
   }
+  // Arbitrarily limit the number of elements to protect against bad data.
+  const uint32_t limit = 50000000;
+  if (reserved < 0 || static_cast<uint32_t>(reserved) > limit) {
+    return false;
+  }
   reserve(reserved);
   if (f->FReadEndian(&size_used_, sizeof(size_used_), 1) != 1) {
     return false;
   }
+  // size_used_ is an independent file field; without this check the reads
+  // below land past the end of the buffer sized from reserved.
+  if (size_used_ < 0 || size_used_ > reserved) {
+    return false;
+  }
   if (cb != nullptr) {
     for (int i = 0; i < size_used_; ++i) {
       if (!cb(f, data_ + i)) {
diff --git a/unittest/genericvector_test.cc b/unittest/genericvector_test.cc
new file mode 100644
index 00000000..269b00f0
--- /dev/null
+++ b/unittest/genericvector_test.cc
@@ -0,0 +1,91 @@
+///////////////////////////////////////////////////////////////////////
+// File:        genericvector_test.cc
+// Description: Tests that the callback form of GenericVector::read
+//              rejects vectors whose size_used_ exceeds reserved (or
+//              whose counts are negative). reserved sizes the buffer
+//              while size_used_ is an independent file field driving
+//              the element loop, so a crafted .traineddata (e.g. the
+//              fontinfo table of a version >= 4 inttemp component)
+//              performs a heap out-of-bounds write during legacy
+//              engine initialization.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+///////////////////////////////////////////////////////////////////////
+
+#include "include_gunit.h"
+
+#include "genericvector.h"
+#include "serialis.h" // for TFile
+
+#include <cstdint>
+#include <vector>
+
+namespace tesseract {
+namespace {
+
+// Appends raw little-endian values to a byte buffer.
+class ByteWriter {
+public:
+  void PutS32(int32_t v) {
+    uint32_t u = static_cast<uint32_t>(v);
+    for (int i = 0; i < 4; ++i) {
+      data_.push_back(static_cast<char>((u >> (8 * i)) & 0xFF));
+    }
+  }
+  const std::vector<char> &data() const { return data_; }
+
+private:
+  std::vector<char> data_;
+};
+
+// A serialized vector header (reserved, size_used_) followed by the
+// given number of int32 elements.
+std::vector<char> MakeVector(int32_t reserved, int32_t size_used, int32_t num_elements) {
+  ByteWriter w;
+  w.PutS32(reserved);
+  w.PutS32(size_used);
+  for (int32_t i = 0; i < num_elements; ++i) {
+    w.PutS32(i);
+  }
+  return w.data();
+}
+
+// reserved=4 but size_used_=0x10000: on unpatched code the callback
+// loop writes 65536 ints past the 4-int buffer (heap out-of-bounds
+// write).
+TEST(GenericVectorTest, RejectsSizeUsedBeyondReserved) {
+  std::vector<char> bytes = MakeVector(4, 0x10000, 0x10000);
+  TFile fp;
+  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
+  GenericVector<int> v;
+  EXPECT_FALSE(v.read(&fp, [](TFile *f, int *p) { return f->DeSerialize(p); }));
+}
+
+// Negative counts must be rejected; on unpatched code the read
+// "succeeds" and leaves size_used_ negative.
+TEST(GenericVectorTest, RejectsNegativeCounts) {
+  std::vector<char> bytes = MakeVector(-1, -1, 0);
+  TFile fp;
+  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
+  GenericVector<int> v;
+  EXPECT_FALSE(v.read(&fp, [](TFile *f, int *p) { return f->DeSerialize(p); }));
+}
+
+// A consistent vector must still be accepted.
+TEST(GenericVectorTest, AcceptsConsistentVector) {
+  std::vector<char> bytes = MakeVector(4, 2, 2);
+  TFile fp;
+  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
+  GenericVector<int> v;
+  ASSERT_TRUE(v.read(&fp, [](TFile *f, int *p) { return f->DeSerialize(p); }));
+  EXPECT_EQ(v.size(), 2);
+  EXPECT_EQ(v[0], 0);
+  EXPECT_EQ(v[1], 1);
+}
+
+} // namespace
+} // namespace tesseract