[3.11] gh-116401: Fix blocking os.fwalk() and shutil.rmtree() on opening a named pipe (GH-116421) (GH-116717)

(cherry picked from commit aa7bcf284f)
This commit is contained in:
Serhiy Storchaka 2024-03-13 12:03:17 +02:00 committed by GitHub
parent 5bd350709d
commit 4a73637966
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 113 additions and 8 deletions

View file

@ -467,7 +467,7 @@ if {open, stat} <= supports_dir_fd and {scandir, stat} <= supports_fd:
# lstat()/open()/fstat() trick. # lstat()/open()/fstat() trick.
if not follow_symlinks: if not follow_symlinks:
orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd) orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd)
topfd = open(top, O_RDONLY, dir_fd=dir_fd) topfd = open(top, O_RDONLY | O_NONBLOCK, dir_fd=dir_fd)
try: try:
if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and
path.samestat(orig_st, stat(topfd)))): path.samestat(orig_st, stat(topfd)))):
@ -516,7 +516,7 @@ if {open, stat} <= supports_dir_fd and {scandir, stat} <= supports_fd:
assert entries is not None assert entries is not None
name, entry = name name, entry = name
orig_st = entry.stat(follow_symlinks=False) orig_st = entry.stat(follow_symlinks=False)
dirfd = open(name, O_RDONLY, dir_fd=topfd) dirfd = open(name, O_RDONLY | O_NONBLOCK, dir_fd=topfd)
except OSError as err: except OSError as err:
if onerror is not None: if onerror is not None:
onerror(err) onerror(err)

View file

@ -662,7 +662,7 @@ def _rmtree_safe_fd(topfd, path, onerror):
continue continue
if is_dir: if is_dir:
try: try:
dirfd = os.open(entry.name, os.O_RDONLY, dir_fd=topfd) dirfd = os.open(entry.name, os.O_RDONLY | os.O_NONBLOCK, dir_fd=topfd)
dirfd_closed = False dirfd_closed = False
except OSError: except OSError:
onerror(os.open, fullname, sys.exc_info()) onerror(os.open, fullname, sys.exc_info())
@ -742,7 +742,7 @@ def rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None):
onerror(os.lstat, path, sys.exc_info()) onerror(os.lstat, path, sys.exc_info())
return return
try: try:
fd = os.open(path, os.O_RDONLY, dir_fd=dir_fd) fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK, dir_fd=dir_fd)
fd_closed = False fd_closed = False
except Exception: except Exception:
onerror(os.open, path, sys.exc_info()) onerror(os.open, path, sys.exc_info())

View file

@ -343,6 +343,18 @@ class GlobTests(unittest.TestCase):
eq(self.rglob('nonexistent', '*'), []) eq(self.rglob('nonexistent', '*'), [])
eq(self.rglob('nonexistent', '**'), []) eq(self.rglob('nonexistent', '**'), [])
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
@unittest.skipIf(sys.platform == "vxworks",
"fifo requires special path on VxWorks")
def test_glob_named_pipe(self):
path = os.path.join(self.tempdir, 'mypipe')
os.mkfifo(path)
self.assertEqual(self.rglob('mypipe'), [path])
self.assertEqual(self.rglob('mypipe*'), [path])
self.assertEqual(self.rglob('mypipe', ''), [])
self.assertEqual(self.rglob('mypipe', 'sub'), [])
self.assertEqual(self.rglob('mypipe', '*'), [])
def test_glob_many_open_files(self): def test_glob_many_open_files(self):
depth = 30 depth = 30
base = os.path.join(self.tempdir, 'deep') base = os.path.join(self.tempdir, 'deep')

View file

