Skip to content

fix: rm_rf now handles directories without S_IXUSR permission#14331

Open
RonnyPfannschmidt wants to merge 8 commits into
pytest-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 8 commits into
pytest-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

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

Copilot AI left a comment

Copy link
Copy Markdown

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.

Comment thread src/_pytest/pathlib.py Outdated
Comment thread src/_pytest/pathlib.py Outdated
Comment thread src/_pytest/pathlib.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown

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.

Comment thread src/_pytest/pathlib.py Outdated
Comment thread src/_pytest/pathlib.py Outdated
Comment thread changelog/7940.bugfix.rst Outdated
Comment thread src/_pytest/pathlib.py Outdated
Comment thread src/_pytest/pathlib.py
Comment thread src/_pytest/pathlib.py Outdated
@nicoddemus

Copy link
Copy Markdown
Member

I did not review this PR yet, but how does it compare with #14279 (which I believe is close to being ready for merging)?

@bluetech

Copy link
Copy Markdown
Member

I did not review this PR yet, but how does it compare with #14279 (which I believe is close to being ready for merging)?

I think they are mostly orthogonal (though there will be git conflicts).

Comment thread src/_pytest/pathlib.py
Comment thread testing/test_tmpdir.py Outdated
Comment thread testing/test_tmpdir.py Outdated
Comment thread testing/test_tmpdir.py Outdated
Comment thread testing/test_tmpdir.py Outdated
Comment thread testing/test_tmpdir.py Outdated
Comment thread testing/test_tmpdir.py Outdated
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the fix/rm-rf-no-exec-permission-7940 branch from 9511d93 to 752ee78 Compare July 20, 2026 15:33
@RonnyPfannschmidt

Copy link
Copy Markdown
Member Author

i have to apologize for the speed-run of slop comments - i updated @cursor and its new default auto-approved external changes like a madman - the evolution of those tools is unhinged

RonnyPfannschmidt and others added 7 commits July 21, 2026 10:52
…-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>
- 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>
- Move `import stat` to module level (no lazy import)
- Bound parent chmod by start_path to prevent escaping rm_rf scope
- Use `p.is_dir()` and `rm_rf(p)` consistently with Path objects
- Rewrite changelog to be user-facing (no internal function names)

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
- Test _chmod_rwx returns False on nonexistent path (OSError branch)
- Test _chmod_rwx returns False when permissions already sufficient
- Test os.open handler returns False when chmod is ineffective

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
…pragmas

- Test os.open handler returns False when unlink raises OSError
- Test os.unlink handler walks multiple read-only parent directories
- Add pragma: no branch to for-loops that always exit via break

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
Move the Path conversion to a single location shared by both the
os.open/os.scandir handler and the os.rmdir/os.unlink handler,
as suggested in review.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Anthropic Claude Sonnet 4 <claude@anthropic.com>
Narrow OSError handling in _chmod_rwx and use stat constants in tests.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Cursor Grok 4.5 <grok@x.ai>
@RonnyPfannschmidt
RonnyPfannschmidt force-pushed the fix/rm-rf-no-exec-permission-7940 branch from 752ee78 to 530f985 Compare July 21, 2026 08:55
Pin the chmod-failure branch with a monkeypatch; a portable real-FS
layout for that case is not available in CI.

Co-authored-by: Cursor AI <ai@cursor.sh>
Co-authored-by: Cursor Grok 4.5 <grok@x.ai>
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

4 participants