1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
|
// 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 <string_view>
#include <tuple>
#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"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.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 SetUpTestSuite() {
temp_dir_ = std::make_unique<ScopedTempDir>().release();
ASSERT_TRUE(temp_dir_->CreateUniqueTempDir());
}
static void TearDownTestSuite() {
// 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;
std::string_view 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);
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;
absl::Cleanup unmapper = [view] { ::UnmapViewOfFile(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
|