Skip to content

Commit 48f1389

Browse files
[3.11] pythongh-116401: Fix blocking os.fwalk() and shutil.rmtree() on opening a named pipe (pythonGH-116421)
(cherry picked from commit aa7bcf2) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 5bd3507 commit 48f1389

File tree

6 files changed

+113
-8
lines changed

6 files changed

+113
-8
lines changed

Lib/os.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ def fwalk(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=
467467
# lstat()/open()/fstat() trick.
468468
if not follow_symlinks:
469469
orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd)
470-
topfd = open(top, O_RDONLY, dir_fd=dir_fd)
470+
topfd = open(top, O_RDONLY | O_NONBLOCK, dir_fd=dir_fd)
471471
try:
472472
if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and
473473
path.samestat(orig_st, stat(topfd)))):
@@ -516,7 +516,7 @@ def _fwalk(topfd, toppath, isbytes, topdown, onerror, follow_symlinks):
516516
assert entries is not None
517517
name, entry = name
518518
orig_st = entry.stat(follow_symlinks=False)
519-
dirfd = open(name, O_RDONLY, dir_fd=topfd)
519+
dirfd = open(name, O_RDONLY | O_NONBLOCK, dir_fd=topfd)
520520
except OSError as err:
521521
if onerror is not None:
522522
onerror(err)

Lib/shutil.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ def _rmtree_safe_fd(topfd, path, onerror):
662662
continue
663663
if is_dir:
664664
try:
665-
dirfd = os.open(entry.name, os.O_RDONLY, dir_fd=topfd)
665+
dirfd = os.open(entry.name, os.O_RDONLY | os.O_NONBLOCK, dir_fd=topfd)
666666
dirfd_closed = False
667667
except OSError:
668668
onerror(os.open, fullname, sys.exc_info())
@@ -742,7 +742,7 @@ def onerror(*args):
742742
onerror(os.lstat, path, sys.exc_info())
743743
return
744744
try:
745-
fd = os.open(path, os.O_RDONLY, dir_fd=dir_fd)
745+
fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK, dir_fd=dir_fd)
746746
fd_closed = False
747747
except Exception:
748748
onerror(os.open, path, sys.exc_info())

Lib/test/test_glob.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,18 @@ def test_glob_non_directory(self):
343343
eq(self.rglob('nonexistent', '*'), [])
344344
eq(self.rglob('nonexistent', '**'), [])
345345

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

Lib/test/test_os.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1278,6 +1278,7 @@ def test_ror_operator(self):
12781278

12791279
class WalkTests(unittest.TestCase):
12801280
"""Tests for os.walk()."""
1281+
is_fwalk = False
12811282

12821283
# Wrapper to hide minor differences between os.walk and os.fwalk
12831284
# to tests both functions with the same code base
@@ -1312,14 +1313,14 @@ def setUp(self):
13121313
self.sub11_path = join(self.sub1_path, "SUB11")
13131314
sub2_path = join(self.walk_path, "SUB2")
13141315
sub21_path = join(sub2_path, "SUB21")
1315-
tmp1_path = join(self.walk_path, "tmp1")
1316+
self.tmp1_path = join(self.walk_path, "tmp1")
13161317
tmp2_path = join(self.sub1_path, "tmp2")
13171318
tmp3_path = join(sub2_path, "tmp3")
13181319
tmp5_path = join(sub21_path, "tmp3")
13191320
self.link_path = join(sub2_path, "link")
13201321
t2_path = join(os_helper.TESTFN, "TEST2")
13211322
tmp4_path = join(os_helper.TESTFN, "TEST2", "tmp4")
1322-
broken_link_path = join(sub2_path, "broken_link")
1323+
self.broken_link_path = join(sub2_path, "broken_link")
13231324
broken_link2_path = join(sub2_path, "broken_link2")
13241325
broken_link3_path = join(sub2_path, "broken_link3")
13251326

@@ -1329,13 +1330,13 @@ def setUp(self):
13291330
os.makedirs(sub21_path)
13301331
os.makedirs(t2_path)
13311332

1332-
for path in tmp1_path, tmp2_path, tmp3_path, tmp4_path, tmp5_path:
1333+
for path in self.tmp1_path, tmp2_path, tmp3_path, tmp4_path, tmp5_path:
13331334
with open(path, "x", encoding='utf-8') as f:
13341335
f.write("I'm " + path + " and proud of it. Blame test_os.\n")
13351336

