Skip to content

Pin the host module instead of wrapping a borrowed HMODULE#147

Open
JustinMDotNet wants to merge 1 commit into
wopss:masterfrom
JustinMDotNet:fix/plugin-system-exe-handle-ownership
Open

Pin the host module instead of wrapping a borrowed HMODULE#147
JustinMDotNet wants to merge 1 commit into
wopss:masterfrom
JustinMDotNet:fix/plugin-system-exe-handle-ownership

Conversation

@JustinMDotNet

Copy link
Copy Markdown

Closes #142.

What this changes

PluginSystem::Load now acquires a real owning reference for the host executable path instead of wrapping the borrowed handle from GetModuleHandleA(nullptr):

HMODULE raw = nullptr;
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN, nullptr, &raw))
{
    handle.reset(raw);
}

GET_MODULE_HANDLE_EX_FLAG_PIN pins the host module for the life of the process. Per the docs, subsequent FreeLibrary calls on a pinned module are a no-op, so the eventual wil::unique_hmodule destructor (here or in PluginBase) is harmless and the loader refcount stays balanced.

Why

The previous code stored the result of GetModuleHandle directly in wil::unique_hmodule. Microsoft's GetModuleHandle docs say:

The GetModuleHandle function does not increment a module's reference count, so this handle should not be passed to the FreeLibrary function.

wil::unique_hmodule calls FreeLibrary on destruction, so the borrowed handle was being passed to FreeLibrary in two paths:

  1. When the host .exe did not export Supports, the local handle went out of scope at the end of LoadFreeLibrary was called on the process image.
  2. When the host .exe was accepted as a plugin, PluginBase::m_module (also a wil::unique_hmodule, see PluginBase.hpp:38) would do the same on unload.

Both paths fire on every startup because of the unconditional Load(m_paths.GetExe(), false) self-probe in PluginSystem.cpp:172. Windows happens to be forgiving about the process image specifically, which is why this has been invisible, but it is still a real Win32 contract violation.

Design choice

Two reasonable alternatives, both rejected for being more invasive:

  • Tagged union / separate non-owning HMODULE. Cleanest, but requires changing PluginBase's member layout and every site that touches it.
  • std::optional<wil::unique_hmodule>. Same problem at the PluginBase boundary.

Pinning is a one-line change that keeps the existing RAII model intact and is exactly what GET_MODULE_HANDLE_EX_FLAG_PIN was designed for. The host executable can never be unloaded anyway, so pinning costs nothing.

If GetModuleHandleExW fails for any reason, handle stays empty and the existing if (!handle) check at PluginSystem.cpp:242 reports it as a load failure, same as before.

What I tested

Local rebuild. Behavior change in the common case is just that the loader refcount stays correct.

PluginSystem::Load stored the result of GetModuleHandleA(nullptr) in
a wil::unique_hmodule for the .exe path. GetModuleHandle returns a
borrowed handle that, per the Microsoft docs, must not be passed to
FreeLibrary. wil::unique_hmodule calls FreeLibrary on destruction,
so the loader refcount on the host module was being decremented in
two paths every startup: the immediate destruct when the host did
not export Supports, and the eventual unload through
PluginBase::m_module when the host was accepted as a plugin.

Acquire a real reference with GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_PIN, ...) instead. Pinning makes the
eventual FreeLibrary a documented no-op, the loader bookkeeping
stays balanced, and the existing RAII model in PluginBase does not
have to change.

Fixes wopss#142

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Borrowed HMODULE from GetModuleHandle is stored in an owning wil::unique_hmodule

2 participants