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
base / files / os_validation_win_unittest.cc [blame]
// 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