diff --git a/include/wil/registry.h b/include/wil/registry.h index 5aa89d01..45f1b7c2 100644 --- a/include/wil/registry.h +++ b/include/wil/registry.h @@ -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; } @@ -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; } @@ -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); } /** @@ -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); } /** diff --git a/include/wil/registry_helpers.h b/include/wil/registry_helpers.h index f796864f..0817e7b8 100644 --- a/include/wil/registry_helpers.h +++ b/include/wil/registry_helpers.h @@ -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 { @@ -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(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 diff --git a/tests/RegistryTests.cpp b/tests/RegistryTests.cpp index f774e95c..effd1321 100644 --- a/tests/RegistryTests.cpp +++ b/tests/RegistryTests.cpp @@ -1326,8 +1326,174 @@ using ThrowingTypesToTest = using NoThrowTypesToTest = std::tuple; using ThrowingTypesToTest = std::tuple; #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(::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(buffer); + const std::wstring targetNtPath(reinterpret_cast(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(targetNtPath.c_str()), + static_cast(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(::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(wil::reg::open_options::none) == 0, "open_options::none must be 0"); + static_assert(static_cast(wil::reg::open_options::open_link) == REG_OPTION_OPEN_LINK, "open_link must map to REG_OPTION_OPEN_LINK"); + static_assert( + static_cast(wil::reg::open_options::backup_restore) == REG_OPTION_BACKUP_RESTORE, + "backup_restore must map to REG_OPTION_BACKUP_RESTORE"); + static_assert( + static_cast(wil::reg::open_options::dont_virtualize) == REG_OPTION_DONT_VIRTUALIZE, + "dont_virtualize must map to REG_OPTION_DONT_VIRTUALIZE"); + static_assert( + static_cast(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));