Skip to content

fix: rm_rf now handles directories without S_IXUSR permission#14331

Open
RonnyPfannschmidt wants to merge 2 commits intopytest-dev:mainfrom
RonnyPfannschmidt:fix/rm-rf-no-exec-permission-7940
Open

fix: rm_rf now handles directories without S_IXUSR permission#14331
RonnyPfannschmidt wants to merge 2 commits intopytest-dev:mainfrom
RonnyPfannschmidt:fix/rm-rf-no-exec-permission-7940

Conversation

@RonnyPfannschmidt
Copy link
Copy Markdown
Member

Summary

Closes #7940.
Supersedes #7941.

The on_rm_rf_error handler had two gaps that prevented rm_rf from removing directories lacking the S_IXUSR (owner execute) permission bit:

  1. chmod_rw only set S_IRUSR | S_IWUSR (read+write) — missing S_IXUSR meant directories couldn't be traversed by shutil.rmtree.
  2. os.open / os.scandir PermissionError was silently skippedshutil.rmtree (Python 3.12+ uses os.open, 3.10–3.11 uses os.scandir) could not enter directories without execute permission, and the error handler simply returned False.

Changes

  • Extracted _chmod_rwx() helper that uses stat.S_IRWXU (read+write+exec) and returns whether permissions actually changed (acts as a recursion guard).
  • os.open and os.scandir failures are now handled: permissions are fixed on the path and its parent, then the entry is removed directly (files via os.unlink, directories via recursive rm_rf).
  • All existing chmod_rw calls replaced with _chmod_rwx.

This takes the "fix it in on_rm_rf_error directly" approach rather than #7941's approach of delegating to tempfile.TemporaryDirectory._rmtree, keeping rm_rf robust for all callers.

Test plan

  • New test test_rm_rf_with_no_exec_permission_directories — exact scenario from Pytest cannot delete tmp_path directories without stat.S_IXUSR mode bit set #7940 (nested dirs + files with chmod(mode=0))
  • New test test_on_rm_rf_error_os_open_handles_fileos.open PermissionError on a file
  • New test test_on_rm_rf_error_os_open_handles_directoryos.open PermissionError on a directory tree
  • All existing TestRmRf tests pass unchanged
  • Full testing/test_tmpdir.py suite passes (41 passed, 1 skipped)
  • All pre-commit hooks pass (ruff, mypy, etc.)

Made with Cursor

…-dev#7940)

The on_rm_rf_error handler previously only set S_IRUSR|S_IWUSR (read+write)
when retrying failed removals, and silently skipped os.open/os.scandir
failures. This meant directories lacking execute permission (S_IXUSR) could
not be traversed or removed by shutil.rmtree.

Now the handler:
- Uses S_IRWXU (read+write+execute) for all permission fixes
- Handles os.open and os.scandir PermissionError by fixing permissions
  on the path and its parent, then removing the entry directly
- Includes a guard against infinite recursion when chmod has no effect

Supersedes pytest-dev#7941 which took the approach of delegating to
tempfile.TemporaryDirectory._rmtree.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Claude Opus <claude@anthropic.com>
Copilot AI review requested due to automatic review settings March 27, 2026 19:26
@psf-chronographer psf-chronographer bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Mar 27, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes rm_rf failures when removing directory trees that include directories missing the owner execute (S_IXUSR) permission bit, by improving on_rm_rf_error and adding regression tests for the affected traversal code paths.

Changes:

  • Added _chmod_rwx() to grant owner rwx permissions and replaced prior chmod_rw usage.
  • Enhanced on_rm_rf_error to handle os.open / os.scandir PermissionError by repairing permissions and removing entries directly.
  • Added new tests covering no-exec directory trees and os.open PermissionError handling; added a changelog entry.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
testing/test_tmpdir.py Adds regression tests for removing trees with no-exec directories and for os.open PermissionError handling.
src/_pytest/pathlib.py Introduces _chmod_rwx() and updates on_rm_rf_error to fix permissions and handle traversal errors from os.open/os.scandir.
changelog/7940.bugfix.rst Documents the bugfix in the changelog.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Use stat.S_IMODE() to extract only permission bits before
  comparing/setting, avoiding file-type bit leakage into os.chmod
- Only add S_IXUSR for directories; regular files get S_IRUSR|S_IWUSR
  only, avoiding unnecessary execute side-effect on files
- Fix recursion guard: track whether *either* parent or path chmod
  changed permissions, so fixing the parent alone (the common case for
  missing S_IXUSR) is sufficient to proceed with removal
- Add test for parent-only permission fix scenario

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Claude Opus <claude@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Returns True if permissions were actually changed, False if they were
already sufficient or couldn't be changed.
"""
import stat
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for lazy import

p = Path(path)
parent_changed = False
if p.parent != p:
parent_changed = _chmod_rwx(str(p.parent))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm isn't chmoding parent kinda dangerous? Say I do rm_rf('/tmp/foo/'), now it might try to go and chmod /tmp?

@@ -0,0 +1 @@
Fixed ``rm_rf`` failing to remove directories when the ``S_IXUSR`` (owner execute) permission bit is missing -- supersedes :pr:`7941`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • User doesn't know what rm_rf is, it's an internal function. Better to explain in terms of user behavior.
  • Is the "supersedes" part needed in a changelog entry?

# on the path and its parent, then remove it ourselves since rmtree
# skips entries after the error handler returns.
# See: https://github.com/pytest-dev/pytest/issues/7940
p = Path(path)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already do p = Path(path) below in the function, I think it will be clearer to just move it up above the if and do everything in terms of it (including _chmod_rwx)

Comment on lines +126 to +127
if func in (os.open, os.scandir):
# Directory traversal failed (e.g. missing S_IXUSR). Fix permissions
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the implicit assumption here that os.open is always on a directory? I think it will be better to make this explicit with an is_dir check.

if not (parent_changed or path_changed):
return False
if os.path.isdir(path):
rm_rf(Path(path))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If for some reason the chmod didn't help (e.g. maybe some sandboxing prevents the deletion via some non-permission mechanism), this will end up in infinite recursion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pytest cannot delete tmp_path directories without stat.S_IXUSR mode bit set

3 participants