13361337
if os_helper.can_symlink():
13371338
os.symlink(os.path.abspath(t2_path), self.link_path)
1338-
os.symlink('broken', broken_link_path, True)
1339+
os.symlink('broken', self.broken_link_path, True)
13391340
os.symlink(join('tmp3', 'broken'), broken_link2_path, True)
13401341
os.symlink(join('SUB21', 'tmp5'), broken_link3_path, True)
13411342
self.sub2_tree = (sub2_path, ["SUB21", "link"],
@@ -1431,6 +1432,11 @@ def test_walk_symlink(self):
14311432
else:
14321433
self.fail("Didn't follow symlink with followlinks=True")
14331434

1435+
walk_it = self.walk(self.broken_link_path, follow_symlinks=True)
1436+
if self.is_fwalk:
1437+
self.assertRaises(FileNotFoundError, next, walk_it)
1438+
self.assertRaises(StopIteration, next, walk_it)
1439+
14341440
def test_walk_bad_dir(self):
14351441
# Walk top-down.
14361442
errors = []
@@ -1452,6 +1458,73 @@ def test_walk_bad_dir(self):
14521458
finally:
14531459
os.rename(path1new, path1)
14541460

1461+
def test_walk_bad_dir2(self):
1462+
walk_it = self.walk('nonexisting')
1463+
if self.is_fwalk:
1464+
self.assertRaises(FileNotFoundError, next, walk_it)
1465+
self.assertRaises(StopIteration, next, walk_it)
1466+
1467+
walk_it = self.walk('nonexisting', follow_symlinks=True)
1468+
if self.is_fwalk:
1469+
self.assertRaises(FileNotFoundError, next, walk_it)
1470+
self.assertRaises(StopIteration, next, walk_it)
1471+
1472+
walk_it = self.walk(self.tmp1_path)
1473+
self.assertRaises(StopIteration, next, walk_it)
1474+
1475+
walk_it = self.walk(self.tmp1_path, follow_symlinks=True)
1476+
if self.is_fwalk:
1477+
self.assertRaises(NotADirectoryError, next, walk_it)
1478+
self.assertRaises(StopIteration, next, walk_it)
1479+
1480+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
1481+
@unittest.skipIf(sys.platform == "vxworks",
1482+
"fifo requires special path on VxWorks")
1483+
def test_walk_named_pipe(self):
1484+
path = os_helper.TESTFN + '-pipe'
1485+
os.mkfifo(path)
1486+
self.addCleanup(os.unlink, path)
1487+
1488+
walk_it = self.walk(path)
1489+
self.assertRaises(StopIteration, next, walk_it)
1490+
1491+
walk_it = self.walk(path, follow_symlinks=True)
1492+
if self.is_fwalk:
1493+
self.assertRaises(NotADirectoryError, next, walk_it)
1494+
self.assertRaises(StopIteration, next, walk_it)
1495+
1496+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
1497+
@unittest.skipIf(sys.platform == "vxworks",
1498+
"fifo requires special path on VxWorks")
1499+
def test_walk_named_pipe2(self):
1500+
path = os_helper.TESTFN + '-dir'
1501+
os.mkdir(path)
1502+
self.addCleanup(shutil.rmtree, path)
1503+
os.mkfifo(os.path.join(path, 'mypipe'))
1504+
1505+
errors = []
1506+
walk_it = self.walk(path, onerror=errors.append)
1507+
next(walk_it)
1508+
self.assertRaises(StopIteration, next, walk_it)
1509+
self.assertEqual(errors, [])
1510+
1511+
errors = []
1512+
walk_it = self.walk(path, onerror=errors.append)
1513+
root, dirs, files = next(walk_it)
1514+
self.assertEqual(root, path)
1515+
self.assertEqual(dirs, [])
1516+
self.assertEqual(files, ['mypipe'])
1517+
dirs.extend(files)
1518+
files.clear()
1519+
if self.is_fwalk:
1520+
self.assertRaises(NotADirectoryError, next, walk_it)
1521+
self.assertRaises(StopIteration, next, walk_it)
1522+
if self.is_fwalk:
1523+
self.assertEqual(errors, [])
1524+
else:
1525+
self.assertEqual(len(errors), 1, errors)
1526+
self.assertIsInstance(errors[0], NotADirectoryError)
1527+
14551528
def test_walk_many_open_files(self):
14561529
depth = 30
14571530
base = os.path.join(os_helper.TESTFN, 'deep')
@@ -1477,6 +1550,7 @@ def test_walk_many_open_files(self):
14771550
@unittest.skipUnless(hasattr(os, 'fwalk'), "Test needs os.fwalk()")
14781551
class FwalkTests(WalkTests):
14791552
"""Tests for os.fwalk()."""
1553+
is_fwalk = True
14801554

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

Lib/test/test_shutil.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,23 @@ def test_rmtree_on_junction(self):
500500
finally:
501501
shutil.rmtree(TESTFN, ignore_errors=True)
502502

503+
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
504+
@unittest.skipIf(sys.platform == "vxworks",
505+
"fifo requires special path on VxWorks")
506+
def test_rmtree_on_named_pipe(self):
507+
os.mkfifo(TESTFN)
508+
try:
509+
with self.assertRaises(NotADirectoryError):
510+
shutil.rmtree(TESTFN)
511+
self.assertTrue(os.path.exists(TESTFN))
512+
finally:
513+
os.unlink(TESTFN)
514+
515+
os.mkdir(TESTFN)
516+
os.mkfifo(os.path.join(TESTFN, 'mypipe'))
517+
shutil.rmtree(TESTFN)
518+
self.assertFalse(os.path.exists(TESTFN))
519+
503520

504521
class TestCopyTree(BaseTest, unittest.TestCase):
505522

Lines changed: 2 additions & 0 deletions
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)