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