Skip to content

USE_POTENTIAL_RZONE=true → deterministic SIGSEGV (missing empty-point guard in 4 connector loops) + fix #1

Description

@soy-tuber

Thanks for releasing the solver and the Cho Chikun tsumego set — reproduction was smooth.

While reproducing on a 2026 toolchain I hit a crash in a documented option, and tracked it down to a small, self-contained bug with what looks like a one-line-per-site fix. Sharing the diagnosis + patch in case it's useful.

Summary

Enabling the documented option USE_POTENTIAL_RZONE=true causes a deterministic segmentation fault early in the search, on both cfg/RZS-TT.cfg and cfg/RZS-PT.cfg, for every problem I tried. With the flag at its default (false) everything runs fine.

Repro

  • Take cfg/RZS-TT.cfg, set USE_POTENTIAL_RZONE=true, leave everything else default.
  • Put a single problem in candidate.list (e.g. chao_vol2_p156.json).
  • Release/CGI -conf_file cfg/RZS-TT.cfg -mode tsumego_solver
  • → SIGSEGV within a few seconds. (Same with cfg/RZS-PT.cfg.)

Environment: native build (no Docker) on WSL2 Ubuntu 24.04, libtorch 2.11.0+cu130 / CUDA 13.2 / gcc 13.3, RTX 5090. The fault is board-logic-level and looks toolchain-independent.

Backtrace (gdb)

#0 WeichiBoard::updateSiblings              WeichiBoard.cpp:351   (grid.getBlock() is null)
#1 WeichiBoard::playLight                   WeichiBoard.cpp:189
#2 WeichiBoard::play                        WeichiBoard.cpp:169
#3 WeichiQuickWinHandler::hasPotentialRZone / hasBensonSequence   WeichiQuickWinHandler.cpp
#4 WeichiQuickWinHandler::hasConnectorPotentialRZone   WeichiQuickWinHandler.cpp:89
#5 WeichiUctAccessor::selectNode            WeichiUctAccessor.cpp:93

Root cause

hasPotentialRZone and hasBensonSequence contain four connector-play loops that call m_board.play(WeichiMove(winColor, pos)) for each pos scanned from a connector/test bitboard, without checking the point is empty first. When a pos is already occupied (e.g. it becomes occupied after an earlier connector play in the same loop), play() produces a null block, which is then dereferenced in WeichiBoard::updateSiblings (WeichiBoard.cpp:351, gridBlock->getLiberty()).

The same file already has exactly this guard in another function (around WeichiQuickWinHandler.cpp:138): if (grid.getColor() != COLOR_NONE) { continue; }. It's just missing in these four loops.

Fix

Add the existing guard before each of the four m_board.play(move) calls:

if (m_board.getGrid(pos).getColor() != COLOR_NONE) { continue; }

Diff:

diff --git a/CGI/WeichiQuickWinHandler.cpp b/CGI/WeichiQuickWinHandler.cpp
--- a/CGI/WeichiQuickWinHandler.cpp
+++ b/CGI/WeichiQuickWinHandler.cpp
@@ -217,6 +217,7 @@ bool WeichiQuickWinHandler::hasPotentialRZone(const WeichiDragon& dragon, Weichi
         int pos = 0;
         while ((pos = bmTestStones.bitScanForward()) != -1) {
             WeichiMove move(winColor, pos);
+            if (m_board.getGrid(pos).getColor() != COLOR_NONE) { continue; }  // skip occupied connector (guard from line ~138); avoids null block in play()->updateSiblings
             m_board.play(move);
             bmPlayed.SetBitOn(pos);
             if (m_board.getGrid(pos).getBlock()->getStatus() == LAD_LIFE) {
@@ -255,6 +256,7 @@ bool WeichiQuickWinHandler::hasBensonSequence(const WeichiDragon& dragon, Weichi
     int pos = 0;
     while ((pos = bmPossibleConnector.bitScanForward()) != -1) {
         WeichiMove move(winColor, pos);
+        if (m_board.getGrid(pos).getColor() != COLOR_NONE) { continue; }  // skip occupied connector (guard from line ~138); avoids null block in play()->updateSiblings
         m_board.play(move);
         bmRetPlayed.SetBitOn(pos);
         if (m_board.getGrid(pos).getBlock()->getStatus() == LAD_LIFE) {
@@ -287,6 +289,7 @@ bool WeichiQuickWinHandler::hasBensonSequence(const WeichiDragon& dragon, Weichi
             int pos = 0;
             while ((pos = bmPossibleConnector.bitScanForward()) != -1) {
                 WeichiMove move(winColor, pos);
+                if (m_board.getGrid(pos).getColor() != COLOR_NONE) { continue; }  // skip occupied connector (guard from line ~138); avoids null block in play()->updateSiblings
                 m_board.play(move);
                 bmRetPlayed.SetBitOn(pos);
                 if (m_board.getGrid(pos).getBlock()->getStatus() == LAD_LIFE) {
@@ -334,6 +337,7 @@ bool WeichiQuickWinHandler::hasBensonSequence(const WeichiDragon& dragon, Weichi
                 int pos = 0;
                 while ((pos = bmPossibleConnector.bitScanForward()) != -1) {
                     WeichiMove move(winColor, pos);
+                    if (m_board.getGrid(pos).getColor() != COLOR_NONE) { continue; }  // skip occupied connector (guard from line ~138); avoids null block in play()->updateSiblings
                     m_board.play(move);
                     bmRetPlayed.SetBitOn(pos);
                     if (m_board.getGrid(pos).getBlock()->getStatus() == LAD_LIFE) {

Validation

After the patch, USE_POTENTIAL_RZONE=true no longer crashes. I re-ran 9 problems that already solve soundly under the default config; all 9 returned the identical verdict (UCT_WIN) with real search (3.7k–34k simulations) — i.e. the fix removes the crash without changing results.

To be clear about scope: this is a correctness/crash fix, not a capability gain. On my hardest set (problems unproven at 300 s by both TT and PT), enabling the now-working potential-RZ did not prove any additional problems within the same time budget. So the patch just makes the documented flag usable and sound; it isn't claimed to solve more.

Happy to open this as a PR instead, or share the full core dump / logs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions