Skip to content

Commit aa7bcf2

Browse files
gh-116401: Fix blocking os.fwalk() and shutil.rmtree() on opening a named pipe (GH-116421)
1 parent 8332e85 commit aa7bcf2

File tree

6 files changed

+113
-8
lines changed

6 files changed

+113
-8
lines changed

Lib/os.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def fwalk(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=
475475
# lstat()/open()/fstat() trick.
476476
if not follow_symlinks:
477477
orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd)
478-
topfd = open(top, O_RDONLY, dir_fd=dir_fd)
478+
topfd = open(top, O_RDONLY | O_NONBLOCK, dir_fd=dir_fd)
479479
try:
480480
if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and
481481
path.samestat(orig_st, stat(topfd)))):
@@ -524,7 +524,7 @@ def _fwalk(topfd, toppath, isbytes, topdown, onerror, follow_symlinks):
524524
assert entries is not None
525525
name, entry = name
526526
orig_st = entry.stat(follow_symlinks=False)
527-
dirfd = open(name, O_RDONLY, dir_fd=topfd)
527+
dirfd = open(name, O_RDONLY | O_NONBLOCK, dir_fd=topfd)
528528
except OSError as err:
529529
if onerror is not None:
530530
onerror(err)

Lib/shutil.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ def _rmtree_safe_fd(topfd, path, onexc):
681681
continue
682682
if is_dir:
683683
try:
684-
dirfd = os.open(entry.name, os.O_RDONLY, dir_fd=topfd)
684+
dirfd = os.open(entry.name, os.O_RDONLY | os.O_NONBLOCK, dir_fd=topfd)
685685
dirfd_closed = False
686686
except FileNotFoundError:
687687
continue
@@ -786,7 +786,7 @@ def onexc(*args):
786786
onexc(os.lstat, path, err)
787787
return
788788
try:
789-
fd = os.open(path, os.O_RDONLY, dir_fd=dir_fd)
789+
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK, dir_fd=dir_fd)
790790
fd_closed = False
791791
except OSError as err:
792792
onexc(os.open, path, err)

Lib/test/test_glob.py

+12
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,18 @@ def test_glob_non_directory(self):
344344
eq(self.rglob('nonexistent', '*'), [])
345345
eq(self.rglob('nonexistent', '**'), [])
346346

347+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
348+
@unittest.skipIf(sys.platform == "vxworks",
349+
"fifo requires special path on VxWorks")
350+
def test_glob_named_pipe(self):
351+
path = os.path.join(self.tempdir, 'mypipe')
352+
os.mkfifo(path)
353+
self.assertEqual(self.rglob('mypipe'), [path])
354+
self.assertEqual(self.rglob('mypipe*'), [path])
355+
self.assertEqual(self.rglob('mypipe', ''), [])
356+
self.assertEqual(self.rglob('mypipe', 'sub'), [])
357+
self.assertEqual(self.rglob('mypipe', '*'), [])
358+
347359
def test_glob_many_open_files(self):
348360
depth = 30
349361
base = os.path.join(self.tempdir, 'deep')

Lib/test/test_os.py

+78-4
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,7 @@ def test_ror_operator(self):
12981298

12991299
class WalkTests(unittest.TestCase):
13001300
"""Tests for os.walk()."""
1301+
is_fwalk = False
13011302

13021303
# Wrapper to hide minor differences between os.walk and os.fwalk
13031304
# to tests both functions with the same code base
@@ -1332,14 +1333,14 @@ def setUp(self):
13321333
self.sub11_path = join(self.sub1_path, "SUB11")
13331334
sub2_path = join(self.walk_path, "SUB2")
13341335
sub21_path = join(sub2_path, "SUB21")
1335-
tmp1_path = join(self.walk_path, "tmp1")
1336+
self.tmp1_path = join(self.walk_path, "tmp1")
13361337
tmp2_path = join(self.sub1_path, "tmp2")
13371338
tmp3_path = join(sub2_path, "tmp3")
13381339
tmp5_path = join(sub21_path, "tmp3")
13391340
self.link_path = join(sub2_path, "link")
13401341
t2_path = join(os_helper.TESTFN, "TEST2")
13411342
tmp4_path = join(os_helper.TESTFN, "TEST2", "tmp4")
1342-
broken_link_path = join(sub2_path, "broken_link")
1343+
self.broken_link_path = join(sub2_path, "broken_link")
13431344
broken_link2_path = join(sub2_path, "broken_link2")
13441345
broken_link3_path = join(sub2_path, "broken_link3")
13451346

@@ -1349,13 +1350,13 @@ def setUp(self):
13491350
os.makedirs(sub21_path)
13501351
os.makedirs(t2_path)
13511352

1352-
for path in tmp1_path, tmp2_path, tmp3_path, tmp4_path, tmp5_path:
1353+
for path in self.tmp1_path, tmp2_path, tmp3_path, tmp4_path, tmp5_path:
13531354
with open(path, "x", encoding='utf-8') as f:
13541355
f.write("I'm " + path + " and proud of it. Blame test_os.\n")
13551356

