Skip to content

Improve CLI error handling a bit#6844

Draft
anematode wants to merge 2 commits into
official-stockfish:masterfrom
anematode:harden-io
Draft

Improve CLI error handling a bit#6844
anematode wants to merge 2 commits into
official-stockfish:masterfrom
anematode:harden-io

Conversation

@anematode

Copy link
Copy Markdown
Contributor

Reduce the number of easy ways to hit UB or an uncaught exception. This builds on Sopel's work on FEN validation.

Obviously SF is not and will never be a security boundary, but at least we can reduce the number of reports about uncaught std::stoi out of range, etc.

I also added a gentle fallback to NumaConfig specifically because that's something that (as a dev) I frequently set by hand in the CLI, but happy to revert if that's too annoying of a change.

@github-actions

Copy link
Copy Markdown

(execution 26320237296 / attempt 1)

@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces exit/assert failure paths with explicit error results: numeric parsing (str_to_size_t) returns std::optional, NumaConfig::from_string returns std::optional, UCI spin parsing validates via errno-aware checks, Position::flip returns std::optional, perft helpers and Engine::perft return std::variant<uint64_t, PositionSetError>, and UCI integration tracks currentCmd and terminates on reported errors.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b44d065c-c4ce-486c-b84a-c27aee12ac4f

📥 Commits

Reviewing files that changed from the base of the PR and between 133731f and 1281517.

📒 Files selected for processing (11)
  • src/engine.cpp
  • src/engine.h
  • src/misc.cpp
  • src/misc.h
  • src/numa.h
  • src/perft.h
  • src/position.cpp
  • src/position.h
  • src/uci.cpp
  • src/uci.h
  • src/ucioption.cpp

Comment thread src/misc.cpp Outdated
Comment thread src/numa.h
Comment on lines +1056 to +1059
for (size_t c = *cfirst; c <= *clast; ++c)
{
indices.emplace_back(c);
}

@coderabbitai coderabbitai Bot May 23, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant section around the reported lines
sed -n '1030,1105p' src/numa.h

# Find the function that owns this loop and related parsing helpers
rg -n "indices_from_shortened_string|from_string|str_to_size_t|cfirst|clast" src/numa.h

# Print the full implementations for the functions we found (to understand reachability of SIZE_MAX)
python3 - <<'PY'
import re, subprocess, os, textwrap, json, sys
path="src/numa.h"
# Extract approximate function blocks by printing from match line to next '}'; heuristic via braces depth.
text=open(path,'r',encoding='utf-8').read().splitlines()
keywords=["indices_from_shortened_string","from_string","str_to_size_t"]
for kw in keywords:
    for i,line in enumerate(text, start=1):
        if kw in line:
            start=i
            break
    else:
        continue
    # naive brace tracking from first '{' after start
    s=start
    # find first '{' after s
    depth=0
    started=False
    out=[]
    for j in range(s-1,len(text)):
        out.append((j+1,text[j]))
        if not started:
            if '{' in text[j]:
                started=True
                depth += text[j].count('{') - text[j].count('}')
        else:
            depth += text[j].count('{') - text[j].count('}')
            if depth<=0:
                break
    print("\n===== Extracted around", kw, "starting at line", start, "=====")
    for ln,content in out[:220]:
        print(f"{ln}:{content}")
PY

Repository: official-stockfish/Stockfish

Length of output: 6223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "str_to_size_t\s*\(" src/numa.h
rg -n "from_string\(" -S src/numa.h
rg -n "NumaConfig::from_string|from_string<|from_string\s*\(" src | head -n 200

# Print str_to_size_t definition (or nearest) in src/numa.h
# We'll search for "str_to_size_t" without relying on line extraction.
python3 - <<'PY'
import subprocess, re, os, textwrap, sys
path="src/numa.h"
lines=open(path,'r',encoding='utf-8').read().splitlines()
for i,l in enumerate(lines,1):
    if "str_to_size_t" in l:
        if "static" in l or "template" in l or "auto" in l or "std::optional" in l:
            pass
