Commit 614e9710 for tesseract

commit 614e971036d2b95d521057ff63d68d6612429b23
Author: Stefan Weil <sw@weilnetz.de>
Date:   Thu Oct 8 09:23:05 2026 +0200

    Neutralise file-sourced pointers in ReadAdaptedClass before use

    ReadAdaptedClass reads an ADAPT_CLASS_STRUCT out of the pre-adapted-
    templates file with one raw FRead, so every member -- not just the data
    but the pointer-bearing ones too -- is left holding file bytes. That
    breaks things in two ways:

    * The TempProtos list and the Config[] union pointers end up holding
      file-controlled values. ~ADAPT_CLASS_STRUCT unconditionally destroys
      all MAX_NUM_CONFIGS Config entries and walks TempProtos, so those file
      bytes reach operator delete: the success path never cleared
      Config[NumConfigs..63], and the NumTempProtos reject path cleared
      Config[] but still held the file-controlled TempProtos.
    * The FRead overwrites the PermProtos and PermConfigs pointers the
      constructor just allocated, before they are ever used, and the
      reassignment right after drops the last reference to them, so the
      constructor's two bitvector allocations leak on every exit path.

    Neutralise the pointer members and release the constructor's bitvectors
    immediately around the read, before any exit path can reach the
    destructor; they are then filled with real allocations. This makes the
    per-path reset loops redundant, so remove them.

    Add adaptive_test (legacy engine), which drives ReadAdaptedClass directly
    with a crafted record whose struct bytes are non-null: the success path
    destructs cleanly and the reject path returns nullptr. Without the fix,
    the success path crashes in ~TEMP_CONFIG_STRUCT on the file-controlled
    pointer and every path leaks the constructor's bitvectors. The CMake
    build discovers the test by globbing unittest/*.cc; register it in the
    autotools build by adding it to check_PROGRAMS and giving it a
    legacy-guarded _SOURCES block that links only $(TESS_LIBS).

    Fixes GHSA-357f-833m-3f83

    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/Makefile.am b/Makefile.am
index fb2a5df4..b60dbc5f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1137,6 +1137,9 @@ unittest_CPPFLAGS += -isystem $(top_srcdir)/unittest/third_party/googletest/goog
 unittest_CPPFLAGS += -isystem $(top_srcdir)/unittest/third_party/googletest/googlemock/include

 check_PROGRAMS = apiexample_test
+if !DISABLED_LEGACY_ENGINE
+check_PROGRAMS += adaptive_test
+endif # !DISABLED_LEGACY_ENGINE
 if ENABLE_TRAINING
 if !DISABLED_LEGACY_ENGINE
 check_PROGRAMS += applybox_test
@@ -1251,6 +1254,12 @@ apiexample_test_CPPFLAGS = $(unittest_CPPFLAGS)
 apiexample_test_LDFLAGS = $(LEPTONICA_LIBS)
 apiexample_test_LDADD = $(TESS_LIBS) $(LEPTONICA_LIBS)

+if !DISABLED_LEGACY_ENGINE
+adaptive_test_SOURCES = unittest/adaptive_test.cc
+adaptive_test_CPPFLAGS = $(unittest_CPPFLAGS)
+adaptive_test_LDADD = $(TESS_LIBS)
+endif # !DISABLED_LEGACY_ENGINE
+
 if !DISABLED_LEGACY_ENGINE
 applybox_test_SOURCES = unittest/applybox_test.cc
 applybox_test_CPPFLAGS = $(unittest_CPPFLAGS)
diff --git a/src/classify/adaptive.cpp b/src/classify/adaptive.cpp
index 96850ab7..92068da6 100644
--- a/src/classify/adaptive.cpp
+++ b/src/classify/adaptive.cpp
@@ -182,7 +182,22 @@ ADAPT_CLASS_STRUCT *ReadAdaptedClass(TFile *fp) {

   // first read high level adapted class structure
   Class = new ADAPT_CLASS_STRUCT;
+  // The FRead below overwrites the whole struct, including the PermProtos and
+  // PermConfigs pointers the constructor just allocated; release those
+  // allocations first or the clobbering leaks them.
+  FreeBitVector(Class->PermProtos);
+  FreeBitVector(Class->PermConfigs);
   fp->FRead(Class, sizeof(ADAPT_CLASS_STRUCT), 1);
+  // The read above overwrote every member with file bytes, including the
+  // pointer-bearing ones (the TempProtos list and the Config[] union
+  // pointers). The destructor unconditionally destroys all MAX_NUM_CONFIGS
+  // Config entries and walks TempProtos, so neutralise the file-sourced
+  // pointers before any exit path can reach it; the members are then filled
+  // with real allocations below.
+  Class->TempProtos = NIL_LIST;
+  for (i = 0; i < MAX_NUM_CONFIGS; i++) {
+    Class->Config[i].Temp = nullptr;
+  }

   // then read in the definitions of the permanent protos and configs
   Class->PermProtos = NewBitVector(MAX_NUM_PROTOS);
@@ -194,14 +209,9 @@ ADAPT_CLASS_STRUCT *ReadAdaptedClass(TFile *fp) {
   fp->FRead(&NumTempProtos, sizeof(int), 1);
   if (NumTempProtos < 0 || NumTempProtos > MAX_NUM_PROTOS) {
     tprintf("Bad read of adapted class!\n");
-    // Reset file-sourced pointers so the destructor does not delete them.
-    for (i = 0; i < MAX_NUM_CONFIGS; i++) {
-      Class->Config[i].Temp = nullptr;
-    }
     delete Class;
     return nullptr;
   }
-  Class->TempProtos = NIL_LIST;
   for (i = 0; i < NumTempProtos; i++) {
     auto TempProto = new TEMP_PROTO_STRUCT;
     fp->FRead(TempProto, sizeof(TEMP_PROTO_STRUCT), 1);
@@ -215,10 +225,6 @@ ADAPT_CLASS_STRUCT *ReadAdaptedClass(TFile *fp) {
   // writing out of bounds.
   if (NumConfigs < 0 || NumConfigs > MAX_NUM_CONFIGS) {
     tprintf("Bad read of adapted class!\n");
-    // Reset file-sourced pointers so the destructor does not delete them.
-    for (i = 0; i < MAX_NUM_CONFIGS; i++) {
-      Class->Config[i].Temp = nullptr;
-    }
     delete Class;
     return nullptr;
   }
diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt
index 66b7f999..af05b341 100644
--- a/unittest/CMakeLists.txt
+++ b/unittest/CMakeLists.txt
@@ -53,6 +53,7 @@ set(TRAINING_TESTS
 set(PANGO_TESTS ligature_table_test.cc pango_font_info_test.cc stringrenderer_test.cc)

 set(LEGACY_TESTS
+    adaptive_test.cc
     applybox_test.cc
     bitvector_test.cc
     equationdetect_test.cc
diff --git a/unittest/adaptive_test.cc b/unittest/adaptive_test.cc
new file mode 100644
index 00000000..a0f6df76
--- /dev/null
+++ b/unittest/adaptive_test.cc
@@ -0,0 +1,97 @@
+///////////////////////////////////////////////////////////////////////
+// File:        adaptive_test.cc
+// Description: Tests that ReadAdaptedClass rejects a corrupt
+//              ADAPT_CLASS_STRUCT without passing file-controlled
+//              pointers to the destructor (which unconditionally deletes
+//              the Config[] entries and walks the TempProtos list).
+//
+// 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 "adaptive.h" // for ADAPT_CLASS_STRUCT, ReadAdaptedClass
+#include "bitvec.h"   // for WordsInVectorOfSize
+#include "serialis.h" // for TFile
+
+#include <cstdint>
+#include <cstring>
+#include <string>
+#include <vector>
+
+namespace tesseract {
+namespace {
+
+// Builds a single ADAPT_CLASS_STRUCT record for ReadAdaptedClass:
+//   [ADAPT_CLASS_STRUCT raw] [PermProtos bitvector] [PermConfigs bitvector]
+//   [int32 NumTempProtos] [int32 NumConfigs]
+// All bytes of the struct are set to a non-null fill value so that the
+// pointer-bearing members (TempProtos, Config[]) hold file-controlled
+// pointers. PermConfigs is left zero (no permanent configs), and
+// NumConfigs controls how many Config[] entries are filled with real
+// (nullptr) allocations.
+std::vector<char> MakeAdaptedClassRecord(int32_t num_temp_protos, int32_t num_configs,
+                                         uint8_t fill) {
+  std::vector<char> data(sizeof(ADAPT_CLASS_STRUCT), static_cast<char>(fill));
+  ADAPT_CLASS_STRUCT *s = reinterpret_cast<ADAPT_CLASS_STRUCT *>(data.data());
+  s->NumPermConfigs = 0;
+  s->MaxNumTimesSeen = 0;
+  // PermProtos and PermConfigs are reallocated by ReadAdaptedClass, so their
+  // initial (file) values are irrelevant; leave them as the fill value.
+  auto append = [&data](const void *p, size_t n) {
+    const char *b = static_cast<const char *>(p);
+    data.insert(data.end(), b, b + n);
+  };
+  std::vector<uint32_t> perm_protos(WordsInVectorOfSize(MAX_NUM_PROTOS), 0);
+  std::vector<uint32_t> perm_configs(WordsInVectorOfSize(MAX_NUM_CONFIGS), 0);
+  append(perm_protos.data(), perm_protos.size() * sizeof(uint32_t));
+  append(perm_configs.data(), perm_configs.size() * sizeof(uint32_t));
+  append(&num_temp_protos, sizeof(int32_t));
+  append(&num_configs, sizeof(int32_t));
+  return data;
+}
+
+class ReadAdaptedClassTest : public testing::Test {
+ protected:
+  // Runs ReadAdaptedClass on the given record. The point is that every exit
+  // path destructs the class cleanly, which ASan verifies; a file-controlled
+  // pointer reaching the destructor aborts the process.
+  ADAPT_CLASS_STRUCT *Read(const std::vector<char> &record) {
+    TFile fp;
+    fp.Open(record.data(), record.size());
+    if (fp.RemainingBytes() != record.size()) {
+      return nullptr;
+    }
+    return ReadAdaptedClass(&fp);
+  }
+};
+
+// Success path with no configs: every Config[] entry is a file-controlled
+// pointer, and TempProtos is a file-controlled (bogus) list. The destructor
+// must not delete them.
+TEST_F(ReadAdaptedClassTest, SuccessPathDoesNotDeleteFilePointers) {
+  auto record = MakeAdaptedClassRecord(/*num_temp_protos=*/0,
+                                       /*num_configs=*/0, /*fill=*/0x42);
+  ADAPT_CLASS_STRUCT *Class = Read(record);
+  EXPECT_NE(Class, nullptr);
+  // Destroying the class must not delete the file-controlled Config[]
+  // entries nor walk the file-controlled TempProtos list.
+  delete Class;
+}
+
+// First reject path: NumTempProtos is negative, so the class is deleted
+// before TempProtos is ever reset. The file-controlled TempProtos list
+// must not be walked by the destructor.
+TEST_F(ReadAdaptedClassTest, RejectPathDoesNotWalkFileTempProtos) {
+  auto record = MakeAdaptedClassRecord(/*num_temp_protos=*/-1,
+                                       /*num_configs=*/0, /*fill=*/0x42);
+  ADAPT_CLASS_STRUCT *Class = Read(record);
+  EXPECT_EQ(Class, nullptr);
+}
+
+} // namespace
+} // namespace tesseract