13561357
if os_helper.can_symlink():
13571358
os.symlink(os.path.abspath(t2_path), self.link_path)
1358-
os.symlink('broken', broken_link_path, True)
1359+
os.symlink('broken', self.broken_link_path, True)
13591360
os.symlink(join('tmp3', 'broken'), broken_link2_path, True)
13601361
os.symlink(join('SUB21', 'tmp5'), broken_link3_path, True)
13611362
self.sub2_tree = (sub2_path, ["SUB21", "link"],
@@ -1451,6 +1452,11 @@ def test_walk_symlink(self):
14511452
else:
14521453
self.fail("Didn't follow symlink with followlinks=True")
14531454

1455+
walk_it = self.walk(self.broken_link_path, follow_symlinks=True)
1456+
if self.is_fwalk:
1457+
self.assertRaises(FileNotFoundError, next, walk_it)
1458+
self.assertRaises(StopIteration, next, walk_it)
1459+
14541460
def test_walk_bad_dir(self):
14551461
# Walk top-down.
14561462
errors = []
@@ -1472,6 +1478,73 @@ def test_walk_bad_dir(self):
14721478
finally:
14731479
os.rename(path1new, path1)
14741480

1481+
def test_walk_bad_dir2(self):
1482+
walk_it = self.walk('nonexisting')
1483+
if self.is_fwalk:
1484+
self.assertRaises(FileNotFoundError, next, walk_it)
1485+
self.assertRaises(StopIteration, next, walk_it)
1486+
1487+
walk_it = self.walk('nonexisting', follow_symlinks=True)
1488+
if self.is_fwalk:
1489+
self.assertRaises(FileNotFoundError, next, walk_it)
1490+
self.assertRaises(StopIteration, next, walk_it)
1491+
1492+
walk_it = self.walk(self.tmp1_path)
1493+
self.assertRaises(StopIteration, next, walk_it)
1494+
1495+
walk_it = self.walk(self.tmp1_path, follow_symlinks=True)
1496+
if self.is_fwalk:
1497+
self.assertRaises(NotADirectoryError, next, walk_it)
1498+
self.assertRaises(StopIteration, next, walk_it)
1499+
1500+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
1501+
@unittest.skipIf(sys.platform == "vxworks",
1502+
"fifo requires special path on VxWorks")
1503+
def test_walk_named_pipe(self):
1504+
path = os_helper.TESTFN + '-pipe'
1505+
os.mkfifo(path)
1506+
self.addCleanup(os.unlink, path)
1507+
1508+
walk_it = self.walk(path)
1509+
self.assertRaises(StopIteration, next, walk_it)
1510+
1511+
walk_it = self.walk(path, follow_symlinks=True)
1512+
if self.is_fwalk:
1513+
self.assertRaises(NotADirectoryError, next, walk_it)
1514+
self.assertRaises(StopIteration, next, walk_it)
1515+
1516+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
1517+
@unittest.skipIf(sys.platform == "vxworks",
1518+
"fifo requires special path on VxWorks")
1519+
def test_walk_named_pipe2(self):
1520+
path = os_helper.TESTFN + '-dir'
1521+
os.mkdir(path)
1522+
self.addCleanup(shutil.rmtree, path)
1523+
os.mkfifo(os.path.join(path, 'mypipe'))
1524+
1525+
errors = []
1526+
walk_it = self.walk(path, onerror=errors.append)
1527+
next(walk_it)
1528+
self.assertRaises(StopIteration, next, walk_it)
1529+
self.assertEqual(errors, [])
1530+
1531+
errors = []
1532+
walk_it = self.walk(path, onerror=errors.append)
1533+
root, dirs, files = next(walk_it)
1534+
self.assertEqual(root, path)
1535+
self.assertEqual(dirs, [])
1536+
self.assertEqual(files, ['mypipe'])
1537+
dirs.extend(files)
1538+
files.clear()
1539+
if self.is_fwalk:
1540+
self.assertRaises(NotADirectoryError, next, walk_it)
1541+
self.assertRaises(StopIteration, next, walk_it)
1542+
if self.is_fwalk:
1543+
self.assertEqual(errors, [])
1544+
else:
1545+
self.assertEqual(len(errors), 1, errors)
1546+
self.assertIsInstance(errors[0], NotADirectoryError)
1547+
14751548
def test_walk_many_open_files(self):
14761549
depth = 30
14771550
base = os.path.join(os_helper.TESTFN, 'deep')
@@ -1537,6 +1610,7 @@ def test_walk_above_recursion_limit(self):
15371610
@unittest.skipUnless(hasattr(os, 'fwalk'), "Test needs os.fwalk()")
15381611
class FwalkTests(WalkTests):
15391612
"""Tests for os.fwalk()."""
1613+
is_fwalk = True
15401614

15411615
def walk(self, top, **kwargs):
15421616
for root, dirs, files, root_fd in self.fwalk(top, **kwargs):

Lib/test/test_shutil.py

+17
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,23 @@ def test_rmtree_on_junction(self):
667667
finally:
668668
shutil.rmtree(TESTFN, ignore_errors=True)
669669

670+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
671+
@unittest.skipIf(sys.platform == "vxworks",
672+
"fifo requires special path on VxWorks")
673+
def test_rmtree_on_named_pipe(self):
674+
os.mkfifo(TESTFN)
675+
try:
676+
with self.assertRaises(NotADirectoryError):
677+
shutil.rmtree(TESTFN)
678+
self.assertTrue(os.path.exists(TESTFN))
679+
finally:
680+
os.unlink(TESTFN)
681+
682+
os.mkdir(TESTFN)
683+
os.mkfifo(os.path.join(TESTFN, 'mypipe'))
684+
shutil.rmtree(TESTFN)
685+
self.assertFalse(os.path.exists(TESTFN))
686+
670687
@unittest.skipIf(sys.platform[:6] == 'cygwin',
671688
"This test can't be run on Cygwin (issue #1071513).")
672689
@os_helper.skip_if_dac_override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix blocking :func:`os.fwalk` and :func:`shutil.rmtree` on opening named
2+
pipe.

0 commit comments

Comments
 (0)