Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -2038,14 +2039,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