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