@ -1278,6 +1278,7 @@ class EnvironTests(mapping_tests.BasicTestMappingProtocol):
class WalkTests(unittest.TestCase): class WalkTests(unittest.TestCase):
"""Tests for os.walk().""" """Tests for os.walk()."""
is_fwalk = False
# Wrapper to hide minor differences between os.walk and os.fwalk # Wrapper to hide minor differences between os.walk and os.fwalk
# to tests both functions with the same code base # to tests both functions with the same code base
@ -1312,14 +1313,14 @@ class WalkTests(unittest.TestCase):
self.sub11_path = join(self.sub1_path, "SUB11") self.sub11_path = join(self.sub1_path, "SUB11")
sub2_path = join(self.walk_path, "SUB2") sub2_path = join(self.walk_path, "SUB2")
sub21_path = join(sub2_path, "SUB21") sub21_path = join(sub2_path, "SUB21")
tmp1_path = join(self.walk_path, "tmp1") self.tmp1_path = join(self.walk_path, "tmp1")
tmp2_path = join(self.sub1_path, "tmp2") tmp2_path = join(self.sub1_path, "tmp2")
tmp3_path = join(sub2_path, "tmp3") tmp3_path = join(sub2_path, "tmp3")
tmp5_path = join(sub21_path, "tmp3") tmp5_path = join(sub21_path, "tmp3")
self.link_path = join(sub2_path, "link") self.link_path = join(sub2_path, "link")
t2_path = join(os_helper.TESTFN, "TEST2") t2_path = join(os_helper.TESTFN, "TEST2")
tmp4_path = join(os_helper.TESTFN, "TEST2", "tmp4") tmp4_path = join(os_helper.TESTFN, "TEST2", "tmp4")
broken_link_path = join(sub2_path, "broken_link") self.broken_link_path = join(sub2_path, "broken_link")
broken_link2_path = join(sub2_path, "broken_link2") broken_link2_path = join(sub2_path, "broken_link2")
broken_link3_path = join(sub2_path, "broken_link3") broken_link3_path = join(sub2_path, "broken_link3")
@ -1329,13 +1330,13 @@ class WalkTests(unittest.TestCase):
os.makedirs(sub21_path) os.makedirs(sub21_path)
os.makedirs(t2_path) os.makedirs(t2_path)
for path in tmp1_path, tmp2_path, tmp3_path, tmp4_path, tmp5_path: for path in self.tmp1_path, tmp2_path, tmp3_path, tmp4_path, tmp5_path:
with open(path, "x", encoding='utf-8') as f: with open(path, "x", encoding='utf-8') as f:
f.write("I'm " + path + " and proud of it. Blame test_os.\n") f.write("I'm " + path + " and proud of it. Blame test_os.\n")
if os_helper.can_symlink(): if os_helper.can_symlink():
os.symlink(os.path.abspath(t2_path), self.link_path) os.symlink(os.path.abspath(t2_path), self.link_path)
os.symlink('broken', broken_link_path, True) os.symlink('broken', self.broken_link_path, True)
os.symlink(join('tmp3', 'broken'), broken_link2_path, True) os.symlink(join('tmp3', 'broken'), broken_link2_path, True)
os.symlink(join('SUB21', 'tmp5'), broken_link3_path, True) os.symlink(join('SUB21', 'tmp5'), broken_link3_path, True)
self.sub2_tree = (sub2_path, ["SUB21", "link"], self.sub2_tree = (sub2_path, ["SUB21", "link"],
@ -1431,6 +1432,11 @@ class WalkTests(unittest.TestCase):
else: else:
self.fail("Didn't follow symlink with followlinks=True") self.fail("Didn't follow symlink with followlinks=True")
walk_it = self.walk(self.broken_link_path, follow_symlinks=True)
if self.is_fwalk:
self.assertRaises(FileNotFoundError, next, walk_it)
self.assertRaises(StopIteration, next, walk_it)
def test_walk_bad_dir(self): def test_walk_bad_dir(self):
# Walk top-down. # Walk top-down.
errors = [] errors = []
@ -1452,6 +1458,73 @@ class WalkTests(unittest.TestCase):
finally: finally:
os.rename(path1new, path1) os.rename(path1new, path1)
def test_walk_bad_dir2(self):
walk_it = self.walk('nonexisting')
if self.is_fwalk:
self.assertRaises(FileNotFoundError, next, walk_it)
self.assertRaises(StopIteration, next, walk_it)
walk_it = self.walk('nonexisting', follow_symlinks=True)
if self.is_fwalk:
self.assertRaises(FileNotFoundError, next, walk_it)
self.assertRaises(StopIteration, next, walk_it)
walk_it = self.walk(self.tmp1_path)
self.assertRaises(StopIteration, next, walk_it)
walk_it = self.walk(self.tmp1_path, follow_symlinks=True)
if self.is_fwalk:
self.assertRaises(NotADirectoryError, next, walk_it)
self.assertRaises(StopIteration, next, walk_it)
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
@unittest.skipIf(sys.platform == "vxworks",
"fifo requires special path on VxWorks")
def test_walk_named_pipe(self):
path = os_helper.TESTFN + '-pipe'
os.mkfifo(path)
self.addCleanup(os.unlink, path)
walk_it = self.walk(path)
self.assertRaises(StopIteration, next, walk_it)
walk_it = self.walk(path, follow_symlinks=True)
if self.is_fwalk:
self.assertRaises(NotADirectoryError, next, walk_it)
self.assertRaises(StopIteration, next, walk_it)
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
@unittest.skipIf(sys.platform == "vxworks",
"fifo requires special path on VxWorks")
def test_walk_named_pipe2(self):
path = os_helper.TESTFN + '-dir'
os.mkdir(path)
self.addCleanup(shutil.rmtree, path)
os.mkfifo(os.path.join(path, 'mypipe'))
errors = []
walk_it = self.walk(path, onerror=errors.append)
next(walk_it)
self.assertRaises(StopIteration, next, walk_it)
self.assertEqual(errors, [])
errors = []
walk_it = self.walk(path, onerror=errors.append)
root, dirs, files = next(walk_it)
self.assertEqual(root, path)
self.assertEqual(dirs, [])
self.assertEqual(files, ['mypipe'])
dirs.extend(files)
files.clear()
if self.is_fwalk:
self.assertRaises(NotADirectoryError, next, walk_it)
self.assertRaises(StopIteration, next, walk_it)
if self.is_fwalk:
self.assertEqual(errors, [])
else:
self.assertEqual(len(errors), 1, errors)
self.assertIsInstance(errors[0], NotADirectoryError)
def test_walk_many_open_files(self): def test_walk_many_open_files(self):
depth = 30 depth = 30
base = os.path.join(os_helper.TESTFN, 'deep') base = os.path.join(os_helper.TESTFN, 'deep')
@ -1477,6 +1550,7 @@ class WalkTests(unittest.TestCase):
@unittest.skipUnless(hasattr(os, 'fwalk'), "Test needs os.fwalk()") @unittest.skipUnless(hasattr(os, 'fwalk'), "Test needs os.fwalk()")
class FwalkTests(WalkTests): class FwalkTests(WalkTests):
"""Tests for os.fwalk().""" """Tests for os.fwalk()."""
is_fwalk = True
def walk(self, top, **kwargs): def walk(self, top, **kwargs):
for root, dirs, files, root_fd in self.fwalk(top, **kwargs): for root, dirs, files, root_fd in self.fwalk(top, **kwargs):

View file

@ -500,6 +500,23 @@ class TestRmTree(BaseTest, unittest.TestCase):
finally: finally:
shutil.rmtree(TESTFN, ignore_errors=True) shutil.rmtree(TESTFN, ignore_errors=True)
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
@unittest.skipIf(sys.platform == "vxworks",
"fifo requires special path on VxWorks")
def test_rmtree_on_named_pipe(self):
os.mkfifo(TESTFN)
try:
with self.assertRaises(NotADirectoryError):
shutil.rmtree(TESTFN)
self.assertTrue(os.path.exists(TESTFN))
finally:
os.unlink(TESTFN)
os.mkdir(TESTFN)
os.mkfifo(os.path.join(TESTFN, 'mypipe'))
shutil.rmtree(TESTFN)
self.assertFalse(os.path.exists(TESTFN))
class TestCopyTree(BaseTest, unittest.TestCase): class TestCopyTree(BaseTest, unittest.TestCase):

View file

@ -0,0 +1,2 @@
Fix blocking :func:`os.fwalk` and :func:`shutil.rmtree` on opening named
pipe.