367 lines
14 KiB
C++
367 lines
14 KiB
C++
|
|
// 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
|