From 9700f6f206da5102c5dfb2414c5e7e13d844fcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miloslav=20Mac=C5=AFrek?= Date: Fri, 10 Jul 2026 20:17:23 +0200 Subject: [PATCH] Fix crash with UCI_LimitStrength and tablebases at the root `Skill::pick_best()` assumes the root moves are sorted by score in descending order: ```cpp Value topScore = rootMoves[0].score; int delta = std::min(topScore - rootMoves[multiPV - 1].score, int(PawnValue)); ``` That does not have to be so with Syzygy tablebases at the root, where the moves are ordered by `tbRank` instead. `rootMoves[0]` is then not the highest score and `delta` can go negative. Since `delta * (rng.rand() % int(weakness))` is evaluated as unsigned, a negative `delta` wraps to a large value and the `int(...)` cast overflows, so the `score + push >= maxScore` check never passes and `best` stays `Move::none()`. The caller ```cpp std::swap(rootMoves[0], *std::find(rootMoves.begin(), rootMoves.end(), skill.best ? skill.best : skill.pick_best(rootMoves, multiPV))); ``` then dereferences `end()`. The result is a crash, or sometimes `bestmove (none)` / an illegal move, in tablebase endgames when `UCI_LimitStrength` is set (or `Skill Level` is below 20). The fix computes the score range over the candidate moves directly, so `delta` stays non-negative and a valid move is always returned. **To reproduce**: ``` setoption name UCI_LimitStrength value true setoption name UCI_Elo value 2900 setoption name SyzygyPath value position fen 8/8/8/4k3/8/8/3BKN2/8 b - - 0 1 go wtime 250 btime 250 winc 100 binc 100 ``` You need to call the go command a couple of times before crash, after which is ends with ``` bestmove a1e5 ponder (none) ``` and on the next `go wtime 250 btime 250 winc 100 binc 100` it crashes printing: ``` info string Available processors: 0-31 info string Using 1 thread info string NNUE evaluation using nn-af1339a6dea3.nnue (106MiB, (83248, 1024, 31, 32, 1)) info string Network replica 1: Shared memory. ``` Bench is unchanged, since this code only runs under `UCI_LimitStrength` / `Skill Level`. closes https://github.com/official-stockfish/Stockfish/pull/6957 No functional change --- AUTHORS | 1 + src/search.cpp | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8e4241384..fe573fe50 100644 --- a/AUTHORS +++ b/AUTHORS @@ -180,6 +180,7 @@ Michel Van den Bergh (vdbergh) Miguel Lahoz (miguel-l) Mikael Bäckman (mbootsector) Mike Babigian (Farseer) +Miloslav Macůrek (maelic13) Mira Miroslav Fontán (Hexik) Moez Jellouli (MJZ1977) diff --git a/src/search.cpp b/src/search.cpp index d6ec50aba..bc922119c 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -2005,9 +2005,16 @@ void update_quiet_histories( Move Skill::pick_best(const RootMoves& rootMoves, usize multiPV) { static PRNG rng(now()); // PRNG sequence should be non-deterministic - // RootMoves are already sorted by score in descending order - Value topScore = rootMoves[0].score; - int delta = std::min(topScore - rootMoves[multiPV - 1].score, int(PawnValue)); + // With tablebases at the root, rootMoves are ordered by tbRank rather than by + // score, so compute the score range explicitly to keep 'delta' non-negative. + Value topScore = rootMoves[0].score; + Value minScore = rootMoves[0].score; + for (usize i = 1; i < multiPV; ++i) + { + topScore = std::max(topScore, rootMoves[i].score); + minScore = std::min(minScore, rootMoves[i].score); + } + int delta = std::min(topScore - minScore, int(PawnValue)); int maxScore = -VALUE_INFINITE; double weakness = 120 - 2 * level;