• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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