Fix FailFast crash in LabelEditNativeWindow due to garbage collected WinEvent hook delegate#14663
Conversation
…WinEvent hook delegate
There was a problem hiding this comment.
Pull request overview
Fixes a GC-induced FailFast crash in LabelEditNativeWindow accessibility scenarios by ensuring WinEvent hooks are reliably unregistered when the underlying native handle is destroyed, preventing callbacks from invoking a collected delegate.
Changes:
- Centralized WinEvent hook cleanup into
UnhookWinEventHooks()and invoked it when the handle is destroyed (OnHandleChangewhen!IsHandleCreated). - Added a
_isReleasingreentrancy guard to prevent hook install/uninstall cycles during teardown. - Added callback-side guards and cleared the stored delegate reference after unhooking to avoid stale callbacks and enable GC.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
The fix has passed testing, and no regressions were found. |
There was a problem hiding this comment.
I removed the _isReleasing = true; line from both ..._DuringRelease_DoesNotReinstallHooks (135) and ..._PreventsReentrantHookInstallation (167), and both still pass. Since those tests exist to verify the _isReleasing guard, being able to delete the guard's own setup without turning them red means they aren't really exercising it; a regression in that guard wouldn't be caught. And the scenario the PR fixes; the hooks still registered when the handle goes away; has no test that would catch a regression. Would a test like this, which drives the unhook path the PR adds, be worth adding instead?
Suggested test (added to the PR and passing):
[WinFormsFact]
public void LabelEditNativeWindow_OnHandleChange_HandleDestroyedWhileHooked_UnhooksHooks()
{
using TreeView treeView = new() { Size = new Size(300, 200) };
treeView.CreateControl();
TreeViewLabelEditNativeWindow labelEdit = new(treeView);
// Reproduce the crash state: hooks still installed after the handle is gone.
labelEdit.TestAccessor.Dynamic._winEventHooksInstalled = true;
labelEdit.TestAccessor.Dynamic._isReleasing = false;
labelEdit.TestAccessor.Dynamic.OnHandleChange();
// The hooks must be removed so the collected delegate can't fire.
Assert.False((bool)labelEdit.TestAccessor.Dynamic._winEventHooksInstalled);
Assert.Null(labelEdit.TestAccessor.Dynamic._winEventProcCallback);
}|
The cleanup path looks targeted to the lifetime issue described here. One useful regression case might repeatedly destroy and recreate the label-edit handle while forcing GC between transitions, then verify that hooks are reinstalled exactly once and that a late callback is safely ignored. That would cover both halves of the lifecycle rather than only final disposal. |
Fixes #14657
Root Cause
Window destroyed → hooks not unregistered → object garbage collected → hook callback invokes freed memory → crash (
FailFast).The original code skipped cleanup when handle was gone, leaving orphaned hooks running.
Proposed changes
This prevents callbacks from invoking garbage-collected delegates.
UnhookWinEventHooks()method to centralize hook cleanupOnHandleChange()_isReleasingflag to prevent reentry during cleanup_winEventProcCallback = nullafter unhooking to allow GCCustomer Impact
PropertyGridorTreeViewwith accessibility enabled.Regression?
Risk
Screenshots
Before
When native window is destroyed, WinEvent hooks are not unregistered. This orphaned hook persists after the managed object is garbage collected, causing late callbacks to invoke freed delegates → FailFast crash.
After
Hooks are properly unregistered when the native window handle is destroyed, preventing orphaned callbacks and GC-related crashes.
Test methodology
Test environment(s)
Microsoft Reviewers: Open in CodeFlow