Skip to content

Commit

Permalink
gh-111877: Fixes stat() handling for inaccessible files on Windows (G…
Browse files Browse the repository at this point in the history
…H-113716)

(cherry picked from commit ed06648)

Co-authored-by: Steve Dower <steve.dower@python.org>
  • Loading branch information
miss-islington and zooba committed Jan 12, 2024
1 parent eb22afb commit 7b7cf75
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 6 deletions.
60 changes: 60 additions & 0 deletions Lib/test/test_os.py
Expand Up @@ -3077,6 +3077,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):
Expand Down
@@ -0,0 +1,2 @@
:func:`os.stat` calls were returning incorrect time values for files that
could not be accessed directly.
16 changes: 10 additions & 6 deletions Modules/posixmodule.c
Expand Up @@ -1864,8 +1864,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;
Expand Down Expand Up @@ -2016,14 +2017,17 @@ 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;
/* Successfully got FileBasicInfo, so we'll pass it along */
pBasicInfo = &basicInfo;

if (GetFileInformationByHandleEx(hFile, FileIdInfo, &idInfo, sizeof(idInfo))) {
/* Successfully got FileIdInfo, so pass it along */
pIdInfo = &idInfo;
}
}

_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:
Expand Down

0 comments on commit 7b7cf75

Please sign in to comment.