From 3255a3dc6c677cf8333a69c82486731188a68f2a Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Thu, 13 Nov 2025 22:32:27 +0300 Subject: [PATCH 1/5] gh-42400: Fix buffer overflow in _Py_wrealpath for long paths --- Lib/test/test_fileutils.py | 85 +++++++++++++++++++ ...5-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst | 3 + Python/fileutils.c | 7 +- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst diff --git a/Lib/test/test_fileutils.py b/Lib/test/test_fileutils.py index ff13498fbfeb5c..b10c1e1b5acf94 100644 --- a/Lib/test/test_fileutils.py +++ b/Lib/test/test_fileutils.py @@ -3,6 +3,8 @@ import os import os.path import unittest +import tempfile +import shutil from test.support import import_helper # Skip this test if the _testcapi module isn't available. @@ -26,5 +28,88 @@ 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 test_realpath_long_path(self): + """Test that realpath handles paths longer than MAXPATHLEN (4096)""" + if os.name == 'nt': + raise unittest.SkipTest('POSIX-specific test') + + base = tempfile.mkdtemp() + original_cwd = os.getcwd() + try: + os.chdir(base) + + for i in range(85): + dirname = f"d{i:03d}_" + "x" * 44 + os.mkdir(dirname) + os.chdir(dirname) + + full_path = os.getcwd() + + self.assertGreater(len(full_path), 4096, + f"Path should exceed MAXPATHLEN, got {len(full_path)}") + + # Main test: realpath should not crash on long paths + result = os.path.realpath(full_path) + + self.assertTrue(os.path.isabs(result)) + self.assertGreater(len(result), 4096) + # Note: os.path.exists() may fail on very long paths + # The important thing is realpath() doesn't crash + + finally: + os.chdir(original_cwd) + shutil.rmtree(base, ignore_errors=True) + + def test_realpath_nonexistent_with_strict(self): + """Test that realpath with strict=True raises for nonexistent paths""" + if os.name == 'nt': + raise unittest.SkipTest('POSIX-specific test') + + base = tempfile.mkdtemp() + try: + nonexistent = os.path.join(base, "does_not_exist", "subdir") + + # Without strict, should return the path + result = os.path.realpath(nonexistent, strict=False) + self.assertIsNotNone(result) + + # With strict=True, should raise an error + with self.assertRaises(OSError): + os.path.realpath(nonexistent, strict=True) + + finally: + shutil.rmtree(base, ignore_errors=True) + + def test_realpath_symlink_long_path(self): + """Test realpath with symlinks in long paths""" + if os.name == 'nt': + raise unittest.SkipTest('POSIX-specific test') + + base = tempfile.mkdtemp() + try: + # Create a long path + current = base + for i in range(30): + dirname = f"d{i:03d}_" + "x" * 44 + current = os.path.join(current, dirname) + os.mkdir(current) + + # Create a symlink pointing to the long path + symlink = os.path.join(base, "link") + os.symlink(current, symlink) + + # Resolve the symlink + result = os.path.realpath(symlink) + + self.assertEqual(os.path.normpath(result), os.path.normpath(current)) + self.assertGreater(len(result), 1500) + + finally: + shutil.rmtree(base, ignore_errors=True) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst b/Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst new file mode 100644 index 00000000000000..156fced80bea91 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst @@ -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. diff --git a/Python/fileutils.c b/Python/fileutils.c index 93abd70a34d420..0c1766b8804500 100644 --- a/Python/fileutils.c +++ b/Python/fileutils.c @@ -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; @@ -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; From a12ee2dd0a61867ccce732f45a30d01360f9c0c2 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Thu, 13 Nov 2025 23:45:18 +0300 Subject: [PATCH 2/5] fix --- Lib/test/test_fileutils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/test_fileutils.py b/Lib/test/test_fileutils.py index 7c8e74a42436f0..efc8d8ab2d59af 100644 --- a/Lib/test/test_fileutils.py +++ b/Lib/test/test_fileutils.py @@ -1,3 +1,5 @@ +# Run tests for functions in Python/fileutils.c. + import os import os.path import unittest From 238ff0ec70c8ff338a4101e4ffe983175b22b1f1 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Fri, 14 Nov 2025 00:13:04 +0300 Subject: [PATCH 3/5] Use os.samefile in realpath symlink test Allow platforms where realpath returns different canonical paths (e.g. Android). Verify resolution with os.samefile and fall back to a length check if samefile is unavailable. --- Lib/test/test_fileutils.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_fileutils.py b/Lib/test/test_fileutils.py index efc8d8ab2d59af..fbaace9958a7ba 100644 --- a/Lib/test/test_fileutils.py +++ b/Lib/test/test_fileutils.py @@ -100,7 +100,16 @@ def test_realpath_symlink_long_path(self): self.skipTest(f"Cannot create symlinks on this platform: {e}") result = os.path.realpath(symlink) - self.assertEqual(os.path.normpath(result), os.path.normpath(current_path)) + + # 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) From 88368ec684fb2cee8e6bdfb5fe4ede3de9c115de Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Fri, 14 Nov 2025 14:05:48 +0300 Subject: [PATCH 4/5] remove tests --- Lib/test/test_fileutils.py | 89 +------------------------------------- 1 file changed, 1 insertion(+), 88 deletions(-) diff --git a/Lib/test/test_fileutils.py b/Lib/test/test_fileutils.py index fbaace9958a7ba..ff13498fbfeb5c 100644 --- a/Lib/test/test_fileutils.py +++ b/Lib/test/test_fileutils.py @@ -3,9 +3,7 @@ import os import os.path import unittest -import tempfile -import shutil -from test.support import import_helper, os_helper +from test.support import import_helper # Skip this test if the _testcapi module isn't available. _testcapi = import_helper.import_module('_testinternalcapi') @@ -28,90 +26,5 @@ 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() From 1cf6fde52ec60ca62cc168f73ce485a7a9561915 Mon Sep 17 00:00:00 2001 From: Shamil Date: Fri, 14 Nov 2025 14:06:04 +0300 Subject: [PATCH 5/5] Update Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst Co-authored-by: Victor Stinner --- .../next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst b/Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst index 156fced80bea91..17dc241aef91d6 100644 --- a/Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst +++ b/Misc/NEWS.d/next/Security/2025-11-13-22-31-56.gh-issue-42400.pqB5Kq.rst @@ -1,3 +1,3 @@ -Fix buffer overflow in ``_Py_wrealpath()`` for paths exceeding 4096 bytes +Fix buffer overflow in ``_Py_wrealpath()`` for paths exceeding ``MAXPATHLEN`` bytes by using dynamic memory allocation instead of fixed-size buffer. Patch by Shamil Abdulaev.