Skip to content

Publish the first version of the Windows 7 Region and Language Restorer mod - #5387

Open
babamohammed2022 wants to merge 14 commits into
ramensoftware:mainfrom
babamohammed2022:patch-43
Open

Publish the first version of the Windows 7 Region and Language Restorer mod#5387
babamohammed2022 wants to merge 14 commits into
ramensoftware:mainfrom
babamohammed2022:patch-43

Conversation

@babamohammed2022

Copy link
Copy Markdown
Contributor

Changelog

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

  • Changelog item 1...
  • Changelog item 2...

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.

@babamohammed2022

Copy link
Copy Markdown
Contributor Author

/ai-review

@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 Sep 7, 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.

@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 Sep 7, 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.


An impressive amount of careful work — the PE mapping, the pinned-digest verification, the FileIdInfo re-check after LoadLibraryExW, and the private-IAT resource substitution are all well thought through. The issues below are mostly about when that work runs and what happens on unload, rather than about the mapping itself.

1. The entire setup runs synchronously in Wh_ModInit, including the network download — this blocks Explorer's startup.

Wh_ModInit calls Environment()Prepare(), and Prepare() does all of this before returning:

  • EnsurePinned() × 2 → Download() on first run, with WinHTTP timeouts of 10/10/15/15 s per payload (WinHttpSetTimeouts), and no overall deadline;
  • re-read and SHA-256 both payloads (~620 KB) on every start, even when cached;
  • CreateActCtxW, LoadLibraryExW of the Win7 input.dll, a full manual map of intl.cpl (copy + relocate + bind imports + RtlAddFunctionTable) and its DllMain(DLL_PROCESS_ATTACH).

Wh_ModInit runs before the target process starts executing, so on a fresh install with a slow link or a captive portal, explorer.exe startup stalls for tens of seconds — and it stalls at every boot/Explorer restart for the cached-path work even when the user never opens Region.

Your own merged mod already solves exactly this: in win7-display-control-panel-restorer.wh.cpp Wh_ModInit only installs hooks and the setup worker starts in Wh_ModAfterInit, and its readme states the reason plainly ("the setup step (download + verification) runs on a background thread so it never blocks explorer.exe startup"). Please do at least that here. Better still for this mod: run Prepare() lazily on the first Region activation from CplHook (CPL_INIT / CPL_DBLCLK), so an Explorer session that never opens Region maps nothing at all and pays nothing.

2. The advertised "redirect-only, no downloads" Explorer mode is unreachable.

SelectedLaunch() returns true unconditionally for Host::Explorer:

if (host == Host::Explorer) return true;