# Find function signature
for i,l in enumerate(lines,1):
    if re.search(r"\bstr_to_size_t\b", l) and ('static' in l or 'inline' in l or 'auto' in l):
        start=i-20
        end=i+80
        start=max(1,start); end=min(len(lines),end)
        print(f"\n--- context around line {i} ({path}) ---")
        for ln in range(start,end+1):
            print(f"{ln}:{lines[ln-1]}")
        break
else:
    print("No obvious definition found in src/numa.h; printing all occurrences with line numbers only.")
    for i,l in enumerate(lines,1):
        if "str_to_size_t" in l:
            print(i, l.strip())
PY

Repository: official-stockfish/Stockfish

Length of output: 4761


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate str_to_size_t definition/guards in numa.h
rg -n "str_to_size_t" src/numa.h

# Find how NumaConfig::from_string is used (likely CLI path)
rg -n "NumaConfig::from_string|from_string\s*\(" src | head -n 200

# Print a larger contextual window around str_to_size_t (in case it's defined elsewhere in the file)
python3 - <<'PY'
import re
path="src/numa.h"
lines=open(path,'r',encoding='utf-8').read().splitlines()
hits=[i for i,l in enumerate(lines,1) if re.search(r"\bstr_to_size_t\b", l)]
print("str_to_size_t occurrences:", hits[:30], ("..." if len(hits)>30 else ""))
# Heuristic: find the first line that looks like a function definition
defline=None
for i,l in enumerate(lines,1):
    if re.search(r"\bstr_to_size_t\b", l) and re.search(r"\bstatic\b|\binline\b|auto\s+\w*\s*\(|std::optional", l):
        defline=i
        break
if defline is None and hits:
    defline=hits[0]
if defline:
    start=max(1,defline-60); end=min(len(lines),defline+200)
    print(f"\n--- Context around str_to_size_t near line {defline} ---")
    for ln in range(start,end+1):
        print(f"{ln}:{lines[ln-1]}")
PY

Repository: official-stockfish/Stockfish

Length of output: 10926


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "str_to_size_t" -S .
rg -n "str_to_size_t\s*\(" -S . | head -n 200

# Try to find a definition pattern (function returning optional size_t)
rg -n "str_to_size_t.*optional<.*size_t" -S . || true
rg -n "optional\s*<\s*size_t\s*>\s*str_to_size_t" -S . || true

# Print likely definition area by searching for "str_to_size_t(const" or "str_to_size_t(std::" etc.
rg -n "str_to_size_t\s*\(const std::string" -S . || true
rg -n "static\s+.*str_to_size_t" -S . || true

Repository: official-stockfish/Stockfish

Length of output: 1198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where str_to_size_t is implemented/validated (to see if SIZE_MAX is rejected)
rg -n "str_to_size_t" -S .
rg -n "str_to_size_t\\s*\\(" -S . | head -n 200

rg -n "optional\\s*<\\s*size_t\\s*>\\s*str_to_size_t|str_to_size_t.*optional<.*size_t|str_to_size_t\\s*\\(.*std::string" -S .

Repository: official-stockfish/Stockfish

Length of output: 1296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '450,540p' src/misc.cpp
sed -n '80,140p' src/misc.h
sed -n '200,270p' src/engine.cpp

Repository: official-stockfish/Stockfish

