Commit b2544450 for tesseract

commit b2544450ca01407259ed53c870b7e5e4c67f7789
Author: Stefan Weil <sw@weilnetz.de>
Date:   Fri Oct 9 14:36:37 2026 +0200

    Neutralise file-sourced pointers in ReadAdaptedTemplates before use

    ReadAdaptedTemplates has the same one-line-too-late reset as
    ReadAdaptedClass: the Class[] reset loop sits below the FRead != 1 reject
    branch, and Templates->Templates is never reset at all. TFile::FRead
    clamps to the bytes remaining and still copies them, so any record between
    8 and sizeof(ADAPT_TEMPLATES_STRUCT) bytes takes the reject path with
    Templates->Templates (offset 0) and every Class[] entry holding file
    bytes. ~ADAPT_TEMPLATES_STRUCT reads (Templates)->NumClasses through the
    file-sourced Templates pointer and deletes each Class[i], so a truncated
    .adapted-templates record dereferences file memory.

    Neutralise Templates->Templates and the Class[] array immediately after
    the read, before the reject path can reach the destructor; they are then
    filled with real allocations, which makes the later Class[] reset loop
    redundant.

    Add a ReadAdaptedTemplatesTest case (a Classify instance, mirroring
    normproto_test) that feeds a 64-byte record with a non-null first word:
    without the fix it aborts in ~ADAPT_TEMPLATES_STRUCT, with the fix it
    rejects cleanly.

    Reported-by: Dongha Kim <kdh101800@gmail.com>
    Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/src/classify/adaptive.cpp b/src/classify/adaptive.cpp
index d64d1109..227d6086 100644
--- a/src/classify/adaptive.cpp
+++ b/src/classify/adaptive.cpp
@@ -262,16 +262,21 @@ ADAPT_TEMPLATES_STRUCT *Classify::ReadAdaptedTemplates(TFile *fp) {
   auto Templates = new ADAPT_TEMPLATES_STRUCT;

   // first read in the high level adaptive template struct
-  if (fp->FRead(Templates, sizeof(ADAPT_TEMPLATES_STRUCT), 1) != 1) {
+  const size_t ReadCount = fp->FRead(Templates, sizeof(ADAPT_TEMPLATES_STRUCT), 1);
+  // The read above overwrote every member with file bytes, including the
+  // Templates pointer and the Class[] array. ~ADAPT_TEMPLATES_STRUCT reads
+  // Templates->NumClasses through Templates and deletes each Class[] entry,
+  // so neutralise the file-sourced pointers before the reject path below can
+  // reach the destructor; they are then filled with real allocations.
+  Templates->Templates = nullptr;
+  for (unsigned i = 0; i < MAX_NUM_CLASSES; i++) {
+    Templates->Class[i] = nullptr;
+  }
+  if (ReadCount != 1) {
     tprintf("Bad read of adapted templates!\n");
     delete Templates;
     return nullptr;
   }
-  // The Class[] array was just filled with pointers read from the file;
-  // those are not valid allocations, so reset it before storing real ones.
-  for (unsigned i = 0; i < MAX_NUM_CLASSES; i++) {
-    Templates->Class[i] = nullptr;
-  }

   // then read in the basic integer templates
   Templates->Templates = ReadIntTemplates(fp);
diff --git a/unittest/adaptive_test.cc b/unittest/adaptive_test.cc
index c3ddd8fe..471b2811 100644
--- a/unittest/adaptive_test.cc
+++ b/unittest/adaptive_test.cc
@@ -17,6 +17,7 @@

 #include "adaptive.h" // for ADAPT_CLASS_STRUCT, ReadAdaptedClass
 #include "bitvec.h"   // for WordsInVectorOfSize
+#include "classify.h" // for Classify (ReadAdaptedTemplates)
 #include "serialis.h" // for TFile

 #include <cstdint>
@@ -109,5 +110,36 @@ TEST_F(ReadAdaptedClassTest, PermBranchDoesNotDeleteFilePointers) {
   delete Class;
 }

+// ReadAdaptedTemplates has the same one-line-too-late reset: a truncated
+// ADAPT_TEMPLATES_STRUCT record takes the FRead != 1 reject path with
+// Templates->Templates (offset 0) still holding file bytes, and the Class[]
+// reset loop sits below that branch. ~ADAPT_TEMPLATES_STRUCT reads
+// Templates->NumClasses through the file-sourced pointer, so the record must
+// be neutralised before the destructor runs.
+class ReadAdaptedTemplatesTest : public testing::Test {
+ protected:
+  // A truncated record: the whole struct is fill, with the first word set to
+  // a non-null pointer so the destructor's Templates != nullptr check is true
+  // and it reads (Templates)->NumClasses through it. Shorter than the struct
+  // size, so FRead copies fewer than sizeof and returns != 1.
+  std::vector<char> MakeTruncatedRecord(uint8_t fill, uint64_t first_word) {
+    std::vector<char> data(sizeof(ADAPT_TEMPLATES_STRUCT), static_cast<char>(fill));
+    std::memcpy(data.data(), &first_word, sizeof(uint64_t));
+    data.resize(64); // smaller than sizeof(ADAPT_TEMPLATES_STRUCT) -> FRead != 1
+    return data;
+  }
+
+  Classify classifier_;
+};
+
+// A short record with a non-null first word must be rejected cleanly, not
+// crash the destructor on the file-sourced Templates pointer.
+TEST_F(ReadAdaptedTemplatesTest, RejectPathDoesNotReadFilePointer) {
+  auto record = MakeTruncatedRecord(/*fill=*/0x45, /*first_word=*/0x4545454545454540ULL);
+  TFile fp;
+  ASSERT_TRUE(fp.Open(record.data(), record.size()));
+  EXPECT_EQ(classifier_.ReadAdaptedTemplates(&fp), nullptr);
+}
+
 } // namespace
 } // namespace tesseract