Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions include/wil/registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,19 @@ namespace reg
* @param subKey The name of the registry subkey to be opened.
* If `nullptr`, then `key` is used without modification.
* @param access The requested access desired for the opened key
* @param options Options controlling how the key is opened (see wil::reg::open_options); flags may be combined
* @return A wil::unique_hkey containing the resulting opened HKEY
* @exception std::exception (including wil::ResultException) will be thrown on all failures
*/
inline ::wil::unique_hkey open_unique_key(HKEY key, _In_opt_ PCWSTR subKey, ::wil::reg::key_access access = ::wil::reg::key_access::read)
inline ::wil::unique_hkey open_unique_key(
HKEY key,
_In_opt_ PCWSTR subKey,
::wil::reg::key_access access = ::wil::reg::key_access::read,
::wil::reg::open_options options = ::wil::reg::open_options::none)
{
const reg_view_details::reg_view regview{key};
::wil::unique_hkey return_value;
regview.open_key(subKey, &return_value, access);
regview.open_key(subKey, &return_value, access, options);
return return_value;
}

Expand Down Expand Up @@ -73,14 +78,19 @@ namespace reg
* @param subKey The name of the registry subkey to be opened.
* If `nullptr`, then `key` is used without modification.
* @param access The requested access desired for the opened key
* @param options Options controlling how the key is opened (see wil::reg::open_options); flags may be combined
* @return A wil::shared_hkey containing the resulting opened HKEY
* @exception std::exception (including wil::ResultException) will be thrown on all failures
*/
inline ::wil::shared_hkey open_shared_key(HKEY key, _In_opt_ PCWSTR subKey, ::wil::reg::key_access access = ::wil::reg::key_access::read)
inline ::wil::shared_hkey open_shared_key(
HKEY key,
_In_opt_ PCWSTR subKey,
::wil::reg::key_access access = ::wil::reg::key_access::read,
::wil::reg::open_options options = ::wil::reg::open_options::none)
{
const reg_view_details::reg_view regview{key};
::wil::shared_hkey return_value;
regview.open_key(subKey, &return_value, access);
regview.open_key(subKey, &return_value, access, options);
return return_value;
}

Expand Down Expand Up @@ -110,13 +120,18 @@ namespace reg
* If `nullptr`, then `key` is used without modification.
* @param[out] hkey A reference to a wil::unique_hkey to receive the opened HKEY
* @param access The requested access desired for the opened key
* @param options Options controlling how the key is opened (see wil::reg::open_options); flags may be combined
* @return HRESULT error code indicating success or failure (does not throw C++ exceptions)
*/
inline HRESULT open_unique_key_nothrow(
HKEY key, _In_opt_ PCWSTR subKey, ::wil::unique_hkey& hkey, ::wil::reg::key_access access = ::wil::reg::key_access::read) WI_NOEXCEPT
HKEY key,
_In_opt_ PCWSTR subKey,
::wil::unique_hkey& hkey,
::wil::reg::key_access access = ::wil::reg::key_access::read,
::wil::reg::open_options options = ::wil::reg::open_options::none) WI_NOEXCEPT
{
const reg_view_details::reg_view_nothrow regview{key};
return regview.open_key(subKey, hkey.put(), access);
return regview.open_key(subKey, hkey.put(), access, options);
}

/**
Expand All @@ -143,13 +158,18 @@ namespace reg
* If `nullptr`, then `key` is used without modification.
* @param[out] hkey A reference to a wil::shared_hkey to receive the opened HKEY
* @param access The requested access desired for the opened key
* @param options Options controlling how the key is opened (see wil::reg::open_options); flags may be combined
* @return HRESULT error code indicating success or failure (does not throw C++ exceptions)
*/
inline HRESULT open_shared_key_nothrow(
HKEY key, _In_opt_ PCWSTR subKey, ::wil::shared_hkey& hkey, ::wil::reg::key_access access = ::wil::reg::key_access::read) WI_NOEXCEPT
HKEY key,
_In_opt_ PCWSTR subKey,
::wil::shared_hkey& hkey,
::wil::reg::key_access access = ::wil::reg::key_access::read,
::wil::reg::open_options options = ::wil::reg::open_options::none) WI_NOEXCEPT
{
const reg_view_details::reg_view_nothrow regview{key};
return regview.open_key(subKey, hkey.put(), access);
return regview.open_key(subKey, hkey.put(), access, options);
}