Length of output: 6062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "set_numa_config_from_option" -S src
rg -n "NumaConfig" src/engine.cpp src/*.cpp src/*.h | head -n 200

# Find UCI option name or parsing of "Numa" options
rg -n "numa.*option|option.*numa|Numa.*Config|set_numa" -S src | head -n 200

Repository: official-stockfish/Stockfish

Length of output: 8930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "set_numa_config_from_option" -S src
rg -n "NumaConfig" src/engine.cpp src/*.cpp src/*.h | head -n 200

# Find UCI option name or parsing of "Numa" options
rg -n "numa.*option|option.*numa|Numa.*Config|set_numa" -S src | head -n 200

Repository: official-stockfish/Stockfish

Length of output: 8930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "NumaPolicy|NumaConfig|numa.*Policy|set_numa_config_from_option|Numa.*Config" -S src
rg -n "setoption name .*Numa|Numa.*value|UCI.*Numa|option.*Numa" -S src

Repository: official-stockfish/Stockfish

Length of output: 5369


🏁 Script executed:

sed -n '40,110p' src/engine.cpp
sed -n '200,260p' src/engine.cpp
sed -n '650,720p' src/numa.h

Repository: official-stockfish/Stockfish

Length of output: 6122


Fix non-terminating CPU range expansion for NumaPolicy when clast == SIZE_MAX.

NumaConfig::from_string() (driven by the UCI option NumaPolicy in src/engine.cpp) uses indices_from_shortened_string(), which expands "first-last" with for (size_t c = *cfirst; c <= *clast; ++c). Since str_to_size_t() accepts SIZE_MAX, the size_t increment can overflow and wrap to 0, making c <= *clast stay true forever.

Proposed fix
-                    for (size_t c = *cfirst; c <= *clast; ++c)
-                    {
-                        indices.emplace_back(c);
-                    }
+                    if (*cfirst <= *clast) {
+                        for (size_t c = *cfirst;; ++c) {
+                            indices.emplace_back(c);
+                            if (c == *clast)
+                                break;
+                        }
+                    }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, you can crash with OOM in other exciting ways as well (just set the values rly big)

probably better is to cap the delta

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anematode Fair point — capping the delta is the more robust fix since it handles both the wraparound edge case and the OOM-via-huge-range vector in one shot.

Something like this would cover both:

else if (parts.size() == 2)
{
    auto cfirst = str_to_size_t(std::string(parts[0]));
    auto clast  = str_to_size_t(std::string(parts[1]));
    if (cfirst.has_value() && clast.has_value()
        && *cfirst <= *clast
        && (*clast - *cfirst) < SYSTEM_THREADS_NB)  // or a fixed cap like 65536
    {
        for (size_t c = *cfirst; c <= *clast; ++c)
            indices.emplace_back(c);
    }
}

Using SYSTEM_THREADS_NB as the cap ties the limit to actual hardware, though a fixed constant (e.g. 65536) may be more predictable if someone explicitly passes a custom config on a large machine. Either way, an invalid/absurdly-wide range silently produces no indices and from_string returns std::nullopt (since n == 0 if all ranges are skipped), which is the right behavior.

Would you like to open a follow-up issue to track this, or address it in this PR?


🧠 Learnings used
Learnt from: robertnurnberg
Repo: official-stockfish/Stockfish PR: 6843
File: src/syzygy/tbprobe.cpp:1766-1769
Timestamp: 2026-05-22T15:45:09.442Z
Learning: In the official-stockfish/Stockfish repository, avoid raising code-review comments about clang-format/code formatting issues. Clang-Format is enforced by dedicated CI, so formatting-related violations should be handled by CI rather than human review comments. In code reviews, focus comments on blocking correctness, security, or functional issues.

@github-actions

Copy link
Copy Markdown

(execution 26320379951 / attempt 1)

Comment thread src/position.cpp

set(f, is_chess960(), st);

assert(pos_is_ok());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uh did you intend to remove an assert?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set already asserts this

Comment thread src/position.cpp
return PositionSetError("Invalid FEN. Board state encoding ended but cursor not at end.");

if (pieces(PAWN) & (RANK_1 | RANK_8))
if (pieces(PAWN) & (Rank1BB | Rank8BB))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was an actual mistake in the original code. It seems like we don't actually crash with pawns on the 1st or 8th rank which is probably why we didn't catch it, but obviously we should have this be corrected

@github-actions

Copy link
Copy Markdown

(execution 26324193120 / attempt 1)

Comment thread src/ucioption.cpp

bool Option::operator!=(const char* s) const { return !(*this == s); }

static bool value_in_range(const std::string& v, int min, int max) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm still thinking about this, mentioned this on the other issue as well, would it make more sense to just clamp the value but allow it ?

that would make future larger settings possible, e.g. for hash sizes beyond what's currently supported. it also means users could specify larger values, and older versions would simply fall back to the largest supported value available at the time. cc @vondele

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not so sure, by defining the behavior we're essentially extending the standard.

Btw, that's probably also something we should catch and not silently fall back to startpos:

$ ./stockfish
Stockfish dev-20260519-133731f3 by the Stockfish developers (see AUTHORS file)
position 5r2/8/8/8/8/8/4Q3/K1k5 w - - 0 1
d

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a bit of a general problem, that we currently have no diagnostics output about invalid option values.. and do nothing

@vondele

vondele commented May 27, 2026

Copy link
Copy Markdown
Member

can you rebase on master so CI passes?

@github-actions

Copy link
Copy Markdown

(execution 26493854483 / attempt 1)

@github-actions

Copy link
Copy Markdown

(execution 26493880992 / attempt 1)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fb5cf0f9-2306-43ec-bcbe-cedbd052d8a5

📥 Commits

Reviewing files that changed from the base of the PR and between 4d0dcb6 and d85ea01.

📒 Files selected for processing (11)
  • src/engine.cpp
  • src/engine.h
  • src/misc.cpp
  • src/misc.h
  • src/numa.h
  • src/perft.h
  • src/position.cpp
  • src/position.h
  • src/uci.cpp
  • src/uci.h
  • src/ucioption.cpp
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/position.h
  • src/perft.h
  • src/engine.h
  • src/numa.h
  • src/ucioption.cpp
  • src/position.cpp
  • src/engine.cpp
  • src/uci.h
  • src/uci.cpp

Comment thread src/misc.cpp
@vondele vondele requested a review from Copilot May 27, 2026 10:08
@vondele vondele marked this pull request as draft May 27, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens command-line/UCI parsing and option handling to reduce undefined behavior and uncaught exceptions from invalid input, extending recent FEN validation work. It also changes some previously “hard-fail” paths (e.g., NUMA config parsing, perft position setup) to return structured errors that can be reported or handled.

Changes:

  • Replace exception-prone numeric parsing (stoi/stoull) with checked strtoll/strtoull-based helpers returning validity/std::optional.
  • Convert several “exit/assert/void” APIs to return std::optional/std::variant error results (flip/perft/NUMA parsing), and propagate them to UCI.
  • Improve case-insensitive string handling by avoiding std::tolower UB on negative char.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/ucioption.cpp Adds a range-checking numeric parser for spin options; fixes tolower UB in comparator.
src/uci.h Adjusts UCIEngine APIs/state for improved error reporting (stores current command).
src/uci.cpp Propagates errors from flip/perft/position; adds parsing failure handling and command context tracking.
src/position.h Changes Position::flip() to return an optional error instead of void.
src/position.cpp Returns errors from flip() via set(); minor FEN validation constant tweak.
src/perft.h Changes perft to return std::variant to report FEN setup errors.
src/numa.h Makes NUMA parsing return std::optional and updates CPU index parsing to be non-terminating on bad input.
src/misc.h Changes str_to_size_t to return std::optional<size_t>.
src/misc.cpp Implements non-throwing str_to_size_t using strtoull/errno checks.
src/engine.h Updates Engine APIs to return error-aware results (variant/optional) and boolean NUMA setter outcome.
src/engine.cpp Propagates NUMA parse failure as a non-fatal option update message; perft now returns variant.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/uci.cpp
Comment thread src/uci.cpp
Comment thread src/perft.h
Comment thread src/numa.h
@github-actions

Copy link
Copy Markdown

(execution 26524714796 / attempt 1)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants