Improve CLI error handling a bit

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.

closes https://github.com/official-stockfish/Stockfish/pull/6844

No functional change
This commit is contained in:
Timothy Herchen
2026-07-10 20:16:02 +02:00
committed by Joost VandeVondele
parent 48a9118251
commit cd75d31127
12 changed files with 215 additions and 44 deletions
+120 -1
View File
@@ -563,6 +563,115 @@ class TestEnPassantSanitization(metaclass=OrderedClassMembers):
self.stockfish.check_output(check_output)
self.stockfish.expect("bestmove d8d7*")
class TestInvalidFEN(metaclass=OrderedClassMembers):
def beforeEach(self):
self.stockfish = None
def afterEach(self):
assert postfix_check(self.stockfish.get_output()) == True
self.stockfish.clear_output()
def _expect_critical(self, fen):
self.stockfish = Stockfish(f"position fen {fen}".split(" "), True)
assert self.stockfish.process.returncode != 0
assert "CRITICAL ERROR" in self.stockfish.process.stdout
def test_no_kings(self):
self._expect_critical("8/8/8/8/8/8/8/8 w - - 0 1")
def test_invalid_piece(self):
self._expect_critical("rnbqkbnr/pppXpppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
def test_invalid_side_to_move(self):
self._expect_critical("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR x KQkq - 0 1")
def test_pawns_on_back_rank(self):
self._expect_critical("pppppppp/8/8/8/8/8/8/4K2k w - - 0 1")
def test_invalid_skip_count(self):
self._expect_critical("9/8/8/8/8/8/8/8 w - - 0 1")
class TestInvalidOptions(metaclass=OrderedClassMembers):
def beforeAll(self):
self.stockfish = Stockfish()
def afterAll(self):
self.stockfish.quit()
assert self.stockfish.close() == 0
def afterEach(self):
assert postfix_check(self.stockfish.get_output()) == True
self.stockfish.clear_output()
# Ignore bogus spin values
def test_spin_non_numeric(self):
self.stockfish.send_command("setoption name Threads value abc")
self.stockfish.send_command("isready")
self.stockfish.equals("readyok")
def test_spin_out_of_range(self):
self.stockfish.send_command("setoption name Threads value 999999999999")
self.stockfish.send_command("isready")
self.stockfish.equals("readyok")
def test_spin_negative(self):
self.stockfish.send_command("setoption name Threads value -5")
self.stockfish.send_command("isready")
self.stockfish.equals("readyok")
# Warn on bogus NUMA configs
def test_numa_garbage(self):
self.stockfish.send_command("setoption name NumaPolicy value zzz")
self.stockfish.expect("*NumaPolicy: invalid value 'zzz', keeping previous config.*")
def test_numa_malformed_range(self):
self.stockfish.send_command("setoption name NumaPolicy value 0-")
self.stockfish.expect("*NumaPolicy: invalid value '0-', keeping previous config.*")
def test_numa_overflow(self):
self.stockfish.send_command(
"setoption name NumaPolicy value 99999999999999999999999"
)
self.stockfish.expect("*NumaPolicy: invalid value*keeping previous config.*")
self.stockfish.send_command("isready")
self.stockfish.equals("readyok")
class TestBenchFile(metaclass=OrderedClassMembers):
def beforeEach(self):
self.stockfish = None
def afterEach(self):
assert postfix_check(self.stockfish.get_output()) == True
self.stockfish.clear_output()
def _bench(self, name, content):
with open(name, "w") as f:
f.write(content)
self.stockfish = Stockfish(f"bench 16 1 4 {name} depth".split(" "), True)
def test_valid_file(self):
self._bench("good.epd", "4k3/8/4K3/8/8/8/8/8 w - - 0 1\n")
assert self.stockfish.process.returncode == 0
assert "Nodes searched" in self.stockfish.process.stderr
def test_empty_file(self):
self._bench("empty.epd", "")
assert self.stockfish.process.returncode == 0
def test_malformed_fen(self):
self._bench("bad.epd", "not a valid fen\n")
assert self.stockfish.process.returncode != 0
assert "CRITICAL ERROR" in self.stockfish.process.stdout
def test_missing_file(self):
self.stockfish = Stockfish(
"bench 16 1 4 does_not_exist.epd depth".split(" "), True
)
assert self.stockfish.process.returncode != 0
def parse_args():
parser = argparse.ArgumentParser(description="Run Stockfish with testing options")
parser.add_argument("--valgrind", action="store_true", help="Run valgrind testing")
@@ -595,7 +704,17 @@ if __name__ == "__main__":
framework = MiniTestFramework()
# Each test suite will be run inside a temporary directory
framework.run([TestCLI, TestInteractive, TestSyzygy, TestEnPassantSanitization])
framework.run(
[
TestCLI,
TestInteractive,
TestSyzygy,
TestEnPassantSanitization,
TestInvalidFEN,
TestInvalidOptions,
TestBenchFile,
]
)
EPD.delete_bench_epd()