xref: /aosp_15_r20/external/cronet/base/files/os_validation_win_unittest.cc (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
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