| name | debug-test-failures |
| version | 1.0.0 |
| description | This skill should be used when debugging test failures, build errors, runtime crashes, or assertion failures in the Sleigh/Ghidra decompiler project. It covers building with libc++ hardening assertions (ci-head-assertions preset), running ctest, obtaining stack traces from SIGABRT or crash signals, narrowing down which datatest triggers a failure, creating patch files for upstream Ghidra source in src/patches/, and recognizing common C++ bug patterns such as strict-weak ordering violations in comparators. Relevant when encountering "the build is failing", "ctest fails", decompiler crashes, or libc++ assertion errors.
|
Debugging Test Failures in the Sleigh Project
This project builds the Ghidra decompiler as a standalone C++ library. Ghidra source
is fetched via CMake FetchContent and patches are applied via git am. The key
challenge is that bugs live in upstream Ghidra code and fixes must be packaged as
patch files.
Quick Reference
cmake --preset ci-head-assertions
cmake --build build/ci-head-assertions
ctest --test-dir build/ci-head-assertions --output-on-failure
rm -rf build/ci-head-assertions/_deps/ghidrasource-*
build/ci-head-assertions/_deps/ghidrasource-src/Ghidra/Features/Decompiler/src/decompile/cpp/
0. Prerequisites
Create CMakeUserPresets.json
The ci-head-assertions preset used throughout this workflow lives in
CMakeUserPresets.json, which is not version-controlled and does not exist on
a fresh checkout. You must create it in the repository root before any build
commands will work.
Create /workspaces/sleigh/CMakeUserPresets.json with this content:
{
"version": 2,
"cmakeMinimumRequired": { "major": 3, "minor": 18, "patch": 0 },
"configurePresets": [
{
"name": "libcxx",
"hidden": true,
"cacheVariables": {
"CMAKE_CXX_COMPILER": "clang++",
"CMAKE_CXX_FLAGS": "-stdlib=libc++ -D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 -fstrict-flex-arrays=3 -fstack-clash-protection -fstack-protector-strong",
"CMAKE_EXE_LINKER_FLAGS": "-stdlib=libc++ -lc++abi",
"CMAKE_SHARED_LINKER_FLAGS": "-stdlib=libc++ -lc++abi"
}
},
{
"name": "ci-head",
"hidden": true,
"inherits": ["ci-ubuntu"],
"cacheVariables": { "sleigh_RELEASE_TYPE": "HEAD" }
},
{
"name": "ci-head-assertions",
"binaryDir": "${sourceDir}/build/${presetName}",
"inherits": ["libcxx", "ci-head"],
"generator": "Ninja",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
}
]
}
This file inherits from ci-ubuntu (defined in CMakePresets.json), so the
repo's version-controlled presets must also be present (they always are in a
normal checkout).
1. Build Presets
The assertions preset
The ci-head-assertions preset (created in Section 0 above) links against libc++
with _LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG. This enables runtime
checks that catch undefined behavior in STL usage — most importantly, it validates
that comparators passed to std::sort satisfy strict-weak ordering.
Other useful presets
| Preset | Source | Purpose |
|---|
ci-head-assertions | CMakeUserPresets.json | HEAD build with libc++ debug assertions (primary debugging preset) |
ci-head | CMakeUserPresets.json | HEAD build without assertions (faster, hidden/base preset) |
ci-sanitize | CMakePresets.json | Stable build with ASan + UBSan (repo-controlled) |
Note: ci-head-assertions and ci-head are user-local presets created in
Section 0. ci-sanitize is the only sanitizer preset version-controlled in the
repo. Add your own sanitizer presets to CMakeUserPresets.json as needed.
2. Running Tests
ctest --test-dir build/ci-head-assertions --output-on-failure
ctest --test-dir build/ci-head-assertions -R decomp_datatest --output-on-failure
There are typically 3 tests:
| Test | What it does |
|---|
sleigh_decomp_unittest | Unit tests (fast) |
sleigh_decomp_datatest | Decompiler data tests — runs XML test files from the datatests/ directory |
sleigh_namespace_std_test | Namespace compilation test |
The datatest exercises the most code paths and is the most likely to trigger
assertion failures from comparator bugs.
How datatest works
The test binary loads XML files from the datatests directory in alphabetical order.
Each XML file contains binary data + expected decompiler output patterns. The test
decompiles each function and checks the output against <stringmatch> patterns.
The exact command ctest uses (check tests/CTestTestfile.cmake for the authoritative version):
./tests/sleigh_decomp_test \
-sleighpath <build-dir> \
-path <build-dir>/_deps/ghidrasource-src/Ghidra/Features/Decompiler/src/decompile/datatests \
datatests
3. Getting a Stack Trace from a Crash
When a test crashes (e.g., SIGABRT from an assertion), you need a stack trace to
identify which comparator or code path is the culprit. The test binaries may not
have full debug symbols, so multiple techniques may be needed.
See references/stack-trace-techniques.md for the full details on each method.
Here's the summary:
- gdb: Use
gdb -batch for full demangled backtraces — the simplest method
- lldb: Use
lldb -b with -k for crash-time backtrace commands
- LD_PRELOAD backtrace: Build a small shared library that catches SIGABRT and
prints a backtrace — works when neither debugger can attach (containers)
- addr2line: Resolve raw hex addresses from the backtrace to function names
and source lines
4. Narrowing Down Which Test Triggers a Crash
Tip: Often the stack trace from Section 3 identifies the exact function and
source line, making this step unnecessary. Use this binary search technique only
when the stack trace is unclear or you need to isolate which specific test input
triggers the crash.
When you know the test crashes but can't immediately identify the cause from the
stack trace, use binary search on the test XML files.
The idea: copy subsets of test files to a temporary directory and run the test
binary against that subset. Keep halving until you isolate the triggering file.
cd build/ci-head-assertions
mkdir -p /tmp/test_subset
SRC=_deps/ghidrasource-src/Ghidra/Features/Decompiler/src/decompile/datatests
for f in $(ls $SRC/*.xml | sort | sed -n '1,20p'); do cp "$f" /tmp/test_subset/; done
./tests/sleigh_decomp_test -sleighpath . -path /tmp/test_subset datatests
Important: some crashes only reproduce when specific files are processed together
(accumulated state from earlier tests), so start by splitting the full set in half
rather than testing individual files. If an individual file doesn't crash alone, try
combining it with files that ran before it in alphabetical order.
5. Creating Patches for Ghidra Source
Patches live in src/patches/HEAD/ (for HEAD builds) and src/patches/stable/
(for stable builds). They are applied via git am during the CMake FetchContent
step. See references/patching-workflow.md for the full step-by-step.
Quick workflow
- Edit the bug in
build/<preset>/_deps/ghidrasource-src/Ghidra/Features/Decompiler/src/decompile/cpp/
- Commit:
cd into the ghidrasource-src dir, git add + git commit
- Generate patches:
git format-patch -o /workspaces/sleigh/src/patches/HEAD/ <base_commit>..HEAD
- Register the new patch in
src/setup-ghidra-source.cmake
- Clean and rebuild:
rm -rf build/<preset>/_deps/ghidrasource-* then reconfigure
The base commit for HEAD is the ghidra_head_git_tag value in src/setup-ghidra-source.cmake.
6. Common Bug Patterns
The most common bugs are strict-weak ordering violations in comparators used
by std::sort or std::set. These are caught by libc++ assertions at runtime.
Key patterns include null pointer sentinels that don't check self-comparison,
special index early-returns without checking the other operand, and unsafe casts
in compareDependency methods of TypePartial* classes.
See references/common-bug-patterns.md for detailed before/after code examples,
real-world instances from the codebase (e.g., PullRecord::operator< in
bitfield.cc, FlowBlock::compareFinalOrder in block.cc), and a systematic
audit checklist for finding new comparator bugs.