| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/chrome_cleaner/test/reboot_deletion_helper.h" |
| |
| #include <windows.h> |
| |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/win/registry.h" |
| #include "chrome/chrome_cleaner/os/file_path_sanitization.h" |
| #include "chrome/chrome_cleaner/os/registry_util.h" |
| |
| namespace chrome_cleaner { |
| |
| namespace { |
| |
| // The moves-pending-reboot is a MULTISZ registry value in the HKLM part of the |
| // registry. |
| const wchar_t kSessionManagerKey[] = |
| L"SYSTEM\\CurrentControlSet\\Control\\Session Manager"; |
| const wchar_t kPendingFileRenameOps[] = L"PendingFileRenameOperations"; |
| |
| const wchar_t kDoubleNullEntry[] = L"\0\0"; |
| |
| // Convert the strings found in |buffer| to a list of string16s that is returned |
| // in |value|. |buffer| is a string which contains a series of pairs of |
| // null-terminated string16s followed by a terminating null character. |value| |
| // is a pointer to an empty vector of pending moves. On success, this vector |
| // contains all of the strings extracted from |buffer|. |
| // Returns false if buffer does not meet the above specification. |
| bool MultiSZToStringArray(const base::string16& buffer, |
| std::vector<PendingMove>* value) { |
| DCHECK(value); |
| DCHECK(value->empty()); |
| |
| const base::char16* data = buffer.c_str(); |
| const base::char16* data_end = data + buffer.size(); |
| |
| // Put null-terminated strings into the sequence. |
| while (data < data_end) { |
| // Parse the source path. |
| base::string16 str_from(data); |
| data += str_from.length() + 1; |
| |
| // Parse the destination path. If the offset is at the end of the string, |
| // we assume the destination is empty. This may happen with corrupt registry |
| // values where there are missing trailing null characters. |
| base::string16 str_to; |
| if (data > data_end) |
| return false; |
| if (data < data_end) |
| str_to = data; |
| |
| // Move to the next entry. |
| data += str_to.length() + 1; |
| value->push_back(std::make_pair(str_from, str_to)); |
| } |
| return true; |
| } |
| |
| // The inverse of MultiSZToStringArray, this function converts a list |
| // of string pairs into a byte array format suitable for writing to the |
| // kPendingFileRenameOps registry value. It concatenates the strings and |
| // appends an additional terminating null character. |
| void StringArrayToMultiSZ(const std::vector<PendingMove>& pending_moves, |
| base::string16* buffer) { |
| DCHECK(buffer); |
| buffer->clear(); |
| |
| if (pending_moves.empty()) { |
| // Leave buffer empty if there are no strings. |
| return; |
| } |
| |
| size_t total_wchars = 0; |
| std::vector<PendingMove>::const_iterator iter(pending_moves.begin()); |
| for (; iter != pending_moves.end(); ++iter) { |
| total_wchars += iter->first.length(); |
| ++total_wchars; // Space for the null char. |
| total_wchars += iter->second.length(); |
| ++total_wchars; // Space for the null char. |
| } |
| ++total_wchars; // Space for the extra terminating null char. |
| |
| buffer->resize(total_wchars); |
| base::char16* write_pointer = |
| reinterpret_cast<base::char16*>(&((*buffer)[0])); |
| // Keep an end pointer around for sanity checking. |
| base::char16* end_pointer = write_pointer + total_wchars; |
| |
| std::vector<PendingMove>::const_iterator copy_iter(pending_moves.begin()); |
| for (; copy_iter != pending_moves.end() && write_pointer < end_pointer; |
| ++copy_iter) { |
| // First copy the source string. |
| size_t string_length = copy_iter->first.length() + 1; |
| ::memcpy(write_pointer, copy_iter->first.c_str(), |
| string_length * sizeof(base::char16)); |
| write_pointer += string_length; |
| // Now copy the destination string. |
| string_length = copy_iter->second.length() + 1; |
| ::memcpy(write_pointer, copy_iter->second.c_str(), |
| string_length * sizeof(base::char16)); |
| write_pointer += string_length; |
| |
| // We should never run off the end while in this loop. |
| DCHECK(write_pointer < end_pointer); |
| } |
| *write_pointer = L'\0'; // Explicitly set the final null char. |
| DCHECK(++write_pointer == end_pointer); |
| } |
| |
| // A helper function for the win32 GetShortPathName that more conveniently |
| // returns a FilePath. Note that if |path| is not present on the file system |
| // then GetShortPathName will return |path| unchanged, unlike the win32 |
| // GetShortPathName which will return an error. |
| base::FilePath GetShortPathName(const base::FilePath& path) { |
| base::string16 short_path; |
| DWORD length = ::GetShortPathName( |
| path.value().c_str(), base::WriteInto(&short_path, MAX_PATH), MAX_PATH); |
| DPLOG_IF(WARNING, length == 0 && ::GetLastError() != ERROR_PATH_NOT_FOUND) |
| << "GetShortPathName an unexpected result."; |
| if (length == 0) { |
| // GetShortPathName fails if the path is no longer present. Instead of |
| // returning an empty string, just return the original string. This will |
| // serve our purpose. |
| return path; |
| } |
| |
| short_path.resize(length); |
| return base::FilePath(short_path); |
| } |
| |
| base::FilePath GetShortPathNameWithoutPrefix(const base::FilePath& reg_path) { |
| // Stores the path stored in each entry. |
| base::string16 match_path(reg_path.value()); |
| |
| // First chomp the prefix since that will mess up GetShortPathName. |
| base::string16 prefix(L"\\??\\"); |
| if (base::StartsWith(match_path, prefix, |
| base::CompareCase::INSENSITIVE_ASCII)) |
| match_path = match_path.substr(4); |
| |
| // Get the short path name of the entry. |
| return GetShortPathName(base::FilePath(match_path)); |
| } |
| |
| } // namespace |
| |
| bool IsFileRegisteredForPostRebootRemoval(const base::FilePath& file_path) { |
| base::FilePath short_path = GetShortPathName(NormalizePath(file_path)); |
| |
| PendingMoveVector pending_moves; |
| // This can only be called in tests, so CHECKing is ok since that ensures the |
| // test fails. |
| CHECK(GetPendingMoves(&pending_moves)) << "Unable to get pending moves"; |
| |
| for (PendingMoveVector::const_iterator iter(pending_moves.begin()); |
| iter != pending_moves.end(); ++iter) { |
| if (!iter->second.empty()) { |
| // If the destination string is not empty, the pending operation is a move |
| // so this isn't a removal. |
| continue; |
| } |
| |
| base::FilePath pending_path( |
| GetShortPathName(NormalizePath(base::FilePath(iter->first)))); |
| pending_path = GetShortPathNameWithoutPrefix(pending_path); |
| if (base::FilePath::CompareEqualIgnoreCase(pending_path.value(), |
| short_path.value())) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| bool UnregisterPostRebootRemovals(const FilePathSet& paths) { |
| // Retrieve pending moves from the registry. |
| PendingMoveVector pending_moves; |
| if (!GetPendingMoves(&pending_moves)) |
| return false; |
| |
| // Build an index of short paths to remove. |
| UnorderedFilePathSet short_paths; |
| for (const auto& path : paths.file_paths()) { |
| short_paths.insert(GetShortPathName(path)); |
| } |
| |
| // Filter paths pending for deletion with the short paths index. |
| PendingMoveVector moves_to_keep; |
| for (PendingMoveVector::const_iterator iter(pending_moves.begin()); |
| iter != pending_moves.end(); ++iter) { |
| if (!iter->second.empty()) { |
| // If the destination string is not empty, the pending operation is a move |
| // and must not be removed. |
| moves_to_keep.push_back(*iter); |
| continue; |
| } |
| base::FilePath pending_path( |
| GetShortPathName(NormalizePath(base::FilePath(iter->first)))); |
| pending_path = GetShortPathNameWithoutPrefix(pending_path); |
| if (short_paths.find(pending_path) == short_paths.end()) |
| moves_to_keep.push_back(*iter); |
| } |
| |
| // Update the remaining pending moves to the registry. |
| if (!SetPendingMoves(moves_to_keep)) |
| return false; |
| |
| return true; |
| } |
| |
| // Retrieves the list of pending moves from the registry and returns a vector |
| // containing pairs of strings that represent the operations. If the list |
| // contains only deletes then every other element will be an empty string |
| // as per http://msdn.microsoft.com/en-us/library/aa365240(VS.85).aspx. |
| bool GetPendingMoves(PendingMoveVector* pending_moves) { |
| DCHECK(pending_moves); |
| pending_moves->clear(); |
| |
| // Get the current value of the key. |
| base::win::RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey, |
| KEY_QUERY_VALUE); |
| HKEY session_manager_handle = session_manager_key.Handle(); |
| if (!session_manager_handle || |
| !session_manager_key.HasValue(kPendingFileRenameOps)) { |
| // If the key or the value is missing, that's totally acceptable. |
| return true; |
| } |
| |
| // Read the content of the registry value to retrieve the pending moves. |
| base::string16 pending_moves_value; |
| uint32_t pending_moves_value_type; |
| if (!ReadRegistryValue(session_manager_key, kPendingFileRenameOps, |
| &pending_moves_value, &pending_moves_value_type, |
| nullptr)) { |
| DLOG(ERROR) << "Cannot read PendingRename registry value."; |
| return false; |
| } |
| |
| if (pending_moves_value_type != REG_MULTI_SZ) { |
| DLOG(ERROR) << "Found PendingRename value of unexpected type."; |
| return false; |
| } |
| |
| // We now have a buffer of bytes that is actually a sequence of |
| // null-terminated char16 strings terminated by an additional null character. |
| // Stick this into a vector of strings for clarity. |
| if (!MultiSZToStringArray(pending_moves_value, pending_moves)) { |
| DLOG(ERROR) << "Cannot decode PendingRename registry value."; |
| return false; |
| } |
| |
| // Remove the last pending moves entry if it is empty. This entry is found on |
| // Vista+ but not on XP. |
| if (!pending_moves->empty() && pending_moves->back().first.empty() && |
| pending_moves->back().second.empty()) { |
| pending_moves->pop_back(); |
| } |
| |
| return true; |
| } |
| |
| bool SetPendingMoves(const PendingMoveVector& pending_moves) { |
| // Retrieve the key content into a buffer. |
| base::win::RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey, |
| KEY_CREATE_SUB_KEY | KEY_SET_VALUE); |
| if (!session_manager_key.Handle()) { |
| // Couldn't open / create the key. |
| LOG(ERROR) << "Failed to open session manager key for writing."; |
| return false; |
| } |
| |
| if (pending_moves.empty()) { |
| // No remaining moves. Don't bother writing that. |
| LONG delete_result = session_manager_key.DeleteValue(kPendingFileRenameOps); |
| return (delete_result == ERROR_SUCCESS || |
| delete_result == ERROR_FILE_NOT_FOUND); |
| } |
| |
| // Serialize pending moves as a sequence of bytes. |
| base::string16 buffer; |
| StringArrayToMultiSZ(pending_moves, &buffer); |
| if (buffer.empty()) |
| return false; |
| |
| // The pending moves format needs a null entry at the end which consists of |
| // two MULTISZ empty string. |
| base::string16 last_entry(kDoubleNullEntry, base::size(kDoubleNullEntry) - 1); |
| buffer = buffer + last_entry; |
| |
| // Write back the serialized values into the registry key. |
| uint32_t size_in_bytes = buffer.size() * sizeof(base::char16); |
| if (session_manager_key.WriteValue(kPendingFileRenameOps, buffer.c_str(), |
| size_in_bytes, |
| REG_MULTI_SZ) != ERROR_SUCCESS) { |
| LOG(ERROR) << "Failed to update the " << kPendingFileRenameOps << " key."; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| } // namespace chrome_cleaner |