Skip to content

Commit 3c890b5

Browse files
authored
pythonGH-89727: Fix os.fwalk() recursion error on deep trees (python#119638)
Implement `os.fwalk()` using a list as a stack to avoid emitting recursion errors on deeply nested trees.
1 parent 2cc3502 commit 3c890b5

File tree

3 files changed

+56
-40
lines changed

3 files changed

+56
-40
lines changed

Lib/os.py

+53-38
Original file line numberDiff line numberDiff line change
@@ -478,24 +478,52 @@ def fwalk(top=".", topdown=True, onerror=None, *, follow_symlinks=False, dir_fd=
478478
"""
479479
sys.audit("os.fwalk", top, topdown, onerror, follow_symlinks, dir_fd)
480480
top = fspath(top)
481-
# Note: To guard against symlink races, we use the standard
482-
# lstat()/open()/fstat() trick.
483-
if not follow_symlinks:
484-
orig_st = stat(top, follow_symlinks=False, dir_fd=dir_fd)
485-
topfd = open(top, O_RDONLY | O_NONBLOCK, dir_fd=dir_fd)
486-
try:
487-
if (follow_symlinks or (st.S_ISDIR(orig_st.st_mode) and
488-
path.samestat(orig_st, stat(topfd)))):
489-
yield from _fwalk(topfd, top, isinstance(top, bytes),
490-
topdown, onerror, follow_symlinks)
491-
finally:
492-
close(topfd)
493-
494-
def _fwalk(topfd, toppath, isbytes, topdown, onerror, follow_symlinks):
481+
stack = [(_fwalk_walk, (True, dir_fd, top, top, None))]
482+
isbytes = isinstance(top, bytes)
483+
while stack:
484+
yield from _fwalk(stack, isbytes, topdown, onerror, follow_symlinks)
485+
486+
# Each item in the _fwalk() stack is a pair (action, args).
487+
_fwalk_walk = 0 # args: (isroot, dirfd, toppath, topname, entry)
488+
_fwalk_yield = 1 # args: (toppath, dirnames, filenames, topfd)
489+
_fwalk_close = 2 # args: dirfd
490+
491+
def _fwalk(stack, isbytes, topdown, onerror, follow_symlinks):
495492
# Note: This uses O(depth of the directory tree) file descriptors: if
496493
# necessary, it can be adapted to only require O(1) FDs, see issue
497494
# #13734.
498495

496+
action, value = stack.pop()
497+
if action == _fwalk_close:
498+
close(value)
499+
return
500+
elif action == _fwalk_yield:
501+
yield value
502+
return
503+
assert action == _fwalk_walk
504+
isroot, dirfd, toppath, topname, entry = value
505+
try:
506+
if not follow_symlinks:
507+
# Note: To guard against symlink races, we use the standard
508+
# lstat()/open()/fstat() trick.
509+
if entry is None:
510+
orig_st = stat(topname, follow_symlinks=False, dir_fd=dirfd)
511+
else:
512+
orig_st = entry.stat(follow_symlinks=False)
513+
topfd = open(topname, O_RDONLY | O_NONBLOCK, dir_fd=dirfd)
514+
except OSError as err:
515+
if isroot:
516+
raise
517+
if onerror is not None:
518+
onerror(err)
519+
return
520+
stack.append((_fwalk_close, topfd))
521+
if not follow_symlinks:
522+
if isroot and not st.S_ISDIR(orig_st.st_mode):
523+
return
524+
if not path.samestat(orig_st, stat(topfd)):
525+
return
526+
499527
scandir_it = scandir(topfd)
500528
dirs = []
501529
nondirs = []
@@ -521,31 +549,18 @@ def _fwalk(topfd, toppath, isbytes, topdown, onerror, follow_symlinks):
521549

522550
if topdown:
523551
yield toppath, dirs, nondirs, topfd
552+
else:
553+
stack.append((_fwalk_yield, (toppath, dirs, nondirs, topfd)))
524554

525-
for name in dirs if entries is None else zip(dirs, entries):
526-
try:
527-
if not follow_symlinks:
528-
if topdown:
529-
orig_st = stat(name, dir_fd=topfd, follow_symlinks=False)
530-
else:
531-
assert entries is not None
532-
name, entry = name
533-
orig_st = entry.stat(follow_symlinks=False)
534-
dirfd = open(name, O_RDONLY | O_NONBLOCK, dir_fd=topfd)
535-
except OSError as err:
536-
if onerror is not None:
537-
onerror(err)
538-
continue
539-
try:
540-
if follow_symlinks or path.samestat(orig_st, stat(dirfd)):
541-
dirpath = path.join(toppath, name)
542-
yield from _fwalk(dirfd, dirpath, isbytes,
543-
topdown, onerror, follow_symlinks)
544-
finally:
545-
close(dirfd)
546-
547-
if not topdown:
548-
yield toppath, dirs, nondirs, topfd
555+
toppath = path.join(toppath, toppath[:0]) # Add trailing slash.
556+
if entries is None:
557+
stack.extend(
558+
(_fwalk_walk, (False, topfd, toppath + name, name, None))
559+
for name in dirs[::-1])
560+
else:
561+
stack.extend(
562+
(_fwalk_walk, (False, topfd, toppath + name, name, entry))
563+
for name, entry in zip(dirs[::-1], entries[::-1]))
549564

550565
__all__.append("fwalk")
551566

Lib/test/test_os.py

-2
Original file line numberDiff line numberDiff line change
@@ -1687,8 +1687,6 @@ def test_fd_leak(self):
16871687

16881688
# fwalk() keeps file descriptors open
16891689
test_walk_many_open_files = None
1690-
# fwalk() still uses recursion
1691-
test_walk_above_recursion_limit = None
16921690

16931691

16941692
class BytesWalkTests(WalkTests):
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix issue with :func:`os.fwalk` where a :exc:`RecursionError` was raised on
2+
deep directory trees by adjusting the implementation to be iterative instead
3+
of recursive.

0 commit comments

Comments
 (0)