From: ankur.tyagi85@gmail.com
To: openembedded-devel@lists.openembedded.org
Cc: Ankur Tyagi <ankur.tyagi85@gmail.com>
Subject: [oe][meta-oe][wrynose][PATCH 18/24] tesseract: patch CVE-2026-88049
Date: Thu, 24 Sep 2026 16:33:08 +1200 [thread overview]
Message-ID: <20260924043315.1663186-18-ankur.tyagi85@gmail.com> (raw)
In-Reply-To: <20260924043315.1663186-1-ankur.tyagi85@gmail.com>
From: Ankur Tyagi <ankur.tyagi85@gmail.com>
Details:
https://nvd.nist.gov/vuln/detail/cve-2026-88049
Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
---
.../tesseract/tesseract/CVE-2026-88049.patch | 316 ++++++++++++++++++
.../tesseract/tesseract_5.5.2.bb | 1 +
2 files changed, 317 insertions(+)
create mode 100644 meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88049.patch
diff --git a/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88049.patch b/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88049.patch
new file mode 100644
index 0000000000..d7528e1825
--- /dev/null
+++ b/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88049.patch
@@ -0,0 +1,316 @@
+From 2a118419439d1bfeb2bf8a78732a0fb2ad33475d Mon Sep 17 00:00:00 2001
+From: Stefan Weil <sw@weilnetz.de>
+Date: Fri, 21 Aug 2026 16:41:51 +0200
+Subject: [PATCH] Validate LSTM gate matrix dimensions against na_/no_ at load
+
+LSTM::DeSerialize read na_ from the untrusted TESSDATA_LSTM component
+and derived ns_ from the CI gate matrix's dim1, but never checked that
+the deserialized dimensions were mutually consistent. The forward pass
+sizes its buffers from na_, no_ and ns_ while the gate matrices drive
+their own dimensions, so a crafted .traineddata performed heap
+out-of-bounds writes and reads during the first recognition step, e.g.
+WriteTimeStepPart writing ns_ floats at offset ni_+nf_ into a source_
+buffer sized from na_ (up to ~256 KB with an attacker-chosen gate
+matrix dim1).
+
+The bounds assertions added by 2f4d2f4 (CVE-2026-73066) covered only
+NetworkIO::CopyTimeStepGeneral and Randomize, leaving
+WriteTimeStepPart and AddTimeStepPart unguarded.
+
+Key changes:
+- weightmatrix.h: add Dim1()/Dim2() accessors for the active weight
+ matrix (int or float), alongside the existing NumOutputs().
+- lstm.cpp: reject the layer in DeSerialize unless na_ ==
+ ni_ + nf_ + (is_2d_ ? 2 : 1) * ns_, every deserialized gate has
+ Dim1() == ns_ and Dim2() == na_ + 1 (the layout InitWeightsFloat
+ always produces), ns_ == no_ for plain NT_LSTM/NT_LSTM_SUMMARY, and
+ the softmax layer's sizes match ns_/no_ for the softmax variants.
+- networkio.cpp: add the same defense-in-depth bounds assertions to
+ WriteTimeStepPart and AddTimeStepPart as 2f4d2f4 added to
+ CopyTimeStepGeneral and Randomize.
+- unittest: add lstm_layer_test with crafted NT_LSTM layers for the
+ na_ mismatch, gate dim1 mismatch, and gate dim2 mismatch cases
+ (each rejected at load; on unpatched code the tests reach Forward
+ and ASan catches the out-of-bounds write in WriteTimeStepPart),
+ plus a positive control that a consistent layer loads and runs.
+
+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 b494ac18925f9d9aff9ef5815475de9943ab19bf)
+
+CVE: CVE-2026-88049
+Upstream-Status: Backport [https://github.com/tesseract-ocr/tesseract/commit/b494ac18925f9d9aff9ef5815475de9943ab19bf]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ Makefile.am | 5 +
+ src/lstm/lstm.cpp | 20 ++++
+ src/lstm/networkio.cpp | 2 +
+ unittest/lstm_layer_test.cc | 177 ++++++++++++++++++++++++++++++++++++
+ 4 files changed, 204 insertions(+)
+ create mode 100644 unittest/lstm_layer_test.cc
+
+diff --git a/Makefile.am b/Makefile.am
+index ea722409..86ac9d0d 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -1200,6 +1200,7 @@ check_PROGRAMS += layout_test
+ check_PROGRAMS += ligature_table_test
+ check_PROGRAMS += linlsq_test
+ check_PROGRAMS += list_test
++check_PROGRAMS += lstm_layer_test
+ if ENABLE_TRAINING
+ check_PROGRAMS += lstm_recode_test
+ check_PROGRAMS += lstm_squashed_test
+@@ -1381,6 +1382,10 @@ loadlang_test_SOURCES = unittest/loadlang_test.cc
+ loadlang_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ loadlang_test_LDADD = $(TESS_LIBS) $(LEPTONICA_LIBS)
+
++lstm_layer_test_SOURCES = unittest/lstm_layer_test.cc
++lstm_layer_test_CPPFLAGS = $(unittest_CPPFLAGS)
++lstm_layer_test_LDADD = $(TESS_LIBS)
++
+ lstm_recode_test_SOURCES = unittest/lstm_recode_test.cc
+ lstm_recode_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ lstm_recode_test_LDADD = $(TRAINING_LIBS)
+diff --git a/src/lstm/lstm.cpp b/src/lstm/lstm.cpp
+index 11722d79..4ce361eb 100644
+--- a/src/lstm/lstm.cpp
++++ b/src/lstm/lstm.cpp
+@@ -274,12 +274,32 @@ bool LSTM::DeSerialize(TFile *fp) {
+ is_2d_ = na_ - nf_ == ni_ + 2 * ns_;
+ }
+ }
++ // The deserialized dimensions must be mutually consistent: the forward
++ // pass sizes its buffers from na_, no_ and ns_ while the gate matrices
++ // drive their own dimensions.
++ if (na_ != ni_ + nf_ + (is_2d_ ? 2 : 1) * ns_) {
++ return false;
++ }
++ for (int w = 0; w < WT_COUNT; ++w) {
++ if (w == GFS && !Is2D()) {
++ continue;
++ }
++ if (gate_weights_[w].Dim1() != ns_ || gate_weights_[w].Dim2() != na_ + 1) {
++ return false;
++ }
++ }
++ if ((type_ == NT_LSTM || type_ == NT_LSTM_SUMMARY) && ns_ != no_) {
++ return false;
++ }
+ delete softmax_;
+ if (type_ == NT_LSTM_SOFTMAX || type_ == NT_LSTM_SOFTMAX_ENCODED) {
+ softmax_ = static_cast<FullyConnected *>(Network::CreateFromFile(fp));
+ if (softmax_ == nullptr) {
+ return false;
+ }
++ if (softmax_->NumInputs() != ns_ || softmax_->NumOutputs() != no_) {
++ return false;
++ }
+ } else {
+ softmax_ = nullptr;
+ }
+diff --git a/src/lstm/networkio.cpp b/src/lstm/networkio.cpp
+index 8636075b..929f5ac4 100644
+--- a/src/lstm/networkio.cpp
++++ b/src/lstm/networkio.cpp
+@@ -641,6 +641,7 @@ void NetworkIO::AddTimeStep(int t, TFloat *inout) const {
+
+ // Adds part of a single timestep to floats.
+ void NetworkIO::AddTimeStepPart(int t, int offset, int num_features, float *inout) const {
++ ASSERT_HOST(offset + num_features <= NumFeatures());
+ if (int_mode_) {
+ const int8_t *line = i_[t] + offset;
+ for (int i = 0; i < num_features; ++i) {
+@@ -662,6 +663,7 @@ void NetworkIO::WriteTimeStep(int t, const TFloat *input) {
+ // Writes a single timestep from floats in the range [-1, 1] writing only
+ // num_features elements of input to (*this)[t], starting at offset.
+ void NetworkIO::WriteTimeStepPart(int t, int offset, int num_features, const TFloat *input) {
++ ASSERT_HOST(offset + num_features <= NumFeatures());
+ if (int_mode_) {
+ int8_t *line = i_[t] + offset;
+ for (int i = 0; i < num_features; ++i) {
+diff --git a/unittest/lstm_layer_test.cc b/unittest/lstm_layer_test.cc
+new file mode 100644
+index 00000000..2c874995
+--- /dev/null
++++ b/unittest/lstm_layer_test.cc
+@@ -0,0 +1,177 @@
++///////////////////////////////////////////////////////////////////////
++// File: lstm_layer_test.cc
++// Description: Tests that an NT_LSTM network layer with mutually
++// inconsistent deserialized dimensions is rejected at
++// load. The forward pass sizes its buffers from na_, no_
++// and ns_ while the gate weight matrices drive their own
++// dimensions, so a crafted .traineddata performs heap
++// out-of-bounds writes/reads during the first
++// recognition step (e.g. WriteTimeStepPart writing ns_
++// floats into a source_ buffer sized from na_).
++//
++// 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 "stridemap.h"
++
++#include <cstdint>
++#include <utility>
++#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));
++}
++
++// A serialized float-mode WeightMatrix with the given dimensions,
++// all weight data zeroed.
++void PutGateMatrix(ByteWriter *w, int32_t dim1, int32_t dim2) {
++ 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);
++ }
++}
++
++// A serialized 1-D NT_LSTM network: header with the given ni/no, na_,
++// then the four gates CI, GI, GF1, GO (GFS is not serialized for 1-D).
++std::vector<char> MakeLstmNetwork(int ni, int no, int32_t na,
++ const int32_t gate_dim1[4], const int32_t gate_dim2[4]) {
++ ByteWriter w;
++ w.PutU8(static_cast<uint32_t>(NT_LSTM));
++ 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
++ w.PutU32(0); // name (empty string)
++ w.PutS32(na);
++ for (int g = 0; g < 4; ++g) {
++ PutGateMatrix(&w, gate_dim1[g], gate_dim2[g]);
++ }
++ return w.data();
++}
++
++// Builds the input a standalone LSTM layer would receive: one row of
++// the given width with ni features.
++NetworkIO MakeInput(int ni, int width) {
++ StrideMap stride_map;
++ stride_map.SetStride({{1, width}});
++ NetworkIO input;
++ input.ResizeToMap(false, stride_map, ni);
++ return input;
++}
++
++// Runs Forward on the loaded network; on unpatched code the out-of-
++// bounds access this regression test guards against fires here.
++void RunForward(Network *net, int ni, int width) {
++ NetworkIO input = MakeInput(ni, width);
++ NetworkScratch scratch;
++ NetworkIO output;
++ net->Forward(false, input, nullptr, &scratch, &output);
++ delete net;
++}
++
++// na_ must equal ni_ + nf_ + ns_ for a 1-D LSTM; here na_=2 but the
++// CI matrix makes ns_=64, so the layer must be rejected. On unpatched
++// code Forward writes 64 floats at offset ni_=1 into a source_ buffer
++// sized for na_=2 (heap out-of-bounds write).
++TEST(LstmLayerTest, RejectsInconsistentNa) {
++ const int32_t dim1[4] = {64, 64, 64, 64};
++ const int32_t dim2[4] = {3, 3, 3, 3};
++ std::vector<char> bytes = MakeLstmNetwork(1, 1, 2, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ if (net == nullptr) {
++ return; // Fixed: the inconsistent layer is rejected at load.
++ }
++ RunForward(net, 1, 2);
++ FAIL() << "crafted LSTM layer with inconsistent na_ was accepted";
++}
++
++// All gate matrices must have dim1 == ns_; here the GI matrix has
++// dim1=9 while ns_=5. On unpatched code the GI gate dot product writes
++// 9 results into a temp line sized for 5 (heap out-of-bounds write).
++TEST(LstmLayerTest, RejectsGateDim1Mismatch) {
++ const int32_t dim1[4] = {5, 9, 5, 5};
++ const int32_t dim2[4] = {7, 7, 7, 7};
++ std::vector<char> bytes = MakeLstmNetwork(1, 5, 6, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ if (net == nullptr) {
++ return; // Fixed: the inconsistent layer is rejected at load.
++ }
++ RunForward(net, 1, 2);
++ FAIL() << "crafted LSTM layer with inconsistent gate dim1 was accepted";
++}
++
++// All gate matrices must have dim2 == na_ + 1; here the GI matrix has
++// dim2=9 while na_=6. On unpatched code the GI gate dot product reads
++// 8 inputs from a buffer sized for 6 (heap out-of-bounds read).
++TEST(LstmLayerTest, RejectsGateDim2Mismatch) {
++ const int32_t dim1[4] = {5, 5, 5, 5};
++ const int32_t dim2[4] = {7, 9, 7, 7};
++ std::vector<char> bytes = MakeLstmNetwork(1, 5, 6, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ if (net == nullptr) {
++ return; // Fixed: the inconsistent layer is rejected at load.
++ }
++ RunForward(net, 1, 2);
++ FAIL() << "crafted LSTM layer with inconsistent gate dim2 was accepted";
++}
++
++// A fully consistent 1-D LSTM layer must still be accepted and usable.
++TEST(LstmLayerTest, AcceptsConsistentLayer) {
++ const int32_t dim1[4] = {5, 5, 5, 5};
++ const int32_t dim2[4] = {7, 7, 7, 7};
++ std::vector<char> bytes = MakeLstmNetwork(1, 5, 6, dim1, dim2);
++ TFile fp;
++ ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++ Network *net = Network::CreateFromFile(&fp);
++ ASSERT_NE(net, nullptr);
++ RunForward(net, 1, 2);
++}
++
++} // 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 a8b3e55c7d..df6ff11d78 100644
--- a/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb
+++ b/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb
@@ -12,6 +12,7 @@ SRC_URI = "git://github.com/${BPN}-ocr/${BPN}.git;branch=main;protocol=https;tag
file://CVE-2026-73067-2.patch \
file://CVE-2026-88052.patch \
file://CVE-2026-88048.patch \
+ file://CVE-2026-88049.patch \
"
next prev parent reply other threads:[~2026-09-24 4:34 UTC|newest]
Thread overview: 28+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-24 4:32 [oe][meta-oe][wrynose][PATCH 1/24] rabbitmq-c: patch CVE-2026-44235 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-oe][wrynose][PATCH 2/24] rabbitmq-c: patch CVE-2026-44236 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-oe][wrynose][PATCH 3/24] redis: patch CVE-2026-81934 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-networking][wrynose][PATCH 4/24] sngrep: patch CVE-2026-90558 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-networking][wrynose][PATCH 5/24] strongswan: patch CVE-2026-78123 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-networking][wrynose][PATCH 6/24] strongswan: patch CVE-2026-78124 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-networking][wrynose][PATCH 7/24] strongswan: patch CVE-2026-78126 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-networking][wrynose][PATCH 8/24] strongswan: patch CVE-2026-78127 ankur.tyagi85
2026-09-24 4:32 ` [oe][meta-networking][wrynose][PATCH 9/24] strongswan: patch CVE-2026-78129 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-networking][wrynose][PATCH 10/24] strongswan: patch CVE-2026-78133 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-networking][wrynose][PATCH 11/24] strongswan: patch CVE-2026-78131 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-networking][wrynose][PATCH 12/24] strongswan: patch CVE-2026-78130 ankur.tyagi85
2026-10-05 9:49 ` Anuj Mittal
2026-10-06 20:25 ` Ankur Tyagi
2026-09-24 4:33 ` [oe][meta-networking][wrynose][PATCH 13/24] strongswan: patch CVE-2026-78132 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-networking][wrynose][PATCH 14/24] strongswan: patch CVE-2026-78135 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-networking][wrynose][PATCH 15/24] strongswan: patch CVE-2026-78134 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 16/24] tesseract: patch CVE-2026-88052 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 17/24] tesseract: patch CVE-2026-88048 ankur.tyagi85
2026-09-24 4:33 ` ankur.tyagi85 [this message]
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 19/24] tesseract: patch CVE-2026-88050 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 20/24] tesseract: patch CVE-2026-88047 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 21/24] tesseract: patch CVE-2026-88051 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 22/24] tesseract: patch CVE-2026-88054 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 23/24] valkey: upgrade 9.0.5 -> 9.0.6 ankur.tyagi85
2026-09-24 4:33 ` [oe][meta-oe][wrynose][PATCH 24/24] valkey: mark CVE-2026-86227 fixed ankur.tyagi85
2026-10-06 4:19 ` [oe][meta-oe][wrynose][PATCH 1/24] rabbitmq-c: patch CVE-2026-44235 Anuj Mittal
2026-10-06 20:27 ` Ankur Tyagi
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260924043315.1663186-18-ankur.tyagi85@gmail.com \
--to=ankur.tyagi85@gmail.com \
--cc=openembedded-devel@lists.openembedded.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.