From c878149ee5367d1fc50f791b7282db912ed9ca92 Mon Sep 17 00:00:00 2001 From: R script Date: Thu, 16 Jul 2026 07:56:42 +0100 Subject: [PATCH 1/2] test(ratchet): ASan repro for multistate heap-corruption OOB Faithful reproduction of the ratchet-multistate-segfault bug: 30x25 3-state EW (ZERO_ONLY) and IW (UPWEIGHT_ONLY) configs, 30 repeated ts_ratchet_search calls per config in one process. Not skip_extended() so the gcc-ASAN workflow runs it and halts on the first OOB access. Diagnostic branch; expected to abort under ASan until the OOB is fixed. Co-Authored-By: Claude Opus 4.8 --- tests/testthat/test-ts-ratchet-asan-oob.R | 62 +++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/testthat/test-ts-ratchet-asan-oob.R diff --git a/tests/testthat/test-ts-ratchet-asan-oob.R b/tests/testthat/test-ts-ratchet-asan-oob.R new file mode 100644 index 000000000..86915e6ea --- /dev/null +++ b/tests/testthat/test-ts-ratchet-asan-oob.R @@ -0,0 +1,62 @@ +# DIAGNOSTIC repro for the ratchet multistate heap-corruption bug +# (memory node: ratchet-multistate-segfault). Deliberately NOT gated by +# skip_extended(), so the gcc-ASAN workflow (ASan.yml, which runs plain +# `tests`) executes it and halts on the first out-of-bounds access, giving +# an exact src/*.cpp file:line trace. +# +# Cumulative per-call heap corruption: a single isolated ratchet call is +# fine; the OOB write only lands somewhere fatal after many repeated calls +# in one process. Worse (fewer calls to crash) with more chars/states/tips. +# On native Windows/MinGW the process dies with exit 139 partway through +# the loop; under ASan the offending access is reported deterministically. +# +# EXPECTED to fail (ASan abort) until the underlying OOB is fixed. Once the +# bug is fixed this becomes a passing regression test. + +# --- Config 1 (PRIMARY): ZERO_ONLY perturbation, 3-state EW ----------------- +# 30 tips x 25 chars, states 0:2, equal weights, perturbMode = 0 (ZERO_ONLY). +# This is the config that crashes soonest in the documented repro. + +test_that("ratchet ZERO_ONLY 3-state EW survives repeated calls (ASan)", { + set.seed(6284) + m <- matrix(sample(0:2, 30 * 25, replace = TRUE), nrow = 30, + dimnames = list(paste0("t", 1:30), NULL)) + dataset <- MatrixToPhyDat(m) + ds <- make_ts_data(dataset) + tr <- as.phylo(1, 30) + + ok <- TRUE + for (i in 1:30) { + set.seed(i) + r <- TreeSearch:::ts_ratchet_search( + tr$edge, ds$contrast, ds$tip_data, ds$weight, ds$levels, + nCycles = 5L, perturbMode = 0L, perturbProb = 0.15) + ok <- ok && is.finite(r$score) && r$score >= 0 + } + expect_true(ok) +}) + +# --- Config 2 (SECONDARY): UPWEIGHT_ONLY perturbation, IW (concavity 3) ------ +# perturbMode = 1 with min_steps + concavity engages the IW pattern_freq +# upweight path. Only reached if Config 1 is clean (ASan halts on first hit). + +test_that("ratchet UPWEIGHT_ONLY IW survives repeated calls (ASan)", { + set.seed(6284) + m <- matrix(sample(0:2, 30 * 25, replace = TRUE), nrow = 30, + dimnames = list(paste0("t", 1:30), NULL)) + dataset <- MatrixToPhyDat(m) + ds <- make_ts_data(dataset) + minSteps <- as.integer(MinimumLength(dataset, compress = TRUE)) + tr <- as.phylo(1, 30) + + ok <- TRUE + for (i in 1:30) { + set.seed(i) + r <- TreeSearch:::ts_ratchet_search( + tr$edge, ds$contrast, ds$tip_data, ds$weight, ds$levels, + nCycles = 5L, perturbMode = 1L, perturbProb = 0.15, + min_steps = minSteps, concavity = 3.0) + ok <- ok && is.finite(r$score) && r$score >= 0 + } + expect_true(ok) +}) From 890122a7613453a4ab228daa615b1425d72a952c Mon Sep 17 00:00:00 2001 From: R script Date: Thu, 16 Jul 2026 12:33:57 +0100 Subject: [PATCH 2/2] test(ratchet): reframe ASan repro as a passing memory-safety stress guard Investigation of the 2026-07-16 exit-139 report (ratchet-multistate- segfault) could not reproduce it on any clean build: Linux gcc-ASAN clean (both configs), and a fresh local Windows -O2 build ran 500 iterations without a crash, with search kernels byte-identical to the reported commit. Most consistent with a stale-object / mixed-ABI incremental build, not a source defect. Keep the test as a guard against a future genuine OOB. Co-Authored-By: Claude Opus 4.8 --- tests/testthat/test-ts-ratchet-asan-oob.R | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tests/testthat/test-ts-ratchet-asan-oob.R b/tests/testthat/test-ts-ratchet-asan-oob.R index 86915e6ea..e3730bc8f 100644 --- a/tests/testthat/test-ts-ratchet-asan-oob.R +++ b/tests/testthat/test-ts-ratchet-asan-oob.R @@ -1,17 +1,21 @@ -# DIAGNOSTIC repro for the ratchet multistate heap-corruption bug -# (memory node: ratchet-multistate-segfault). Deliberately NOT gated by -# skip_extended(), so the gcc-ASAN workflow (ASan.yml, which runs plain -# `tests`) executes it and halts on the first out-of-bounds access, giving -# an exact src/*.cpp file:line trace. +# Memory-safety stress guard for the C++ ratchet on multistate data. +# Deliberately NOT gated by skip_extended(), so the gcc-ASAN workflow +# (ASan.yml runs plain `tests`) executes it: many repeated ts_ratchet_search +# calls in one process on a 30-tip x 25-char 3-state matrix, which is exactly +# the shape that would surface a heap out-of-bounds write in the TBR/fitch +# undo/rescore machinery if one were ever introduced. # -# Cumulative per-call heap corruption: a single isolated ratchet call is -# fine; the OOB write only lands somewhere fatal after many repeated calls -# in one process. Worse (fewer calls to crash) with more chars/states/tips. -# On native Windows/MinGW the process dies with exit 139 partway through -# the loop; under ASan the offending access is reported deterministically. +# Origin: a 2026-07-16 report of an exit-139 heap-corruption crash here +# (memory node: ratchet-multistate-segfault). Investigation could NOT +# reproduce it on ANY clean build — Linux gcc-ASAN clean, and a fresh local +# Windows -O2 build ran 500 iterations without a crash — while the search +# kernels were byte-identical to the reported commit. The crash was most +# consistent with a stale-object / mixed-ABI incremental build (see memory +# node stale-object-abi-gotcha), not a defect in committed source. # -# EXPECTED to fail (ASan abort) until the underlying OOB is fixed. Once the -# bug is fixed this becomes a passing regression test. +# So: this test is EXPECTED TO PASS. If it ever fails, first REBUILD CLEAN +# (rm src/*.o; CCACHE_DISABLE=1 R CMD INSTALL --preclean) and re-run before +# treating it as a genuine source regression. # --- Config 1 (PRIMARY): ZERO_ONLY perturbation, 3-state EW ----------------- # 30 tips x 25 chars, states 0:2, equal weights, perturbMode = 0 (ZERO_ONLY).