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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions