Commit d20daaf2 for tesseract

commit d20daaf23ae4224643e76516868ff3b70da7ae6e
Author: Stefan Weil <sw@weilnetz.de>
Date:   Fri Nov 13 17:31:00 2026 +0100

    training: fix use after free in DeleteDeadSamples

    TrainingSampleSet::DeleteDeadSamples() erased the entry of a dead
    sample from the vector and then deleted *it. As erase() moves the
    following entries down, that deleted the next (live) sample and left
    a dangling pointer in the vector, while the dead sample was leaked.
    shapeclustering and mftraining crashed.

    Delete the sample first, and continue with the iterator which erase()
    returns.

    This was a regression from commit af131241a.

    Fix proposed by the Anthropic OSS Scanner (finding ANT-2026-6OZE8ZWP).

    Assisted-by: OpenCode / Claude Fable 5.2 (Anthropic)
    Signed-off-by: Stefan Weil <sw@weilnetz.de>

diff --git a/src/training/common/trainingsampleset.cpp b/src/training/common/trainingsampleset.cpp
index d05459b0..d9f7adae 100644
--- a/src/training/common/trainingsampleset.cpp
+++ b/src/training/common/trainingsampleset.cpp
@@ -539,10 +539,12 @@ void TrainingSampleSet::KillSample(TrainingSample *sample) {
 // Deletes all samples with zero features marked by KillSample.
 void TrainingSampleSet::DeleteDeadSamples() {
   using namespace std::placeholders; // for _1
-  for (auto &&it = samples_.begin(); it < samples_.end();) {
+  for (auto it = samples_.begin(); it != samples_.end();) {
     if (*it == nullptr || (*it)->class_id() < 0) {
-      samples_.erase(it);
+      // Delete before erasing: erase shifts the following samples down, so
+      // *it would otherwise refer to the next (live) sample.
       delete *it;
+      it = samples_.erase(it);
     } else {
       ++it;
     }