// Copyright (c) 2010 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. // // This file defines helper methods used to schedule files for deletion // on next reboot. The code here is heavily borrowed and simplified from // http://code.google.com/p/omaha/source/browse/trunk/common/file.cc and // http://code.google.com/p/omaha/source/browse/trunk/common/utils.cc // // This implementation really is not fast, so do not use it where that will // matter. #include "chrome/installer/util/delete_after_reboot_helper.h" #include #include #include "base/files/file_enumerator.h" #include "base/files/file_util.h" #include "base/numerics/safe_conversions.h" #include "base/strings/string_util.h" #include "base/win/registry.h" // The moves-pending-reboot is a MULTISZ registry key in the HKLM part of the // registry. const wchar_t kSessionManagerKey[] = L"SYSTEM\\CurrentControlSet\\Control\\Session Manager"; const wchar_t kPendingFileRenameOps[] = L"PendingFileRenameOperations"; namespace { // Returns true if this directory name is 'safe' for deletion (doesn't contain // "..", doesn't specify a drive root) bool IsSafeDirectoryNameForDeletion(const base::FilePath& dir_name) { // empty name isn't allowed if (dir_name.empty()) return false; // require a character other than \/:. after the last : // disallow anything with ".." bool ok = false; const wchar_t* dir_name_str = dir_name.value().c_str(); for (const wchar_t* s = dir_name_str; *s; ++s) { if (*s != L'\\' && *s != L'/' && *s != L':' && *s != L'.') ok = true; if (*s == L'.' && s > dir_name_str && *(s - 1) == L'.') return false; if (*s == L':') ok = false; } return ok; } } // end namespace // Must only be called for regular files or directories that will be empty. bool ScheduleFileSystemEntityForDeletion(const base::FilePath& path) { // Check if the file exists, return false if not. WIN32_FILE_ATTRIBUTE_DATA attrs = {0}; if (!::GetFileAttributesEx(path.value().c_str(), ::GetFileExInfoStandard, &attrs)) { PLOG(WARNING) << path.value() << " does not exist."; return false; } DWORD flags = MOVEFILE_DELAY_UNTIL_REBOOT; if (!base::DirectoryExists(path)) { // This flag valid only for files flags |= MOVEFILE_REPLACE_EXISTING; } if (!::MoveFileEx(path.value().c_str(), NULL, flags)) { PLOG(ERROR) << "Could not schedule " << path.value() << " for deletion."; return false; } #ifndef NDEBUG // Useful debugging code to track down what files are in use. if (flags & MOVEFILE_REPLACE_EXISTING) { // Attempt to open the file exclusively. HANDLE file = ::CreateFileW(path.value().c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (file != INVALID_HANDLE_VALUE) { VLOG(1) << " file not in use: " << path.value(); ::CloseHandle(file); } else { PLOG(WARNING) << " file in use (or not found?): " << path.value(); } } #endif VLOG(1) << "Scheduled for deletion: " << path.value(); return true; } bool ScheduleDirectoryForDeletion(const base::FilePath& dir_name) { if (!IsSafeDirectoryNameForDeletion(dir_name)) { LOG(ERROR) << "Unsafe directory name for deletion: " << dir_name.value(); return false; } // Make sure the directory exists (it is ok if it doesn't) DWORD dir_attributes = ::GetFileAttributes(dir_name.value().c_str()); if (dir_attributes == INVALID_FILE_ATTRIBUTES) { if (::GetLastError() == ERROR_FILE_NOT_FOUND) { return true; // Ok if directory is missing } else { PLOG(ERROR) << "Could not GetFileAttributes for " << dir_name.value(); return false; } } // Confirm it is a directory if (!(dir_attributes & FILE_ATTRIBUTE_DIRECTORY)) { LOG(ERROR) << "Scheduled directory is not a directory: " << dir_name.value(); return false; } // First schedule all the normal files for deletion. { bool success = true; base::FileEnumerator file_enum(dir_name, false, base::FileEnumerator::FILES); for (base::FilePath file = file_enum.Next(); !file.empty(); file = file_enum.Next()) { success = ScheduleFileSystemEntityForDeletion(file); if (!success) { LOG(ERROR) << "Failed to schedule file for deletion: " << file.value(); return false; } } } // Then recurse to all the subdirectories. { bool success = true; base::FileEnumerator dir_enum(dir_name, false, base::FileEnumerator::DIRECTORIES); for (base::FilePath sub_dir = dir_enum.Next(); !sub_dir.empty(); sub_dir = dir_enum.Next()) { success = ScheduleDirectoryForDeletion(sub_dir); if (!success) { LOG(ERROR) << "Failed to schedule subdirectory for deletion: " << sub_dir.value(); return false; } } } // Now schedule the empty directory itself if (!ScheduleFileSystemEntityForDeletion(dir_name)) { LOG(ERROR) << "Failed to schedule directory for deletion: " << dir_name.value(); } return true; } // Converts the strings found in |buffer| to a list of wstrings that is returned // in |value|. // |buffer| points to a series of pairs of null-terminated wchar_t strings // followed by a terminating null character. // |byte_count| is the length of |buffer| in bytes. // |value| is a pointer to an empty vector of wstrings. On success, this vector // contains all of the strings extracted from |buffer|. // Returns S_OK on success, E_INVALIDARG if buffer does not meet tha above // specification. HRESULT MultiSZBytesToStringArray(const char* buffer, size_t byte_count, std::vector* value) { DCHECK(buffer); DCHECK(value); DCHECK(value->empty()); DWORD data_len = byte_count / sizeof(wchar_t); const wchar_t* data = reinterpret_cast(buffer); const wchar_t* data_end = data + data_len; if (data_len > 1) { // must be terminated by two null characters if (data[data_len - 1] != 0 || data[data_len - 2] != 0) { DLOG(ERROR) << "Invalid MULTI_SZ found."; return E_INVALIDARG; } // put null-terminated strings into arrays while (data < data_end) { std::wstring str_from(data); data += str_from.length() + 1; if (data < data_end) { std::wstring str_to(data); data += str_to.length() + 1; value->push_back(std::make_pair(str_from, str_to)); } } } return S_OK; } void StringArrayToMultiSZBytes(const std::vector& strings, std::vector* buffer) { DCHECK(buffer); buffer->clear(); if (strings.empty()) { // Leave buffer empty if we have no strings. return; } size_t total_wchars = 0; { std::vector::const_iterator iter(strings.begin()); for (; iter != strings.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. } size_t total_length = total_wchars * sizeof(wchar_t); buffer->resize(total_length); wchar_t* write_pointer = reinterpret_cast(&((*buffer)[0])); // Keep an end pointer around for sanity checking. wchar_t* end_pointer = write_pointer + total_wchars; std::vector::const_iterator copy_iter(strings.begin()); for (; copy_iter != strings.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(wchar_t)); 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(wchar_t)); 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); } base::FilePath GetShortPathName(const base::FilePath& path) { std::wstring short_path; DWORD length = GetShortPathName(path.value().c_str(), base::WriteInto(&short_path, MAX_PATH), MAX_PATH); DWORD last_error = ::GetLastError(); DLOG_IF(WARNING, length == 0 && last_error != ERROR_PATH_NOT_FOUND) << __FUNCTION__ << " gle=" << last_error; 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 purposes. return path; } short_path.resize(length); return base::FilePath(short_path); } HRESULT GetPendingMovesValue(std::vector* pending_moves) { DCHECK(pending_moves); pending_moves->clear(); // Get the current value of the key // If the Key is missing, that's totally acceptable. 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) return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); // The base::RegKey Read code squashes the return code from // ReqQueryValueEx, we have to do things ourselves: DWORD buffer_size = 0; std::vector buffer; buffer.resize(1); DWORD type; DWORD result = RegQueryValueEx(session_manager_handle, kPendingFileRenameOps, 0, &type, reinterpret_cast(&buffer[0]), &buffer_size); if (result == ERROR_FILE_NOT_FOUND) { // No pending moves were found. return HRESULT_FROM_WIN32(result); } if (result != ERROR_MORE_DATA) { // That was unexpected. DLOG(ERROR) << "Unexpected result from RegQueryValueEx: " << result; return HRESULT_FROM_WIN32(result); } if (type != REG_MULTI_SZ) { DLOG(ERROR) << "Found PendingRename value of unexpected type."; return E_UNEXPECTED; } if (buffer_size % 2) { // The buffer size should be an even number (since we expect wchar_ts). // If this is not the case, fail here. DLOG(ERROR) << "Corrupt PendingRename value."; return E_UNEXPECTED; } // There are pending file renames. Read them in. buffer.resize(buffer_size); result = RegQueryValueEx(session_manager_handle, kPendingFileRenameOps, 0, &type, reinterpret_cast(&buffer[0]), &buffer_size); if (result != ERROR_SUCCESS) { DLOG(ERROR) << "Failed to read from " << kPendingFileRenameOps; return HRESULT_FROM_WIN32(result); } // We now have a buffer of bytes that is actually a sequence of // null-terminated wchar_t strings terminated by an additional null character. // Stick this into a vector of strings for clarity. HRESULT hr = MultiSZBytesToStringArray(&buffer[0], buffer.size(), pending_moves); return hr; } bool MatchPendingDeletePath(const base::FilePath& short_form_needle, 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::StringPiece16 prefix(L"\\??\\"); if (base::StartsWith(match_path, prefix, base::CompareCase::SENSITIVE)) match_path = match_path.substr(prefix.size()); // Get the short path name of the entry. base::FilePath short_match_path(GetShortPathName(base::FilePath(match_path))); // Now compare the paths. It's a match if short_form_needle is a // case-insensitive prefix of short_match_path. if (short_match_path.value().size() < short_form_needle.value().size()) return false; DWORD prefix_len = base::saturated_cast(short_form_needle.value().size()); return ::CompareString(LOCALE_USER_DEFAULT, NORM_IGNORECASE, short_match_path.value().data(), prefix_len, short_form_needle.value().data(), prefix_len) == CSTR_EQUAL; } // Removes all pending moves for the given |directory| and any contained // files or subdirectories. Returns true on success bool RemoveFromMovesPendingReboot(const base::FilePath& directory) { std::vector pending_moves; HRESULT hr = GetPendingMovesValue(&pending_moves); if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { // No pending moves, nothing to do. return true; } if (FAILED(hr)) { // Couldn't read the key or the key was corrupt. return false; } // Get the short form of |directory| and use that to match. base::FilePath short_directory(GetShortPathName(directory)); std::vector strings_to_keep; for (std::vector::const_iterator iter(pending_moves.begin()); iter != pending_moves.end(); ++iter) { base::FilePath move_path(iter->first); if (!MatchPendingDeletePath(short_directory, move_path)) { // This doesn't match the deletions we are looking for. Preserve // this string pair, making sure that it is in fact a pair. strings_to_keep.push_back(*iter); } } if (strings_to_keep.size() == pending_moves.size()) { // Nothing to remove, return true. return true; } // Write the key back 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 (strings_to_keep.size() <= 1) { // We have only the trailing NULL string. Don't bother writing that. return (session_manager_key.DeleteValue(kPendingFileRenameOps) == ERROR_SUCCESS); } std::vector buffer; StringArrayToMultiSZBytes(strings_to_keep, &buffer); DCHECK_GT(buffer.size(), 0U); if (buffer.empty()) return false; return (session_manager_key.WriteValue(kPendingFileRenameOps, &buffer[0], buffer.size(), REG_MULTI_SZ) == ERROR_SUCCESS); }