Repository navigation
馃悰 fix(path): preserve link targets on delete errors - #3427
Merged
Merged
Conversation
Reported by Anthropic's OSS Scanner: ANT-2026-UK90QC27.
POSIX deletion failures do not justify changing the entry mode. Windows still needs to clear read-only attributes, using a handle that refuses reparse points and prevents replacement during the update.
Importing virtualenv without ctypes already works on main: _win.py imported ctypes inside get_short_path_name, which only Windows calls. The fragment described a non-change.
gaborbernat
force-pushed
the
security/deletion-link-mode
branch
from
October 17, 2026 10:01
46804c1 to
9e2bf1c
Compare
gaborbernat
marked this pull request as ready for review
October 17, 2026 10:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
safe_deleteremoves an environment or a cache tree withshutil.rmtreeand retried a failedunlinkorrmdirafter adding the owner write bit to the entry (old lines). That mode change went by pathname. When the entry was a symbolic link, the change landed on the file the link pointed at, and the mode it applied was the link's own. A directory the deleting user cannot write, holding a link to a file that user owns, therefore turned avirtualenv --clearor a cache refresh into a mode change on that file. 馃悰 After this PR a failed deletion on POSIX propagates its error without changing any mode, and on Windows the read-only attribute is cleared through a handle on the entry itself, which refuses reparse points. This addresses draft advisory ANT-2026-UK90QC27.The retry changed the mode of the link's target
The handler #3222 made run let a
FileNotFoundErrorpass, re-raised anything that was not anunlinkorrmdirfailure, and otherwise ranos.chmod(path, os.lstat(path).st_mode | S_IWUSR)and the deletion again (old lines).os.accessandos.chmodfollow symbolic links by default, as chmod(2) does for a pathname, andos.supports_follow_symlinksdoes not listos.chmodon Linux, so there was no portable way to change the link itself.os.lstatread the link's own mode, which symlink(7) fixes at0777on Linux. The target therefore received0777: readable, writable and executable by everyone. The retried deletion then failed again, since unlink(2) andrmdirneed write permission on the parent directory and never consult the entry's mode. The caller got the same error as before, and the target kept its new mode.The handler now re-raises every error on POSIX other than a missing entry. The retry remains for Windows, where a read-only attribute does block deletion, and skips symbolic links there as well.
sequenceDiagram participant H as handler participant L as link in the tree participant T as link target (user-owned file) H->>L: unlink fails, parent directory not writable rect rgb(255, 214, 214) H->>L: lstat: mode 0777, the link's own H->>T: chmod(path) follows the link: target set to 0777 H->>L: unlink again, fails for the same reason end rect rgb(216, 243, 220) H-->>H: now: raise the original error, no chmod endWindows recovery binds the attribute change to the opened entry
On Windows the retry goes through
clear_readonly. It opens the path withCreateFileWforFILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES, withFILE_SHARE_READ | FILE_SHARE_WRITEbut notFILE_SHARE_DELETE, asOPEN_EXISTING, and withFILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT. Backup semantics lets the call open a directory. Opening the reparse point gives a handle to a link or junction itself rather than to its target. Withholding delete sharing means nobody can rename or remove the entry while the handle is open, so the change below applies to the entry that was opened.GetFileInformationByHandleExreads the attributes into aFILE_BASIC_INFO(the struct). An entry withFILE_ATTRIBUTE_REPARSE_POINTraisesOSError. Otherwise onlyFILE_ATTRIBUTE_READONLYis cleared, withFILE_ATTRIBUTE_NORMALsubstituted when no attribute would remain, andSetFileInformationByHandlewrites the structure back with the timestamps as read. A failed call raises the Windows error.ctypesis imported only on Windows.flowchart LR E[rmtree reports a failure] --> N{FileNotFoundError?} N -- yes --> I[ignore] N -- no --> P{platform} P -- before, any --> B[lstat the link, chmod by path: target gets 0777, retry fails] P -- POSIX --> R[raise the original error] P -- Windows --> F{unlink or rmdir, not a symlink, not writable?} F -- no --> R F -- yes --> O[CreateFileW on the entry itself, no FILE_SHARE_DELETE] O --> T{FILE_ATTRIBUTE_REPARSE_POINT?} T -- yes --> X[OSError, nothing changed] T -- no --> C[clear READONLY by handle, retry] classDef bad fill:#ffd6d6,stroke:#c1121f,color:#5c0000; classDef ok fill:#d8f3dc,stroke:#2d6a4f,color:#1b4332; class B bad; class R,X,C ok;Behavior changes
A POSIX deletion that fails for any reason other than a missing entry raises that error at once, and every mode in the tree stays as it was. On Windows, read-only files and directories are still removed. A symbolic link or junction that Windows refuses to delete now raises instead of being touched, as does any entry whose attributes cannot be read or written; the error is a
PermissionErrorcarrying the Windows error code. A parent directory swapped between rmtree's failure and the handle open is outside this protection: the handle guards the entry it opened, not the path that led to it.Reported by Anthropic's OSS Scanner: ANT-2026-UK90QC27.