| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <windows.h> |
| |
| #include <shlobj.h> |
| |
| #include <iterator> |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/win/scoped_handle.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| #define FPL FILE_PATH_LITERAL |
| |
| namespace base { |
| |
| // A basic test harness that creates a temporary directory during test case |
| // setup and deletes it during teardown. |
| class OsValidationTest : public ::testing::Test { |
| protected: |
| // ::testing::Test: |
| static void SetUpTestCase() { |
| temp_dir_ = std::make_unique<ScopedTempDir>().release(); |
| ASSERT_TRUE(temp_dir_->CreateUniqueTempDir()); |
| } |
| |
| static void TearDownTestCase() { |
| // Explicitly delete the dir to catch any deletion errors. |
| ASSERT_TRUE(temp_dir_->Delete()); |
| auto temp_dir = base::WrapUnique(temp_dir_); |
| temp_dir_ = nullptr; |
| } |
| |
| // Returns the path to the test's temporary directory. |
| static const FilePath& temp_path() { return temp_dir_->GetPath(); } |
| |
| private: |
| static ScopedTempDir* temp_dir_; |
| }; |
| |
| // static |
| ScopedTempDir* OsValidationTest::temp_dir_ = nullptr; |
| |
| // A test harness for exhaustively evaluating the conditions under which an open |
| // file may be operated on. Template parameters are used to turn off or on |
| // various bits in the access rights and sharing mode bitfields. These template |
| // parameters are: |
| // - The standard access right bits (except for WRITE_OWNER, which requires |
| // admin rights): SYNCHRONIZE, WRITE_DAC, READ_CONTROL, DELETE. |
| // - Generic file access rights: FILE_GENERIC_READ, FILE_GENERIC_WRITE, |
| // FILE_EXECUTE. |
| // - The sharing bits: FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE. |
| class OpenFileTest : public OsValidationTest, |
| public ::testing::WithParamInterface< |
| std::tuple<std::tuple<DWORD, DWORD, DWORD, DWORD>, |
| std::tuple<DWORD, DWORD, DWORD>, |
| std::tuple<DWORD, DWORD, DWORD>>> { |
| protected: |
| OpenFileTest() = default; |
| OpenFileTest(const OpenFileTest&) = delete; |
| OpenFileTest& operator=(const OpenFileTest&) = delete; |
| |
| // Returns a dwDesiredAccess bitmask for use with CreateFileW containing the |
| // test's access right bits. |
| static DWORD GetAccess() { |
| // Extract the two tuples of standard and generic file rights. |
| std::tuple<DWORD, DWORD, DWORD, DWORD> standard_rights; |
| std::tuple<DWORD, DWORD, DWORD> generic_rights; |
| std::tie(standard_rights, generic_rights, std::ignore) = GetParam(); |
| |
| // Extract the five standard rights bits. |
| auto [synchronize_bit, write_dac_bit, read_control_bit, delete_bit] = |
| standard_rights; |
| |
| // Extract the three generic file rights masks. |
| auto [file_generic_read_bits, file_generic_write_bits, |
| file_generic_execute_bits] = generic_rights; |
| |
| // Combine and return the desired access rights. |
| return synchronize_bit | write_dac_bit | read_control_bit | delete_bit | |
| file_generic_read_bits | file_generic_write_bits | |
| file_generic_execute_bits; |
| } |
| |
| // Returns a dwShareMode bitmask for use with CreateFileW containing the |
| // tests's share mode bits. |
| static DWORD GetShareMode() { |
| // Extract the tuple of sharing mode bits. |
| std::tuple<DWORD, DWORD, DWORD> sharing_bits; |
| std::tie(std::ignore, std::ignore, sharing_bits) = GetParam(); |
| |
| // Extract the sharing mode bits. |
| auto [share_read_bit, share_write_bit, share_delete_bit] = sharing_bits; |
| |
| // Combine and return the sharing mode. |
| return share_read_bit | share_write_bit | share_delete_bit; |
| } |
| |
| // Appends string representation of the access rights bits present in |access| |
| // to |result|. |
| static void AppendAccessString(DWORD access, std::string* result) { |
| #define ENTRY(a) \ |
| { a, #a } |
| static constexpr BitAndName kBitNames[] = { |
| // The standard access rights: |
| ENTRY(SYNCHRONIZE), |
| ENTRY(WRITE_OWNER), |
| ENTRY(WRITE_DAC), |
| ENTRY(READ_CONTROL), |
| ENTRY(DELETE), |
| // The file-specific access rights: |
| ENTRY(FILE_WRITE_ATTRIBUTES), |
| ENTRY(FILE_READ_ATTRIBUTES), |
| ENTRY(FILE_EXECUTE), |
| ENTRY(FILE_WRITE_EA), |
| ENTRY(FILE_READ_EA), |
| ENTRY(FILE_APPEND_DATA), |
| ENTRY(FILE_WRITE_DATA), |
| ENTRY(FILE_READ_DATA), |
| }; |
| #undef ENTRY |
| ASSERT_NO_FATAL_FAILURE(AppendBitsToString(access, std::begin(kBitNames), |
| std::end(kBitNames), result)); |
| } |
| |
| // Appends a string representation of the sharing mode bits present in |
| // |share_mode| to |result|. |
| static void AppendShareModeString(DWORD share_mode, std::string* result) { |
| #define ENTRY(a) \ |
| { a, #a } |
| static constexpr BitAndName kBitNames[] = { |
| ENTRY(FILE_SHARE_DELETE), |
| ENTRY(FILE_SHARE_WRITE), |
| ENTRY(FILE_SHARE_READ), |
| }; |
| #undef ENTRY |
| ASSERT_NO_FATAL_FAILURE(AppendBitsToString( |
| share_mode, std::begin(kBitNames), std::end(kBitNames), result)); |
| } |
| |
| // Returns true if we expect that a file opened with |access| access rights |
| // and |share_mode| sharing can be moved via MoveFileEx, and can be deleted |
| // via DeleteFile so long as it is not mapped into a process. |
| static bool CanMoveFile(DWORD access, DWORD share_mode) { |
| // A file can be moved as long as it is opened with FILE_SHARE_DELETE or |
| // if nothing beyond the standard access rights (save DELETE) has been |
| // requested. It can be deleted under those same circumstances as long as |
| // it has not been mapped into a process. |
| constexpr DWORD kStandardNoDelete = STANDARD_RIGHTS_ALL & ~DELETE; |
| return ((share_mode & FILE_SHARE_DELETE) != 0) || |
| ((access & ~kStandardNoDelete) == 0); |
| } |
| |
| // OsValidationTest: |
| void SetUp() override { |
| OsValidationTest::SetUp(); |
| |
| // Determine the desired access and share mode for this test. |
| access_ = GetAccess(); |
| share_mode_ = GetShareMode(); |
| |
| // Make a ScopedTrace instance for comprehensible output. |
| std::string access_string; |
| ASSERT_NO_FATAL_FAILURE(AppendAccessString(access_, &access_string)); |
| std::string share_mode_string; |
| ASSERT_NO_FATAL_FAILURE( |
| AppendShareModeString(share_mode_, &share_mode_string)); |
| scoped_trace_ = std::make_unique<::testing::ScopedTrace>( |
| __FILE__, __LINE__, access_string + ", " + share_mode_string); |
| |
| // Make a copy of imm32.dll in the temp dir for fiddling. |
| ASSERT_TRUE(CreateTemporaryFileInDir(temp_path(), &temp_file_path_)); |
| ASSERT_TRUE(CopyFile(FilePath(FPL("c:\\windows\\system32\\imm32.dll")), |
| temp_file_path_)); |
| |
| // Open the file |
| file_handle_.Set(::CreateFileW(temp_file_path_.value().c_str(), access_, |
| share_mode_, nullptr, OPEN_EXISTING, |
| FILE_ATTRIBUTE_NORMAL, nullptr)); |
| ASSERT_TRUE(file_handle_.is_valid()) << ::GetLastError(); |
| |
| // Get a second unique name in the temp dir to which the file might be |
| // moved. |
| temp_file_dest_path_ = temp_file_path_.InsertBeforeExtension(FPL("bla")); |
| } |
| |
| void TearDown() override { |
| file_handle_.Close(); |
| |
| // Manually delete the temp files since the temp dir is reused across tests. |
| ASSERT_TRUE(DeleteFile(temp_file_path_)); |
| ASSERT_TRUE(DeleteFile(temp_file_dest_path_)); |
| } |
| |
| DWORD access() const { return access_; } |
| DWORD share_mode() const { return share_mode_; } |
| const FilePath& temp_file_path() const { return temp_file_path_; } |
| const FilePath& temp_file_dest_path() const { return temp_file_dest_path_; } |
| HANDLE file_handle() const { return file_handle_.get(); } |
| |
| private: |
| struct BitAndName { |
| DWORD bit; |
| StringPiece name; |
| }; |
| |
| // Appends the names of the bits present in |bitfield| to |result| based on |
| // the array of bit-to-name mappings bounded by |bits_begin| and |bits_end|. |
| static void AppendBitsToString(DWORD bitfield, |
| const BitAndName* bits_begin, |
| const BitAndName* bits_end, |
| std::string* result) { |
| while (bits_begin < bits_end) { |
| const BitAndName& bit_name = *bits_begin; |
| if (bitfield & bit_name.bit) { |
| if (!result->empty()) |
| result->append(" | "); |
| result->append(bit_name.name.data(), bit_name.name.size()); |
| bitfield &= ~bit_name.bit; |
| } |
| ++bits_begin; |
| } |
| ASSERT_EQ(bitfield, DWORD{0}); |
| } |
| |
| DWORD access_ = 0; |
| DWORD share_mode_ = 0; |
| std::unique_ptr<::testing::ScopedTrace> scoped_trace_; |
| FilePath temp_file_path_; |
| FilePath temp_file_dest_path_; |
| win::ScopedHandle file_handle_; |
| }; |
| |
| // Tests that an opened but not mapped file can be deleted as expected. |
| TEST_P(OpenFileTest, DeleteFile) { |
| if (CanMoveFile(access(), share_mode())) { |
| EXPECT_NE(::DeleteFileW(temp_file_path().value().c_str()), 0) |
| << "Last error code: " << ::GetLastError(); |
| } else { |
| EXPECT_EQ(::DeleteFileW(temp_file_path().value().c_str()), 0); |
| } |
| } |
| |
| // Tests that an opened file can be moved as expected. |
| TEST_P(OpenFileTest, MoveFileEx) { |
| if (CanMoveFile(access(), share_mode())) { |
| EXPECT_NE(::MoveFileExW(temp_file_path().value().c_str(), |
| temp_file_dest_path().value().c_str(), 0), |
| 0) |
| << "Last error code: " << ::GetLastError(); |
| } else { |
| EXPECT_EQ(::MoveFileExW(temp_file_path().value().c_str(), |
| temp_file_dest_path().value().c_str(), 0), |
| 0); |
| } |
| } |
| |
| // Tests that an open file cannot be moved after it has been marked for |
| // deletion. |
| TEST_P(OpenFileTest, DeleteThenMove) { |
| // Don't test combinations that cannot be deleted. |
| if (!CanMoveFile(access(), share_mode())) |
| return; |
| ASSERT_NE(::DeleteFileW(temp_file_path().value().c_str()), 0) |
| << "Last error code: " << ::GetLastError(); |
| // Move fails with ERROR_ACCESS_DENIED (STATUS_DELETE_PENDING under the |
| // covers). |
| EXPECT_EQ(::MoveFileExW(temp_file_path().value().c_str(), |
| temp_file_dest_path().value().c_str(), 0), |
| 0); |
| } |
| |
| // Tests that an open file that is mapped into memory can be moved but not |
| // deleted. |
| TEST_P(OpenFileTest, MapThenDelete) { |
| // There is nothing to test if the file can't be read. |
| if (!(access() & FILE_READ_DATA)) |
| return; |
| |
| // Pick the protection option that matches the access rights used to open the |
| // file. |
| static constexpr struct { |
| DWORD access_bits; |
| DWORD protection; |
| } kAccessToProtection[] = { |
| // Sorted from most- to least-bits used for logic below. |
| {FILE_READ_DATA | FILE_WRITE_DATA | FILE_EXECUTE, PAGE_EXECUTE_READWRITE}, |
| {FILE_READ_DATA | FILE_WRITE_DATA, PAGE_READWRITE}, |
| {FILE_READ_DATA | FILE_EXECUTE, PAGE_EXECUTE_READ}, |
| {FILE_READ_DATA, PAGE_READONLY}, |
| }; |
| |
| DWORD protection = 0; |
| for (const auto& scan : kAccessToProtection) { |
| if ((access() & scan.access_bits) == scan.access_bits) { |
| protection = scan.protection; |
| break; |
| } |
| } |
| ASSERT_NE(protection, DWORD{0}); |
| |
| win::ScopedHandle mapping(::CreateFileMappingA( |
| file_handle(), nullptr, protection | SEC_IMAGE, 0, 0, nullptr)); |
| auto result = ::GetLastError(); |
| ASSERT_TRUE(mapping.is_valid()) << result; |
| |
| auto* view = ::MapViewOfFile(mapping.get(), FILE_MAP_READ, 0, 0, 0); |
| result = ::GetLastError(); |
| ASSERT_NE(view, nullptr) << result; |
| ScopedClosureRunner unmapper( |
| BindOnce([](const void* view) { ::UnmapViewOfFile(view); }, view)); |
| |
| // Mapped files cannot be deleted under any circumstances. |
| EXPECT_EQ(::DeleteFileW(temp_file_path().value().c_str()), 0); |
| |
| // But can still be moved under the same conditions as if it weren't mapped. |
| if (CanMoveFile(access(), share_mode())) { |
| EXPECT_NE(::MoveFileExW(temp_file_path().value().c_str(), |
| temp_file_dest_path().value().c_str(), 0), |
| 0) |
| << "Last error code: " << ::GetLastError(); |
| } else { |
| EXPECT_EQ(::MoveFileExW(temp_file_path().value().c_str(), |
| temp_file_dest_path().value().c_str(), 0), |
| 0); |
| } |
| } |
| |
| // These tests are intentionally disabled by default. They were created as an |
| // educational tool to understand the restrictions on moving and deleting files |
| // on Windows. There is every expectation that once they pass, they will always |
| // pass. It might be interesting to run them manually on new versions of the OS, |
| // but there is no need to run them on every try/CQ run. Here is one possible |
| // way to run them all locally: |
| // |
| // base_unittests.exe --single-process-tests --gtest_also_run_disabled_tests \ |
| // --gtest_filter=*OpenFileTest* |
| INSTANTIATE_TEST_SUITE_P( |
| DISABLED_Test, |
| OpenFileTest, |
| ::testing::Combine( |
| // Standard access rights except for WRITE_OWNER, which requires admin. |
| ::testing::Combine(::testing::Values(0, SYNCHRONIZE), |
| ::testing::Values(0, WRITE_DAC), |
| ::testing::Values(0, READ_CONTROL), |
| ::testing::Values(0, DELETE)), |
| // Generic file access rights. |
| ::testing::Combine(::testing::Values(0, FILE_GENERIC_READ), |
| ::testing::Values(0, FILE_GENERIC_WRITE), |
| ::testing::Values(0, FILE_GENERIC_EXECUTE)), |
| // File sharing mode. |
| ::testing::Combine(::testing::Values(0, FILE_SHARE_READ), |
| ::testing::Values(0, FILE_SHARE_WRITE), |
| ::testing::Values(0, FILE_SHARE_DELETE)))); |
| |
| } // namespace base |