blob: 7792cb16b807434145e2a06c909784596879ce73 [file] [log] [blame]
// 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) {
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) {
if (pending_moves.empty()) {
// Leave buffer empty if there are no strings.
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.
base::char16* write_pointer =
// 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;
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,
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.
base::FilePath pending_path(
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()) {
// 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.
base::FilePath pending_path(
pending_path = GetShortPathNameWithoutPrefix(pending_path);
if (short_paths.find(pending_path) == short_paths.end())
// 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
bool GetPendingMoves(PendingMoveVector* pending_moves) {
// Get the current value of the key.
base::win::RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey,
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()) {
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,
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(),
LOG(ERROR) << "Failed to update the " << kPendingFileRenameOps << " key.";
return false;
return true;
} // namespace chrome_cleaner