Skip to content

fix(build-test-workflow): make ASan-enabled Python tests work under debug-*-cpu presets#1061

Open
IvanaGyro wants to merge 4 commits into
masterfrom
claude/issue-1052-skill-asan-preload
Open

fix(build-test-workflow): make ASan-enabled Python tests work under debug-*-cpu presets#1061
IvanaGyro wants to merge 4 commits into
masterfrom
claude/issue-1052-skill-asan-preload

Conversation

@IvanaGyro

@IvanaGyro IvanaGyro commented Jul 15, 2026

Copy link
Copy Markdown
Member

Problem

.claude/skills/build-test-workflow/scripts/build_preset.sh is the repo's single required entry point for building/testing Cytnx (per CLAUDE.md). It already has special-cased ASAN_OPTIONS handling for debug-*-cuda presets, but no equivalent handling existed for debug-*-cpu presets (debug-openblas-cpu, debug-mkl-cpu) when running Python-target tests (pytest against an ASan-instrumented cytnx.so).

Running Python tests against these presets hit two independent problems, neither fixed by LD_PRELOAD=libasan.so alone:

  1. __cxa_throw interceptor CHECK-fail. ASan's __cxa_throw interceptor needs to resolve the real __cxa_throw, which lives in libstdc++ — but a plain python process never links libstdc++. The instant any C++ code inside cytnx.so throws (routine: cytnx_error_msg throws cytnx::error, surfaced in Python as cytnx.CytnxError; ordinary error-path tests hit this constantly), the interceptor CHECK-fails and the process dies instantly. Under pytest's output capture this looks like a bare exit=1 with no traceback — the ASan report goes to a captured fd pytest never shows.
  2. LeakSanitizer false positives from CPython itself. Even with (1) fixed, default leak detection reports hundreds of "leaks" that are actually CPython's own deliberate non-cleanup at interpreter shutdown (interned strings, static type objects, allocator arenas) — unrelated to whether Cytnx leaks, and unfixable from the Cytnx side.

Neither problem is specific to any one test; both would silently break every Python-target --test run of debug-openblas-cpu/debug-mkl-cpu under this script.

Fix

In build_preset.sh, right before the script invokes pytest (not before the build — only the pytest invocation needs this), for debug-*-cpu presets on Linux (gated on command -v gcc, since the mechanism is gcc/.so-specific; debug-openblas-apple is a different, dylib-based ASan mechanism and is explicitly out of scope/unverified here):

export LD_PRELOAD="$(gcc -print-file-name=libasan.so) $(gcc -print-file-name=libstdc++.so)"
export ASAN_OPTIONS='detect_leaks=0'

This mirrors the existing debug-*-cuda branch's convention of unconditionally exporting ASAN_OPTIONS (rather than deferring to a caller-supplied value), for consistency with that existing code.

Scope notes:

  • Does not touch the test_main/gpu_test_main (ctest) path — that's a natively-linked ASan executable with a clean process exit, needs no shim, and is the one place a genuine Cytnx C++ leak would actually be caught, so leak detection stays on there.
  • Does not apply to debug-openblas-apple — the debug-*-cpu glob pattern already excludes it (preset name ends in -apple, not -cpu).
  • Only applied in the code path that actually invokes pytest, not during the build — so it never touches pip install/cmake/ninja invocations, which don't expect an ASan-preloaded environment.

Also updated SKILL.md's "GPU rules" section with a new "ASan + Python rules" subsection documenting this, mirroring the existing debug-*-cuda bullet.

Testing

Verified directly on this branch (GCC 13 / Ubuntu, debug-openblas-cpu):

  • Pre-fix crash, reproduced firsthand: ran the unmodified script's pytest path (build_preset.sh debug-openblas-cpu --target pycytnx --test pytests/binding_exception_test.py, a file that exercises cytnx_error_msgcytnx.CytnxError via pytest.raises). Result: pytest starts ("test session starts"), then dies with zero further output and EXIT_CODE=1 — no test results printed at all, matching the described silent-kill signature.
  • Post-fix, same test file: with the fix in place, the identical command now runs to completion with no manual environment exports from me: 4 passed, 1 warning in 8.26s, EXIT_CODE=0.
  • Pre-commit: pre-commit run --files .claude/skills/build-test-workflow/SKILL.md .claude/skills/build-test-workflow/scripts/build_preset.sh — clean.