/**
Expand Down
27 changes: 24 additions & 3 deletions include/wil/registry_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,25 @@ namespace reg
readwrite64,
};

// Options that control how a registry key is opened. These map to the ulOptions parameter of RegOpenKeyExW and can be
// combined with the bitwise OR operator. See https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regopenkeyexw.
enum class open_options : DWORD
{
// No options; open the key normally (ulOptions == 0).
none = 0,

// Open the symbolic link key itself rather than its target (REG_OPTION_OPEN_LINK).
open_link = REG_OPTION_OPEN_LINK,

// Open the key using the caller's backup/restore privileges, ignoring the requested access (REG_OPTION_BACKUP_RESTORE).
backup_restore = REG_OPTION_BACKUP_RESTORE,

// Do not apply registry virtualization when opening the key (REG_OPTION_DONT_VIRTUALIZE).
dont_virtualize = REG_OPTION_DONT_VIRTUALIZE,
};

DEFINE_ENUM_FLAG_OPERATORS(open_options);

/// @cond
namespace reg_view_details
{
Expand Down Expand Up @@ -1105,11 +1124,13 @@ namespace reg
reg_view_t& operator=(reg_view_t&&) = delete;

typename err_policy::result open_key(
_In_opt_ PCWSTR subKey, _Out_ HKEY* hkey, ::wil::reg::key_access access = ::wil::reg::key_access::read) const
_In_opt_ PCWSTR subKey,
_Out_ HKEY* hkey,
::wil::reg::key_access access = ::wil::reg::key_access::read,
::wil::reg::open_options options = ::wil::reg::open_options::none) const
{
constexpr DWORD zero_options{0};
return err_policy::HResult(
HRESULT_FROM_WIN32(::RegOpenKeyExW(m_key, subKey, zero_options, get_access_flags(access), hkey)));
HRESULT_FROM_WIN32(::RegOpenKeyExW(m_key, subKey, static_cast<DWORD>(options), get_access_flags(access), hkey)));
}

typename err_policy::result create_key(PCWSTR subKey, _Out_ HKEY* hkey, ::wil::reg::key_access access = ::wil::reg::key_access::read) const
Expand Down
166 changes: 166 additions & 0 deletions tests/RegistryTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1326,8 +1326,174 @@ using ThrowingTypesToTest =
using NoThrowTypesToTest = std::tuple<DwordFns, GenericDwordFns, QwordFns, GenericQwordFns>;
using ThrowingTypesToTest = std::tuple<DwordFns, GenericDwordFns, QwordFns, GenericQwordFns>;
#endif // defined(WIL_ENABLE_EXCEPTIONS)

// Creates a volatile registry symbolic link under HKEY_CURRENT_USER at |linkSubkey| that targets the (already existing)
// key at HKEY_CURRENT_USER\|targetSubkey|. A registry symbolic link stores the absolute NT path of its target in a
// REG_LINK value named "SymbolicLinkValue"; that path is resolved with NtQueryKey(KeyNameInformation), which is not part
// of the public SDK and so is resolved dynamically from ntdll. Returns false if the link could not be created.
inline bool CreateVolatileRegistrySymlink(PCWSTR targetSubkey, PCWSTR linkSubkey)
{
wil::unique_hkey target;
if (FAILED(HRESULT_FROM_WIN32(
::RegCreateKeyExW(HKEY_CURRENT_USER, targetSubkey, 0, nullptr, 0, KEY_READ | KEY_WRITE, nullptr, target.put(), nullptr))))
{
return false;
}

const auto ntdll = ::GetModuleHandleW(L"ntdll.dll");
if (!ntdll)
{
return false;
}

using NtQueryKey_t = LONG(__stdcall*)(HANDLE, int, PVOID, ULONG, PULONG);
const auto pfnNtQueryKey = reinterpret_cast<NtQueryKey_t>(::GetProcAddress(ntdll, "NtQueryKey"));
if (!pfnNtQueryKey)
{
return false;
}

// KEY_NAME_INFORMATION { ULONG NameLength; WCHAR Name[1]; }; KeyNameInformation == 3.
constexpr int c_KeyNameInformation = 3;
BYTE buffer[512]{};
ULONG resultSize{};
if (pfnNtQueryKey(target.get(), c_KeyNameInformation, buffer, sizeof(buffer), &resultSize) < 0)
{
return false;
}

const auto nameLength = *reinterpret_cast<const ULONG*>(buffer);
const std::wstring targetNtPath(reinterpret_cast<const wchar_t*>(buffer + sizeof(ULONG)), nameLength / sizeof(wchar_t));

wil::unique_hkey link;
if (FAILED(HRESULT_FROM_WIN32(::RegCreateKeyExW(
HKEY_CURRENT_USER, linkSubkey, 0, nullptr, REG_OPTION_CREATE_LINK | REG_OPTION_VOLATILE, KEY_WRITE | KEY_CREATE_LINK, nullptr, link.put(), nullptr))))
{
return false;
}

return SUCCEEDED(HRESULT_FROM_WIN32(::RegSetValueExW(
link.get(),
L"SymbolicLinkValue",
0,
REG_LINK,
reinterpret_cast<const BYTE*>(targetNtPath.c_str()),
static_cast<DWORD>(targetNtPath.size() * sizeof(wchar_t)))));
}

// Deletes the registry symbolic-link key at HKEY_CURRENT_USER\|linkSubkey| if it exists. A dangling symbolic link cannot
// be removed by RegDeleteKeyW/RegDeleteTreeW (they follow the link), so the link key is opened with REG_OPTION_OPEN_LINK
// and deleted via NtDeleteKey, which is resolved dynamically from ntdll. This must be done before deleting the target to
// avoid leaving a dangling link behind.
inline void DeleteRegistrySymlinkKey(PCWSTR linkSubkey)
{
wil::unique_hkey link;
if (FAILED(HRESULT_FROM_WIN32(::RegOpenKeyExW(HKEY_CURRENT_USER, linkSubkey, REG_OPTION_OPEN_LINK, DELETE, link.put()))))
{
return;
}

const auto ntdll = ::GetModuleHandleW(L"ntdll.dll");
if (!ntdll)
{
return;
}

using NtDeleteKey_t = LONG(__stdcall*)(HANDLE);
if (const auto pfnNtDeleteKey = reinterpret_cast<NtDeleteKey_t>(::GetProcAddress(ntdll, "NtDeleteKey")))
{
pfnNtDeleteKey(link.get());
}
}
} // namespace

TEST_CASE("BasicRegistryTests::open_options", "[registry]")
{
// Use a dedicated subkey: a dangling symbolic link can prevent RegDeleteTreeW from cleaning up, so this test must not
// share the common testSubkey used by the rest of the suite.
constexpr auto* openOptionsSubkey = L"Software\\Microsoft\\BasicRegistryTestOpenOptions";
const std::wstring targetSubkey = std::wstring(openOptionsSubkey) + L"\\Target";
const std::wstring linkSubkey = std::wstring(openOptionsSubkey) + L"\\Link";

// Always remove any symbolic link first (it would otherwise block deletion of the subtree) and then the subtree.
DeleteRegistrySymlinkKey(linkSubkey.c_str());
const auto deleteHr = HRESULT_FROM_WIN32(::RegDeleteTreeW(HKEY_CURRENT_USER, openOptionsSubkey));
if (deleteHr != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
{
REQUIRE_SUCCEEDED(deleteHr);
}

// open_options values must forward unchanged to the ulOptions parameter of RegOpenKeyExW.
static_assert(static_cast<DWORD>(wil::reg::open_options::none) == 0, "open_options::none must be 0");
static_assert(static_cast<DWORD>(wil::reg::open_options::open_link) == REG_OPTION_OPEN_LINK, "open_link must map to REG_OPTION_OPEN_LINK");
static_assert(
static_cast<DWORD>(wil::reg::open_options::backup_restore) == REG_OPTION_BACKUP_RESTORE,
"backup_restore must map to REG_OPTION_BACKUP_RESTORE");
static_assert(
static_cast<DWORD>(wil::reg::open_options::dont_virtualize) == REG_OPTION_DONT_VIRTUALIZE,
"dont_virtualize must map to REG_OPTION_DONT_VIRTUALIZE");
static_assert(
static_cast<DWORD>(wil::reg::open_options::open_link | wil::reg::open_options::dont_virtualize) ==
(REG_OPTION_OPEN_LINK | REG_OPTION_DONT_VIRTUALIZE),
"open_options flags must combine with the bitwise OR operator");

SECTION("open_options::none behaves like a normal open")
{
REQUIRE_SUCCEEDED(wil::reg::set_value_dword_nothrow(HKEY_CURRENT_USER, targetSubkey.c_str(), dwordValueName, test_dword_two));

wil::unique_hkey key;
REQUIRE_SUCCEEDED(wil::reg::open_unique_key_nothrow(
HKEY_CURRENT_USER, targetSubkey.c_str(), key, wil::reg::key_access::read, wil::reg::open_options::none));
DWORD result{};
REQUIRE_SUCCEEDED(wil::reg::get_value_dword_nothrow(key.get(), dwordValueName, &result));
REQUIRE(result == test_dword_two);
}

SECTION("open_options::open_link opens the symbolic link itself instead of its target")
{
REQUIRE_SUCCEEDED(wil::reg::set_value_dword_nothrow(HKEY_CURRENT_USER, targetSubkey.c_str(), dwordValueName, test_dword_two));
REQUIRE(CreateVolatileRegistrySymlink(targetSubkey.c_str(), linkSubkey.c_str()));

// Opening without open_link follows the link to its target, where dwordValueName is visible.
wil::unique_hkey followed;
REQUIRE_SUCCEEDED(wil::reg::open_unique_key_nothrow(HKEY_CURRENT_USER, linkSubkey.c_str(), followed, wil::reg::key_access::read));
DWORD result{};
REQUIRE_SUCCEEDED(wil::reg::get_value_dword_nothrow(followed.get(), dwordValueName, &result));
REQUIRE(result == test_dword_two);

// Opening with open_link returns the link key itself, which does not expose the target's value.
wil::unique_hkey rawLink;
REQUIRE_SUCCEEDED(wil::reg::open_unique_key_nothrow(
HKEY_CURRENT_USER, linkSubkey.c_str(), rawLink, wil::reg::key_access::read, wil::reg::open_options::open_link));
DWORD throughLink{};
REQUIRE(wil::reg::get_value_dword_nothrow(rawLink.get(), dwordValueName, &throughLink) == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
}

#if defined(WIL_ENABLE_EXCEPTIONS)
SECTION("open_options::open_link with the throwing open_unique_key overload")
{
REQUIRE_SUCCEEDED(wil::reg::set_value_dword_nothrow(HKEY_CURRENT_USER, targetSubkey.c_str(), dwordValueName, test_dword_two));
REQUIRE(CreateVolatileRegistrySymlink(targetSubkey.c_str(), linkSubkey.c_str()));

const auto followed = wil::reg::open_unique_key(HKEY_CURRENT_USER, linkSubkey.c_str(), wil::reg::key_access::read);
REQUIRE(wil::reg::get_value_dword(followed.get(), dwordValueName) == test_dword_two);

const auto rawLink = wil::reg::open_unique_key(
HKEY_CURRENT_USER, linkSubkey.c_str(), wil::reg::key_access::read, wil::reg::open_options::open_link);
DWORD throughLink{};
REQUIRE(wil::reg::get_value_dword_nothrow(rawLink.get(), dwordValueName, &throughLink) == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND));
}
#endif // defined(WIL_ENABLE_EXCEPTIONS)

// Remove the link before the subtree so RegDeleteTreeW does not encounter a link it cannot follow.
DeleteRegistrySymlinkKey(linkSubkey.c_str());
const auto cleanupHr = HRESULT_FROM_WIN32(::RegDeleteTreeW(HKEY_CURRENT_USER, openOptionsSubkey));
if (cleanupHr != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
{
REQUIRE_SUCCEEDED(cleanupHr);
}
}

TEMPLATE_LIST_TEST_CASE("BasicRegistryTests::simple types typed nothrow gets/sets", "[registry]", NoThrowTypesToTest)
{
const auto deleteHr = HRESULT_FROM_WIN32(::RegDeleteTreeW(HKEY_CURRENT_USER, testSubkey));
Expand Down
Loading