diff mbox series

[meta-oe,wrynose,17/24] tesseract: patch CVE-2026-88048

Message ID 20260924043315.1663186-17-ankur.tyagi85@gmail.com
State New
Headers show
Series [meta-oe,wrynose,1/24] rabbitmq-c: patch CVE-2026-44235 | expand

Commit Message

Ankur Tyagi Sept. 24, 2026, 4:33 a.m. UTC
From: Ankur Tyagi <ankur.tyagi85@gmail.com>

Details:
https://nvd.nist.gov/vuln/detail/cve-2026-88048

Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
---
 .../tesseract/tesseract/CVE-2026-88048.patch  | 228 ++++++++++++++++++
 .../tesseract/tesseract_5.5.2.bb              |   1 +
 2 files changed, 229 insertions(+)
 create mode 100644 meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88048.patch
diff mbox series

Patch

diff --git a/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88048.patch b/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88048.patch
new file mode 100644
index 0000000000..bc1beb5753
--- /dev/null
+++ b/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88048.patch
@@ -0,0 +1,228 @@ 
+From 4dd118c5b87df1a4422ebb3ab3efaa3588b88cfa Mon Sep 17 00:00:00 2001
+From: Stefan Weil <sw@weilnetz.de>
+Date: Fri, 21 Aug 2026 15:23:59 +0200
+Subject: [PATCH] Validate FullyConnected weight matrix dimensions at load
+
+FullyConnected::DeSerialize read the WeightMatrix without checking
+that its dimensions match the layer's declared ni/no. MatrixDotVector
+drives the dot product from the matrix dimensions (writes w.dim1()
+results, reads w.dim2()-1 inputs) while the scratch buffers are sized
+from no_ and ni_, so a crafted .traineddata with a mismatched matrix
+performed a heap out-of-bounds write (up to 65535 rows) and out-of-
+bounds read on the first recognition step (crash, or heap corruption
+with attacker-influenced size and content).
+
+Key changes:
+- weightmatrix.h: add Dim1()/Dim2() accessors for the active weight
+  matrix (int or float), alongside the existing NumOutputs().
+- fullyconnected.cpp: reject the layer in DeSerialize unless
+  Dim1() == no_ and Dim2() == ni_ + 1 (the second dimension
+  includes the bias column), the layout that InitWeightsFloat
+  always produces.
+- unittest: new fullyconnected_test that builds a Softmax network
+  with mismatched matrix dimensions and expects CreateFromFile to
+  return nullptr; on unpatched code the test reaches Forward and
+  ASan catches the out-of-bounds write in MatrixDotVector. A
+  second test verifies that matching dimensions are still accepted.
+
+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>
+(cherry picked from commit 103dc134eb36411ddc6833ec20aa2c76795bd0ff)
+
+CVE: CVE-2026-88048
+Upstream-Status: Backport [https://github.com/tesseract-ocr/tesseract/commit/103dc134eb36411ddc6833ec20aa2c76795bd0ff]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ Makefile.am                     |   5 ++
+ src/lstm/fullyconnected.cpp     |  11 ++-
+ src/lstm/weightmatrix.h         |   7 ++
+ unittest/fullyconnected_test.cc | 115 ++++++++++++++++++++++++++++++++
+ 4 files changed, 137 insertions(+), 1 deletion(-)
+ create mode 100644 unittest/fullyconnected_test.cc
+
+diff --git a/Makefile.am b/Makefile.am
+index 1a0a6771..ea722409 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -1187,6 +1187,7 @@ if !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += equationdetect_test
+ endif # !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += fileio_test
++check_PROGRAMS += fullyconnected_test
+ check_PROGRAMS += heap_test
+ check_PROGRAMS += imagedata_test
+ if !DISABLED_LEGACY_ENGINE
+@@ -1319,6 +1320,10 @@ fileio_test_SOURCES = unittest/fileio_test.cc
+ fileio_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ fileio_test_LDADD = $(TRAINING_LIBS)
+ 
++fullyconnected_test_SOURCES = unittest/fullyconnected_test.cc
++fullyconnected_test_CPPFLAGS = $(unittest_CPPFLAGS)
++fullyconnected_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/lstm/fullyconnected.cpp b/src/lstm/fullyconnected.cpp
+index 85989f40..380707a1 100644
+--- a/src/lstm/fullyconnected.cpp
++++ b/src/lstm/fullyconnected.cpp
+@@ -121,7 +121,16 @@ bool FullyConnected::Serialize(TFile *fp) const {
+ 
+ // Reads from the given file. Returns false in case of error.
+ bool FullyConnected::DeSerialize(TFile *fp) {
+-  return weights_.DeSerialize(IsTraining(), fp);
++  if (!weights_.DeSerialize(IsTraining(), fp)) {
++    return false;
++  }
++  // The weight matrix must match the declared sizes (the second dimension
++  // includes the bias column); otherwise Forward would read or write
++  // outside the scratch buffers sized from ni_ and no_.
++  if (weights_.Dim1() != no_ || weights_.Dim2() != ni_ + 1) {
++    return false;
++  }
++  return true;
+ }
+ 
+ // Runs forward propagation of activations on the input line.
+diff --git a/src/lstm/weightmatrix.h b/src/lstm/weightmatrix.h
+index a2cdaa52..66b69dfa 100644
+--- a/src/lstm/weightmatrix.h
++++ b/src/lstm/weightmatrix.h
+@@ -107,6 +107,13 @@ public:
+   int NumOutputs() const {
+     return int_mode_ ? wi_.dim1() : wf_.dim1();
+   }
++  // The dimensions of the active weight matrix (wi_ in int mode, else wf_).
++  int Dim1() const {
++    return int_mode_ ? wi_.dim1() : wf_.dim1();
++  }
++  int Dim2() const {
++    return int_mode_ ? wi_.dim2() : wf_.dim2();
++  }
+   // Provides one set of weights. Only used by peep weight maxpool.
+   const TFloat *GetWeights(int index) const {
+     return wf_[index];
+diff --git a/unittest/fullyconnected_test.cc b/unittest/fullyconnected_test.cc
+new file mode 100644
+index 00000000..f9697de4
+--- /dev/null
++++ b/unittest/fullyconnected_test.cc
+@@ -0,0 +1,115 @@
++///////////////////////////////////////////////////////////////////////
++// File:        fullyconnected_test.cc
++// Description: Tests that a FullyConnected (softmax) network layer with
++//              weight-matrix dimensions that do not match the declared
++//              ni/no is rejected at load. Without the check,
++//              MatrixDotVector writes w.dim1() results into a scratch
++//              buffer sized from no_ and reads w.dim2()-1 inputs from
++//              a buffer sized from ni_ (heap out-of-bounds write/read)
++//              on the first recognition step.
++//
++// 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 "network.h" // for Network, NetworkType
++#include "networkio.h"
++#include "networkscratch.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 PutU8(uint32_t v) { data_.push_back(static_cast<char>(v & 0xFF)); }
++  void PutU32(uint32_t v) {
++    for (int i = 0; i < 4; ++i) {
++      data_.push_back(static_cast<char>((v >> (8 * i)) & 0xFF));
++    }
++  }
++  void PutS32(int32_t v) { PutU32(static_cast<uint32_t>(v)); }
++  const std::vector<char> &data() const { return data_; }
++
++private:
++  std::vector<char> data_;
++};
++
++void PutDoubleLE(ByteWriter *w, double d) {
++  union {
++    double d;
++    uint64_t u;
++  } conv;
++  conv.d = d;
++  w->PutU32(static_cast<uint32_t>(conv.u & 0xFFFFFFFF));
++  w->PutU32(static_cast<uint32_t>(conv.u >> 32));
++}
++
++// Builds a serialized NT_SOFTMAX network: header with the given ni/no,
++// then a float-mode WeightMatrix with the given (corrupt) dimensions.
++// The matrix is stored as doubles on disk (see WeightMatrix::DeSerialize).
++std::vector<char> MakeSoftmaxNetwork(int ni, int no, int32_t dim1, int32_t dim2) {
++  ByteWriter w;
++  w.PutU8(static_cast<uint32_t>(NT_SOFTMAX));
++  w.PutU8(0); // training: TS_DISABLED
++  w.PutU8(0); // needs_to_backprop
++  w.PutU32(0); // network_flags
++  w.PutU32(static_cast<uint32_t>(ni));
++  w.PutU32(static_cast<uint32_t>(no));
++  w.PutU32(0); // num_weights (not cross-checked, kept consistent anyway)
++  w.PutU32(0); // name (empty string)
++  // WeightMatrix::DeSerialize:
++  w.PutU8(128); // mode: kDoubleFlag, float mode
++  w.PutS32(dim1);
++  w.PutS32(dim2);
++  PutDoubleLE(&w, 0.0); // empty_ cell
++  for (int32_t i = 0; i < dim1 * dim2; ++i) {
++    PutDoubleLE(&w, 0.0); // weight data
++  }
++  return w.data();
++}
++
++// A FullyConnected layer whose weight matrix does not match the declared
++// sizes must be rejected by CreateFromFile; on unpatched code the test
++// reaches Forward, where MatrixDotVector performs the out-of-bounds
++// write this regression test guards against.
++TEST(FullyconnectedTest, RejectsWeightMatrixDimensionMismatch) {
++  // ni_=no_=1 but dim1=3 (OOB write of 3 results into a 1-result buffer)
++  // and dim2=5 (OOB read of 4 inputs from a 1-input buffer).
++  std::vector<char> bytes = MakeSoftmaxNetwork(1, 1, 3, 5);
++  TFile fp;
++  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++  Network *net = Network::CreateFromFile(&fp);
++  if (net == nullptr) {
++    return; // Fixed: the mismatched layer is rejected at load.
++  }
++  NetworkIO input;
++  input.Resize2d(false, /*width=*/1, /*num_features=*/1);
++  NetworkScratch scratch;
++  NetworkIO output;
++  net->Forward(false, input, nullptr, &scratch, &output);
++  delete net;
++  FAIL() << "crafted FullyConnected layer with mismatched weight matrix was accepted";
++}
++
++// Consistent dimensions must still be accepted.
++TEST(FullyconnectedTest, AcceptsMatchingDimensions) {
++  std::vector<char> bytes = MakeSoftmaxNetwork(1, 2, 2, 2);
++  TFile fp;
++  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++  Network *net = Network::CreateFromFile(&fp);
++  ASSERT_NE(net, nullptr);
++  delete net;
++}
++
++} // namespace
++} // namespace tesseract
diff --git a/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb b/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb
index a80407b749..a8b3e55c7d 100644
--- a/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb
+++ b/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb
@@ -11,6 +11,7 @@  SRC_URI = "git://github.com/${BPN}-ocr/${BPN}.git;branch=main;protocol=https;tag
            file://CVE-2026-73067-1.patch \
            file://CVE-2026-73067-2.patch \
            file://CVE-2026-88052.patch \
+           file://CVE-2026-88048.patch \
 "