Skip to content

Never Auto-Expand Explorer Tree Items v1.1.1 - #5149

Open
AromaKitsune wants to merge 3 commits into
ramensoftware:mainfrom
AromaKitsune:patch-1
Open

Never Auto-Expand Explorer Tree Items v1.1.1#5149
AromaKitsune wants to merge 3 commits into
ramensoftware:mainfrom
AromaKitsune:patch-1

Conversation

@AromaKitsune

Copy link
Copy Markdown
Contributor

Changelog

If this pull request updates an existing mod, describe the changes below:

  • Resolved an issue where navigating back to an external drive from a pinned folder unexpectedly auto-expands the "This PC" item.
  • Fixed a bug that caused OneDrive to crash during the sign-in process.

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 18, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@AromaKitsune

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 18, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Two of the three changes here are fine (the IsExplorerNavigationPane / mouse-branch cleanups are behavior-preserving simplifications). The IsBadReadPtr fix and the new keyboard gate both need rework.

1. IsBadReadPtr is the wrong fix for the OneDrive crash — validate the sender instead

IsBadReadPtr is a deprecated API that shouldn't be used (The Old New Thing). Three separate problems here:

  • It can itself destabilize the host process. IsBadReadPtr probes the memory range under SEH; when the range crosses a thread's stack guard page, the probe consumes the guard, so the stack silently stops growing and that thread crashes later, somewhere unrelated. NMHDR/NMTREEVIEWW notification structs are almost always stack-allocated, so this is exactly the documented failure case.
  • It doesn't reliably fix the bug it's aimed at. sizeof(NMTREEVIEWW) is ~152 bytes vs. 24 for a bare NMHDR. If the sender passed a smaller struct, the extra ~128 bytes are almost always still readable (same stack page), so IsBadReadPtr returns FALSE and the garbage action read happens anyway. It only trips in the rare page-boundary case — which is also the case where it may hit a guard page.
  • It's now on a very hot path. With @include *, the check runs for every WM_NOTIFY sent via SendMessageW in every process on the system — including NM_CUSTOMDRAW, which fires several times per control repaint. IsBadReadPtr is not a cheap call (SEH frame + page probing), so this is a measurable system-wide cost added for a check that mostly does nothing.

The actual root cause is that the WM_NOTIFY branch casts to LPNMTREEVIEWW and reads action before confirming the sender is Explorer's nav pane tree view. pNotifyHeader->code is not a reliable type discriminator — notification codes are only unique within a control class, and any app (OneDrive included) is free to define its own NMHDR-based codes with arbitrary values. Note the TVM_EXPAND branch already does the right thing: it checks GetClassNameW(hWnd) == L"SysTreeView32" first.

Fix: only touch NMHDR members (which the WM_NOTIFY contract guarantees) until the sender is confirmed, then cast:

else if (uMsg == WM_NOTIFY)
{
    auto* pNotifyHeader = reinterpret_cast<LPNMHDR>(lParam);
    if (pNotifyHeader != nullptr &&
        pNotifyHeader->code == TVN_ITEMEXPANDINGW)
    {
        HWND hTreeView = pNotifyHeader->hwndFrom;

        // Confirm the sender really is the nav pane tree view before
        // treating lParam as an NMTREEVIEWW - other controls are free to
        // use the same numeric notification code with a different struct.
        WCHAR szClassName[16];
        if (GetClassNameW(hTreeView, szClassName, ARRAYSIZE(szClassName)) &&
            _wcsicmp(szClassName, L"SysTreeView32") == 0 &&
            IsExplorerNavigationPane(hTreeView))
        {
            auto* pTreeViewNotify = reinterpret_cast<LPNMTREEVIEWW>(lParam);
            if (pTreeViewNotify->action == TVE_EXPAND &&
                !IsUserInteractingWithTreeView(hTreeView) &&
                !IsSingleRootItem(hTreeView, pTreeViewNotify->itemNew.hItem) &&
                !(settings.allowTopLevelItemAutoExpansion &&
                    IsTopLevelItem(hTreeView, pTreeViewNotify->itemNew.hItem)))
            {
                return TRUE;
            }
        }
    }
}