Still in progress / not yet confirmed on this branch (posting this draft now per reviewer request, rather than blocking on the full matrix — this PR only touches repo tooling under .claude/skills/, which has no CI coverage, so a human/bot reviewer reading the diff can proceed in parallel):

  • Full pytest pytests/ run under debug-openblas-cpu with the fix (only a single test file has been run so far, not the whole suite).
  • debug-mkl-cpu (only debug-openblas-cpu verified so far; the fix branches on debug-*-cpu so debug-mkl-cpu should behave identically, but this hasn't been separately run).
  • CUDA compile-check (build_preset.sh debug-openblas-cuda, no --test, no GPU needed) confirming the neighboring debug-*-cuda ASAN_OPTIONS branch is unaffected — running now, not yet complete.
  • Plain C++ path (build_preset.sh debug-openblas-cpu --target test_main --test) confirming this change doesn't affect the ctest path — not yet run (expected to pass except for the pre-existing, unrelated BlockUniTensorTest.CombineBondForceDoesNotRewriteUserHeldBond failure tracked in ASan memcpy-param-overlap in BlockUniTensor::permute_ (via combineBond), CombineBondForceDoesNotRewriteUserHeldBond #1052, out of scope for this PR).

Related to #1052 (part of the same ASan-hardening effort; does not fix the underlying bug directly).

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request documents and implements preloading rules for ASan-instrumented Python-target test runs on CPU presets to prevent crashes from C++ exceptions and false-positive leak reports. The reviewer provided valuable feedback regarding the robustness of hardcoding gcc to locate libasan.so and libstdc++.so, highlighting potential issues with compiler mismatches or multiple installed GCC versions, and suggested a more robust approach by extracting the compiler path from CMakeCache.txt.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread .claude/skills/build-test-workflow/scripts/build_preset.sh
IvanaGyro added a commit that referenced this pull request Jul 15, 2026
gemini-code-assist flagged (high priority, PR #1061) that hardcoding
`gcc` to locate libasan.so/libstdc++.so for the debug-*-cpu Python-test
LD_PRELOAD shim is fragile: it preloads whatever `gcc` resolves to on
PATH regardless of which compiler actually built cytnx.so, so a
Clang-built preset or a non-default GCC version would preload a
mismatched ASan runtime, and -print-file-name silently returns its
bare input filename when it can't resolve a library, yielding an
unusable relative LD_PRELOAD path.

Read CMAKE_CXX_COMPILER from the configured build's own CMakeCache.txt
instead, fall back to `gcc` only if the cache entry is unavailable,
confirm the resolved compiler identifies as GCC (Clang's --version has
no "Free Software Foundation" line), and require both resolved library
paths to be absolute before preloading anything -- otherwise the shim
is skipped rather than exporting a broken LD_PRELOAD.

Verified: build/debug-openblas-cpu/CMakeCache.txt's
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++ resolves to GCC 13.3.0 and
absolute library paths; re-ran pytests/binding_exception_test.py
(the same file used to verify the original fix) with the new lookup
in place: 4 passed, 1 warning -- same outcome as the gcc-hardcoded
version, confirming no regression on the working case.

Co-Authored-By: Claude <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.67%. Comparing base (1908a05) to head (7441df0).
⚠️ Report is 54 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff             @@
##           master    #1061       +/-   ##
===========================================
+ Coverage   60.49%   72.67%   +12.18%     
===========================================
  Files         229      226        -3     
  Lines       31873    28177     -3696     
  Branches       71       71               
===========================================
+ Hits        19282    20479     +1197     
+ Misses      12570     7677     -4893     
  Partials       21       21               
Flag Coverage Δ
cpp 72.79% <ø> (+12.34%) ⬆️
python 64.13% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
C++ backend 71.89% <ø> (+13.84%) ⬆️
Python bindings 77.70% <ø> (+2.69%) ⬆️
Python package 64.13% <ø> (ø)
see 40 files with indirect coverage changes

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1908a05...7441df0. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@IvanaGyro IvanaGyro left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Skill.md should not explain the reason of adding env var. The reason can be put into script command.

For easy to test, if gtest binary can contain ASan, why not also let pycynx contain ASan for debug* preset? Is there any drawback?

IvanaGyro added a commit that referenced this pull request Jul 15, 2026
…te once

Two review findings from IvanaGyro on PR #1060:

1. Hardcoding `gcc` to locate libasan.so/libstdc++.so has the same
   compiler-mismatch gap gemini-code-assist flagged in PR #1061's copy
   of this exact pattern: it preloads whatever `gcc` resolves to on
   PATH regardless of which compiler actually built cytnx.so, and
   -print-file-name silently returns its bare input filename (a
   relative path) when it can't resolve a library, yielding an
   unusable relative LD_PRELOAD path. Read CMAKE_CXX_COMPILER from the
   configured build's own CMakeCache.txt instead, confirm it
   identifies as GCC (Clang's --version has no "Free Software
   Foundation" line), and require both resolved library paths to be
   absolute. Unlike the skill script's graceful degradation (silently
   skip the shim), this CI job's toolchain is always GCC by
   construction (the `compilers` conda package installed earlier in
   this job) -- an unexpected result here means the environment
   changed unexpectedly, so fail loudly with `::error::` rather than
   silently skip the shim and hit a confusing crash two steps later.

2. Compute the resolved LD_PRELOAD value once and reuse it, instead of
   recomputing `gcc -print-file-name` twice (once per Python-hosted
   step). Added a dedicated "Resolve ASan runtime paths" step that
   writes the resolved value to $GITHUB_OUTPUT, referenced by both
   "Sanity-check the install" and "Run pytest with Python coverage"
   via their own step-scoped `env:`. Deliberately NOT hoisted to
   $GITHUB_ENV (job-wide): that would leak ASAN_OPTIONS=detect_leaks=0
   into "Run gtest via ctest" too, undoing the existing design that
   keeps leak detection on for the one place a genuine Cytnx C++ leak
   would actually be caught.

Verified locally: build/debug-openblas-cpu/CMakeCache.txt's
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++ resolves via the new lookup
to GCC 13.3.0 and absolute paths for both libraries; re-ran the
resulting LD_PRELOAD/ASAN_OPTIONS values against `python -c "import
cytnx"` end-to-end -- succeeds, matching the prior gcc-hardcoded
behavior.

Co-Authored-By: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Member Author

Replying to your review (pullrequestreview-4703567269) — no inline thread to reply on since it was a top-level review comment.

SKILL.md wording: agreed, fixed in 25a8f74. Trimmed the ASan + Python rules bullet down to stating the behavior, matching the terser style the adjacent GPU rules bullet already uses; the full reasoning stays in build_preset.sh's own comments where it already lived.

"Why not build ASan into pycytnx?": checked before assuming — it already is. cytnx.cpython-311-x86_64-linux-gnu.so links libasan.so.8 as a DT_NEEDED dependency and contains 39 __asan_* symbols. -fsanitize=address is appended to the global CMAKE_CXX_FLAGS in CMakeLists.txt before pybind11_add_module(pycytnx ...) is declared, so pycytnx inherits it exactly the way test_main does — there's no build-config gap to close here.

The actual constraint is different: running python -c "import cytnx" with zero LD_PRELOAD/ASAN_OPTIONS set fails immediately with ASan's own message: "ASan runtime does not come first in initial library list; you should either link runtime to your application or manually preload it with LD_PRELOAD." ASan requires its runtime to be the first thing loaded into a process. test_main satisfies that automatically because it's the process itself. python -c "import cytnx" doesn't — import is a dlopen() of an already-ASan-instrumented .so happening after the (uninstrumented) python process has already started, and ASan has no way to retroactively insert itself first. LD_PRELOAD is the standard, documented fix for exactly this scenario (the same pattern numpy/PyTorch/etc. use to ASan-test their own C extensions from Python) — not a workaround for something missing in pycytnx's build.

I don't see a way to avoid the shim without shipping a custom ASan-linked Python interpreter, which seems like a much larger cost than the current one-time-computed LD_PRELOAD. Posted the same finding on #1060's compiler-mismatch thread too, since it raised the same underlying question.

Also fixed the compiler-mismatch issue you flagged on #1060 (line 152) here as well, since this PR's build_preset.sh had the identical gcc-hardcoded pattern — see f362a99 (resolves the compiler from CMakeCache.txt, confirms it's GCC, requires absolute paths).


Generated by Claude Code

Comment thread .claude/skills/build-test-workflow/SKILL.md Outdated
@IvanaGyro
IvanaGyro marked this pull request as ready for review July 15, 2026 18:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3007b3e40

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .claude/skills/build-test-workflow/scripts/build_preset.sh Outdated
…er debug-*-cpu presets

A plain `python` process under debug-openblas-cpu/debug-mkl-cpu's
ASan-instrumented cytnx.so has two problems the CUDA branch's ASAN_OPTIONS
export doesn't cover: ASan's __cxa_throw interceptor CHECK-fails because
python never links libstdc++ (the first C++ exception surfaced through
cytnx_error_msg as cytnx.CytnxError kills the process instantly, silently
under pytest's captured fds), and LeakSanitizer reports hundreds of false
positives from CPython's own interpreter-shutdown non-cleanup.

Preload both libasan.so and libstdc++.so and set ASAN_OPTIONS=detect_leaks=0
for a Python-target --test run on debug-*-cpu presets (Linux only; the shim
is skipped -- not force-fitted -- whenever the resolved compiler isn't GCC
or a library path can't be resolved to an absolute path. debug-openblas-apple's
dylib-based ASan is out of scope). Scoped to the pytest invocation only, not
the build or the test_main/gpu_test_main ctest path, which stays untouched
and keeps leak detection on.

The compiler used for the preload is read from the configured build's own
CMakeCache.txt CMAKE_CXX_COMPILER entry, not a bare `gcc` guess on PATH:
preloading one GCC's libasan.so into a binary built by a different GCC (or
by Clang, if one happens to be on PATH too) is an ASan runtime/ABI mismatch,
and `-print-file-name` silently returns its bare input filename (a relative
path) when it can't resolve a library, which LD_PRELOAD then fails to find.
The cache entry's type is normally FILEPATH (auto-detected) but STRING when
the build was configured with an explicit -DCMAKE_CXX_COMPILER=..., so the
lookup matches either type; under this script's `set -euo pipefail`, a grep
with no match at all would otherwise abort the whole script on this line
before the `gcc` fallback runs, breaking Python test runs for an otherwise
valid GCC ASan build -- `|| true` keeps that case falling through instead.
Falls back to a bare `gcc` on PATH only if CMakeCache.txt has no entry, and
confirms whichever compiler was resolved actually identifies as GCC
(Clang's --version has no "Free Software Foundation" line) before trusting
its `-print-file-name` output.

Verified: build/debug-openblas-cpu/CMakeCache.txt's
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++ resolves to GCC 13.3.0 and
absolute library paths; ran pytests/binding_exception_test.py against the
resulting LD_PRELOAD/ASAN_OPTIONS -- 4 passed. Confirmed the STRING-type
cache entry and the CMakeCache.txt-missing paths both fall through to the
`gcc` fallback without aborting the script, by exercising the grep-and-cut
lookup standalone against synthetic CMakeCache.txt fixtures under
`set -euo pipefail`.

Co-Authored-By: Claude <noreply@anthropic.com>
@IvanaGyro
IvanaGyro force-pushed the claude/issue-1052-skill-asan-preload branch from f3007b3 to c99797d Compare July 15, 2026 18:57
@IvanaGyro
IvanaGyro requested review from pcchen and yingjerkao July 16, 2026 08:41
IvanaGyro and others added 3 commits July 17, 2026 06:27
…ing test runs

HPTT opens an OpenMP team of Device.Ncpus threads on every permute, even
for 50-element tensors, and idle workers busy-spin between the
microsecond-spaced parallel regions the test suites trigger. Under
build_preset.sh's parallel ctest (--parallel nproc) the spinners from
concurrent test processes oversubscribe the CPU and inflate test wall time
several-fold; the Lanczos/Arnoldi suites are hit hardest (issue #1058).

Export OMP_WAIT_POLICY=passive (governs every OpenMP runtime in the
process, including an openmp-variant OpenBLAS) and OPENBLAS_THREAD_TIMEOUT=4
(a pthreads-variant OpenBLAS pool ignores OMP_WAIT_POLICY) for the test
paths. Both are set because either OpenBLAS variant may be the one linked.
benchmarks_main is exempt so measured numbers keep default threading
behavior.

Measured (4-core runner, debug-openblas-cpu, ctest -j4 -R '^Lanczos_Ut\.'):
23.9 s wall without the exports, 0.9 s with them.

Co-Authored-By: Claude <noreply@anthropic.com>
…ositive

A conda-forge openmp-variant OpenBLAS carries NEEDED libgomp.so.1 plus an
$ORIGIN RPATH, and in a conda env with _openmp_mutex=llvm that soname is a
symlink to libomp.so, so LLVM's OpenMP runtime loads into every ASan test
process regardless of LD_LIBRARY_PATH. Its startup allocation
(__kmp_allocate_align) is reported by LeakSanitizer as a leak and fails
every ctest test under the debug-*-cpu presets. Document the recovery in
SKILL.md's ASan section: repoint the libgomp.so.1 soname at GNU libgomp
(symlink it, or LD_PRELOAD libasan.so followed by the system libgomp)
instead of disabling leak detection.

Co-Authored-By: Claude <noreply@anthropic.com>
…rks too

Export OMP_WAIT_POLICY=passive and OPENBLAS_THREAD_TIMEOUT=4 for
benchmarks_main runs as well, not only ctest/pytest, so every timed run
through build_preset.sh -- tests and benchmarks alike -- executes under one
and the same threading environment and benchmark numbers stay comparable
with test-suite behavior.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

1 participant