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