This removes both IsBadReadPtr calls, makes the two branches symmetric, and keeps the hot path down to one integer compare for unrelated WM_NOTIFY traffic.

If you still see the crash after this, please check which read actually faulted — if it's pNotifyHeader->code itself, the sender is passing something that isn't an NMHDR at all, which would be worth knowing (and would also mean the IsBadReadPtr(pNotifyHeader, sizeof(NMHDR)) check wasn't what fixed it).

2. The new keyboard gate silently blocks explicit user expansions

IsUserInteractingWithTreeView now requires VK_RIGHT/VK_ADD/VK_MULTIPLY to be physically held at the exact moment the notification is processed. That correctly kills the "focus is in the tree, so allow everything" false positive, but it also rejects several expansions the user really did ask for:

  • Nav pane context menu → "Expand". The menu item is invoked on button-up, so by the time TVN_ITEMEXPANDING fires no mouse button is down, and the cursor is over the popup menu (class #32768), not over the tree view — so both branches return false and the user's explicit command does nothing. Please test this path.
  • UI Automation (ExpandCollapsePattern.Expand(), i.e. screen readers and automation tools) — no physical key or button is involved, so it's always blocked.
  • Slow enumeration. If the tree has to enumerate a network/removable location before sending the notification, the user may have released the key already, and their expand is dropped.
  • Mirrored (RTL) tree views swap the expand/collapse arrows, so VK_RIGHT is the wrong key on Arabic/Hebrew UI. Worth handling or at least noting.

A deterministic alternative that avoids polling async key state entirely: subclass the tree view, set a flag around the DefSubclassProc call for input messages that can trigger an expansion (WM_KEYDOWN with the expand keys, WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, and whatever command the context menu dispatches), and check that flag when intercepting the notification. Anything the tree does synchronously in response to a real input message is then allowed by construction, with no timing races and no dependence on which key happens to be down.

3. Consider replacing the global SendMessageW hook with a subclass on the nav pane tree

@include * plus a hook on SendMessageW means every SendMessageW call in every process on the machine runs through this mod's code. That's the widest possible blast radius for a mod that only cares about one control class, and it's also what made the OneDrive crash possible in the first place — the mod sees notifications it has no business inspecting.

The nav pane tree is easy to target directly: hook CreateWindowExW, and when a SysTreeView32 is created whose parent is a NamespaceTreeControl, subclass it (plus its parent, to intercept TVN_ITEMEXPANDING on the receiving side instead of the sending side). mods/explorer-tree-click-expand.wh.cpp does exactly this for the same control and is a good template — see TrySubclassTreeView, the CreateWindowExW hook, the Wh_ModAfterInit EnumWindows pass that picks up already-open windows, and the Wh_ModUninit teardown that removes every subclass before the mod unloads. Use WindhawkUtils::SetWindowSubclassFromAnyThread / RemoveWindowSubclassFromAnyThread, since each Explorer window runs on its own thread.

Benefits: no per-message cost outside the nav pane, the WM_NOTIFY type-confusion class of bug disappears entirely (you only ever see your own tree's notifications), and it also gives you the clean place to implement the input-scoped flag from item 2. You can keep @include * for file-picker coverage — the cost then only applies to processes that actually create a nav pane.

I realize this is a larger rework than a patch release; if you'd rather ship the IsBadReadPtr and keyboard fixes first and do this separately, that's reasonable, but items 1 and 2 should land in this PR.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • -lcomctl32 in @compilerOptions looks unused. The mod only uses the TreeView_* macros from commctrl.h, which expand to SendMessage calls — no comctl32 export is referenced. The #include <commctrl.h> is still needed for the message/notification constants.
  • The anonymous global struct { bool allowTopLevelItemAutoExpansion; } settings; could just be a plain bool g_allowTopLevelItemAutoExpansion; — the struct wrapper doesn't buy anything for a single field.
  • L"SysTreeView32" / L"NamespaceTreeControl" / L"CabinetWClass" / L"#32770" and their buffer sizes are spelled out inline in two places; constexpr WCHAR constants (as in explorer-tree-click-expand.wh.cpp#L97) would keep the buffer size and the string from drifting apart.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • if (uMsg == TVM_EXPAND && (wParam & TVE_EXPAND)) also matches TVE_TOGGLE (0x0003), because TVE_TOGGLE & TVE_EXPAND != 0. So a TVM_EXPAND/TVE_TOGGLE sent against an already-expanded item — which would collapse it — gets blocked too. If you want to be precise, either use (wParam & TVE_ACTIONMASK) == TVE_EXPAND, or for TVE_TOGGLE check TreeView_GetItemState(..., TVIS_EXPANDED) and only block when the toggle would expand.
  • The mod only sees expansions routed through SendMessageW. SendMessageA, SendNotifyMessageW, SendMessageTimeoutW, and any path internal to user32/comctl32 that doesn't go through the SendMessageW export are not intercepted. That's presumably fine for the shell's current code paths, but it's a fragile contract across Windows builds — another argument for the subclass approach in item 3, which sees the message regardless of how it was sent.
  • IsSingleRootItem / IsTopLevelItem issue TVM_GETNEXTITEM sends back into the tree view from inside the SendMessageW hook, i.e. while the tree is mid-TVN_ITEMEXPANDING. These are read-only queries so it's very likely harmless, and the hook's fast path makes the re-entry cheap, but it's worth being aware of if you ever add anything that mutates tree state from those helpers.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 18, 2026
@AromaKitsune

Copy link
Copy Markdown
Contributor Author

IsBadReadPtr is the wrong fix for the OneDrive crash — validate the sender instead

I left IsBadReadPtr in place because OneDrive's React Native framework (OneDriveReactNativeWin32WindowClass) actively broadcasts WM_NOTIFY messages with invalid/garbage memory addresses (e.g., 0xaa00) in the lParam.

Validating the sender first as suggested by the AI is not possible, because merely reading pNotifyHeader->code to verify the notification type dereferences that garbage pointer and causes an immediate 0xc0000005 Access Violation before hwndFrom can even be checked.

Here is the crash dump output demonstrating the access violation on the garbage pointer when IsBadReadPtr is removed:

CONTEXT:  (.ecxr)
rax=000000000003beae rbx=000000000000024f rcx=0000000000bba838
rdx=000000000000004e rsi=000001b269b53700 rdi=000001b2698675c0
rip=00007ff82c571625 rsp=0000001dacf0dd80 rbp=0000001dacf0df00
 r8=000000000003beae  r9=000000000000aa00 r10=0000000000000000
r11=0000001dacf0dc50 r12=0000000000000000 r13=0000000000000001
r14=000001b269b53798 r15=000001b2698675c0
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
local_never_auto_expand_explorer_tree_items_1_1_1_641662!Z17SendMessageW_HookP6HWND__jyx+0xa5:
00007ff8`2c571625 418179103afeffff cmp     dword ptr [r9+10h],0FFFFFE3Ah ds:00000000`0000aa10=????????
Resetting default scope

EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ff82c571625 (local_never_auto_expand_explorer_tree_items_1_1_1_641662!Z17SendMessageW_HookP6HWND__jyx+0x00000000000000a5)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 0000000000000000
   Parameter[1]: 000000000000aa10
Attempt to read from address 000000000000aa10

PROCESS_NAME:  OneDrive.exe

READ_ADDRESS:  000000000000aa10

ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at 0x%p referenced memory at 0x%p. The memory could not be %s.

EXCEPTION_CODE_STR:  c0000005

EXCEPTION_PARAMETER1:  0000000000000000

EXCEPTION_PARAMETER2:  000000000000aa10

STACK_TEXT:
0000001d`acf0dd80 00007fff`b17def7b     : 0000001d`acf0de70 000001b2`69b53700 000001b2`69b53700 00007ff7`453a0000 : local_never_auto_expand_explorer_tree_items_1_1_1_641662!Z17SendMessageW_HookP6HWND__jyx+0xa5
0000001d`acf0de00 00007fff`b17dd40c     : 0000001d`acf0e340 000001b2`687e74d0 0000001d`acf0e340 000001b2`687e7558 : FileSyncClient!DownloadScenario::~DownloadScenario+0xff82b
0000001d`acf0df70 00007fff`b17ce964     : 000001b2`69418300 000001b2`687e74d0 00000000`00bba838 00007fff`b13ec6c2 : FileSyncClient!DownloadScenario::~DownloadScenario+0xfdcbc
0000001d`acf0e100 00007fff`b18e75f0     : 0000001d`acf0e320 00000000`00000004 000001b2`696f61f0 00000000`00000006 : FileSyncClient!DownloadScenario::~DownloadScenario+0xef214
0000001d`acf0e240 00007fff`b18e726a     : 0000001d`acf0e498 000001b2`696f61f0 000001b2`664a0000 000001b2`696f61f0 : FileSyncClient!DimePurchaseScenario::Report+0x49650
0000001d`acf0e3f0 00007fff`b18e5564     : 000001b2`6651ae70 00000000`ffffffff 000001b2`69aef680 00000000`00000000 : FileSyncClient!DimePurchaseScenario::Report+0x492ca
0000001d`acf0e4d0 00007fff`b18e56e9     : 000001b2`6980af00 000001b2`6980af00 000001b2`69aef680 0000001d`acf0e5a0 : FileSyncClient!DimePurchaseScenario::Report+0x475c4
0000001d`acf0e550 00007fff`b18b22fd     : 00007fff`b1c9c001 000001b2`6980af00 000001b2`6980af00 000001b2`69418300 : FileSyncClient!DimePurchaseScenario::Report+0x47749
0000001d`acf0e680 00007fff`b18b3bc8     : 000001b2`690f3d10 00000000`ffffffff 000001b2`69860ae0 000001b2`69860ae0 : FileSyncClient!DimePurchaseScenario::Report+0x1435d
0000001d`acf0e7a0 00007fff`b18b1fba     : 00000000`00000000 000001b2`69860ae0 00000000`00000000 000001b2`69860ae0 : FileSyncClient!DimePurchaseScenario::Report+0x15c28
0000001d`acf0e850 00007fff`b150d6a7     : 00000000`00000000 000001b2`00000000 00000000`00000000 00000000`00000000 : FileSyncClient!DimePurchaseScenario::Report+0x1401a
0000001d`acf0e990 00007fff`b151a5ed     : 000001b2`697c39e0 000001b2`687b24e0 00000000`00000000 000001b2`69340dc0 : FileSyncClient!ShareScenario::Report+0x8ecb7
0000001d`acf0ed50 00007ff8`2b9c7e4e     : 00007ff8`2b9d4da0 000001b2`69340ea8 000001b2`69340ea8 000001b2`69340dc0 : FileSyncClient!ShareScenario::Report+0x9bbfd
0000001d`acf0ede0 00007ff8`2b9cadae     : 000001b2`687b2508 000001b2`687b24f0 000001b2`687b2508 00000000`ffffffff : FileSyncEvents!CreateMainThreadProxy+0x690e
0000001d`acf0eed0 00007ff8`2b9c38b4     : 000001b2`686f9c10 000001b2`6959cba8 000001b2`687b24e0 000001b2`691152b0 : FileSyncEvents!CreateMainThreadProxy+0x986e
0000001d`acf0efc0 00007ff8`2b9cbf2c     : 000001b2`6959cb80 00007ff8`2b9cba54 00000000`00000000 000001b2`6959cb80 : FileSyncEvents!CreateMainThreadProxy+0x2374
0000001d`acf0f0e0 00007ff8`2b9c2235     : 000001b2`686f9c30 000001b2`00000000 000001b2`686f9c30 0000001d`acf0f1b9 : FileSyncEvents!CreateMainThreadProxy+0xa9ec
0000001d`acf0f120 00007ff8`2b9c6cf8     : 00000000`0000031f 000001b2`68756ec0 0000001d`acf0f309 0000001d`acf0f2d0 : FileSyncEvents!CreateMainThreadProxy+0xcf5
0000001d`acf0f220 00007ff8`2b9c69fb     : 00000000`00000020 000001b2`68756ec0 00000000`00000000 00000000`00000004 : FileSyncEvents!CreateMainThreadProxy+0x57b8
0000001d`acf0f260 00007ff8`2b9c721d     : 00000000`00000018 00007ff8`00000000 00000000`00000000 000001b2`66972710 : FileSyncEvents!CreateMainThreadProxy+0x54bb
0000001d`acf0f2a0 00007ff8`3c301028     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00006011 : FileSyncEvents!CreateMainThreadProxy+0x5cdd
0000001d`acf0f370 00007ff8`61aac366     : 00000000`00000000 0000001d`acf0f639 00000000`00000001 00000000`00000000 : atlthunk!AtlThunk_0x00+0x18
0000001d`acf0f3b0 00007ff8`61aaa7bd     : 0000001d`acf0f620 00007ff8`3c301010 00000000`0173a5c0 00000000`00000000 : user32!UserCallWinProcCheckWow+0x356
0000001d`acf0f510 00007fff`b1509102     : 0000001d`acf0f620 000001b2`69418300 000001b2`69569940 00000000`00000002 : user32!DispatchMessageWorker+0x1dd
0000001d`acf0f590 00007fff`b13e9a1d     : 0000001d`acf0f6f0 0000001d`acf0f6f0 000001b2`69418300 0000001d`acf0f6f0 : FileSyncClient!ShareScenario::Report+0x8a712
0000001d`acf0f6a0 00007ff7`453a3f0c     : 00007ff7`453a0000 00007fff`b13e96c0 00000000`00000000 00000000`00000000 : FileSyncClient!RunClient+0x35d
0000001d`acf0f7f0 00007ff7`4541ad32     : 00000000`00000000 00000000`00000005 00000000`00000000 00000000`00000000 : OneDrive+0x3f0c
0000001d`acf0ff20 00007ff8`61c9ccb7     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : OneDrive+0x7ad32
0000001d`acf0ff60 00007ff8`62c8ad6c     : 00000000`00000000 00000000`00000000 000004f0`fffffb30 000004d0`fffffb30 : kernel32!BaseThreadInitThunk+0x17
0000001d`acf0ff90 00000000`00000000     : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x2c

The new keyboard gate silently blocks explicit user expansions

I have partially addressed this by adding VK_LEFT for RTL locales. However, I am leaving the context menu and UI automation blocks unaddressed.

While the "Expand" context menu item is blocked, manual expansions still function perfectly when clicking the chevron or pressing the standard keys (, , +, and *). Implementing a complex state tracker or subclassing architecture just to support the right-click context menu edge case introduces unnecessary complexity and instability, especially since the primary interactions work flawlessly.

Consider replacing the global SendMessageW hook with a subclass on the nav pane tree

I am leaving the global SendMessageW hook intact. Switching to a subclass architecture requires hooking CreateWindowExW and manually enumerating existing windows, which drastically overcomplicates the mod's scope. The current global hook, combined with the IsBadReadPtr validation, successfully mitigates the OneDrive crash without requiring a massive architectural rewrite for a minor patch release.

@AromaKitsune

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 19, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Something went wrong while preparing the AI review, so there is nothing to post this time. This is a problem on the reviewer's side, not with this pull request.

Comment /ai-review to try again.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 19, 2026
@AromaKitsune

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 19, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


The -lcomctl32 removal is correct (all the TreeView_* macros used here are plain SendMessage wrappers), and the two return-simplification refactors are behavior-preserving. The OneDrive fix, though, is done with an API that shouldn't be used, and the underlying cause is worth fixing directly.

1. IsBadReadPtr is not a safe way to validate lParam. IsBadReadPtr is documented as never-use: it probes memory by touching it, so if the pointer happens to land on a thread's stack guard page it converts that page into a normal page and permanently breaks that thread's stack growth — producing a crash later, somewhere unrelated. It's also a TOCTOU check (the mapping can change between the probe and the read) and it returns "readable" for a valid-but-wrong pointer, so it doesn't actually guarantee the NMTREEVIEWW fields are meaningful. Since this mod is @include *, that hazard is applied to every process on the system.

The real problem is that the hook reinterprets lParam as NMHDR*/NMTREEVIEWW* for every WM_NOTIFY sent in every process, before knowing anything about the sender. Gate on the receiving window's class first — WM_NOTIFY from the nav pane tree goes to its parent, which is always the NamespaceTreeControl the mod already requires in IsExplorerNavigationPane, so nothing is lost:

else if (uMsg == WM_NOTIFY)
{
    // The tree view sends WM_NOTIFY to its parent, which for the navigation
    // pane is always a NamespaceTreeControl. Checking that first means we
    // never dereference lParam for some other window's notification.
    WCHAR szClassName[32];
    if (GetClassNameW(hWnd, szClassName, ARRAYSIZE(szClassName)) &&
        _wcsicmp(szClassName, L"NamespaceTreeControl") == 0)
    {
        auto* pNotifyHeader = reinterpret_cast<LPNMHDR>(lParam);
        if (pNotifyHeader->code == TVN_ITEMEXPANDINGW)
        {
            auto* pTreeViewNotify = reinterpret_cast<LPNMTREEVIEWW>(lParam);
            if (pTreeViewNotify->action == TVE_EXPAND)
            {
                ...
            }
        }
    }
}

This fixes the OneDrive crash at the source instead of papering over it, and it also makes the hook much cheaper for every unrelated WM_NOTIFY in every process. Please drop both IsBadReadPtr calls.

2. Consider replacing the process-wide SendMessageW hook with a subclass on the nav pane tree. SendMessageW is one of the hottest APIs in the system, and with @include * the mod adds a hook frame to every SendMessageW call in every process — plus it has to guess, from a raw hWnd/lParam, whether a message belongs to it. That guessing is exactly what produced the OneDrive crash, and a class-name gate only narrows it.

The established pattern for this is to hook CreateWindowExW, identify the nav pane tree (SysTreeView32 whose parent is NamespaceTreeControl), and subclass it with WindhawkUtils::SetWindowSubclassFromAnyThread — handling TVM_EXPAND in the tree's subclass proc and WM_NOTIFY/TVN_ITEMEXPANDING in a subclass on the parent. Two mods in the repo do this on the same control:

A subclass also gives you a much better signal than GetAsyncKeyState for the "is the user interacting?" logic (see the functionality notes below): you'd see the actual WM_KEYDOWN/WM_LBUTTONDOWN on the tree and could record a timestamp, instead of sampling instantaneous physical key state at an unrelated moment.

If you'd rather keep this PR minimal, item 1 is the must-have and this can be a follow-up — but the two are related, and doing item 2 makes item 1 unnecessary.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • -luser32 can go too, along with -lcomctl32. mingw-w64 links user32 by default; e.g. explorer-tree-click-expand calls GetParent, GetClassNameW, EnumWindows and GetKeyState with only -lcomctl32.
  • The class names are repeated as literals with hand-picked buffer sizes (WCHAR szClassName[16] vs [32]). Hoisting them into constexpr WCHAR constants and using one buffer size avoids the "is 16 big enough for SysTreeView32?" question entirely — see kNavPaneParentClassName / kTreeViewClassName.
  • Once the class gate from item 1 is in, the pNotifyHeader != nullptr check is redundant — WM_NOTIFY always carries a notification structure.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • The narrowed keyboard gate may block some genuinely user-initiated expansions. Previously "tree view has focus" was enough; now an expand key must be physically held at the moment the hook runs. Worth testing these paths:
    • Context menu → "Expand" on a nav pane item. No key is down, and WindowFromPoint returns the menu window (#32768), not a child of the tree, so both branches of IsUserInteractingWithTreeView return false and the expansion is suppressed. This used to work via the focus check.
    • Slow locations (network shares, drives that need to spin up). If the namespace tree defers the actual expansion until its background enumeration completes, the TVM_EXPAND/TVN_ITEMEXPANDING arrives after the key has been released.
    • Numpad * (expand all) — the recursive descendant expansions are generated by the control itself and may outlive the keypress.
    • A more robust signal is to record when the tree last saw relevant input (e.g. a timestamp updated on WM_KEYDOWN for the expand keys and on WM_LBUTTONDOWN/WM_CONTEXTMENU, then accept expansions within a few hundred ms). That needs the subclass from item 2, but it removes the timing race entirely.
  • wParam & TVE_EXPAND also matches TVE_TOGGLE. TVE_TOGGLE is 0x0003, so wParam & TVE_EXPAND is true for it — meaning a programmatic toggle on an already expanded item (which would collapse it) gets swallowed as well. Either use (wParam & TVE_TOGGLE) == TVE_EXPAND to handle only explicit expands, or resolve TVE_TOGGLE against the item's current TVIS_EXPANDED state.
  • VK_RBUTTON / VK_MBUTTON in the mouse check don't trigger any expansion in a tree view; they only widen the window during which auto-expansion is permitted. Narrowing that to VK_LBUTTON would make the check tighter, though the practical impact is small.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 19, 2026
@AromaKitsune

Copy link
Copy Markdown
Contributor Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 19, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Two things stood out: the WM_NOTIFY reordering made the hottest path of a system-wide hook much more expensive, and the narrowed input heuristic looks like it now blocks some genuinely user-initiated expansions.

1. The WM_NOTIFY fast path now costs a GetClassNameW call for every notification, in every process

The mod is @include * and hooks SendMessageW, so this branch runs for every WM_NOTIFY sent anywhere on the system. WM_NOTIFY is one of the highest-frequency messages in Win32 — NM_CUSTOMDRAW alone fires per item/subitem/paint for every list view, tree view, toolbar and header control. v1.1 filtered on the cheap hdr.code comparison first; v1.1.1 puts GetClassNameW (a win32k syscall — there is no user-mode class-name cache) ahead of it, so every process pays a syscall per notification. The lParam != nullptr guard was also dropped along the way.

The crash fix itself is right, but the ordering isn't needed for it. The OneDrive crash was almost certainly the NMTREEVIEW-specific reads on what was really only a plain NMHDR: on x64 NMHDR is 24 bytes and NMTREEVIEWW::action sits at offset 24, with itemNew.hItem around offset 96 — so a sender passing a bare NMHDR (as any non-tree-view control does) made v1.1 read past the end of the allocation, which AVs whenever it lands at a page boundary. hdr.code, by contrast, is always valid for any legitimate WM_NOTIFY, so testing it first is safe and keeps the class check off the fast path:

else if (uMsg == WM_NOTIFY)
{
    auto* pTreeViewNotify = reinterpret_cast<LPNMTREEVIEWW>(lParam);
    WCHAR szClassName[32];
    // Only the NMHDR part is guaranteed valid for an arbitrary WM_NOTIFY, so
    // test it first, and confirm the window is the navigation pane before
    // touching any NMTREEVIEW-specific field.
    if (pTreeViewNotify &&
        pTreeViewNotify->hdr.code == TVN_ITEMEXPANDINGW &&
        GetClassNameW(hWnd, szClassName, ARRAYSIZE(szClassName)) &&
        _wcsicmp(szClassName, L"NamespaceTreeControl") == 0 &&
        pTreeViewNotify->action == TVE_EXPAND)
    {
        ...
    }
}

Worth considering as the more thorough fix: both messages the mod cares about are delivered to windows it can identifyTVM_EXPAND to the SysTreeView32, TVN_ITEMEXPANDINGW to its NamespaceTreeControl parent — so a subclass sees them just as well as a SendMessageW hook does, and the global hot-path hook can go away entirely. Hook CreateWindowExW to catch nav pane tree views as they're created (plus an EnumWindows pass in Wh_ModAfterInit for windows that already exist), then WindhawkUtils::SetWindowSubclassFromAnyThread. mods/explorer-tree-click-expand.wh.cpp does exactly this for the same control and is a close template. That would leave CreateWindowExW as the only global hook — vastly colder than SendMessageW — and would also remove the need to class-check anything per message.

2. Requiring a live key/button state likely blocks expansions the user did ask for

IsUserInteractingWithTreeView now demands that a specific key be physically down at the moment the message is processed. GetAsyncKeyState samples the current physical state, which isn't the same as "this expansion was caused by user input". Cases worth testing before merging:

  • Nav pane context menu → "Expand": when the command runs, no expand key is down and the cursor was last over the menu window (#32768), not the tree view — so this returns false and the expansion is blocked. v1.1 allowed it, because focus alone was enough. Same for any expansion driven from the ribbon/command bar or a shortcut that isn't one of the four listed keys.
  • UI Automation (Narrator and other assistive tech use the ExpandCollapse pattern) — no keyboard or mouse state at all, so it's always treated as automatic.
  • Touch/pen taps, and any expansion delivered a moment after the button/key is released (tap-to-click, a laggy input queue).
  • GetFocus() only reports the focus window of the calling thread's queue. If the shell issues TVM_EXPAND from a worker thread rather than the UI thread, the keyboard branch can never match. GetGUIThreadInfo(GetWindowThreadProcessId(hTreeView, nullptr), &gui) gives the tree's own focus regardless of which thread the hook runs on.

If you move to the subclassing approach above, a much more precise signal becomes available: set a flag in the subclass proc while dispatching WM_KEYDOWN / WM_LBUTTONDOWN / WM_LBUTTONDBLCLK / WM_CONTEXTMENU, clear it on the way out, and treat an expand arriving while the flag is set as user-initiated. That covers the context menu too, since the menu is tracked synchronously inside the WM_CONTEXTMENU handler, and it doesn't depend on physical key timing at all.

Optional improvements

Minor polish — none of this affects users, so it's your call.

  • (wParam & 0x0003) == TVE_EXPAND: the mask is TVE_TOGGLE, so (wParam & TVE_TOGGLE) == TVE_EXPAND says the same thing without the magic number.
  • IsExplorerNavigationPane: collapsing the function into one boolean expression moved GetAncestor ahead of the parent-class comparison, so it now runs even for windows that are rejected immediately after. Putting the cheap NamespaceTreeControl test first keeps the early-out cheap.
  • Dropping -lcomctl32 -luser32 is correct — the TreeView_* macros are pure SendMessage wrappers and user32 is in the default link set — no action needed, just confirming it isn't an oversight.

Functionality notes

Non-critical observations and ideas about the feature behavior itself.

  • Narrowing to (wParam & 0x0003) == TVE_EXPAND correctly stops the mod from blocking collapses, but it also means TVE_TOGGLE now passes through untouched. If any shell path auto-expands with TVE_TOGGLE, it will slip past the mod. Handling it would mean reading the item's current TVIS_EXPANDED state and only blocking when the toggle would expand — worth doing only if you actually observe it.
  • The keyboard set (VK_LEFT/VK_RIGHT/VK_ADD/VK_MULTIPLY) matches what the tree view control itself acts on, so it looks complete for direct key input; VK_SUBTRACT is correctly absent since collapses aren't blocked.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-author The author's turn: request an AI review, or respond to one that was posted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant