Skip to content

Commit aae371b

Browse files
[3.12] GH-89727: Fix os.fwalk() recursion error on deep trees (GH-119638) (#119765)
GH-89727: Fix `os.fwalk()` recursion error on deep trees (GH-119638) Implement `os.fwalk()` using a list as a stack to avoid emitting recursion errors on deeply nested trees. (cherry picked from commit 3c890b5) Co-authored-by: Barney Gale <[email protected]>
1 parent 681d7da commit aae371b

File tree

3 files changed

+56
-40
lines changed

3 files changed

+56
-40
lines changed

Lib/os.py

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

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

520548
if topdown:
521549
yield toppath, dirs, nondirs, topfd
550+
else:
551+
stack.append((_fwalk_yield, (toppath, dirs, nondirs, topfd)))
522552

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

548563
__all__.append("fwalk")
549564

Lib/test/test_os.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1673,8 +1673,6 @@ def test_fd_leak(self):
16731673

16741674
# fwalk() keeps file descriptors open
16751675
test_walk_many_open_files = None
1676-
# fwalk() still uses recursion
1677-
test_walk_above_recursion_limit = None
16781676

16791677

16801678
class BytesWalkTests(WalkTests):
Lines changed: 3 additions & 0 deletions
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)