so Environment() succeeds in every Explorer that passes the arch/OS checks, and the redirect-only branch in Wh_ModInit (if (!Environment()) { if (redirectWanted && g_envViable && g_host == Host::Explorer …) is only reachable when CommandLineToArgvW itself fails. The design comment above RedirectShellExecuteExW and the ledger line [ ] redirect ON + plain explorer ...... mod stays resident, no downloads describe behaviour that never happens — every Explorer takes the full download + manual-map path. Fixing (1) makes this consistent.

3. Unload can hang indefinitely.

Wh_ModBeforeUninit spins with no bound:

while (g_active.load(...) != 0 || g_jobs.load(...) != 0) {
    CloseOwnedWindows();
    HANDLE event = g_active.load() != 0 ? g_idle : g_jobsIdle;
    if (event) WaitForSingleObject(event, 100);
    ...
}

and the log line inside it ("Close child dialogs or the elevation prompt; code will not be unmapped while in use") acknowledges that the user has to intervene. Two concrete problems:

  • Own() silently drops windows once g_owned[64] is full — for (auto& w : g_owned) if (!w) { w = window; break; } has no overflow path. An untracked window never receives the WM_CLOSE from CloseOwnedWindows(), so the loop can never terminate. Make g_owned a growable container under the existing g_windowsLock (the copy-then-release-then-post pattern in CloseOwnedWindows is already correct and keeps working).
  • PropertySheetW on CPL_DBLCLK is modal, and the "Change system locale" flow raises an out-of-process UAC consent dialog that will not go away on WM_CLOSE. Disabling or updating the mod at that moment blocks whichever thread Windhawk called Wh_ModBeforeUninit on, for as long as the prompt is up.

A hang on unload hangs the user's Windhawk operation, so this is worth designing around rather than documenting. Deferring the mapping until a sheet is actually requested (item 1) shrinks the window a lot; the g_owned overflow is a straightforward fix on its own.

4. Resuming from arbitrary hardware faults by longjmp-ing out of a vectored continue handler.

CrashHandler records the fault and longjmps back into GuardCall. In practice the fault will be inside comctl32 / user32 / ntdll / ole32 frames called by the Win7 code, and abandoning those frames leaves those subsystems' locks and internal state inconsistent — inside explorer.exe, which then keeps running. Specifics:

  • STATUS_STACK_OVERFLOW (0xC00000FD) is in the resume set, but the guard page has already been consumed by the time the handler runs; the thread will hard-fault on the next deep call. STATUS_HEAP_CORRUPTION and STATUS_STACK_BUFFER_OVERRUN are fail-fast conditions that shouldn't be continued from at all. Please drop the fatal codes from the resume set and let those crash honestly.
  • The comment block above CrashGuard states the resume has "no unwinding, no destructors" — that isn't right: msvcrt's x64 longjmp calls RtlUnwindEx, so unwind handlers in the abandoned frames do run. Worth correcting so the next reader isn't misled.
  • AddVectoredExceptionHandler(1, FatalWatch) installs a first-chance handler at the head of the chain for the entire Explorer session, invoked for every exception on every thread in the process — even though it only ever logs while g_guard->armed. Explorer raises a lot of first-chance exceptions in normal operation. Install and remove the VEH pair around armed legacy calls, or drop FatalWatch entirely (CrashHandler is the load-bearing one, as your own comment says).

5. A Windows 7 input.dll is resident in every explorer.exe for the whole session.

Prepare() does LoadLibraryExW(g_inputPath, …) and patches three of its IAT slots (AdaptInputIat), unconditionally at init, even when Region is never opened. input.dll is a real System32 module that Explorer itself loads for TSF / the language bar, so keeping a second, 2010-vintage module with the same base name mapped in the shell is a compatibility risk that is being taken on every Explorer session for a feature most sessions never use. The lazy-init fix in (1) covers this too.

6. Runtime download of executable code from an external server.

Windhawk's stated principle is that a mod is self-contained and doesn't fetch remote resources; EnsurePinned() / Download() pull intl.cpl and input.dll from msdl.microsoft.com and then execute them. The implementation is about as careful as this can be (HTTPS + scheme check, host pinned to msdl.microsoft.com, exact size and SHA-256 verified before any use, re-verified from the pinned handle, FileIdInfo identity check after LoadLibraryExW, CheckMitigations() respecting dynamic-code/signature policy), and the same approach is already merged in your win7-display-control-panel-restorer and performance-info-tools-restorer. So this is the maintainer's call rather than something for you to change — noting it so it's explicitly on the record for this submission.

7. The file still carries the private development ledger and acceptance checklist.

Lines 112-218 are a ~110-line "ANALYSIS LEDGER" / "ACCEPTANCE CHECKLIST" block with per-version proof lists. Please strip it down to a short provenance note (the pinned hashes/URLs/PE contract facts are genuinely useful; the per-version [x]/[ ] tracking is not). More importantly, most of the boxes are still unchecked — including the two that map directly onto items 1-3 above:

// [ ] explorer Control Panel -> Region ...... same Win7 sheet
// [ ] Disable mod mid-dialog .............. dialogs close, no crash/hang

explorer.exe is one of the three @include targets and is the path that carries all of the cost described above, and "disable mid-dialog" is exactly the unload-hang case. Please verify both before this is handed to a human reviewer.

8. The README has no screenshot.

This mod restores a visible UI, and your other mods (win7-display-control-panel-restorer, performance-info-tools-restorer, win7-language-switcher-restorer) all include one. A shot of the restored 4-tab Region sheet would help users a lot. Also, the "Credits — Based on the technique of the example restorer mods" line is vague enough that it doesn't say anything; either name the mods or drop it.

Optional improvements

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

  • Drop the [IntlRestore] prefix from Wh_Log calls. Windhawk already prefixes the mod name in the log, so every line currently reads doubled. There are ~150 sites, so a single find-and-replace of L"[IntlRestore] L" covers it.

  • Use the typed hook helper. InstallRedirectHook() uses the raw Wh_SetFunctionHook with void* casts; since you already have the resolved address, WindhawkUtils::SetFunctionHook(pShellExecuteExW, RedirectShellExecuteExW, &g_origShellExecuteExW) works directly and drops the casts.

  • EnsurePinned's DeleteFileW never does anything. It runs inside the scope that still holds f.pin open with FILE_SHARE_READ (no FILE_SHARE_DELETE), so it always fails with ERROR_SHARING_VIOLATION:

    {
        ReadFile f;
        if (OpenRead(dest, f) && ...) return true;
        if (!f.bytes.empty()) DeleteFileW(dest.c_str()); // handle still open here
    }

    It's harmless in practice because WriteAtomic's MoveFileExW(MOVEFILE_REPLACE_EXISTING) runs after the scope closes and succeeds, but as written the line is dead. Move it below the closing brace or remove it.

  • First-run downloads aren't serialized across processes. Prepare() can run in explorer.exe and control.exe at the same time and both will fetch the same payload. performance-info-tools-restorer already uses a named mutex for exactly this.

  • @github is missing from the metadata; your other mods all have it.

  • Dead try/catch (...) blocks. A number of shims wrap bodies that cannot throw — ShimWinSqmAddToStream / ShimWinSqmSetString (return a constant), PrivateGetVersion, IsVersionQuery (strcmp chain), PrivateDelete. As the file itself notes, catch (...) can't catch hardware faults, so these handlers protect nothing and add noise. If they're an AI artifact rather than deliberate, they're worth removing.

  • Undocumented Server no-op. Environment() requires wProductType == VER_NT_WORKSTATION, so the mod silently does nothing on Windows Server. Worth one line in "Known limitations".

Functionality notes

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

  • FakeControlPanel::Open reports success unconditionally. Both ShellExecuteExW calls (ms-settings:privacy-location, then ms-settings:) can fail and the method still returns S_OK, so the "Default location" link silently does nothing. Since the genuine caller shows no UI on failure either, this is defensible — but returning the real failure would at least make the log honest.
  • Dialog geometry stays en-US for all 19 translation packs (as your comment at the top of the language-pack section says). German, Polish, Russian and Hungarian labels are the usual clipping victims. Widening the handful of worst controls per language is possible with the template builder you already have, since BuildDlgTemplate writes the geometry itself.
  • TagLanguage accepts a bare primary tag (it, de, …) in addition to the full tag, but $options only ever produces full tags, so that branch is unreachable from the settings UI. Harmless, just noting it in case it was meant for something else.
  • AutoLanguage() matches on PRIMARYLANGID only, so pt-PT gets the pt-BR pack and every Spanish variant gets es-ES. Reasonable as a fallback; a readme line would set expectations.
  • 19 mod-authored translations of Microsoft UI text is a large surface to maintain. The header is admirably explicit that these are not Microsoft strings. If some of them haven't been checked by a native speaker, shipping fewer packs and letting the rest fall back to the genuine English tables (which the code already supports via nullptr entries) would be a safer default.


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 Sep 7, 2026
@babamohammed2022

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 Sep 7, 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.


Impressive amount of work, and the pinning/verification of the payloads is careful. The findings below are concentrated in the unload path, which currently has a reachable crash and a reachable hang, and in the init path, which does a lot of blocking work in every Explorer session.

1. g_owned is always empty, so the unload path can never close a dialog.

std::vector<HWND> g_owned; (line 320) is default-constructed empty and is never push_back'd or resized. Own() therefore iterates zero elements:

if (!exists) for (auto& w : g_owned) if (!w) { w = window; break; }   // line 5389

Every Own(window, true) from SheetCallback (line 5401) and DialogCallback (line 5550) is a no-op, so CloseOwnedWindows() (line 6204) never posts a single WM_CLOSE. The whole "request a normal close of the private-provider dialogs" mechanism in Wh_ModBeforeUninit does nothing, which is what makes finding 2 reachable. Use push_back (with a cap), or a fixed-size array like g_blobs.

2. Forced teardown after 5 s while legacy code is still executing → use-after-free in the host.

Wh_ModBeforeUninit gives up on the wait unconditionally:

if (GetTickCount64() - start > TIMEOUT_MS) {
    Wh_Log(L"[IntlRestore] TIMEOUT: forcing unload after 5 seconds");
    break;                                        // lines 6456-6459
}

and Wh_ModUninitCleanup() then unmaps everything the still-running thread is standing on: VirtualFree(g_image.base, 0, MEM_RELEASE) (line 6242), the input.dll IAT slot restore (line 6232), FreeLibrary(g_inputModule) (line 6244) — and Windhawk FreeLibrarys the mod image itself immediately after Wh_ModUninit returns. A thread with g_active != 0 is inside CplHook, i.e. inside both the mapped Win7 image and the mod's own adapters (MainPropertySheet, PrivateMalloc, SheetCallback, …).

This is reachable in normal use, not a corner case: the Region sheet is modal, so g_active stays non-zero for as long as the user has it open, and with finding 1 in place nothing ever asks it to close. Disabling or updating the mod while the Region page is open crashes explorer.exe / control.exe.

There is no safe "force" here — the mod image cannot be unmapped while its code is on a stack. The wait needs to be unbounded once the close requests actually work (finding 1), and any remaining timeout should be a diagnostic, not a licence to tear down.

3. Wh_ModUninit can hang the host forever.

Cleanup() starts with WaitForJobs() (line 6219), which loops with no timeout at all:

while (g_jobs.load(std::memory_order_acquire) != 0) {
    DWORD status = MsgWaitForMultipleObjectsEx(1, &g_jobsIdle, 100, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
    ...                                          // lines 5738-5747
}

If a private Win7 worker never returns, Windhawk's unload never completes. That is a hang of the host process, and it is inconsistent with the bounded wait one callback earlier. Note this also pumps arbitrary messages on the Windhawk teardown thread.

4. Private worker threads are never joined; ThreadBridge outlives g_jobs == 0.

    EndJob();
    return result;                                // lines 5673-5674
}

EndJob() signals "idle" before ThreadBridge returns. At that instant teardown may proceed and Windhawk may FreeLibrary the mod, while the thread is still executing ThreadBridge's epilogue and its return into the thread-start thunk — both of which live in the mod's image. The comment on line 5671 is right that no private-image frames remain, but the mod image is the one being unmapped. PrivateSHCreateThread makes this worse by CloseHandleing the thread immediately (line 5711), so there is no join point at all.

Per the unloadability contract, keep the thread handles and WaitForSingleObject on them in Wh_ModUninit before freeing anything (see Mod lifetime). A duplicated handle stored in a small table, waited on and closed during teardown, would cover both PrivateCreateThread and PrivateSHCreateThread.

5. The whole provider setup — including a network download — runs synchronously in Wh_ModInit, in every Explorer session.

SelectedLaunch accepts any explorer.exe session (line 193), so on every Explorer start and every Explorer restart Wh_ModInitPrepare() (line 6398) does all of this before Explorer begins executing:

  • EnsurePinnedDownload on a cold cache, with 10 s connect / 15 s receive timeouts (line 415) — and on a machine with no connectivity that cost is paid on every start, since the failure isn't cached;
  • SHA-256 of both payloads, manual map + relocate + import bind of intl.cpl;
  • LoadLibraryExW of a Win7 input.dll permanently into the shell (line 6093);
  • running the Win7 DllMain inside explorer.exe (line 6115);
  • RtlAddFunctionTable (line 6110) and a process-wide first-chance VEH (line 949).

All of that for a Control Panel page the user may never open in that session. Wh_ModInit runs before the target process starts executing, so this is a direct delay to Explorer startup.

Concrete fix: in Wh_ModInit install only the cheap hooks (CPlApplet, and ShellExecuteExW when redirectSettings is on), and run Prepare() lazily on the first CPL_INIT/activation — the CPL hook already has a clean native-fallback path if the provider isn't ready. At minimum, move it off the init path onto a worker started from Wh_ModAfterInit, which is what your own merged Display restorer does: win7-display-control-panel-restorer.wh.cpp#L15041.

6. The crash guard does not guard anything, and its recovery machinery is dead code.

CrashHandler is registered with AddVectoredContinueHandler (line 948), which runs only after every frame-based handler has already declined — at that point the process is going down. The handler then returns EXCEPTION_CONTINUE_SEARCH on every path (lines 891, 916, 921), so setting g_useLegacy = false changes nothing about the outcome. The comment "Explorer stays stable, the mod degrades gracefully to Windows default behavior" (lines 909-910) is not what happens.

Correspondingly, nothing in the file ever writes guard->code, guard->address, guard->info0/info1/infoCount, and longjmp is never called. So:

  • the setjmp(guard.resume) != 0 branch in GuardCall (lines 986-1012) is unreachable;
  • exception is always 0 on that path, and the guard.infoCount >= 2 AV-detail logging (lines 902, 1008) can never fire;
  • g_vehBusy is only ever assigned false (lines 919, 985, 988), so the re-entrancy check on line 874 is inert.

This matters beyond dead code: the safety argument for running foreign Win7 binaries inside explorer.exe rests on this guard. Either make it real (frame-based __try/__except around the legacy calls, which can stop the unwind, unlike a VCH) or remove the machinery and state plainly in the README that a fault in the legacy provider takes the host down.

7. Logging from an exception handler on stack overflow / heap corruption.

FatalWatch (a first-chance VEH, line 949) matches 0xC00000FD (STATUS_STACK_OVERFLOW) and 0xC0000374 (STATUS_HEAP_CORRUPTION) at line 936 and then calls Wh_Log — formatting and allocating on a stack that has just overflowed, or on a heap that is already corrupt. That will very likely fault again. CrashHandler does the same at line 887. Drop those codes from the logged set. Given FatalWatch is a pure observer that never changes behaviour, consider dropping it entirely rather than keeping a process-wide first-chance handler in Explorer just for logging.

8. WaitForJobs() pumps arbitrary messages from inside the CPL_EXIT callback.

if (message == CPL_EXIT) WaitForJobs();          // line 6174

WaitForJobs runs PeekMessageW/DispatchMessageW (lines 5742-5744) on the host UI thread, re-entering arbitrary window procedures from inside a Control Panel callback while the host is in the middle of shutting the applet down. Wait without pumping here, or bound this to the specific window you need to service.

9. Runtime download of external binaries.

The repository guideline is that a mod must be self-contained and must not fetch remote files or contact external servers; this mod downloads intl.cpl and input.dll from msdl.microsoft.com on first use (lines 225-226, 6051). Flagging it for the maintainer's call rather than as a hard blocker, since your already-merged Classic Display Control Panel Restorer and Windows Update Control Panel Page Restorer use the same pinned-payload pattern. The pinning itself is done well here — HTTPS-only, host allowlist, exact size + SHA-256 before any use, SameMappedFile identity check against the pinned handle, and a MicrosoftSignedOnly/ProhibitDynamicCode policy check.

Optional improvements

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

  • Use WindhawkUtils::SetFunctionHook instead of raw Wh_SetFunctionHook with void* casts (lines 6317 and 6408). It's type-safe and matches the rest of the catalog:
    WindhawkUtils::SetFunctionHook(pShellExecuteExW, RedirectShellExecuteExW, &g_origShellExecuteExW);
  • ShowSheet's exception path leaves g_sheet dangling. g_sheet = &context; (line 5487) is restored on line 5490, but the catch (...) on line 5493 returns without restoring it, leaving the thread-local pointing at a dead stack frame for any later SheetCallback on that thread. A small RAII guard around the assignment would make the two paths consistent (the same pattern would help g_uiCreated/g_uiFailed).
  • PrivateNew's catch (const std::bad_alloc&) (line 1147) won't do what the comment says. g_realNew is msvcrt's operator new, built with the MSVC exception ABI; the mod is built by Clang/mingw. A foreign MSVC C++ exception will not type-match std::bad_alloc in a mingw frame, so the fallback is unreachable and a real throw escapes. Also, std::bad_alloc is declared in <new>, not <stdexcept> — worth including it explicitly.
  • Drop the [IntlRestore] prefix from Wh_Log calls. Windhawk already prefixes log lines with the mod name, so it's duplicated on every line.
  • try { ... } catch (...) on functions that cannot throw. e.g. ShimWinSqmAddToStream (line 545), ShimWinSqmSetString (line 554), PrivateGetVersion (line 1246), ShimRtlGetUILanguageInfo (line 564) — each wraps a single return in a handler. It's a lot of noise in an already very large file; if this came from the AI pass rather than a deliberate choice, it's safe to strip.
  • @architecture x86-64amd64. The mod #errors on non-x64 (line 155) and Environment() bails when the native architecture isn't AMD64 (line 6322), and the README says ARM64 is unsupported. amd64 declares that accurately and avoids injecting into ARM64 shells only to no-op. (x86-64 isn't wrong, it just also covers ARM64, which you then reject at runtime.)

Functionality notes

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

  • ContainsRegionSettingsUrl scans the whole parameter string. for (const wchar_t* p = s; *p; ++p) if (IsRegionSettingsUrl(p)) (line 6278) matches the URL at any offset in lpParameters, so an unrelated launch whose parameters merely contain ms-settings:regionlanguage as text (a browser, a script host, a shortcut passing it along) gets redirected to control.exe intl.cpl. Anchoring the scan to token starts (after a quote, space or =) would tighten this without losing the launcher cases you're targeting.
  • BeginJob admits new jobs during shutdown. !g_stopping.load() || g_active.load() != 0 || g_jobs.load() != 0 (line 5647) means that while any legacy call is in flight, g_stopping doesn't actually stop new workers from being spawned — so teardown can be prolonged by work started after the stop request. Worth making the stop flag authoritative.
  • 19 mod-provided translations against en-US dialog geometry. The README documents possible clipping, which is the honest call. Only uk-UA is labelled "community translation" in the code comment (line 4015) — since none of the 19 are Microsoft text (as your own comment at line 1727 says), consider making that clear in the $options labels too, so users don't read e.g. "Русский" as the genuine Win7 wording the way "English (genuine Microsoft)" implies.
  • ShimCheckElevationEnabled maps EnableLUA to "elevation enabled" (line 604). That's a reasonable approximation of the real policy check; just noting that the two aren't strictly the same thing, so admin-flow behaviour on unusual policy configurations may differ from Win7.
  • No overlap concern. win7-legacy-applet-restorer adds Control Panel task links that point at Microsoft.RegionAndLanguage, but doesn't restore the Win7 Region dialog itself, so this mod is complementary rather than duplicative.


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 Sep 7, 2026
@babamohammed2022

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 Sep 8, 2026
@babamohammed2022

babamohammed2022 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

It seems like that changing the architecture to amd64 makes the validation script fail while having it to x86-64 makes it work but the compatibility checks stop working. If it's a problem on my side, I'll try to fix it but I have not understood what is it in this case.

@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 mapping/verification pipeline is careful work — the SHA-256 + pinned-handle + GetFileInformationByHandleEx(FileIdInfo) identity check closes the TOCTOU window properly, the PE parsing is bounds-checked throughout, and every phrase map matches its control array exactly. The issues below are about where that work runs and about scaffolding that no longer does anything.

1. The cold-cache setup (two HTTPS downloads) runs synchronously on the host UI thread.

CplHook calls EnsurePrepared() on the first CPL message it sees — including CPL_INIT, i.e. as soon as the Control Panel enumerates intl.cpl:

if (g_useLegacy.load() && !g_legacyInitialized.load()) EnsurePrepared();

EnsurePrepared() takes g_prepareLock exclusively and runs Prepare()EnsurePinned()Download(), which is WinHTTP with WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY (WPAD discovery) and 10 s connect / 15 s send / 15 s receive timeouts, twice (intl.cpl and input.dll). On a cold cache, a captive portal, or no network, the calling thread blocks for tens of seconds. That thread is the Control Panel window's UI thread in explorer.exe, or the whole process in control.exe/rundll32.exe — so the first Control Panel open after enabling the mod freezes that window with no feedback. A second Explorer window hitting the applet blocks on g_prepareLock for the same duration.

Your own merged win7-display-control-panel-restorer already solves this: setup runs on a dedicated worker (g_setupThread.emplace(RunSetupNoexcept)), joined in Wh_ModUninit. Please do the same here — kick the prepare off in the background and keep returning the native applet from CplHook until it's ready. CplHook already has that fallback path.

2. SetThreadDpiAwarenessContext is a static import, but Environment() accepts build 10240.

DpiScope calls SetThreadDpiAwarenessContext directly, so it's a hard user32 import. That export only exists from Windows 10 1607 (14393), and DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 only from 1703 (15063). Environment() accepts dwBuildNumber >= 10240:

version.dwBuildNumber < 10240 || version.wProductType != VER_NT_WORKSTATION) return false;

On 1507/1511 the mod DLL fails to load outright (unresolved import) rather than degrading, so the advertised support range doesn't match reality. Either resolve it with GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetThreadDpiAwarenessContext") and skip DPI scoping when it's absent, or raise the minimum build to 15063.

3. Unload waits are unbounded and the in-flight download isn't cancellable.

Wh_ModBeforeUninit spins with no timeout:

while (g_active.load(...) != 0 || g_jobs.load(...) != 0) {
    CloseOwnedWindows();
    ...
    HANDLE event = g_active.load() != 0 ? g_idle : g_jobsIdle;
    if (event) WaitForSingleObject(event, 100);
}

and Cleanup() follows with WaitForJobs() and JoinTrackedThreads() (WaitForMultipleObjects(..., INFINITE)). Three ways this hangs the Windhawk disable/update operation indefinitely:

  • A nested modal window created by the legacy code that isn't in g_owned (a MessageBoxW from intl.cpl, for example) never receives the WM_CLOSE that CloseOwnedWindows() posts, and blocks the thread that would process it.
  • g_active != 0 while a thread is inside EnsurePrepared() → the unload waits out the full WinHTTP timeouts. Download() has no g_stopping check and no way to abort; compare CancelInFlightDownload() in the display mod.
  • A legacy worker stuck in a SendMessage/broadcast.

Please bound the wait (log and escalate after a few seconds), and make Download() abortable from the stop path by closing the WinHTTP handles.

Related: WaitForJobs() is also called from CplHook on CPL_EXIT — i.e. on the host UI thread — with pumpMessages = false, so it blocks that thread without dispatching messages. If any legacy worker sends a message to that thread while it's waiting, that's a self-deadlock.

4. The crash guard doesn't contain crashes, and most of it is unreachable.

CrashHandler is registered with AddVectoredContinueHandler and returns EXCEPTION_CONTINUE_SEARCH on every path. A vectored continue handler only runs after every frame-based handler has already declined — at that point the exception is unhandled and the process is going to UnhandledExceptionFilter. So the comment

// DO NOT longjmp - let the exception continue search naturally.
// The system will handle it or ignore it. Explorer continues running.

is not what happens: an access violation inside the mapped Windows 7 intl.cpl takes Explorer down. Setting g_useLegacy = false on the way out has no effect because there is no "way out".

Consequently a large block of scaffolding is dead: nothing in the file calls longjmp, and nothing ever assigns guard->code, guard->address, guard->info0/info1/infoCount. That makes the else branch of GuardCall (lines 1051-1077, including the fault-location and AV-detail logging) unreachable, exception = guard.code always 0, and jmp_buf resume / <csetjmp> vestigial. FatalWatch is then a process-wide first-chance VEH whose only job is logging.

Please either implement real containment or delete the machinery and the claims — right now a reader (and a future maintainer) is told the mod is crash-safe when it isn't, which matters given it runs unmodified Win7 binaries inside explorer.exe.

5. Legacy CPL_EXIT and DllMain(DLL_PROCESS_DETACH) run on the Windhawk unload thread.

void Cleanup() {
    ...
    if (g_legacyInitialized.exchange(false) && g_image.cpl) {
        GuardCpl(g_image.cpl, nullptr, CPL_EXIT, 0, 0, ignored, exception);
    }
    if (g_image.attached && g_image.entry) {
        GuardEntry(g_image.entry, ..., DLL_PROCESS_DETACH, ignored, exception);
    }

Wh_ModUninit runs on an arbitrary thread. The applet was initialized on the host UI thread (CplHookCPL_INIT), and Cicero/TSF and COM objects that intl.cpl creates there have thread affinity — the unload thread doesn't even have a COM apartment initialized. This path is taken whenever the mod is disabled while Explorer still holds the applet initialized, which is the normal case. Record the thread that ran CPL_INIT and post the shutdown to it (then wait for it) rather than calling into the legacy provider from the unload thread.

6. PrivateLoadLibraryW hands out extra references on the private input.dll that are never released.

if (Equal(leaf, L"input.dll")) {
    ...
    HMODULE input = LoadLibraryExW(g_inputPath.c_str(), nullptr, LOAD_LIBRARY_SEARCH_SYSTEM32);
    if (input != g_inputModule) { ... }
    return input;   // refcount incremented, handed to legacy code
}

Cleanup() calls FreeLibrary(g_inputModule) exactly once. Win7's intl.cpl loads input.dll and (like most CPLs) keeps it for the process lifetime, so the reference taken here is never balanced: after disabling the mod the private Win7 input.dll stays mapped in explorer.exe and its file stays locked, and the count grows with each enable/disable cycle. Keep a counter — increment in PrivateLoadLibraryW, decrement in PrivateFreeLibrary when the module is g_inputModule — and release the remaining balance in Cleanup().

Optional improvements

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

  • Drop the [IntlRestore] log prefix (~100 occurrences). Windhawk already prefixes log lines with the mod name, so Wh_Log(L"[IntlRestore] ...") renders it twice.
  • The "redirect-only mode in Explorer" branch in Wh_ModInit is unreachable. SelectedLaunch short-circuits on if (host == Host::Explorer) return true;, and g_envViable is set before that call, so Environment() returns true for every explorer.exe — the if (!Environment()) { if (redirectWanted && g_envViable && g_host == Host::Explorer ...) } path can only be entered if CommandLineToArgvW fails. Either delete it or make redirect-only a real mode.
  • WaitForJobs(bool pumpMessages) is always called with the default false (two call sites, both WaitForJobs()), so the MsgWaitForMultipleObjectsEx branch and the quit/quitCode/PostQuitMessage bookkeeping are dead.
  • DiagnoseCrtHeap() runs unconditionally in production, not just when logging is on — it performs a real msvcrt!malloc(0x100) and a HeapAlloc probe and walks the import descriptors on every Prepare(). Fine as a debugging aid, but gate it or drop it.
  • Force-loading the native intl.cpl into every explorer.exe. Wh_ModInit unconditionally LoadLibraryExWs system32\intl.cpl and hooks CPlApplet, so every Explorer process loads (and keeps) the module even if Control Panel is never opened. The repo's usual pattern is to hook LoadLibraryExW — resolved from kernelbase.dll, not the kernel32 import, since internal callers go straight to kernelbase — and install the hook when the DLL actually loads, plus handle the already-loaded case:
    HMODULE kernelBase = GetModuleHandleW(L"kernelbase.dll");
    auto pLoadLibraryExW = (decltype(&LoadLibraryExW))GetProcAddress(kernelBase, "LoadLibraryExW");
    WindhawkUtils::SetFunctionHook(pLoadLibraryExW, LoadLibraryExW_hook, &LoadLibraryExW_orig);
  • DpiScope in RedirectShellExecuteW / RedirectCreateProcessW has no effect — the thread DPI awareness context isn't inherited by a newly created process.
  • [[clang::musttail]] in CplHook and the #ifndef __clang__ #error guard aren't needed. Windhawk always builds mods with Clang, and the tail call isn't load-bearing here.
  • Large stack buffers. Environment() declares three wchar_t[32768] arrays (192 KB), FullPath() two more (128 KB), Prepare() one plus a FullPath() call, and SameMappedFile() one. Prepare() runs deep inside the host's CPL call stack; consider std::wstring/heap or MAX_PATH-sized buffers with the \\?\ case handled explicitly.
  • Includes: std::bad_alloc comes from <new>, which is only pulled in transitively here; <winternl.h> doesn't appear to be used.
  • PrivateNew's cross-CRT catch. catch (const std::bad_alloc&) around a call into msvcrt.dll's operator new won't do what the comment says ("MSVC matches cross-CRT by type name") — Windhawk builds mods with clang/mingw-w64, whose exception personality doesn't recognise an MSVC-thrown C++ exception, so it won't be matched by type or by catch (...). Checking for a null return from a non-throwing path would be more robust.
  • Leftover build tooling in the shipped source: // BEGIN/END PORTABLE_SELECTOR, "Kept platform-independent so the exact selector can be unit-tested on Linux", // ===== BEGIN GENERATED RESOURCE TABLES =====. Also the provenance comment at the top says the hashes, URLs, PE-contract values and version numbers "are documented in the mod README" — they aren't.
  • README nits: a stray -- line after the screenshot, and "Windows systems file are not modified" → "system files".

Functionality notes

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

  • The out-of-process "Change keyboards..." path loses everything the mod adds. PrivateShellExecuteExW rewrites the launch to rundll32.exe shell32.dll,Control_RunDLL "<storage>\input.dll". In that new rundll32.exe, SelectedLaunch requires argv[2] to be intl.cpl, so it returns false and Wh_ModInit bails — meaning the Win7 input.dll runs there with no version spoof, no IAT adaptation and no localization, regardless of the language setting. If that's the path the applet actually takes, the Text Services dialog will be English-only and running unadapted against Win10. Also, the match is exact:
    !Equal(info->lpParameters, L"shell32.dll,Control_RunDLL input.dll")
    so any deviation falls through to the real ShellExecuteExW, which launches the system input.dll — the one that lacks ordinals 104-114 that Prepare() checks for.
  • TranslateInputWindow's text-matching fallback is broad. It runs at every WM_INITDIALOG and PSCB_INITIALIZED for windows the mod owns — including the Region pages — and rewrites any child whose text matches a kInputUiLabels entry. Several of those are very generic (General, Remove, Add..., Properties..., Move Up), so a same-named control on an unrelated page will get the Text-Services translation. Scoping the pass to g_inputModule dialogs would make it safer.
  • The Settings redirect also captures IME sub-pages. SettingsUrlPrefix accepts - as a terminator, so ms-settings:regionlanguage-chsime-*, -bpmfime, -jpnime, -adddisplaylanguage etc. all match IsRegionSettingsUrl and get redirected to the classic Region dialog, which has no equivalent for them. Consider matching only the exact page names plus ?/#.
  • Windows Server is silently excluded. Environment() requires wProductType == VER_NT_WORKSTATION; worth stating in the README's Requirements section alongside the ARM64 note.
  • Downloading the pinned Microsoft binaries from msdl.microsoft.com is consistent with your other merged restorers, and the verification chain here (size + SHA-256 + FILE_SHARE_READ pin + FileIdInfo identity match against the loaded module) is the strongest of the set — nothing to change, just noting it's the mod's only external dependency.


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 Sep 8, 2026
@m417z

m417z commented Sep 8, 2026

Copy link
Copy Markdown
Member

It seems like that changing the architecture to amd64 makes the validation script fail while having it to x86-64 makes it work but the compatibility checks stop working

It's asking for a manual review, because often it is a mistake. In this case, it's probably OK. GitHub doesn't have a way to show warnings, so alerting it by returning an error is the only way.

I'll think about improving it in the future, but for now, you can ignore it as long as there are no other failures.

@babamohammed2022

Copy link
Copy Markdown
Contributor Author

The last push fixed the IOpenPanel vtable (thanks — that one is fully resolved) and added the credits/notes text, but the other findings from the previous round are still present in the code. Item 3 below is the one worth looking at first: the new commit wrote the correct behavior into the comment on g_legacyThreads and changed the container to std::pair<DWORD, HWND>, but CloseOwnedWindows still ignores the HWND and does exactly what the comment says it must not do. The rest are restated compactly.

1. ShutdownLegacyOnInitThread leaks a reference on the mod's own module, so the mod is never unloaded. Unchanged at lines 7938-7941:

HMODULE self = nullptr;
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
    reinterpret_cast<LPCWSTR>(&ShutdownHookProc), &self);

Without GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT this increments the refcount, and nothing releases it. Windhawk unloads a mod with a single FreeLibrary right after Wh_ModUninit returns, so after the first Region activation that FreeLibrary is a no-op: the mod image, its globals, its VEH registrations and the mapped Win7 provider stay in explorer.exe permanently, and the next enable/update loads a second copy. Your own win7-display-control-panel-restorer.wh.cpp#L11266 already passes the flag — do the same here (or FreeLibrary(self) after the UnhookWindowsHookEx).

Related, same function: ShutdownHookProc signals g_shutdownDone (line 7922) and only then executes return CallNextHookEx(...). Once the refcount above is fixed, the waiting Wh_ModUninit thread resumes at that SetEvent and Windhawk can unmap the image while the hook proc's epilogue and return address are still live on the init thread. UnhookWindowsHookEx doesn't wait for an in-flight hook proc either. Have the hook proc set a flag only and signal the event from a point the unload thread can genuinely join.

2. Wh_ModBeforeUninit still has no deadline. Lines 8327-8346:

while (g_active.load(...) != 0 || g_jobs.load(...) != 0) { CloseOwnedWindows(); ... }

The added periodic Wh_Log makes a stuck unload visible, but it still hangs. WM_CLOSE is only a request — a modal Win7 dialog whose DLGPROC ignores it, or one sitting on a nested modal child, never closes, and the loop spins forever. Because Wh_ModSettingsChanged unconditionally sets *reload = TRUE, this path runs on every settings change, not just on disable. WaitForJobs() (line 7243) and JoinTrackedThreads()'s WaitForMultipleObjects(..., INFINITE) (line 410) have the same shape. Bound the outer wait (10-20 s); on expiry, leave the provider mapped and put the hooks in a permanently-disabled pass-through state instead of blocking. A hang is worse for the user than a leak.

3. CloseOwnedWindows posts WM_CLOSE to every top-level window of any thread that happens to be inside a legacy call. The new commit changed g_legacyThreads to std::vector<std::pair<DWORD, HWND>> and documented why a thread-ID filter is unsafe, but CloseOwnedWindows (lines 7865-7890) throws the HWND away and enumerates the whole thread anyway:

for (const auto& entry : g_legacyThreads) legacyThreadIds.push_back(entry.first);
...
for (DWORD tid : legacyThreadIds)
    EnumThreadWindows(tid, [](HWND window, LPARAM) -> BOOL {
        PostMessageW(window, WM_CLOSE, 0, 0); return TRUE; }, 0);

So the stored host HWND is dead state and the behavior is identical to before. The root cause is that RedirectShellExecuteExW calls EnterLegacy() (line 8040) for every ShellExecuteExW in Explorer, not just for legacy-provider work — including the calls the taskbar, Start menu and desktop make to launch programs. Two consequences:

  • Disabling or reloading the mod (i.e. any settings change) while such a call is in flight broadcasts WM_CLOSE to Shell_TrayWnd, Progman, the Start menu and every CabinetWClass window on that thread. Even in the plain CPL case it closes the user's Control Panel window, not just the mod's dialog.
  • ShellExecuteExW blocks while a UAC consent prompt is on screen, so g_active stays non-zero for as long as the user leaves that prompt up — which feeds the unbounded wait in item 2.

Two fixes, both small: don't take the legacy rundown gate in RedirectShellExecuteExW (it only rewrites a URL and never touches the mapped image), and restrict CloseOwnedWindows to windows the mod actually created (g_owned) plus windows whose owner chain traces back to a tracked/host window — never to whole threads.

4. Double-close of WinHTTP handles when an unload cancels an in-flight download. CancelInFlightDownload() (line 305) closes g_activeSession from the unload thread, but Download()'s HttpHandle destructors then run unconditionally on the already-closed session — and on the connect/request handles, which the session close already destroyed as children:

struct HttpHandle { HINTERNET value; ~HttpHandle() { if (value) WinHttpCloseHandle(value); } };

explorer.exe uses WinHTTP itself, so a recycled handle value means the mod closes an unrelated component's handle. Your display restorer already solves exactly this — see the hUrl = nullptr; // already closed by CancelInFlightDownload check-and-clear at win7-display-control-panel-restorer.wh.cpp#L1634. Decide ownership under g_downloadLock so exactly one side closes each handle.

5. The first-use download blocks the activation thread with no overall deadline and no stall detection. CplHookEnsurePrepared()Prepare() → two EnsurePinned() downloads, all under g_prepareLock, on the thread handling the Control Panel activation. WinHttpSetTimeouts(10000, 10000, 15000, 15000) is per-operation, and the WinHttpReadData loop (lines 540-546) has no progress check at all, so a server that trickles bytes keeps the UI frozen indefinitely with no feedback. Add a no-progress detector (your display restorer does this at win7-display-control-panel-restorer.wh.cpp#L1605) plus an overall deadline, and consider falling back to the native CPL immediately while the fetch runs in the background.

6. Uncontained faults in the manually mapped Win7 image — still undocumented. The CrashGuard comment (lines 918-935) is explicit that a hardware fault inside the mapped intl.cpl "still takes the host process down" and that the guard only observes it. The README note added in the last commit covers feature parity, not this. Please do one of:

  • implement the containment the comment scopes out (-fms-extensions + __try/__except around the GuardCpl/GuardEntry calls, so a fault degrades to "legacy disabled, native fallback"), or
  • state in the README that a fault in the legacy provider can take down Explorer.

Also still open from last round: input.dll is loaded with the normal Windows loader and IAT-patched (AdaptInputIat), which gets you real unwind info, TLS and loader semantics for free, while intl.cpl gets the hand-written mapper (CopyImage/Relocate/BindImports/ProtectImage + RtlAddFunctionTable + manual DllMain) — several hundred lines of the riskiest code in the mod. If the reason is that the private msvcrt heap overrides must be in place before _CRT_INIT runs in DllMain, that's a legitimate constraint, but please write it down in the PR or in a comment rather than leaving it implicit.

Optional improvements

The mod seems to work fine for me at the moment. If the maintainer believes that these problems need to be addressed, I'll do it. In alternative, I can fix them in a future update.
/ready-for-reviewer

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-reviewer Ready for a human reviewer, and in the queue for one. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Sep 9, 2026
@m417z

m417z commented Sep 11, 2026

Copy link
Copy Markdown
Member

Fix 1 and 4 from the last review.

Regarding "Vectored crash guard" - why do you need it? What crashes without it? Generally just swallowing all crashes in a system process is not a good idea, and I prefer not having mods that do that.

@m417z m417z added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-reviewer Ready for a human reviewer, and in the queue for one. labels Sep 11, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

Fix 1 and 4 from the last review.

Regarding "Vectored crash guard" - why do you need it? What crashes without it? Generally just swallowing all crashes in a system process is not a good idea, and I prefer not having mods that do that.

I'll fix the points 1 and 4 and thanks for clarifying, I tried this approach to try to make the mod less likely to crash but I'll remove it to adhere to the policy.

@babamohammed2022

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 Sep 11, 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 two things the maintainer asked for are done: ShutdownLegacyOnInitThread now passes GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, the WinHTTP handle tree has a single owner (HttpSession + g_cancelledSession), and the vectored crash guard is gone. The background prefetch and the owner-chain-scoped CloseOwnedWindows are both real improvements. The problem is the other change that came with this push: the new bounded unload does not degrade safely, and as written it is worse than the hang it replaced.

1. The 12-second "abandon" path converts an unload hang into a host crash.

Abandon() (line 447) and the g_abandoned branch in Cleanup() (line 8227) are built on the premise that the mod can survive a failed teardown by leaving things resident. That premise doesn't hold: what Cleanup() leaves mapped is the Win7 provider image, but Windhawk unloads the mod's own image with a single FreeLibrary the moment Wh_ModUninit returns, unconditionally. Everything that is still pointing into the mod image at that moment becomes a dangling pointer:

  • The mapped intl.cpl's compatibility IAT. BindImports(g_image, true) routes ~40 imports through ResolvePrivate (line 7351) to PrivateMalloc, PrivateFree, PrivateLoadStringW, MainPropertySheet, MainDialogBox, PrivateShellExecuteExW, PrivateCreateThread, … — all mod code. The abandon branch deliberately skips restoring these.
  • The open dialogs. MainDialogBox/ShowSheet install DialogCallback/SheetCallback (lines 6995, 6808) as the real DLGPROC/PFNPROPSHEETCALLBACK, so a Region sheet that is still up dispatches its next message straight into unmapped memory.
  • JoinTrackedThreads returning false means a worker is still executing inside ThreadBridge (line 7133) — again mod code, with its return address in the mod image.

Cleanup() states the rule correctly for input.dll ("a dangling pointer into an unmapped image is not [survivable]", line 8240) and then applies it to exactly one of the four cases.

This is reachable in ordinary use, not a corner case. Wh_ModSettingsChanged sets *reload = TRUE unconditionally (line 8600), so any settings edit runs the full teardown; g_active is non-zero for the entire lifetime of the modal Region sheet (CplHook holds EnterLegacy across CPL_DBLCLK); and CloseOwnedWindows' WM_CLOSE cannot end a nested modal child that ignores it, or a thread blocked in PrivateShellExecuteExW on an out-of-process elevation prompt (that window isn't in this process, so CloseScanProc never sees it). Twelve seconds is easily exceeded by a user reading a UAC prompt.

The previous unbounded wait was recoverable — it ended as soon as the user closed the dialog. This isn't. Note also that the maintainer only asked for items 1 and 4 of the last review; the bounded wait wasn't requested. Suggested direction, in order of value:

  • Go back to waiting, and remove the reasons it can block, rather than capping it. Your own merged win7-display-control-panel-restorer.wh.cpp#L15156 does exactly this — "Post WM_CLOSE too as a second line of defense, then unconditionally join the worker before Windhawk is allowed to unmap this mod image" — and the comment at L12027 describes this same crash and the fix (make the blocking UI something the unload path can always end).
  • Stop forcing a reload on every settings change. That alone removes the most common way to enter this path while a dialog is open. redirectSettings can be a runtime flag checked inside the redirect hooks instead of deciding whether they are installed; the language needs BuildEmbeddedResources() to re-run, which you could do on the next activation when g_active == 0 instead of tearing the mod down.
  • Longer term, consider not mapping the Win7 provider into explorer.exe at all — redirect an in-process Region activation to the dedicated control.exe host the mod already supports and already knows how to serve. That removes this whole class of problem from the shell, and also shrinks the blast radius of the hardware-fault risk you documented in the README.

Two smaller things on the same path:

  • Wh_ModUninit closes g_idle and g_jobsIdle unconditionally (lines 8670-8671), including on the abandoned path where LeaveLegacy/EndJob may still SetEvent them. g_prefetchDone and the actctx are correctly skipped; these two aren't. (Moot once the wait is unconditional, but inconsistent as written.)
  • ShutdownLegacyOnInitThread uses its own fixed budgets (kShutdownMarshalMs + 2000 for the sentinel, then another 2000 in ShutdownMarshalRelease) instead of taking a slice from RemainingUnloadMs(). So the worst case is ~12 s + ~9 s, not the "~kUnloadDeadlineMs and not the sum of the individual waits" the comment on g_unloadDeadline promises, nor the "about 12 seconds" the README states.

2. EnsurePrepared() blocks on CPL_INIT, so a cold cache freezes the Control Panel window for 5 seconds before the user ever opens Region.

CplHook line 7934 runs it for every CPL message:

if (g_useLegacy.load() && !g_legacyInitialized.load()) EnsurePrepared();

CPL_INIT/CPL_GETCOUNT/CPL_INQUIRE are sent when the shell enumerates the applet — i.e. when the Control Panel folder is opened — and that is also when LoadLibraryExW_hook first installs the hook. On a cold cache that enumeration blocks for kPrepareGraceMs (5 s) on the folder's UI thread.

It also defeats itself: g_prepareWaitUsed is a single process-wide budget, so the enumeration consumes all of it, and the subsequent double-click on Region gets grace = 0 and falls back to the modern page even if the download finished a second later. Gate the wait on the activation messages you already compute:

const bool activation = message == CPL_DBLCLK || message == CPL_STARTWPARMSA || message == CPL_STARTWPARMSW;
...
if (g_useLegacy.load() && !g_legacyInitialized.load()) {
    if (activation) EnsurePrepared();
    else StartPrefetch();   // kick the fetch off, don't wait for it
}

Relatedly, the README says the download "runs in the background and never blocks the Control Panel thread", which isn't accurate while this wait exists — worth rewording either way.

3. StartPrefetch() can create a worker after Cleanup() has already joined.

void StartPrefetch() {
    if (g_stopping.load(std::memory_order_acquire)) return;
    ...
    HANDLE thread = CreateThread(nullptr, 0, PrefetchThread, nullptr, 0, nullptr);
    ...
    RegisterThread(thread);

The g_stopping read isn't fenced against Wh_ModBeforeUninit's store, which is made under g_gate held exclusively — that is precisely why BeginJob() (line 7121) takes g_gate shared around its own check. A caller that reads g_stopping == false just before the store, and reaches RegisterThread after JoinTrackedThreads has swapped g_threads, leaves PrefetchThread running in the mod image when Windhawk unmaps it. Same shape as BeginJob:

AcquireSRWLockShared(&g_gate);
const bool allowed = !g_stopping.load(std::memory_order_acquire);
if (allowed) { /* CreateThread + RegisterThread here */ }
ReleaseSRWLockShared(&g_gate);
Optional improvements

Minor polish — none of this affects users, so it's your call. The first five are unchanged from previous rounds.

  • EnsurePinned's DeleteFileW is still dead code. OpenRead opens with FILE_SHARE_READ and no FILE_SHARE_DELETE (line 566), and the delete on line 698 is still inside the scope holding that handle, so it always fails with ERROR_SHARING_VIOLATION. Harmless (WriteAtomic's MoveFileExW cleans up afterwards), but move it below the closing brace or drop it.
  • 67 catch (...) blocks, many around bodies that cannot throw (ShimWinSqmAddToStream, PrivateGetVersion, PrivateDelete, ForwardKernelBase). It makes the genuinely load-bearing ones — the OS-invoked callbacks — harder to spot.
  • Cleanup() clears g_blobStore but not g_inpBlobStore / g_inpBlobCount / g_inpBlobForDialog. The asymmetry looks unintentional and leaves the input blob indices pointing at freed storage.
  • LoadLibraryExW_hook still matches any intl.cpl by leaf name (line 8440) — reuse SystemFileToken, which you already use elsewhere to reject exactly this. It also builds two std::wstrings per LoadLibraryExW call until g_explorerCplHooked flips, which in the common case (Region never opened) is every DLL load for the whole Explorer session; a pointer scan for the leaf avoids the allocations.
  • RedirectShellExecuteW returns FALSE when the original is missing (line 8350). ShellExecuteW returns an HINSTANCE-typed error code, and 0 means ERROR_OUT_OF_MEMORY to callers; (HINSTANCE)SE_ERR_ACCESSDENIED is more honest. It also doesn't check g_redirectExe.empty() the way RedirectShellExecuteExW does.
  • RedirectCreateProcessW hooks kernel32's CreateProcessW (line 8392), whose real implementation is in kernelbase — callers that go straight there are missed. You already resolve LoadLibraryExW from kernelbase for exactly this reason (line 8482); same treatment would make the redirect consistent.
  • The payloads are read and SHA-256'd three times on a warm start. PrefetchPayloadsPayloadsCached(), then Prepare()PayloadsCached() → and again the explicit OpenRead/Hash pair on line 7699. Only the last one feeds g_intlFile/g_inputFile; the other two are pure duplicates.
  • First-run downloads still aren't serialized across processes. explorer.exe and control.exe can both fetch the same 620 KB. performance-info-tools-restorer uses a named mutex for this at L1725.
  • Indentation glitch at line 6813 (if (context && message == PSCB_INITIALIZED) { starts at column 0 inside SheetCallback).

Functionality notes

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

  • The README's stability-risk note is a good addition — it's honest about what running a 2010 binary in the shell means, and it's the right call now that the vectored guard is gone.
  • g_owned silently caps at 64 windows (kMaxOwnedWindows). Practically unreachable, but a Wh_Log when the cap is hit would make it diagnosable.
  • TranslateInputWindow translates by matching English window text (SameInputText), so it only works while input.dll serves its genuine en-US resources, and a control whose text happens to match gets rewritten too. Matching by control ID where possible would be more robust.
  • Traditional Chinese falls back to English rather than Simplified, and AutoLanguage() matches on PRIMARYLANGID only (pt-PT gets the pt-BR pack, every Spanish variant gets es-ES). Both are reasonable; a line in "Known limitations" would set expectations.
  • TagLanguage accepts a bare primary tag (it, de, …) but $options only ever produces full tags, so that branch is unreachable from the settings UI.


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 Sep 11, 2026
@babamohammed2022

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 Sep 11, 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 three findings from the last round are genuinely addressed: the Abandon() path is gone and the rundown now escalates (EndDialog for the dialogs it serves, PSM_PRESSBUTTON/Cancel for its sheets) instead of tearing down over a live frame, Wh_ModSettingsChanged no longer forces a reload, enumeration messages route to StartPrefetch() instead of burning the grace budget, and StartPrefetch() now takes g_gate the same way BeginJob() does. The problem is the mechanism the Explorer path and the new runtime redirect toggle both depend on.

1. Hooks registered after Wh_ModInit are never applied, so the Explorer route and the runtime redirect toggle silently do nothing.

Wh_SetFunctionHook (and therefore WindhawkUtils::SetFunctionHook) only registers a hook operation. Windhawk applies registered operations automatically exactly once, right after Wh_ModInit returns; anything registered later needs an explicit Wh_ApplyHookOperations() (windhawk_api.h: "Applies hook operations registered by Wh_SetFunctionHook ... Called automatically by Windhawk after Wh_ModInit"). The file never calls it, and two registration sites run after init:

  • HookNativeCpl() from LoadLibraryExW_hook (line 8694) — this is the entire in-process Control Panel > Region route in explorer.exe, i.e. the scenario item 1 of the third review asked you to cover. The bookkeeping all succeeds (g_explorerCplHooked flips to true, the extra intl.cpl reference is pinned into g_nativeModule, StartPrefetch() runs), but CPlApplet is never actually detoured: CplHook is never entered and g_nativeCpl stays nullptr. Region in Explorer keeps showing the modern page.
  • InstallRedirectHook() from Wh_ModSettingsChanged (line 8863) — turning Redirect Settings pages on at runtime logs "Settings redirect installed at runtime" but hooks nothing. g_origShellExecuteExW also stays nullptr (the trampoline is only written when the operation is applied), so the !g_origShellExecuteExW guard stays true and every subsequent settings change re-registers the same three targets. Since the whole point of the no-reload rework is that the option applies immediately, this makes the option a no-op until the mod is reloaded for some other reason.

The eager path in Wh_ModInit (HookNativeCpl(g_nativeModule) for control.exe/rundll32.exe, InstallExplorerCplHook()'s already-loaded branch, and the LoadLibraryExW hook itself) is fine — those are applied for you. Only the two deferred sites are affected, which is probably why this survived testing through control.exe intl.cpl.

Fix: apply after a successful registration. Your own merged Display restorer already does exactly this for its deferred hook surface (win7-display-control-panel-restorer.wh.cpp#L14831 and #L13009), and explorer-frame-classic.wh.cpp#L843-L845 shows the same shape from inside a loader hook:

if (pinned && HookNativeCpl(pinned) && Wh_ApplyHookOperations()) {
    g_explorerCplHooked.store(true, std::memory_order_release);
    StartPrefetch();
    Wh_Log(L"intl.cpl loaded in Explorer; CPlApplet hook installed");
} else if (pinned) {
    FreeLibrary(pinned);
}

and the same after InstallRedirectHook() in Wh_ModSettingsChanged. Please verify the Explorer Control Panel > Region route end-to-end once this is in - it is the one path in the ledger that has never been confirmed working.

2. Runtime download of external binaries - noted for the record, not a request to you.

The repository guideline is that a mod is self-contained and does not fetch remote resources; this one downloads intl.cpl and input.dll from msdl.microsoft.com on first use. The verification chain remains the strongest of the set (HTTPS + scheme check, host pinned, exact size and SHA-256 before any use, FILE_SHARE_READ pin re-verified, GetFileInformationByHandleEx(FileIdInfo) identity match against the loaded module, ProhibitDynamicCode/MicrosoftSignedOnly policy check that fails closed), and the overall deadline plus no-progress detector added to Download() close the last gap from the previous rounds. Same pattern as your already-merged restorers, so this stays the maintainer's call.

Optional improvements

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

  • Use the void Wh_ModSettingsChanged() form. It now sets *reload = FALSE unconditionally (line 8853) and never asks for a reload, so the BOOL(BOOL*) variant no longer earns its place; the void overload is equivalent and clearer about the new contract.
  • ApplyPendingLanguageChange() has a settings-change-only race with a second activation. Thread A clears g_pendingLang under g_langLock and then rebuilds g_blobStore/g_inpBlobStore (lines 7984-7991). Thread B entering CplHook meanwhile sees g_pendingLang < 0 at line 7973, returns immediately, and dispatches into the provider while the blobs A handed to comctl32 as dialog templates are half-rebuilt. Holding g_langLock shared around the dispatch in CplHook (exclusive only for the rebuild) would close it; it needs both a settings edit and two concurrent activations, so it is not urgent.
  • Dead state. g_nativeInitialized is now only ever written (lines 8111, 8126, 8134, 8148) and never read, and g_envViable (line 8755) is still write-only from the removed redirect-only branch.
  • Cleanup() clears g_blobStore but not g_inpBlobStore/g_inpBlobCount/g_inpBlobForDialog (line 8498). Harmless today, but the asymmetry still looks unintentional and leaves the input-blob indices pointing at storage that is about to go away.
  • EnsurePinned's DeleteFileW is still dead code (line 691): it runs inside the scope that still holds f.pin open with FILE_SHARE_READ and no FILE_SHARE_DELETE, so it always fails with ERROR_SHARING_VIOLATION. WriteAtomic's MoveFileExW replaces the file anyway - move the line below the closing brace or drop it.
  • LoadLibraryExW_hook still matches any intl.cpl by leaf name (line 8686), including a third-party one from an arbitrary path, which it then pins and hooks. You already have SystemFileToken for exactly this rejection. It also builds two std::wstrings per LoadLibraryExW call until g_explorerCplHooked flips - in the common case (Region never opened) that is every DLL load for the whole Explorer session; a pointer scan for the leaf avoids the allocations.
  • RedirectCreateProcessW hooks kernel32's CreateProcessW (line 8646), whose real implementation lives in kernelbase - callers that go straight there are missed. You already resolve LoadLibraryExW from kernelbase for this exact reason (line 8728).
  • RedirectShellExecuteW returns FALSE when the original is missing (line 8590). ShellExecuteW returns an HINSTANCE-typed error code and 0 means ERROR_OUT_OF_MEMORY to callers; (HINSTANCE)SE_ERR_ACCESSDENIED is more honest. Unlike RedirectShellExecuteExW it also doesn't check g_redirectExe.empty() before substituting.
  • 68 catch (...) blocks, many around bodies that cannot throw (ShimWinSqmAddToStream, PrivateGetVersion, PrivateDelete, ForwardKernelBase). It makes the load-bearing ones - the OS-invoked callbacks - harder to spot in a 9000-line file.
  • @license is missing from the metadata; your other mods all have it.
  • #include <stdexcept> is pulled in for a single std::runtime_error at line 8799 that is caught by the catch (...) two lines of context away; a plain return FALSE after the existing cleanup would be simpler.

Functionality notes

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

  • The deferred InitializeLegacyProvider() path is now the normal path, not a fallback, and it skips CPL_INQUIRE. g_prefetchSucceeded only becomes true once the prefetch worker has finished, and in control.exe the worker is started in Wh_ModInit a few milliseconds before CPL_INIT arrives - so even on a warm cache the enumeration messages take the StartPrefetch() branch and the provider comes up on CPL_DBLCLK instead. That path replays CPL_INIT + CPL_GETCOUNT but not CPL_INQUIRE/CPL_NEWINQUIRE, which a real host always sends before CPL_DBLCLK. It evidently works for this applet, but since it is now the common case rather than the cold-cache corner, it is worth exercising deliberately.
  • A language change applies at the next CPL session, not the next time the page is opened. ApplyPendingLanguageChange() returns early while g_legacyInitialized is true (line 7974), which it stays for as long as the host keeps the applet initialized. The setting $description and the readme both say "the next time the page is opened"; "after the Control Panel window is closed and reopened" would match the code.
  • The unload wait still has no escalation for g_jobs. The UI escalation covers dialogs and sheets, but a private worker that never returns leaves Wh_ModBeforeUninit spinning with nothing to act on. Given the mod image cannot be unmapped under a live ThreadBridge frame there is no better option than waiting - just noting the one remaining shape of a long unload besides the documented UAC case.
  • Two silent caps. g_owned stops tracking past kMaxOwnedWindows (64) and RegisterThread silently drops a thread past kMaxTrackedThreads (256, line 455) - the latter means an untracked worker would not be joined before the image is unmapped. Both are practically unreachable, but a Wh_Log when either cap is hit would make them diagnosable.
  • The out-of-process "Change keyboards..." relaunch still loses everything the mod adds. PrivateShellExecuteExW rewrites to rundll32.exe shell32.dll,Control_RunDLL "<storage>\input.dll" (line 7319), but SelectedLaunch requires argv[2] to be intl.cpl (line 248), so Wh_ModInit bails in that rundll32.exe and the Win7 input.dll runs there with no version spoof, no IAT adaptation and no localization. Accepting the mod's own cached input.dll path in SelectedLaunch would close the gap.
  • ContainsRegionSettingsUrl still matches at any offset (line 8537), so an unrelated launch whose parameters merely contain ms-settings:regionlanguage as text gets rewritten to control.exe intl.cpl. Anchoring the scan to token starts (after a quote, space or =) would tighten it without losing the launcher cases.
  • The readme's stability-risk note is the right call now that the vectored guard is gone, and the new "Disabling the mod..." paragraph describes the actual behaviour accurately, including the UAC case. Good documentation of a genuinely unusual design.


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 Sep 11, 2026
Added error handling for CPlApplet hook registration and improved logging for various conditions related to the Control Panel integration.
@babamohammed2022

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 Sep 11, 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 last round's finding is properly fixed: both deferred registration sites now apply (Wh_ApplyHookOperations() at lines 8717 and 8917), the failure paths are split correctly (!registered drops the pin, "registered but not applied" keeps it because the operation is still queued), and the new !g_nativeCpl guard in CplHook is a reasonable belt-and-braces check. Two things left, both on paths that already exist.

1. ShutdownHookProc signals the unload thread before it has finished running mod code.

Lines 8326-8350:

if (code == HC_ACTION && !g_shutdownRan.exchange(true)) {
    try { ShutdownLegacyOnCurrentThread(); } catch (...) { ... }
    if (g_shutdownWorked) SetEvent(g_shutdownWorked);      // <- releases the unload thread
}
return CallNextHookEx(nullptr, code, wParam, lParam);      // <- still mod code, still runs

SetEvent wakes the sentinel, the sentinel exits, ShutdownLegacyOnInitThread returns, Cleanup() finishes, Wh_ModUninit returns and Windhawk unmaps this image with a single FreeLibrary. Meanwhile the init thread is only now entering CallNextHookEx, which dispatches the rest of the WH_GETMESSAGE chain - other mods, other in-process hooks - and only then runs this function's epilogue. The comment on line 8340 argues that what remains is "its epilogue plus the tail call below", which is true only if the compiler actually emits a tail call; the function has a try block, so that is an optimizer-dependent property, not something to rest memory safety on. If it isn't a tail call, a mod return address stays live on the init thread's stack for the whole duration of the downstream hook chain.

The fix is one line - do the chain call first, signal last, so that the only mod code after the signal is a mov/ret:

LRESULT CALLBACK ShutdownHookProc(int code, WPARAM wParam, LPARAM lParam) {
    const bool ran = code == HC_ACTION && !g_shutdownRan.exchange(true);
    if (ran) {
        try { ShutdownLegacyOnCurrentThread(); } catch (...) { /* as today */ }
    }
    const LRESULT result = CallNextHookEx(nullptr, code, wParam, lParam);
    if (ran && g_shutdownWorked) SetEvent(g_shutdownWorked);
    return result;
}

That plus the existing sentinel-thread wake latency closes the window to the point where it stops being a realistic concern. As written, the window is "however long the rest of the hook chain takes".

2. Merely browsing the Control Panel folder maps the Win7 provider into explorer.exe and runs its DllMain, even when Region is never opened.

Line 8111:

if (!enumeration || g_prefetchSucceeded.load(std::memory_order_acquire)) EnsurePrepared();
else StartPrefetch();

enumeration covers CPL_INIT/CPL_GETCOUNT/CPL_INQUIRE, i.e. exactly the messages the shell sends when it lists the applet. With a warm cache (g_prefetchSucceeded == true, which is the normal case from the second Control Panel open onwards) that branch runs the full Prepare() synchronously on the shell's UI thread: both payloads read and SHA-256'd twice over (PayloadsCached() at 7818, then the explicit OpenRead/Hash pair at 7819-7821), GetFileVersionInfo on both, CreateActCtxW, the input.dll import preflight plus its real LoadLibraryExW, AdaptInputIat, then CopyImage/Relocate/BindImports/ProtectImage and CallEntry(DLL_PROCESS_ATTACH) for intl.cpl.

Two consequences for a user who opens the Control Panel folder and never clicks Region:

  • a hitch on the folder's UI thread while all of that runs, and
  • the 2010 intl.cpl is mapped and its DllMain has executed inside explorer.exe - which is precisely the "can take the host process down" surface your README's Stability risk section documents. Scoping that to "the user asked for Region" instead of "the user opened the Control Panel" is a meaningful reduction for free.

You already have the machinery to avoid it: InitializeLegacyProvider() (line 8041) exists exactly to bring the provider up on the activation, and per the previous round's notes it is already the path the first open takes. Gating the wait on the activation messages only -

if (!enumeration) EnsurePrepared();
else StartPrefetch();
  • makes the deferred path the single path. The one thing that changes is that CPL_INQUIRE/CPL_NEWINQUIRE are never replayed to the provider (InitializeLegacyProvider replays CPL_INIT + CPL_GETCOUNT only), which is the gap the previous round flagged. If you'd rather keep the eager warm-cache path, adding the CPL_INQUIRE replay to InitializeLegacyProvider and then dropping the eager branch gets you both.

3. Runtime download of external binaries - noted for the record, not a request to you.

The repository guideline is that a mod is self-contained and does not fetch remote resources; this one downloads intl.cpl and input.dll from msdl.microsoft.com on first use. The verification chain is still the strongest of the set (HTTPS + scheme check, host pinned, exact size and SHA-256 before any use, FILE_SHARE_READ pin re-verified, GetFileInformationByHandleEx(FileIdInfo) identity match against the loaded module, ProhibitDynamicCode/MicrosoftSignedOnly policy check that fails closed), and Download() now has both an overall deadline and a no-progress detector. Same pattern as your already-merged restorers, so this stays the maintainer's call.

Optional improvements

Minor polish - none of this affects users, so it's your call. Most of these are unchanged from previous rounds.

  • SetWindowsHookExW's hMod should be nullptr here (line 8408). The documented contract is explicit: "The hMod parameter must be set to NULL if the dwThreadId parameter specifies a thread created by the current process and if the hook procedure is within the code associated with the current process." Both hold - tid is g_cplInitThreadId in this process and ShutdownHookProc is in-process code. It evidently works today, but the failure mode if it ever stops working is silent: g_shutdownHook stays null, and the legacy CPL_EXIT/DLL_PROCESS_DETACH runs inline on the arbitrary Windhawk unload thread - exactly the thread-affinity problem this marshalling exists to avoid. Passing nullptr also lets you delete the whole GetModuleHandleExW block at 8397-8407, which is where the refcount bug from two rounds ago lived.
  • Use the void Wh_ModSettingsChanged() form. Line 8896 sets *reload = FALSE unconditionally and nothing else ever writes it, so the BOOL(BOOL*) variant no longer earns its place.
  • The payloads are read and SHA-256'd three times on a warm start. PrefetchPayloads()PayloadsCached() (7758), then Prepare()PayloadsCached() (7818), then the explicit OpenRead/Hash pair at 7819-7821. Only the last one feeds g_intlFile/g_inputFile; the other two are pure duplicates. This matters more than it did last round, because item 2 above puts that work on the shell's UI thread.
  • ApplyPendingLanguageChange() has a settings-change-only race with a second activation. Thread A clears g_pendingLang under g_langLock and then rebuilds g_blobStore/g_inpBlobStore (7984-7991). Thread B entering CplHook meanwhile sees g_pendingLang < 0 at 7973, returns immediately, and dispatches into the provider while the blobs A handed to comctl32 as dialog templates are half-rebuilt. Holding g_langLock shared around the dispatch in CplHook (exclusive only for the rebuild) would close it; it needs both a settings edit and two concurrent activations.
  • EnsurePinned's DeleteFileW is still dead code (line 691): it runs inside the scope that still holds f.pin open with FILE_SHARE_READ and no FILE_SHARE_DELETE, so it always fails with ERROR_SHARING_VIOLATION. WriteAtomic's MoveFileExW replaces the file anyway - move the line below the closing brace or drop it.
  • Cleanup() clears g_blobStore but not g_inpBlobStore/g_inpBlobCount/g_inpBlobForDialog (line 8508). Harmless today, but the asymmetry still looks unintentional.
  • Dead state. g_nativeInitialized (406) is written at 8121/8136/8144/8158 and never read; g_envViable (8522) is still write-only from the removed redirect-only branch.
  • LoadLibraryExW_hook still matches any intl.cpl by leaf name (line 8699), including a third-party one from an arbitrary path, which it then pins (with a full image load) and hooks. You already have SystemFileToken for exactly this rejection. It also builds two std::wstrings per LoadLibraryExW call until g_explorerCplHooked flips - in the common case that is every DLL load for the whole Explorer session; a pointer scan for the leaf avoids the allocations.
  • RedirectCreateProcessW hooks kernel32's CreateProcessW (line 8649), whose real implementation lives in kernelbase - callers that go straight there are missed. You already resolve LoadLibraryExW from kernelbase for this exact reason (line 8774).
  • RedirectShellExecuteW returns FALSE when the original is missing (line 8600). ShellExecuteW returns an HINSTANCE-typed error code and 0 means ERROR_OUT_OF_MEMORY to callers; (HINSTANCE)SE_ERR_ACCESSDENIED is more honest. Unlike RedirectShellExecuteExW it also doesn't check g_redirectExe.empty() before substituting.
  • ShowSheet leaves g_sheet dangling on its exception path. g_sheet = &context (6924) is only restored at 6927, after original(&copy) returns; the catch (...) at 6930 skips it, leaving the thread-local pointing at a dead stack frame. Unreachable in practice (the only writer of g_sheet is ShowSheet itself, so the stale value is always overwritten before SheetCallback can read it), but a small RAII guard would remove the reasoning step.
  • PrivateRealloc/PrivateCalloc can hand out an untracked fallback block. FallbackAdd(n) at 1166 isn't checked; if the 32-slot g_fallback table is full the process-heap block is returned untracked, and a later PrivateFree routes it to msvcrt!free. Only reachable after the real CRT has already failed 32 times, so it's academic - but returning nullptr when FallbackAdd fails would match what PrivateMalloc already does.
  • First-run downloads still aren't serialized across processes. explorer.exe and control.exe can both fetch the same 620 KB. performance-info-tools-restorer uses a named mutex for this at L1725.
  • 68 catch (...) blocks, many around bodies that cannot throw (ShimWinSqmAddToStream, PrivateGetVersion, PrivateDelete, ForwardKernelBase). It makes the load-bearing ones - the OS-invoked callbacks - harder to spot in a 9000-line file.
  • @license is missing from the metadata; your other mods all have it.
  • #include <stdexcept> is pulled in for a single std::runtime_error at line 8842 that is caught by the catch (...) a few lines below; a plain return FALSE after the existing cleanup would be simpler.
  • Indentation glitch at line 6827 (if (context && message == PSCB_INITIALIZED) { starts at column 0 inside SheetCallback).

Functionality notes

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

  • The deferred InitializeLegacyProvider() path skips CPL_INQUIRE. It replays CPL_INIT + CPL_GETCOUNT but not CPL_INQUIRE/CPL_NEWINQUIRE, which a real host always sends before CPL_DBLCLK. It evidently works for this applet; if you take the suggestion in item 2 it becomes the only path, so it's worth exercising deliberately either way.
  • A language change applies at the next CPL session, not the next time the page is opened. ApplyPendingLanguageChange() returns early while g_legacyInitialized is true (7974), which it stays for as long as the host keeps the applet initialized. The setting $description and the readme both say "the next time the page is opened"; "after the Control Panel window is closed and reopened" would match the code.
  • The out-of-process "Change keyboards..." relaunch still loses everything the mod adds. PrivateShellExecuteExW rewrites to rundll32.exe shell32.dll,Control_RunDLL "<storage>\input.dll" (7319), but SelectedLaunch requires argv[2] to be intl.cpl (248), so Wh_ModInit bails in that rundll32.exe and the Win7 input.dll runs there with no version spoof, no IAT adaptation and no localization. Accepting the mod's own cached input.dll path in SelectedLaunch would close the gap.
  • ContainsRegionSettingsUrl still matches at any offset (8544), so an unrelated launch whose parameters merely contain ms-settings:regionlanguage as text gets rewritten to control.exe intl.cpl. Anchoring the scan to token starts (after a quote, space or =) would tighten it without losing the launcher cases.
  • TranslateInputWindow translates by matching English window text (SameInputText, 7579), so it only works while input.dll serves its genuine en-US resources, and a control whose text happens to match gets rewritten too. Matching by control ID where possible would be more robust. (It's a documented fallback pass, so this is an FYI rather than a request.)
  • Traditional Chinese falls back to English rather than Simplified, and AutoLanguage() matches on PRIMARYLANGID only (pt-PT gets the pt-BR pack, every Spanish variant gets es-ES). Both are reasonable; a line in "Known limitations" would set expectations. Relatedly, TagLanguage accepts a bare primary tag (it, de, …) but $options only ever produces full tags, so that branch is unreachable from the settings UI.
  • Two silent caps. g_owned stops tracking past kMaxOwnedWindows (64) and RegisterThread silently drops a thread past kMaxTrackedThreads (256, line 455) - the latter means an untracked worker would not be joined before the image is unmapped. Both are practically unreachable, but a Wh_Log when either cap is hit would make them diagnosable.
  • The unload wait still has no escalation for g_jobs. The UI escalation covers dialogs and sheets, but a private worker that never returns leaves Wh_ModBeforeUninit spinning with nothing to act on. Given the mod image cannot be unmapped under a live ThreadBridge frame there is no better option than waiting - just noting the one remaining shape of a long unload besides the documented UAC case.
  • The readme's stability-risk note remains the right call now that the vectored guard is gone, and the "Disabling the mod..." paragraph describes the actual behaviour accurately, including the UAC case. Good documentation of a genuinely unusual design.


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 Sep 11, 2026
@babamohammed2022

Copy link
Copy Markdown
Contributor Author

@m417z I've tried to address the first problem reported by the AI reviewer. However, the amount of AI reviews for this day has reached its limit, so I can't receive another. Is the mod ready?

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.

2 participants