From 771e32367554bd257f28796e596471972c166279 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Wed, 7 Oct 2026 18:37:29 -0500 Subject: [PATCH 1/4] gh-158998: Fix os.statx() ignoring follow_symlinks=False --- Modules/posixmodule.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 028270e7b841fda..7879599491faee0 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -3669,6 +3669,9 @@ os_statx_impl(PyObject *module, path_t *path, unsigned int mask, int flags, "use dir_fd kwarg instead of AT_EMPTY_PATH flag"); return NULL; } + if (!follow_symlinks) { + flags |= AT_SYMLINK_NOFOLLOW; + } /* Future bits may refer to members beyond the current size of struct statx, so we need to mask them off to prevent memory corruption. */ From 64bccc9e7872d6b357aa99f986df094c2969d433 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Thu, 8 Oct 2026 15:58:15 -0500 Subject: [PATCH 2/4] test statx follow_symlinks behavior --- Lib/test/test_os/test_os.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 81b3043eb7e75bc..3473677ff1de75f 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -752,13 +752,13 @@ def test_stat_result_pickle(self): unpickled = pickle.loads(p) self.assertEqual(result, unpickled) - def check_statx_attributes(self, filename): + def check_statx_attributes(self, filename, follow_symlinks=True): maximal_mask = 0 for name in dir(os): if name.startswith('STATX_'): maximal_mask |= getattr(os, name) - result = os.statx(filename, maximal_mask) - stat_result = os.stat(filename) + result = os.statx(filename, maximal_mask, follow_symlinks=follow_symlinks) + stat_result = os.stat(filename, follow_symlinks=follow_symlinks) time_attributes = ('stx_atime', 'stx_btime', 'stx_ctime', 'stx_mtime') # gh-83714: stx_btime can be None on tmpfs even if STATX_BTIME mask @@ -873,6 +873,28 @@ def getmask(name): def test_statx_attributes(self): self.check_statx_attributes(self.fname) + @unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()') + @os_helper.skip_unless_symlink + def test_statx_attributes_symlink(self): + link = self.fname + '-link' + os.symlink(os.path.basename(self.fname), link) + self.addCleanup(os_helper.unlink, link) + + with self.subTest(broken=False, follow_symlinks=True): + self.check_statx_attributes(link, follow_symlinks=True) + + with self.subTest(broken=False, follow_symlinks=False): + self.check_statx_attributes(link, follow_symlinks=False) + + # break the symlink + os.unlink(self.fname) + with self.subTest(broken=True, follow_symlinks=True): + with self.assertRaises(FileNotFoundError): + self.check_statx_attributes(link, follow_symlinks=True) + + with self.subTest(broken=True, follow_symlinks=False): + self.check_statx_attributes(link, follow_symlinks=False) + @unittest.skipUnless(hasattr(os, 'statx'), 'test needs os.statx()') def test_statx_attributes_bytes(self): try: From afe07b8c53fc969a71a4c0b467105b320e42274a Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 21:02:41 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-10-08-21-02-38.gh-issue-158998.DZYJI7.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-08-21-02-38.gh-issue-158998.DZYJI7.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-08-21-02-38.gh-issue-158998.DZYJI7.rst b/Misc/NEWS.d/next/Library/2026-10-08-21-02-38.gh-issue-158998.DZYJI7.rst new file mode 100644 index 000000000000000..65ce9201fb01352 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-08-21-02-38.gh-issue-158998.DZYJI7.rst @@ -0,0 +1 @@ +Fix :func:`os.statx` ignoring the *follow_symlinks* argument. From c3288644978d02b0abe4f5b0bb21e957b4c6c384 Mon Sep 17 00:00:00 2001 From: "Jonathan J. Helmus" Date: Thu, 8 Oct 2026 17:56:31 -0500 Subject: [PATCH 4/4] directly call statx for a broken symlink --- Lib/test/test_os/test_os.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 3473677ff1de75f..79aa9e96dc91a6b 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -890,7 +890,7 @@ def test_statx_attributes_symlink(self): os.unlink(self.fname) with self.subTest(broken=True, follow_symlinks=True): with self.assertRaises(FileNotFoundError): - self.check_statx_attributes(link, follow_symlinks=True) + os.statx(link, os.STATX_BASIC_STATS, follow_symlinks=True) with self.subTest(broken=True, follow_symlinks=False): self.check_statx_attributes(link, follow_symlinks=False)