From fe4c1bff153fea59c0331bf2df188afe2bb308d0 Mon Sep 17 00:00:00 2001 From: Steve Dower Date: Thu, 4 Jan 2024 21:16:57 +0000 Subject: [PATCH] gh-111877: Fixes stat() handling for inaccessible files on Windows --- Lib/test/test_os.py | 60 +++++++++++++++++++ ...-01-04-21-16-31.gh-issue-111877.fR-B4c.rst | 2 + Modules/posixmodule.c | 17 ++++-- 3 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2024-01-04-21-16-31.gh-issue-111877.fR-B4c.rst diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index c66c5797471413..ee370e7a35287b 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -3085,6 +3085,66 @@ def test_stat_unlink_race(self): except subprocess.TimeoutExpired: proc.terminate() + @support.requires_subprocess() + def test_stat_inaccessible_file(self): + filename = os_helper.TESTFN + ICACLS = os.path.expandvars(r"%SystemRoot%\System32\icacls.exe") + + with open(filename, "wb") as f: + f.write(b'Test data') + + stat1 = os.stat(filename) + + try: + # Remove all permissions from the file + subprocess.check_output([ICACLS, filename, "/inheritance:r"], + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as ex: + if support.verbose: + print(ICACLS, filename, "/inheritance:r", "failed.") + print(ex.stdout.decode("oem", "replace").rstrip()) + try: + os.unlink(filename) + except OSError: + pass + self.skipTest("Unable to create inaccessible file") + + def cleanup(): + # Give delete permission. We are the file owner, so we can do this + # even though we removed all permissions earlier. + subprocess.check_output([ICACLS, filename, "/grant", "Everyone:(D)"], + stderr=subprocess.STDOUT) + os.unlink(filename) + + self.addCleanup(cleanup) + + if support.verbose: + print("File:", filename) + print("stat with access:", stat1) + + # First test - we shouldn't raise here, because we still have access to + # the directory and can extract enough information from its metadata. + stat2 = os.stat(filename) + + if support.verbose: + print(" without access:", stat2) + + # We cannot get st_dev/st_ino, so ensure those are 0 or else our test + # is not set up correctly + self.assertEqual(0, stat2.st_dev) + self.assertEqual(0, stat2.st_ino) + + # st_mode and st_size should match (for a normal file, at least) + self.assertEqual(stat1.st_mode, stat2.st_mode) + self.assertEqual(stat1.st_size, stat2.st_size) + + # st_ctime and st_mtime should be the same + self.assertEqual(stat1.st_ctime, stat2.st_ctime) + self.assertEqual(stat1.st_mtime, stat2.st_mtime) + + # st_atime should be the same or later + self.assertGreaterEqual(stat1.st_atime, stat2.st_atime) + @os_helper.skip_unless_symlink class NonLocalSymlinkTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Windows/2024-01-04-21-16-31.gh-issue-111877.fR-B4c.rst b/Misc/NEWS.d/next/Windows/2024-01-04-21-16-31.gh-issue-111877.fR-B4c.rst new file mode 100644 index 00000000000000..99ed8d34af7cc2 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2024-01-04-21-16-31.gh-issue-111877.fR-B4c.rst @@ -0,0 +1,2 @@ +:func:`os.stat` calls were returning incorrect time values for files that +could not be accessed directly. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 39b1f3cb7b2b9b..eb4e4794afd374 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -1886,8 +1886,9 @@ win32_xstat_slow_impl(const wchar_t *path, struct _Py_stat_struct *result, HANDLE hFile; BY_HANDLE_FILE_INFORMATION fileInfo; FILE_BASIC_INFO basicInfo; + FILE_BASIC_INFO *pBasicInfo = NULL; FILE_ID_INFO idInfo; - FILE_ID_INFO *pIdInfo = &idInfo; + FILE_ID_INFO *pIdInfo = NULL; FILE_ATTRIBUTE_TAG_INFO tagInfo = { 0 }; DWORD fileType, error; BOOL isUnhandledTag = FALSE; @@ -2038,14 +2039,18 @@ win32_xstat_slow_impl(const wchar_t *path, struct _Py_stat_struct *result, retval = -1; goto cleanup; } - } - if (!GetFileInformationByHandleEx(hFile, FileIdInfo, &idInfo, sizeof(idInfo))) { - /* Failed to get FileIdInfo, so do not pass it along */ - pIdInfo = NULL; + pBasicInfo = &basicInfo; + pIdInfo = &idInfo; + + if (!GetFileInformationByHandleEx(hFile, FileIdInfo, &idInfo, sizeof(idInfo))) { + /* Failed to get FileIdInfo, so do not pass it along */ + pIdInfo = NULL; + } } - _Py_attribute_data_to_stat(&fileInfo, tagInfo.ReparseTag, &basicInfo, pIdInfo, result); + _Py_attribute_data_to_stat(&fileInfo, tagInfo.ReparseTag, + pBasicInfo, pIdInfo, result); update_st_mode_from_path(path, fileInfo.dwFileAttributes, result); cleanup: