Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
89 changes: 88 additions & 1 deletion Lib/test/test_fileutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import os
import os.path
import unittest
from test.support import import_helper
import tempfile
import shutil
from test.support import import_helper, os_helper

# Skip this test if the _testcapi module isn't available.
_testcapi = import_helper.import_module('_testinternalcapi')
Expand All @@ -26,5 +28,90 @@ def test_capi_normalize_path(self):
msg=f'input: {filename!r} expected output: {expected!r}')


class RealpathTests(unittest.TestCase):
"""Tests for _Py_wrealpath used by os.path.realpath."""

def setUp(self):
self.base = None

def tearDown(self):
if self.base and os.path.exists(self.base):
shutil.rmtree(self.base, ignore_errors=True)

def test_realpath_long_path(self):
"""Test that realpath handles paths longer than MAXPATHLEN."""
if os.name == 'nt':
self.skipTest('POSIX-specific test')

self.base = tempfile.mkdtemp()
current_path = self.base

for i in range(25):
dirname = f"d{i:03d}_" + "x" * 195
current_path = os.path.join(current_path, dirname)
try:
os.mkdir(current_path)
except OSError as e:
self.skipTest(f"Cannot create long paths on this platform: {e}")

full_path = os.path.abspath(current_path)
if len(full_path) <= 4096:
self.skipTest(f"Path not long enough ({len(full_path)} bytes)")

result = os.path.realpath(full_path)
self.assertTrue(os.path.isabs(result))
self.assertGreater(len(result), 4096)

def test_realpath_nonexistent_with_strict(self):
"""Test that realpath with strict=True raises for nonexistent paths."""
if os.name == 'nt':
self.skipTest('POSIX-specific test')

self.base = tempfile.mkdtemp()
nonexistent = os.path.join(self.base, "does_not_exist", "subdir")

result = os.path.realpath(nonexistent, strict=False)
self.assertIsNotNone(result)

with self.assertRaises(OSError):
os.path.realpath(nonexistent, strict=True)

@os_helper.skip_unless_symlink
def test_realpath_symlink_long_path(self):
"""Test realpath with symlinks in long paths."""
if os.name == 'nt':
self.skipTest('POSIX-specific test')

self.base = tempfile.mkdtemp()
current_path = self.base

for i in range(15):
dirname = f"d{i:03d}_" + "x" * 195
current_path = os.path.join(current_path, dirname)
try:
os.mkdir(current_path)
except OSError as e:
self.skipTest(f"Cannot create long paths on this platform: {e}")

symlink = os.path.join(self.base, "link")
try:
os.symlink(current_path, symlink)
except (OSError, NotImplementedError) as e:
self.skipTest(f"Cannot create symlinks on this platform: {e}")

result = os.path.realpath(symlink)

# On some platforms (like Android), realpath may return a different
# canonical path due to filesystem mounts/symlinks.
# Use samefile() to verify the symlink was resolved correctly.
try:
self.assertTrue(os.path.samefile(result, current_path))
except (OSError, NotImplementedError):
# If samefile() is not available, just check path length
self.assertGreater(len(result), 1500)

self.assertGreater(len(result), 1500)


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix buffer overflow in ``_Py_wrealpath()`` for paths exceeding 4096 bytes
by using dynamic memory allocation instead of fixed-size buffer.
Patch by Shamil Abdulaev.
7 changes: 4 additions & 3 deletions Python/fileutils.c
Original file line number Diff line number Diff line change
Expand Up @@ -2118,7 +2118,6 @@ _Py_wrealpath(const wchar_t *path,
wchar_t *resolved_path, size_t resolved_path_len)
{
char *cpath;
char cresolved_path[MAXPATHLEN];
wchar_t *wresolved_path;
char *res;
size_t r;
Expand All @@ -2127,12 +2126,14 @@ _Py_wrealpath(const wchar_t *path,
errno = EINVAL;
return NULL;
}
res = realpath(cpath, cresolved_path);
res = realpath(cpath, NULL);
PyMem_RawFree(cpath);
if (res == NULL)
return NULL;

wresolved_path = Py_DecodeLocale(cresolved_path, &r);
wresolved_path = Py_DecodeLocale(res, &r);
free(res);

if (wresolved_path == NULL) {
errno = EINVAL;
return NULL;
Expand Down
Loading