Claude-skill-registry custom-memory-heap-crash
Debugging crashes related to custom memory heap implementations, particularly release-only crashes involving static destruction order, use-after-free during program shutdown, and memory lifecycle issues between custom allocators and standard library internals. This skill should be used when debugging segfaults or memory corruption that only occur in release/optimized builds, involve custom memory allocators or heap managers, or manifest during static object destruction after main() returns.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/custom-memory-heap-crash" ~/.claude/skills/majiayu000-claude-skill-registry-custom-memory-heap-crash && rm -rf "$T"
skills/data/custom-memory-heap-crash/SKILL.mdCustom Memory Heap Crash Debugging
Overview
This skill provides guidance for debugging crashes related to custom memory heap implementations, with particular focus on release-only crashes that occur during static destruction. These issues are notoriously difficult to diagnose because they involve subtle interactions between custom allocators, C++ standard library internals, and static initialization/destruction order.
When to Apply This Skill
Apply this skill when encountering:
- Crashes that only occur in release/optimized builds but not in debug builds
- Segfaults during program shutdown (after
returns)main() - Use-after-free errors involving custom heap managers
- Memory corruption traced to static object destruction
- Crashes in standard library internals (locale, iostream, facets) with custom allocators
Diagnostic Workflow
Phase 1: Initial Crash Analysis
-
Identify crash timing: Determine if the crash occurs:
- During normal execution
- After
returns (static destruction phase)main() - Only in release builds (not debug builds)
-
Gather crash information:
- Use debugger to get stack trace at crash point
- Note which memory addresses are involved
- Identify if crash is in user code or library code
-
Compare debug vs release behavior:
- Build both configurations and test
- Note any differences in crash location or timing
- Debug builds may use different allocation strategies (per-object vs pooled)
Phase 2: Memory Lifecycle Analysis
-
Map static object lifetimes: Identify all static/global objects and their destruction order:
- Custom heap managers
- Singleton objects
- Static containers or caches
- Standard library static objects (locale facets, iostream buffers)
-
Trace allocation sources: For each allocation involved in the crash:
- Determine which allocator was used (custom heap vs standard malloc)
- Track when the allocation occurred relative to static object creation
- Understand which destructor will free it
-
Identify ordering conflicts: Look for scenarios where:
- Memory allocated from custom heap is accessed after heap destruction
- Standard library internals allocate from custom heap unexpectedly
- Static destruction order differs between debug and release builds
Phase 3: Root Cause Investigation
-
Examine library internals: When crashes involve standard library:
- Locale/facet registration (
,_Facet_Register_impl
)_Fac_tidy_reg_t - iostream initialization
- Thread-local storage cleanup
- Check library source code if available
- Locale/facet registration (
-
Understand optimization effects: Release builds may:
- Inline functions, changing allocation timing
- Reorder operations
- Eliminate debug-only code paths
- Use different standard library implementations
-
Verify assumptions: Add temporary instrumentation to confirm:
- When allocations actually occur
- Which allocator services each allocation
- Destruction order of static objects
Solution Strategies
Strategy 1: Force Early Initialization
Trigger library allocations before custom heap is created, ensuring they use standard malloc:
void force_early_initialization() { // Force locale/facet initialization std::ostringstream oss; oss << 42; // Triggers numeric facet registration std::locale loc = std::locale(); // Force iostream initialization std::cout.flush(); std::cerr.flush(); } // Call this BEFORE creating custom heap int main() { force_early_initialization(); // Standard malloc used create_custom_heap(); // Now safe to create heap // ... }
Strategy 2: Extend Heap Lifetime
Keep custom heap alive until after all dependent static objects are destroyed:
// Use shared_ptr to extend lifetime static std::shared_ptr<CustomHeap> g_heap; // Or use atexit() for manual cleanup void cleanup_heap() { // Destroy heap last } int main() { atexit(cleanup_heap); // ... }
Strategy 3: Track Allocation Sources
Implement tracking to identify which allocations came from custom heap:
class CustomHeap { std::unordered_set<void*> tracked_allocations; public: void* allocate(size_t size) { void* ptr = internal_alloc(size); tracked_allocations.insert(ptr); return ptr; } bool owns(void* ptr) { return tracked_allocations.count(ptr) > 0; } };
Strategy 4: Override Global Operators
Ensure all allocations route through custom heap consistently:
void* operator new(size_t size) { if (g_custom_heap && g_custom_heap->is_active()) { return g_custom_heap->allocate(size); } return std::malloc(size); } void operator delete(void* ptr) noexcept { if (g_custom_heap && g_custom_heap->owns(ptr)) { g_custom_heap->deallocate(ptr); } else { std::free(ptr); } }
Verification Checklist
After implementing a fix, verify:
- Crash no longer occurs in release build
- Application still works correctly in debug build
- Memory sanitizers (Valgrind, ASan) report no errors
- Fix addresses root cause, not just symptoms
- Solution is robust to library implementation changes
Common Pitfalls
Pitfall 1: Incomplete Initialization Forcing
Simply calling
std::cout.flush() may not trigger all necessary library initializations. Use operations that exercise the specific subsystems involved (locale formatting, facet registration, etc.).
Pitfall 2: Assuming Consistent Ordering
Static destruction order can vary between:
- Debug and release builds
- Different compiler versions
- Different standard library implementations
Pitfall 3: Ignoring "Still Reachable" Warnings
Valgrind's "still reachable" memory often indicates intentionally leaked static data, but verify it's not masking the actual issue.
Pitfall 4: Over-Relying on Debug Builds
Debug builds may mask timing-dependent issues due to:
- Per-object allocation instead of pooling
- Additional safety checks
- Different optimization levels
Debugging Tools and Techniques
GDB Commands for Static Destruction
# Set breakpoint on static destructors break __cxa_finalize # Print static destruction order info frame # Watch memory access watch *0xaddress
Valgrind Usage
# Full memory check valgrind --leak-check=full --track-origins=yes ./program # Check with release build (may need debug symbols) valgrind --leak-check=full ./program_release
Compilation for Debugging Release Builds
# Release optimization with debug symbols g++ -O2 -g -o program_release_debug source.cpp # Address sanitizer (may change behavior) g++ -O2 -fsanitize=address -o program_asan source.cpp
References
For detailed technical information about memory lifecycle analysis and library internals, see
references/debugging_guide.md.