1 // Copyright 2019 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include <windows.h>
6
7 #include <shlobj.h>
8
9 #include <iterator>
10 #include <memory>
11 #include <string>
12 #include <tuple>
13
14 #include "base/files/file_path.h"
15 #include "base/files/file_util.h"
16 #include "base/files/scoped_temp_dir.h"
17 #include "base/functional/callback_helpers.h"
18 #include "base/memory/ptr_util.h"
19 #include "base/strings/string_util.h"
20 #include "base/win/scoped_handle.h"
21 #include "testing/gtest/include/gtest/gtest.h"
22
23 #define FPL FILE_PATH_LITERAL
24
25 namespace base {
26
27 // A basic test harness that creates a temporary directory during test case
28 // setup and deletes it during teardown.
29 class OsValidationTest : public ::testing::Test {
30 protected:
31 // ::testing::Test:
SetUpTestSuite()32 static void SetUpTestSuite() {
33 temp_dir_ = std::make_unique<ScopedTempDir>().release();
34 ASSERT_TRUE(temp_dir_->CreateUniqueTempDir());
35 }
36
TearDownTestSuite()37 static void TearDownTestSuite() {
38 // Explicitly delete the dir to catch any deletion errors.
39 ASSERT_TRUE(temp_dir_->Delete());
40 auto temp_dir = base::WrapUnique(temp_dir_);
41 temp_dir_ = nullptr;
42 }
43
44 // Returns the path to the test's temporary directory.
temp_path()45 static const FilePath& temp_path() { return temp_dir_->GetPath(); }
46
47 private:
48 static ScopedTempDir* temp_dir_;
49 };
50
51 // static
52 ScopedTempDir* OsValidationTest::temp_dir_ = nullptr;
53
54 // A test harness for exhaustively evaluating the conditions under which an open
55 // file may be operated on. Template parameters are used to turn off or on
56 // various bits in the access rights and sharing mode bitfields. These template
57 // parameters are:
58 // - The standard access right bits (except for WRITE_OWNER, which requires
59 // admin rights): SYNCHRONIZE, WRITE_DAC, READ_CONTROL, DELETE.
60 // - Generic file access rights: FILE_GENERIC_READ, FILE_GENERIC_WRITE,
61 // FILE_EXECUTE.
62 // - The sharing bits: FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE.
63 class OpenFileTest : public OsValidationTest,
64 public ::testing::WithParamInterface<
65 std::tuple<std::tuple<DWORD, DWORD, DWORD, DWORD>,
66 std::tuple<DWORD, DWORD, DWORD>,
67 std::tuple<DWORD, DWORD, DWORD>>> {
68 protected:
69 OpenFileTest() = default;
70 OpenFileTest(const OpenFileTest&) = delete;
71 OpenFileTest& operator=(const OpenFileTest&) = delete;
72
73 // Returns a dwDesiredAccess bitmask for use with CreateFileW containing the
74 // test's access right bits.
GetAccess()75 static DWORD GetAccess() {
76 // Extract the two tuples of standard and generic file rights.
77 std::tuple<DWORD, DWORD, DWORD, DWORD> standard_rights;
78 std::tuple<DWORD, DWORD, DWORD> generic_rights;
79 std::tie(standard_rights, generic_rights, std::ignore) = GetParam();
80
81 // Extract the five standard rights bits.
82 auto [synchronize_bit, write_dac_bit, read_control_bit, delete_bit] =
83 standard_rights;
84
85 // Extract the three generic file rights masks.
86 auto [file_generic_read_bits, file_generic_write_bits,
87 file_generic_execute_bits] = generic_rights;
88
89 // Combine and return the desired access rights.
90 return synchronize_bit | write_dac_bit | read_control_bit | delete_bit |
91 file_generic_read_bits | file_generic_write_bits |
92 file_generic_execute_bits;
93 }
94
95 // Returns a dwShareMode bitmask for use with CreateFileW containing the
96 // tests's share mode bits.
GetShareMode()97 static DWORD GetShareMode() {
98 // Extract the tuple of sharing mode bits.
99 std::tuple<DWORD, DWORD, DWORD> sharing_bits;
100 std::tie(std::ignore, std::ignore, sharing_bits) = GetParam();
101
102 // Extract the sharing mode bits.
103 auto [share_read_bit, share_write_bit, share_delete_bit] = sharing_bits;
104
105 // Combine and return the sharing mode.
106 return share_read_bit | share_write_bit | share_delete_bit;
107 }
108
109 // Appends string representation of the access rights bits present in |access|
110 // to |result|.
AppendAccessString(DWORD access,std::string * result)111 static void AppendAccessString(DWORD access, std::string* result) {
112 #define ENTRY(a) \
113 { a, #a }
114 static constexpr BitAndName kBitNames[] = {
115 // The standard access rights:
116 ENTRY(SYNCHRONIZE),
117 ENTRY(WRITE_OWNER),
118 ENTRY(WRITE_DAC),
119 ENTRY(READ_CONTROL),
120 ENTRY(DELETE),
121 // The file-specific access rights:
122 ENTRY(FILE_WRITE_ATTRIBUTES),
123 ENTRY(FILE_READ_ATTRIBUTES),
124 ENTRY(FILE_EXECUTE),
125 ENTRY(FILE_WRITE_EA),
126 ENTRY(FILE_READ_EA),
127 ENTRY(FILE_APPEND_DATA),
128 ENTRY(FILE_WRITE_DATA),
129 ENTRY(FILE_READ_DATA),
130 };
131 #undef ENTRY
132 ASSERT_NO_FATAL_FAILURE(AppendBitsToString(access, std::begin(kBitNames),
133 std::end(kBitNames), result));
134 }
135
136 // Appends a string representation of the sharing mode bits present in
137 // |share_mode| to |result|.
AppendShareModeString(DWORD share_mode,std::string * result)138 static void AppendShareModeString(DWORD share_mode, std::string* result) {
139 #define ENTRY(a) \
140 { a, #a }
141 static constexpr BitAndName kBitNames[] = {
142 ENTRY(FILE_SHARE_DELETE),
143 ENTRY(FILE_SHARE_WRITE),
144 ENTRY(FILE_SHARE_READ),
145 };
146 #undef ENTRY
147 ASSERT_NO_FATAL_FAILURE(AppendBitsToString(
148 share_mode, std::begin(kBitNames), std::end(kBitNames), result));
149 }
150
151 // Returns true if we expect that a file opened with |access| access rights
152 // and |share_mode| sharing can be moved via MoveFileEx, and can be deleted
153 // via DeleteFile so long as it is not mapped into a process.
CanMoveFile(DWORD access,DWORD share_mode)154 static bool CanMoveFile(DWORD access, DWORD share_mode) {
155 // A file can be moved as long as it is opened with FILE_SHARE_DELETE or
156 // if nothing beyond the standard access rights (save DELETE) has been
157 // requested. It can be deleted under those same circumstances as long as
158 // it has not been mapped into a process.
159 constexpr DWORD kStandardNoDelete = STANDARD_RIGHTS_ALL & ~DELETE;
160 return ((share_mode & FILE_SHARE_DELETE) != 0) ||
161 ((access & ~kStandardNoDelete) == 0);
162 }
163
164 // OsValidationTest:
SetUp()165 void SetUp() override {
166 OsValidationTest::SetUp();
167
168 // Determine the desired access and share mode for this test.
169 access_ = GetAccess();
170 share_mode_ = GetShareMode();
171
172 // Make a ScopedTrace instance for comprehensible output.
173 std::string access_string;
174 ASSERT_NO_FATAL_FAILURE(AppendAccessString(access_, &access_string));
175 std::string share_mode_string;
176 ASSERT_NO_FATAL_FAILURE(
177 AppendShareModeString(share_mode_, &share_mode_string));
178 scoped_trace_ = std::make_unique<::testing::ScopedTrace>(
179 __FILE__, __LINE__, access_string + ", " + share_mode_string);
180
181 // Make a copy of imm32.dll in the temp dir for fiddling.
182 ASSERT_TRUE(CreateTemporaryFileInDir(temp_path(), &temp_file_path_));
183 ASSERT_TRUE(CopyFile(FilePath(FPL("c:\\windows\\system32\\imm32.dll")),
184 temp_file_path_));
185
186 // Open the file
187 file_handle_.Set(::CreateFileW(temp_file_path_.value().c_str(), access_,
188 share_mode_, nullptr, OPEN_EXISTING,
189 FILE_ATTRIBUTE_NORMAL, nullptr));
190 ASSERT_TRUE(file_handle_.is_valid()) << ::GetLastError();
191
192 // Get a second unique name in the temp dir to which the file might be
193 // moved.
194 temp_file_dest_path_ = temp_file_path_.InsertBeforeExtension(FPL("bla"));
195 }
196
TearDown()197 void TearDown() override {
198 file_handle_.Close();
199
200 // Manually delete the temp files since the temp dir is reused across tests.
201 ASSERT_TRUE(DeleteFile(temp_file_path_));
202 ASSERT_TRUE(DeleteFile(temp_file_dest_path_));
203 }
204
access() const205 DWORD access() const { return access_; }
share_mode() const206 DWORD share_mode() const { return share_mode_; }
temp_file_path() const207 const FilePath& temp_file_path() const { return temp_file_path_; }
temp_file_dest_path() const208 const FilePath& temp_file_dest_path() const { return temp_file_dest_path_; }
file_handle() const209 HANDLE file_handle() const { return file_handle_.get(); }
210
211 private:
212 struct BitAndName {
213 DWORD bit;
214 StringPiece name;
215 };
216
217 // Appends the names of the bits present in |bitfield| to |result| based on
218 // the array of bit-to-name mappings bounded by |bits_begin| and |bits_end|.
AppendBitsToString(DWORD bitfield,const BitAndName * bits_begin,const BitAndName * bits_end,std::string * result)219 static void AppendBitsToString(DWORD bitfield,
220 const BitAndName* bits_begin,
221 const BitAndName* bits_end,
222 std::string* result) {
223 while (bits_begin < bits_end) {
224 const BitAndName& bit_name = *bits_begin;
225 if (bitfield & bit_name.bit) {
226 if (!result->empty())
227 result->append(" | ");
228 result->append(bit_name.name);
229 bitfield &= ~bit_name.bit;
230 }
231 ++bits_begin;
232 }
233 ASSERT_EQ(bitfield, DWORD{0});
234 }
235
236 DWORD access_ = 0;
237 DWORD share_mode_ = 0;
238 std::unique_ptr<::testing::ScopedTrace> scoped_trace_;
239 FilePath temp_file_path_;
240 FilePath temp_file_dest_path_;
241 win::ScopedHandle file_handle_;
242 };
243
244 // Tests that an opened but not mapped file can be deleted as expected.
TEST_P(OpenFileTest,DeleteFile)245 TEST_P(OpenFileTest, DeleteFile) {
246 if (CanMoveFile(access(), share_mode())) {
247 EXPECT_NE(::DeleteFileW(temp_file_path().value().c_str()), 0)
248 << "Last error code: " << ::GetLastError();
249 } else {
250 EXPECT_EQ(::DeleteFileW(temp_file_path().value().c_str()), 0);
251 }
252 }
253
254 // Tests that an opened file can be moved as expected.
TEST_P(OpenFileTest,MoveFileEx)255 TEST_P(OpenFileTest, MoveFileEx) {
256 if (CanMoveFile(access(), share_mode())) {
257 EXPECT_NE(::MoveFileExW(temp_file_path().value().c_str(),
258 temp_file_dest_path().value().c_str(), 0),
259 0)
260 << "Last error code: " << ::GetLastError();
261 } else {
262 EXPECT_EQ(::MoveFileExW(temp_file_path().value().c_str(),
263 temp_file_dest_path().value().c_str(), 0),
264 0);
265 }
266 }
267
268 // Tests that an open file cannot be moved after it has been marked for
269 // deletion.
TEST_P(OpenFileTest,DeleteThenMove)270 TEST_P(OpenFileTest, DeleteThenMove) {
271 // Don't test combinations that cannot be deleted.
272 if (!CanMoveFile(access(), share_mode()))
273 return;
274 ASSERT_NE(::DeleteFileW(temp_file_path().value().c_str()), 0)
275 << "Last error code: " << ::GetLastError();
276 // Move fails with ERROR_ACCESS_DENIED (STATUS_DELETE_PENDING under the
277 // covers).
278 EXPECT_EQ(::MoveFileExW(temp_file_path().value().c_str(),
279 temp_file_dest_path().value().c_str(), 0),
280 0);
281 }
282
283 // Tests that an open file that is mapped into memory can be moved but not
284 // deleted.
TEST_P(OpenFileTest,MapThenDelete)285 TEST_P(OpenFileTest, MapThenDelete) {
286 // There is nothing to test if the file can't be read.
287 if (!(access() & FILE_READ_DATA))
288 return;
289
290 // Pick the protection option that matches the access rights used to open the
291 // file.
292 static constexpr struct {
293 DWORD access_bits;
294 DWORD protection;
295 } kAccessToProtection[] = {
296 // Sorted from most- to least-bits used for logic below.
297 {FILE_READ_DATA | FILE_WRITE_DATA | FILE_EXECUTE, PAGE_EXECUTE_READWRITE},
298 {FILE_READ_DATA | FILE_WRITE_DATA, PAGE_READWRITE},
299 {FILE_READ_DATA | FILE_EXECUTE, PAGE_EXECUTE_READ},
300 {FILE_READ_DATA, PAGE_READONLY},
301 };
302
303 DWORD protection = 0;
304 for (const auto& scan : kAccessToProtection) {
305 if ((access() & scan.access_bits) == scan.access_bits) {
306 protection = scan.protection;
307 break;
308 }
309 }
310 ASSERT_NE(protection, DWORD{0});
311
312 win::ScopedHandle mapping(::CreateFileMappingA(
313 file_handle(), nullptr, protection | SEC_IMAGE, 0, 0, nullptr));
314 auto result = ::GetLastError();
315 ASSERT_TRUE(mapping.is_valid()) << result;
316
317 auto* view = ::MapViewOfFile(mapping.get(), FILE_MAP_READ, 0, 0, 0);
318 result = ::GetLastError();
319 ASSERT_NE(view, nullptr) << result;
320 ScopedClosureRunner unmapper(
321 BindOnce([](const void* view) { ::UnmapViewOfFile(view); }, view));
322
323 // Mapped files cannot be deleted under any circumstances.
324 EXPECT_EQ(::DeleteFileW(temp_file_path().value().c_str()), 0);
325
326 // But can still be moved under the same conditions as if it weren't mapped.
327 if (CanMoveFile(access(), share_mode())) {
328 EXPECT_NE(::MoveFileExW(temp_file_path().value().c_str(),
329 temp_file_dest_path().value().c_str(), 0),
330 0)
331 << "Last error code: " << ::GetLastError();
332 } else {
333 EXPECT_EQ(::MoveFileExW(temp_file_path().value().c_str(),
334 temp_file_dest_path().value().c_str(), 0),
335 0);
336 }
337 }
338
339 // These tests are intentionally disabled by default. They were created as an
340 // educational tool to understand the restrictions on moving and deleting files
341 // on Windows. There is every expectation that once they pass, they will always
342 // pass. It might be interesting to run them manually on new versions of the OS,
343 // but there is no need to run them on every try/CQ run. Here is one possible
344 // way to run them all locally:
345 //
346 // base_unittests.exe --single-process-tests --gtest_also_run_disabled_tests \
347 // --gtest_filter=*OpenFileTest*
348 INSTANTIATE_TEST_SUITE_P(
349 DISABLED_Test,
350 OpenFileTest,
351 ::testing::Combine(
352 // Standard access rights except for WRITE_OWNER, which requires admin.
353 ::testing::Combine(::testing::Values(0, SYNCHRONIZE),
354 ::testing::Values(0, WRITE_DAC),
355 ::testing::Values(0, READ_CONTROL),
356 ::testing::Values(0, DELETE)),
357 // Generic file access rights.
358 ::testing::Combine(::testing::Values(0, FILE_GENERIC_READ),
359 ::testing::Values(0, FILE_GENERIC_WRITE),
360 ::testing::Values(0, FILE_GENERIC_EXECUTE)),
361 // File sharing mode.
362 ::testing::Combine(::testing::Values(0, FILE_SHARE_READ),
363 ::testing::Values(0, FILE_SHARE_WRITE),
364 ::testing::Values(0, FILE_SHARE_DELETE))));
365
366 } // namespace base
367