fix(clr): drain ROCr async thread in hip::Device::~Device() to prevent ASAN mempool UAF - #8534
Conversation
❌ PR Check — Action Required
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
|
🚫 Please fix the failed policies before requesting reviews. The following policy checks failed:
The |
There was a problem hiding this comment.
Pull request overview
Fixes ASAN use-after-free during HIP graph teardown by ensuring ROCr async completion handlers are drained before HIP-layer pools are freed, and by correcting graph execution teardown/lifetime management around devices and parallel streams.
Changes:
- Drain ROCr async handler thread(s) early in
hip::Device::~Device()to prevent graph completion callbacks from touching freed HIP pools. - Remove
g_devices[captureDeviceId_]retain/release usage from graph/graph-exec code paths to avoid refcount corruption. - Centralize
parallel_streams_teardown inGraphExecBase::~GraphExecBase()so both Classic and Segmented paths clean up streams consistently.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| projects/clr/hipamd/src/hip_graph_internal.hpp | Moves parallel stream teardown into GraphExecBase destructor; removes Classic-only teardown (but leaves commented block to clean up). |
| projects/clr/hipamd/src/hip_graph_internal.cpp | Removes g_devices[captureDeviceId_] retains across init/capture paths to avoid device refcount imbalance. |
| projects/clr/hipamd/src/hip_device.cpp | Adds an async-handler drain at the start of hip::Device destruction to prevent teardown-time callback races. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…t ASAN mempool UAF Graph completion callbacks run on the ROCr async-events thread and can cascade into GraphMemAllocNode -> MemoryPool/Heap::DecrementRefCount. At process exit, hip::Device is released (via RuntimeTearDown) *before* amd::roc::Device is destroyed, so the drain in roc::Device::~Device() runs too late to protect graph_mem_pool_. Add WaitForHsaAsyncHandlersIdle() at the top of hip::Device::~Device() to guarantee the async thread is idle before any HIP-layer pools are freed. The drain in roc::Device::~Device() is kept as an independent guard for backend-level resources and OpenCL builds (CLR_BUILD_OCL). Fixes ASAN failures in hipGraphMemPool tests. Related: #8254, #8400
fbc1491 to
97a061e
Compare
…vice teardown Move WaitForHsaAsyncHandlersIdle() out of hip::Device::~Device() and into a RuntimeTearDown callback registered at init time. The teardown order is: 1. TearDownCallbacks (reverse registration order) ← drain fires here 2. external_ release loop ← hip::Device released 3. Runtime::tearDown() ← roc::Device deleted Draining in a callback is correct because: - roc::Device is still alive (step 3 hasn't run), so the call is safe - It fires once before any hip::Device is released, making the ordering explicit at the teardown level rather than buried in each destructor - hip::Device::~Device() no longer needs to touch roc::Device at all The previous approach drained inside hip::Device::~Device() which worked but was fragile — any future hip::Device field with the same async-thread race would not be protected. Fixes ASAN use-after-free in hipGraphMem tests (ROCM-27628 adjacent). Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…SAN UAF Replace the per-device drain in hip::Device::~Device() with an unconditional DrainAllDevices() in ProfilerAtExit, which is always registered at init time via HipProfilerInitExt(). This ensures SyncAllStreams + WaitForHsaAsyncHandlersIdle always runs before any hip::Device (and its memory pools) is released, regardless of whether a profiler is attached. Previously: - DrainAllDevices() was gated on IsProfilingActive() — skipped with no profiler - WaitForHsaAsyncHandlersIdle() in roc::Device::~Device() ran too late (after hip::Device pools were freed), causing ASAN heap-use-after-free in graph mem pool tests Now: - ProfilerAtExit always drains (SyncAllStreams first, then WaitForHsaAsyncHandlersIdle) before any teardown callback or hip::Device release runs - roc::Device::~Device() no longer needs its own drain (redundant) - Correct ordering: flush GPU work → drain async handlers → release hip::Device Co-Authored-By: Claude <noreply@anthropic.com>
… for OCL safety The drain was removed in a prior commit on the assumption that the RuntimeTearDown callback covers all cases, but that callback is HIP-only. OCL teardown goes through roc::Device::~Device() directly without any prior drain, so restoring it here keeps OCL safe. For HIP this is a harmless no-op since the drain already ran earlier. Co-Authored-By: Claude <noreply@anthropic.com>
|
PR fixed the Asan failures. unittest is not required. |
## Motivation On systems where `hipDeviceCanAccessPeer` returns true (e.g., gfx1250 with AIFM fabric), `Unit_hipGraphAddMemcpyNode1D_Functional`'s peer device section proceeds to call `hipGraphInstantiate`, which fails with `hipErrorNotSupported` (error 801) because multi-device graphs are not yet supported on either the classic or segmented graph scheduling paths in CLR. The test hard-fails rather than skipping, producing a misleading failure on otherwise healthy systems. ## Technical Details In `validateMemcpyNode1DArray`, replace the unconditional `HIP_CHECK(hipGraphInstantiate(...))` in the peer device path with an explicit return-code check. When `hipGraphInstantiate` returns `hipErrorNotSupported`, clean up allocated resources and call `SKIP()` (Catch2 v3.3+) instead of failing. All other error codes continue through `HIP_CHECK` as before. The root cause is a known CLR limitation documented in both `GraphExecClassic::Init()` and `GraphExecSegmented::FindStreamsReqPerDevForSegments()`: [hipGraph] Multi-device graph is not supported on the segment scheduling path The fix is test-side only. `validateMemcpyNode1DArray` is `static` and called from exactly three SECTION blocks within a single test case; the new code path triggers only on `hipErrorNotSupported`, leaving the single-device sections unaffected. ## JIRA ID ROCM-28108 ## Test Plan Rebuilt `GraphsTest1` from the patched source on a gfx1250 system with AIFM fabric active (4 GPUs, all ACCEL_STATE: READY), with PRs #8254 and #8534 applied to CLR/ROCr. Ran with AMD_COMGR_HOTSWAP_ENTRY_TRAMPOLINES=1 HSA_ENABLE_SDMA=1, all 4 GPUs visible. ## Test Result Test | Before | After --------------------------------------------------------------|--------------|---------- Unit_hipGraphAddMemcpyNode1D_Functional (default device) | Passed | Passed Unit_hipGraphAddMemcpyNode1D_Functional (peer device) | Failed (801) | Skipped ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Fixes ASAN use-after-free failures in hipGraphMemPool tests.
Root cause
Graph completion callbacks run on the ROCr async-events thread and can cascade into
GraphMemAllocNode -> MemoryPool/Heap::DecrementRefCount. At process exit,hip::Deviceis released (viaRuntimeTearDown's external-object loop) beforeamd::roc::Deviceis destroyed — so the drain inroc::Device::~Device()runs too late to protectgraph_mem_pool_.JIRA ID
ROCM-27628
Fix
Add
WaitForHsaAsyncHandlersIdle()at the top ofhip::Device::~Device()to guarantee the ROCr async thread is idle before any HIP-layer pools are freed.The drain in
roc::Device::~Device()is intentionally kept as an independent guard for backend-level resources and OpenCL builds (CLR_BUILD_OCL).Related PRs