123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- // Copyright 2019 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 <windows.h>
- #include <shlobj.h>
- #include <iterator>
- #include <memory>
- #include <string>
- #include <tuple>
- #include "base/callback_helpers.h"
- #include "base/files/file_path.h"
- #include "base/files/file_util.h"
- #include "base/files/scoped_temp_dir.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
|