diff --git a/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88047.patch b/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88047.patch
new file mode 100644
index 0000000000..04e9953d2a
--- /dev/null
+++ b/meta-oe/recipes-graphics/tesseract/tesseract/CVE-2026-88047.patch
@@ -0,0 +1,226 @@
+From a6e329ff576ad4a90952f08359dbffab7e480157 Mon Sep 17 00:00:00 2001
+From: Stefan Weil <sw@weilnetz.de>
+Date: Mon, 24 Aug 2026 13:39:22 +0200
+Subject: [PATCH] Limit unichar extraction in ReadNormProtos to the buffer size
+
+Classify::ReadNormProtos parsed each normproto line with
+`stream >> unichar >> NumProtos` into a char unichar[2 * UNICHAR_LEN + 1]
+stack buffer, but char* extraction from an istream has no length limit
+(the stream width was never set). A crafted TESSDATA_NORMPROTO component
+in a .traineddata file whose first proto-line token exceeds 60
+characters (the 100-byte line buffer allows up to 99) overflows the
+stack buffer during legacy engine initialization (CWE-121).
+
+Toolchain note: Apple's libc++ provides a C++20 array overload of
+operator>>(basic_istream&, char(&)[N]) that implicitly bounds the
+extraction to the array size, so builds against that standard library
+are incidentally protected. Standard libraries without that overload
+(e.g. libstdc++) still take the unbounded char* overload, so the
+explicit width limit below makes the behavior defined on all
+toolchains.
+
+Key changes:
+- normmatch.cpp: read the unichar token with
+  std::setw(2 * UNICHAR_LEN + 1); char* extraction takes at most
+  width - 1 characters, which fits the buffer exactly. Overlong
+  tokens are truncated and the line is rejected like any other
+  unparseable line.
+- unittest: add normproto_test covering a 99-character token (the
+  maximum a 100-byte line can hold), a token of exactly 2 *
+  UNICHAR_LEN characters, and a well-formed component. A minimal
+  reproduction of the unbounded extraction crashes an ASan build
+  with a stack-buffer-overflow.
+
+Reported-by: Tristan Madani <tristan@talencesecurity.com>
+Assisted-by: OpenCode / qwen3.8-27b-thinking (Alibaba Cloud)
+Signed-off-by: Stefan Weil <sw@weilnetz.de>
+(cherry picked from commit 1bda5079b1c8a7e25f523486837426903d29ce84)
+
+CVE: CVE-2026-88047
+Upstream-Status: Backport [https://github.com/tesseract-ocr/tesseract/commit/1bda5079b1c8a7e25f523486837426903d29ce84]
+
+Signed-off-by: Ankur Tyagi <ankur.tyagi85@gmail.com>
+---
+ Makefile.am                |   5 ++
+ src/classify/normmatch.cpp |   6 +-
+ unittest/CMakeLists.txt    |   1 +
+ unittest/normproto_test.cc | 111 +++++++++++++++++++++++++++++++++++++
+ 4 files changed, 122 insertions(+), 1 deletion(-)
+ create mode 100644 unittest/normproto_test.cc
+
+diff --git a/Makefile.am b/Makefile.am
+index c1491263..48e7dcbc 100644
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -1213,6 +1213,7 @@ check_PROGRAMS += mastertrainer_test
+ endif # !DISABLED_LEGACY_ENGINE
+ check_PROGRAMS += matrix_test
+ check_PROGRAMS += networkio_test
++check_PROGRAMS += normproto_test
+ if ENABLE_TRAINING
+ check_PROGRAMS += normstrngs_test
+ endif # ENABLE_TRAINING
+@@ -1417,6 +1418,10 @@ networkio_test_SOURCES = unittest/networkio_test.cc
+ networkio_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ networkio_test_LDADD = $(TESS_LIBS)
+ 
++normproto_test_SOURCES = unittest/normproto_test.cc
++normproto_test_CPPFLAGS = $(unittest_CPPFLAGS)
++normproto_test_LDADD = $(TESS_LIBS)
++
+ normstrngs_test_SOURCES = unittest/normstrngs_test.cc
+ normstrngs_test_CPPFLAGS = $(unittest_CPPFLAGS)
+ normstrngs_test_LDADD = $(TRAINING_LIBS) $(ICU_I18N_LIBS) $(ICU_UC_LIBS)
+diff --git a/src/classify/normmatch.cpp b/src/classify/normmatch.cpp
+index d5bd7e6a..1ce7c928 100644
+--- a/src/classify/normmatch.cpp
++++ b/src/classify/normmatch.cpp
+@@ -28,6 +28,7 @@
+ 
+ #include <cmath>
+ #include <cstdio>
++#include <iomanip> // for std::setw
+ #include <sstream> // for std::istringstream
+ 
+ namespace tesseract {
+@@ -190,7 +191,10 @@ NORM_PROTOS *Classify::ReadNormProtos(TFile *fp) {
+   while (fp->FGets(line, kMaxLineSize) != nullptr) {
+     std::istringstream stream(line);
+     stream.imbue(std::locale::classic());
+-    stream >> unichar >> NumProtos;
++    // unichar holds at most 2 * UNICHAR_LEN characters; the width limit
++    // (width - 1 characters for char* extraction) keeps the extraction
++    // from overflowing the buffer on overlong lines.
++    stream >> std::setw(2 * UNICHAR_LEN + 1) >> unichar >> NumProtos;
+     if (stream.fail()) {
+       continue;
+     }
+diff --git a/unittest/CMakeLists.txt b/unittest/CMakeLists.txt
+index 6a91c62f..b65cf922 100644
+--- a/unittest/CMakeLists.txt
++++ b/unittest/CMakeLists.txt
+@@ -61,6 +61,7 @@ set(LEGACY_TESTS
+     indexmapbidi_test.cc
+     intfeaturemap_test.cc
+     mastertrainer_test.cc
++    normproto_test.cc
+     osd_test.cc
+     params_model_test.cc
+     shapetable_test.cc)
+diff --git a/unittest/normproto_test.cc b/unittest/normproto_test.cc
+new file mode 100644
+index 00000000..574b2f3d
+--- /dev/null
++++ b/unittest/normproto_test.cc
+@@ -0,0 +1,111 @@
++///////////////////////////////////////////////////////////////////////
++// File:        normproto_test.cc
++// Description: Tests that Classify::ReadNormProtos handles a normproto
++//              line whose first (unichar) token exceeds the
++//              unichar[2 * UNICHAR_LEN + 1] stack buffer. The
++//              istream extraction has no intrinsic length limit, so a
++//              crafted NORMPROTO component in a .traineddata file
++//              could overflow the stack buffer during legacy engine
++//              initialization.
++//
++// 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 "classify.h"
++#include "serialis.h" // for TFile
++
++#include <cstdio>
++#include <cstdlib>
++#include <string>
++#include <vector>
++
++namespace tesseract {
++namespace {
++
++// Minimal unicharset (space and 'a').
++const char kMinUnicharset[] =
++    "2\n"
++    "NULL 1 0,255,0,255,0,0,0,0,0,0 Latin 2 0 2\n"
++    "a 1 0,255,0,255,0,0,0,0,0,0 Latin 2 0 2\n";
++
++// Builds a normproto component: a sample-size line (5), five parameter
++// description lines, then the given raw proto lines.
++std::vector<char> MakeNormproto(const std::string &lines) {
++  std::string data = "5\n";
++  for (int i = 0; i < 5; ++i) {
++    data += "e e 0 1\n";
++  }
++  data += lines;
++  return std::vector<char>(data.begin(), data.end());
++}
++
++class NormprotoTest : public testing::Test {
++protected:
++  void SetUp() override {
++    tmpl_ = "/tmp/tess_normproto_test_XXXXXX";
++    char *dir = mkdtemp(tmpl_.data());
++    ASSERT_NE(dir, nullptr);
++    dir_ = dir;
++    std::string uc_path = dir_ + "/eng.unicharset";
++    FILE *f = fopen(uc_path.c_str(), "w");
++    ASSERT_NE(f, nullptr);
++    ASSERT_EQ(fwrite(kMinUnicharset, 1, sizeof(kMinUnicharset) - 1, f),
++              sizeof(kMinUnicharset) - 1);
++    fclose(f);
++    // Load the minimal unicharset into the classifier's inherited
++    // unicharset member.
++    ASSERT_TRUE(classifier_.unicharset.load_from_file(uc_path.c_str()));
++  }
++  void TearDown() override {
++    std::remove((dir_ + "/eng.unicharset").c_str());
++    rmdir(dir_.c_str());
++  }
++  std::string dir_;
++  std::string tmpl_;
++  Classify classifier_;
++};
++
++// A 99-character first token (the maximum FGets can return) overflows
++// unichar[2 * UNICHAR_LEN + 1] on unpatched code; the width-limited
++// extraction must reject the line instead.
++TEST_F(NormprotoTest, ToleratesOverlongUnicharToken) {
++  std::vector<char> bytes = MakeNormproto(std::string(99, 'A') + "\n");
++  TFile fp;
++  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++  classifier_.NormProtos = classifier_.ReadNormProtos(&fp);
++  ASSERT_NE(classifier_.NormProtos, nullptr);
++  classifier_.FreeNormProtos();
++  EXPECT_EQ(classifier_.NormProtos, nullptr);
++}
++
++// A token of exactly 2 * UNICHAR_LEN characters is the maximum legitimate
++// size; it must not be truncated or rejected by the width limit.
++TEST_F(NormprotoTest, ToleratesMaxLenUnicharToken) {
++  std::vector<char> bytes = MakeNormproto(std::string(2 * UNICHAR_LEN, 'A') + " 0\n");
++  TFile fp;
++  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++  classifier_.NormProtos = classifier_.ReadNormProtos(&fp);
++  ASSERT_NE(classifier_.NormProtos, nullptr);
++  classifier_.FreeNormProtos();
++  EXPECT_EQ(classifier_.NormProtos, nullptr);
++}
++
++// A well-formed normproto component must still parse.
++TEST_F(NormprotoTest, ReadsValidNormprotos) {
++  std::vector<char> bytes = MakeNormproto("a 0\n");
++  TFile fp;
++  ASSERT_TRUE(fp.Open(bytes.data(), bytes.size()));
++  classifier_.NormProtos = classifier_.ReadNormProtos(&fp);
++  ASSERT_NE(classifier_.NormProtos, nullptr);
++  classifier_.FreeNormProtos();
++  EXPECT_EQ(classifier_.NormProtos, nullptr);
++}
++
++} // 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 756e780659..61cb1f7cad 100644
--- a/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb
+++ b/meta-oe/recipes-graphics/tesseract/tesseract_5.5.2.bb
@@ -14,6 +14,7 @@ SRC_URI = "git://github.com/${BPN}-ocr/${BPN}.git;branch=main;protocol=https;tag
            file://CVE-2026-88048.patch \
            file://CVE-2026-88049.patch \
            file://CVE-2026-88050.patch \
+           file://CVE-2026-88047.patch \
 "
 
 
