This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: tempfile.TemporaryDirectory fails removing dir in some edge cases related to symlinks
Type: crash Stage:
Components: Library (Lib) Versions: Python 3.9
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: afeblot
Priority: normal Keywords:

Created on 2022-03-10 17:22 by afeblot, last changed 2022-04-11 14:59 by admin.

Messages (1)
msg414871 - (view) Author: Alexandre Feblot (afeblot) Date: 2022-03-10 17:22
```python
#!/usr/bin/env python3

import os
import tempfile

def createUnremovableDir(workdir):
    print(workdir)
    os.mkdir(f'{workdir}/mydir')
    os.symlink('/bin/bash', f'{workdir}/mydir/mylink') # Symlink to a root owned file
    os.chmod(f'{workdir}/mydir', 0o555)

with tempfile.TemporaryDirectory() as workdir:
    createUnremovableDir(workdir)
```

Fails because `tempfile.TemporaryDirectory._rmtree` tries to execute os.chmod(path, 0o700) on the symlink, which by default tries to change the root owned file symlink target instead of the symlink itself:

```
/tmp/tmp1_dy42ef
Traceback (most recent call last):
  File "/usr/lib/python3.9/shutil.py", line 682, in _rmtree_safe_fd
    os.unlink(entry.name, dir_fd=topfd)
PermissionError: [Errno 13] Permission denied: 'mylink'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "~/tempfile_demo_bug.py", line 13, in <module>
    createUnremovableDir(workdir)
  File "/usr/lib/python3.9/tempfile.py", line 969, in __exit__
    self.cleanup()
  File "/usr/lib/python3.9/tempfile.py", line 973, in cleanup
    self._rmtree(self.name)
  File "/usr/lib/python3.9/tempfile.py", line 955, in _rmtree
    _rmtree(name, onerror=onerror)
  File "/usr/lib/python3.9/shutil.py", line 727, in rmtree
    _rmtree_safe_fd(fd, path, onerror)
  File "/usr/lib/python3.9/shutil.py", line 664, in _rmtree_safe_fd
    _rmtree_safe_fd(dirfd, fullname, onerror)
  File "/usr/lib/python3.9/shutil.py", line 684, in _rmtree_safe_fd
    onerror(os.unlink, fullname, sys.exc_info())
  File "/usr/lib/python3.9/tempfile.py", line 941, in onerror
    resetperms(path)
  File "/usr/lib/python3.9/tempfile.py", line 936, in resetperms
    _os.chmod(path, 0o700)
PermissionError: [Errno 1] Operation not permitted: '/tmp/tmp1_dy42ef/mydir/mylink'
```

and leaves:

```
(.venv python 3.9.9) $ find /tmp/tmp1_dy42ef -ls
   148228      4 drwx------   3 myuser myuser     4096 Mar 10 16:54 /tmp/tmp1_dy42ef
   148229      4 drwx------   2 myuser myuser     4096 Mar 10 16:54 /tmp/tmp1_dy42ef/mydir
   148230      0 lrwxrwxrwx   1 myuser myuser        9 Mar 10 16:54 /tmp/tmp1_dy42ef/mydir/mylink -> /bin/bash

```

This fixes it:

``` python
#!/usr/bin/env python3

import os
import tempfile

def createUnremovableDir(workdir):
    print(workdir)
    os.mkdir(f'{workdir}/mydir')
    os.symlink('/bin/bash', f'{workdir}/mydir/mylink') # Symlink to a root owned file
    os.chmod(f'{workdir}/mydir', 0o555)

def _rmtree(cls, name, ignore_errors=False):
    def onerror(func, path, exc_info):
        if issubclass(exc_info[0], PermissionError):
            def resetperms(path):
                try:
                    if os.chflags in os.supports_follow_symlinks:  # This is the patch
                        os.chflags(path, 0, follow_symlinks=False) # This is the patch
                    elif not os.path.islink(path):                 # This is the patch
                        os.chflags(path, 0)
                except AttributeError:
                    pass
                if os.chmod in os.supports_follow_symlinks:        # This is the patch
                    os.chmod(path, 0o700, follow_symlinks=False)   # This is the patch
                elif not os.path.islink(path):                     # This is the patch
                    os.chmod(path, 0o700)

            try:
                if path != name:
                    resetperms(os.path.dirname(path))
                resetperms(path)

                try:
                    os.unlink(path)
                # PermissionError is raised on FreeBSD for directories
                except (IsADirectoryError, PermissionError):
                    cls._rmtree(path, ignore_errors=ignore_errors)
            except FileNotFoundError:
                pass
        elif issubclass(exc_info[0], FileNotFoundError):
            pass
        else:
            if not ignore_errors:
                raise

    shutil.rmtree(name, onerror=onerror)

# Monkey patch the class method tempfile.TemporaryDirectory._rmtree
from types import MethodType
import shutil
tempfile.TemporaryDirectory._rmtree = MethodType(_rmtree, tempfile.TemporaryDirectory)

with tempfile.TemporaryDirectory() as workdir:
    createUnremovableDir(workdir)
History
Date User Action Args
2022-04-11 14:59:57adminsetgithub: 91133
2022-03-10 17:25:32afeblotsettype: crash
2022-03-10 17:22:08afeblotcreate