Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tempfile module misinterprets access denied error on Windows #66305

Open
rupole mannequin opened this issue Jul 30, 2014 · 51 comments
Open

tempfile module misinterprets access denied error on Windows #66305

rupole mannequin opened this issue Jul 30, 2014 · 51 comments
Labels
3.10 only security fixes 3.11 only security fixes 3.12 bugs and security fixes OS-windows performance Performance or resource usage stdlib Python modules in the Lib dir

Comments

@rupole
Copy link
Mannequin

rupole mannequin commented Jul 30, 2014

BPO 22107
Nosy @pfmoore, @ncoghlan, @tjguk, @bobince, @takluyver, @zware, @serhiy-storchaka, @eryksun, @zooba, @RoccoMatano, @earonesty
Files
  • tempfile_bad_tempdir.patch
  • tempfile_bad_tempdir_2.patch
  • tempfile_bad_tempdir_3.patch
  • tempfile_bad_tempdir_4.patch
  • master...bjmcculloch_patch-1.diff: fix PermissionError exception handlers
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2014-07-30.12:40:41.492>
    labels = ['3.8', '3.9', '3.10', 'performance', 'library', 'OS-windows']
    title = 'tempfile module misinterprets access denied error on Windows'
    updated_at = <Date 2021-07-08.18:35:20.084>
    user = 'https://bugs.python.org/rupole'

    bugs.python.org fields:

    activity = <Date 2021-07-08.18:35:20.084>
    actor = 'bugale bugale'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)', 'Windows']
    creation = <Date 2014-07-30.12:40:41.492>
    creator = 'rupole'
    dependencies = []
    files = ['38145', '38151', '39421', '39434', '42704']
    hgrepos = []
    issue_num = 22107
    keywords = ['patch']
    message_count = 43.0
    messages = ['224304', '236004', '236028', '236029', '236047', '236050', '236052', '236053', '236055', '236058', '236060', '236112', '236118', '236130', '236132', '236133', '236143', '243514', '243608', '243611', '243617', '243626', '243628', '243787', '243788', '243805', '243808', '243812', '243814', '243815', '259559', '264779', '264971', '265040', '282403', '285131', '337735', '340738', '347076', '347095', '353353', '353360', '357075']
    nosy_count = 20.0
    nosy_names = ['paul.moore', 'ncoghlan', 'rupole', 'tim.golden', 'aclover', 'python-dev', 'takluyver', 'zach.ware', 'serhiy.storchaka', 'eryksun', 'steve.dower', 'Tor.Colvin', 'rocco.matano', 'V\xc3\xa1clav Dvo\xc5\x99\xc3\xa1k', 'Billy McCulloch', 'Paul Doom', 'earonesty', 'bugale bugale', 'JDM', 'Jonathan Mills']
    pr_nums = []
    priority = 'normal'
    resolution = None
    stage = None
    status = 'open'
    superseder = None
    type = 'resource usage'
    url = 'https://bugs.python.org/issue22107'
    versions = ['Python 3.8', 'Python 3.9', 'Python 3.10']

    @rupole
    Copy link
    Mannequin Author

    rupole mannequin commented Jul 30, 2014

    _mkstemp_inner assumes that an access denied error means that it
    has generated a filename that matches an existing foldername.
    However, in the case of a folder for which you don't have permissions to
    create a file, this means it will loop thru the maximum possible number of files.
    This causes it to hang for several seconds and eventually return a bogus
    FileExistsError.

    Similar behaviour exists in 2.7.7, but it throws an IOError instead.

    http://bugs.python.org/issue18849 seems to be where this was introduced.

    @rupole rupole mannequin added the OS-windows label Jul 30, 2014
    @serhiy-storchaka serhiy-storchaka added the type-bug An unexpected behavior, bug, or error label Aug 2, 2014
    @BreamoreBoy
    Copy link
    Mannequin

    BreamoreBoy mannequin commented Feb 14, 2015

    changeset 035b61b52caa has this:-

                 return (fd, _os.path.abspath(file))
             except FileExistsError:
                 continue    # try again
    +        except PermissionError:
    +            # This exception is thrown when a directory with the chosen name
    +            # already exists on windows.
    +            if _os.name == 'nt':
    +                continue
    +            else:
    +                raise
     
         raise FileExistsError(_errno.EEXIST,
                               "No usable temporary file name found")

    Could we simply set a flag saying it's a PermissionError and then raise the appropriate PermissionError or FileExistsError at the end of the loop?

    @serhiy-storchaka
    Copy link
    Member

    What exceptions are raised on Windows when try to open a file in bad directory?

    open('foo').close()
    open('foo/bar') # what raised?
    open('nonexistent/bar') # what raised?

    If raised the same exceptions as on Linux, then perhaps the following patch fixes this issue.

    @serhiy-storchaka
    Copy link
    Member

    And what returns os.access for writable directories and non-existent files?

    os.mkdir('dir')
    os.access('dir', os.W_OK)          # what returns?
    os.access('nonexistent', os.W_OK)  # what returns or raises?

    @BreamoreBoy
    Copy link
    Mannequin

    BreamoreBoy mannequin commented Feb 15, 2015

    >>> os.mkdir('dir')
    >>> os.access('dir', os.W_OK)
    True
    >>> os.access('nonexistent', os.W_OK)
    False
    >>> open('dir/bar')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [Errno 2] No such file or directory: 'dir/bar'
    >>> open('nonexistent/bar')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent/bar'

    @serhiy-storchaka
    Copy link
    Member

    Thank you Mark. Could you please make first test in msg236028 (when first part of the path is a file, not a directory)?

    @BreamoreBoy
    Copy link
    Mannequin

    BreamoreBoy mannequin commented Feb 15, 2015

    >>> open('README').close()
    >>> open('README/bar')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [Errno 2] No such file or directory: 'README/bar'

    @zooba
    Copy link
    Member

    zooba commented Feb 15, 2015

    Is there a difference if you do open(..., 'w')? It's a different enough operation that it may have a different error.

    @serhiy-storchaka
    Copy link
    Member

    Is there a difference if you do open(..., 'w')? It's a different enough operation that it may have a different error.

    Oh, yes, I forgot the 'w' mode.

    Mark, could you please run following test on Windows?

    import os
    open('foo', 'wb').close()
    flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_BINARY', 0)
    os.open('foo/bar', flags, 0o600)          # what raised?
    os.open('nonexistent/bar', flags, 0o600)  # what raised?

    @BreamoreBoy
    Copy link
    Mannequin

    BreamoreBoy mannequin commented Feb 15, 2015

    >>> open('foo', 'wb').close()
    >>> flags = os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, 'O_NOFOLLOW', 0) | getattr(os, 'O_BINARY', 0)
    >>> os.open('foo/bar', flags, 0o600)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [Errno 2] No such file or directory: 'foo/bar'
    >>> os.open('nonexistent/bar', flags, 0o600)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [Errno 2] No such file or directory: 'nonexistent/bar'

    @serhiy-storchaka
    Copy link
    Member

    Great. There is only one difference between Windows and Linux, but it affects only error type in tests. Here is a patch with updated test. It should now work on Windows.

    @rupole
    Copy link
    Mannequin Author

    rupole mannequin commented Feb 16, 2015

    os.access doesn't check filesystem permissions, so the patch will not catch the condition that creates the problem.

    @serhiy-storchaka
    Copy link
    Member

    It is best that we can do. How else we can check filesystem permissions? Only trying to create a file, But we just tried this and it failed.

    @rupole
    Copy link
    Mannequin Author

    rupole mannequin commented Feb 17, 2015

    It doesn't actually do anything, so why do it at all? In order to distinguish why it failed, you might try checking if the file actually exists, and if it is a folder.

    @tjguk
    Copy link
    Member

    tjguk commented Feb 17, 2015

    And, just to be clear to Serhiy who I know doesn't use Windows,
    os.access really is a worthless function in its present form: worse,
    even, because it can be misleading. I have a long-standing patch to
    convert it to use AccessCheck but I've never quite had the guts to
    commit it because I fear the breakage would be too great.

    @serhiy-storchaka
    Copy link
    Member

    The main issue is not tempfile raises a FileExistsError, but that it hangs for several seconds (for example if the temp dir doesn't exist). The patch allows to fail early and try other temp dir.

    os.access() is not enough, we can add os.path.isdir(). Could you please run tests on patched Python on Windows and say what tests are failed?

    @BreamoreBoy
    Copy link
    Mannequin

    BreamoreBoy mannequin commented Feb 17, 2015

    The feedback here https://mail.python.org/pipermail/python-dev/2011-May/111530.html seems positive. It references bpo-2528 which is still open but strikes me as the way forward. Why don't we go for it and nail this issue once and for all?

    @serhiy-storchaka
    Copy link
    Member

    Added os.path.isdir().

    Could anybody please run tests on Windows?

    @pfmoore
    Copy link
    Member

    pfmoore commented May 19, 2015

    ======================================================================
    ERROR: test_read_only_directory (test.test_tempfile.TestMkdtemp)
    ----------------------------------------------------------------------

    Traceback (most recent call last):
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 267, in _inside_empty_temp_dir
        yield
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 286, in test_read_only_directory
        self.skipTest("can't set the directory read-only")
      File "C:\Work\Projects\cpython\lib\unittest\case.py", line 645, in skipTest
        raise SkipTest(reason)
    unittest.case.SkipTest: can't set the directory read-only
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 289, in test_read_only_directory
        self.assertEqual(os.listdir(tempfile.tempdir), [])
      File "C:\Work\Projects\cpython\lib\contextlib.py", line 77, in __exit__
        self.gen.throw(type, value, traceback)
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 269, in _inside_empty_temp_dir
        support.rmtree(dir)
      File "C:\Work\Projects\cpython\lib\test\support\__init__.py", line 374, in rmtree
        _rmtree(path)
      File "C:\Work\Projects\cpython\lib\test\support\__init__.py", line 354, in _rmtree
        _waitfor(os.rmdir, path)
      File "C:\Work\Projects\cpython\lib\test\support\__init__.py", line 301, in _waitfor
        func(pathname)
    PermissionError: [WinError 5] Access is denied: 'C:\\Users\\Gustav\\AppData\\Local\\Temp\\tmpe53kiky0'

    ======================================================================
    ERROR: test_read_only_directory (test.test_tempfile.TestMkstempInner)
    ----------------------------------------------------------------------

    Traceback (most recent call last):
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 267, in _inside_empty_temp_dir
        yield
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 286, in test_read_only_directory
        self.skipTest("can't set the directory read-only")
      File "C:\Work\Projects\cpython\lib\unittest\case.py", line 645, in skipTest
        raise SkipTest(reason)
    unittest.case.SkipTest: can't set the directory read-only
    
    During handling of the above exception, another exception occurred:
    
    Traceback (most recent call last):
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 289, in test_read_only_directory
        self.assertEqual(os.listdir(tempfile.tempdir), [])
      File "C:\Work\Projects\cpython\lib\contextlib.py", line 77, in __exit__
        self.gen.throw(type, value, traceback)
      File "C:\Work\Projects\cpython\lib\test\test_tempfile.py", line 269, in _inside_empty_temp_dir
        support.rmtree(dir)
      File "C:\Work\Projects\cpython\lib\test\support\__init__.py", line 374, in rmtree
        _rmtree(path)
      File "C:\Work\Projects\cpython\lib\test\support\__init__.py", line 354, in _rmtree
        _waitfor(os.rmdir, path)
      File "C:\Work\Projects\cpython\lib\test\support\__init__.py", line 301, in _waitfor
        func(pathname)
    PermissionError: [WinError 5] Access is denied: 'C:\\Users\\Gustav\\AppData\\Local\\Temp\\tmp0qwkkr7l'

    @serhiy-storchaka
    Copy link
    Member

    Thank you Paul. What with updated patch?

    @pfmoore
    Copy link
    Member

    pfmoore commented May 19, 2015

    Works fine with the new patch:

    .\rt.bat -x64 -q test_tempfile

    C:\Work\Projects\cpython\PCbuild>"C:\Work\Projects\cpython\PCbuild\amd64\python.exe" -Wd -E -bb "C:\Work\Projects\cpython\PCbuild\..\lib\test\regrtest.py" test_tempfile

    [1/1] test_tempfile
    1 test OK.

    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented May 19, 2015

    New changeset 63f0ae6e218a by Serhiy Storchaka in branch '2.7':
    Issue bpo-22107: tempfile.gettempdir() and tempfile.mkdtemp() now try again
    https://hg.python.org/cpython/rev/63f0ae6e218a

    New changeset 3a387854d106 by Serhiy Storchaka in branch '3.4':
    Issue bpo-22107: tempfile.gettempdir() and tempfile.mkdtemp() now try again
    https://hg.python.org/cpython/rev/3a387854d106

    New changeset 1134198e23bd by Serhiy Storchaka in branch 'default':
    Issue bpo-22107: tempfile.gettempdir() and tempfile.mkdtemp() now try again
    https://hg.python.org/cpython/rev/1134198e23bd

    @serhiy-storchaka
    Copy link
    Member

    Unfortunately the patch doesn't fix original issue and looks as this can't be fixed until os.access will be more useful on Windows. But it fixes several related issues. mkstemp() now fails early if parent directory doesn't exist or is a file. gettempdir() and mkdtemp() now try again in case of collision on Windows as well as on Unix. I hope that fixing os.access will automatically fix original issue.

    Thanks Mark and Paul for testing on Windows. Thanks Tim for explaining issue with os.access.

    @tjguk
    Copy link
    Member

    tjguk commented May 21, 2015

    My reluctance to commit the os.access patch is because it will cause
    such a behaviour change in a function which has been pretty stable for a
    long while. It'll certainly be more correct, but at the undoubted
    expense of breaking someone's long-working code.

    @pfmoore
    Copy link
    Member

    pfmoore commented May 21, 2015

    I'm not sure I follow. Isn't the point of this patch to try again in certain cases of a PermissionError, where currently the code breaks out of the loop early? How can the result be worse than the current behaviour? Suerly sometimes (maybe not always) it works now where it previously failed, but that's a good thing?

    @eryksun
    Copy link
    Contributor

    eryksun commented May 22, 2015

    Shouldn't it be checking whether file (or filename) is a directory [1]? For example:

        except PermissionError:
            # This exception is thrown when a directory with
            # the chosen name already exists on windows.
            if _os.name == 'nt' and _os.path.isdir(file):
                continue
            # If the directory allows write access, continue 
            # trying names. On Windows, currently this test 
            # doesn't work. The implementation assumes all 
            # directories allow write access, but it really 
            # depends on the directory's discretionary and 
            # system access control lists (DACL & SACL).
            elif _os.access(dir, _os.W_OK):
                continue
            else:
                raise

    [1]: Windows sets the last error to ERROR_ACCESS_DENIED when a system
    call returns the NTSTATUS code STATUS_FILE_IS_A_DIRECTORY. This
    status code is returned by NtCreateFile and NtOpenFile when the
    target file is a directory, but the caller asked for anything
    but a directory (i.e. CreateOptions contains
    FILE_NON_DIRECTORY_FILE).

    @serhiy-storchaka
    Copy link
    Member

    There is a risk of race condition. One process can create a directory file, then other process fails to create a file with the same name file, then the first process removes directory file, then the second process handles PermissionError. The same is possible also with threads.

    @bobince
    Copy link
    Mannequin

    bobince mannequin commented Sep 27, 2019

    Attempting to answer the question "did this open call fail because the path was a directory" by implication from "do we think we ought to be able to write a file to this directory" is IMO doomed. There's no reliable way to determine whether one should be able to write to a location, short of trying to write to it. There isn't in general and there especially isn't on Windows, for the reasons discussed by Eryk and more.

    I believe Billy's patch is an improvement over what we have, in as much as it's specifically checking for the condition we care about to work around a shortcoming of Windows error reporting. I would further remove the use of os.access, which does nothing useful (it reflects the read-only file attribute, which no-one uses, and which doesn't exist for directories anyway).

    Yes, there is a race condition if the directory goes away between use and check, but it seems vanishingly unlikely this could happen by accident, and it could only happen on purpose if an attacker can guess the random filenames in which case they already have worse attacks than just making mkstemp fail. In general failure is a much better outcome than hanging for hours on end.

    We may be overthinking this. Maybe it is OK to treat all permission errors as maybe-file-exists errors, like bpo-18849 originally did, and just retry without trying to pick apart the entrails.

    ...just not quite as many as 2147483647 times. Given how unlikely an accidental filename clash is in the first place, I'm thinking a more realistic number of retries might be something like, 2.

    os.TMP_MAX probably isn't a good choice in any case, as it indicates the number of names C's tmpnam() can come up with, which (a) is unrelated to the number of names _RandomNameSequence can come up with and (b) we have no particular reason to try to completely exhaust anyway.

    @earonesty
    Copy link
    Mannequin

    earonesty mannequin commented Sep 27, 2019

    i would like to point out that the primary reason any of this nonsense exists is because of short filename restrictions.

    i've replaces nearly all of my temp file creation code in all of my project to return os.urandom(32).hex() ... which is reliable secure, fast, and doesn't require any fileops

    sure, it doesn't work on 8.3 limited systems, but maybe the NamedTemp code should check that *first*.... and *if* it's OK to use long names... just use them, otherwise revert to existing behavior

    @earonesty
    Copy link
    Mannequin

    earonesty mannequin commented Nov 20, 2019

    This is the fist of what I'm using:
    https://gist.github.com/earonesty/a052ce176e99d5a659472d0dab6ea361

    Seems OK for my use cases. There's probably issues with relying on __del__ this way. But it solves the Windows close/reopen problem, too.

    @JonathanMills JonathanMills mannequin added the 3.8 (EOL) end of life label Jan 8, 2020
    @eryksun eryksun added 3.9 only security fixes 3.10 only security fixes and removed 3.7 (EOL) end of life labels Mar 4, 2021
    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @iritkatriel iritkatriel added 3.11 only security fixes 3.12 bugs and security fixes and removed 3.9 only security fixes 3.8 (EOL) end of life labels Sep 12, 2022
    @nethe-GitHub
    Copy link

    I can't believe this issue haven't been solved after ten years. And some package really takes tempfile.TemporaryFile() for writability check. Hope Windows users enjoy the 2147483647 loops.

    @zooba
    Copy link
    Member

    zooba commented Feb 14, 2023

    Maybe there really is a reason for PathYetAnotherMakeUniqueName?

    @TheRealQuantam
    Copy link

    Ran into this issue yesterday when I tried using a module that used Numba on Windows. Somebody has probably already mentioned this, but I'm not reading this whole thread. The root of the problem is this comment in posixmodule.c:

    (Directories cannot be read-only on Windows.)

    This is NOT true. While there is no exact (single) equivalent, Windows has multiple permissions that make directories effectively read-only. In this case the permission that is lacking is FILE_ADD_FILE, which allows files to be created in that directory (an alias of FILE_WRITE_DATA, which is the effect it has when applied to files). Related is FILE_ADD_SUBDIRECTORY (an alias of FILE_APPEND_DATA).

    After a bit of searching, I found a straightforward solution in a 2004 (cough 10 years before the problem appeared in Python cough) post by old Chen. Here is a hacky workaround I made for my own use. It probably isn't sufficiently robust for production code, but it gets the job done until Python properly fixes it:

    import win32api
    import win32
    import win32file
    import pywintypes
    
    orig_access = os.access
    def my_access(path, mode, *, dir_fd=None, effective_ids=False, follow_symlinks=True):
    	res = orig_access(path, mode, dir_fd = dir_fd, effective_ids = effective_ids, follow_symlinks = follow_symlinks)
    	if not res or not mode & os.W_OK or not os.path.isdir(path):
    		return res
    	
    	try:
    		hdl = win32file.CreateFile(
    			str(path), 
    			2, # FILE_ADD_FILE
    			win32file.FILE_SHARE_READ | win32file.FILE_SHARE_WRITE | win32file.FILE_SHARE_DELETE, 
    			None, 
    			win32file.OPEN_EXISTING, 
    			win32file.FILE_FLAG_BACKUP_SEMANTICS, 
    			None,
    		)
    		win32file.CloseHandle(hdl)
    		
    	except pywintypes.error as e:
    		if e.winerror == 5: # ERROR_ACCESS_DENIED
    			return False
    		
    	except Exception as e:
    		pass
    	
    	return True
    
    os.access = my_access```
    

    @zooba
    Copy link
    Member

    zooba commented Mar 18, 2024

    Changing os.access to detect a directory (which it already does) and testing for FILE_ADD_FILE access as shown by @TheRealQuantam seems reasonable. If someone wants to work up a PR, I'm sure we could move that forward.

    I'm not sure whether it would unblock the original issue, but only because I also haven't re-read the entire thread.

    @TheRealQuantam
    Copy link

    TheRealQuantam commented Mar 18, 2024

    For an official fix it would probably be best to make os.access test FILE_ADD_FILE | FILE_ADD_SUBDIRECTORY (6), as that's the more conservative option. That is, due to there being multiple permissions in Windows it's impossible for os.access to be 100% accurate in the edge case where a directory has add file but not add subdirectory permissions (or vice-versa); in such cases I'd think it's better to report that the directory is not writable even though files or directories can be created than report that it is writable only for attempts to create things to then fail.

    I can confirm that at least in the case of Numba (which used tempfile to create temporary files) my workaround does in fact fix the issue with _mkstemp_inner (the topic of this issue). The attempt to create a temp file in a non-writable directory fails immediately for access denied, and Numba falls back to using the user's temp directory (the proper solution).

    @EsbernTK
    Copy link

    I have just encountered this issue, when huggingface_hub tried to create a temporary dir in C:\\Users. In this case it seems that os.access("C:\\Users", os.W_OK) just totally misjudges the process permissions, as it returns True, while the directory requires administrator permissions for write access.
    Both os.makedirs("C:\\Users\\tmp12345") and fp = open("C:\\Users\\tmpfile.txt", "w") raise a PermissionError: [Errno 13], so it isn't even an edgecase where the process has FILE_ADD_FILE permission but lacks FILE_ADD_SUBDIRECTORY, as @TheRealQuantam suggests.

    I also tested os.access("C:\\Windows\\System32", os.W_OK), which also returns True. A pretty shocking result.
    The only time i've made os.access return False, is when the directory does not exist.
    From this it seems that os.access does not actually check any permissions, but just if the directory actually exists or not, making its purpose a bit confusing on Windows.
    For the original error in mkdtemp, i might be missing some security/performance nuance in the original implementation and in this discussion, but if the function is misinterpreting the PermissionError to mean that the directory already exists, then why not check for that before the os.mkdir call. Furthermore, checking if the dir even exists and is a valid dir, before attempting to create a temporary directory inside it.

    def mkdtemp(suffix=None, prefix=None, dir=None):
        """User-callable function to create and return a unique temporary
        directory.  The return value is the pathname of the directory.
    
        Arguments are as for mkstemp, except that the 'text' argument is
        not accepted.
    
        The directory is readable, writable, and searchable only by the
        creating user.
    
        Caller is responsible for deleting the directory when done with it.
        """
    
        prefix, suffix, dir, output_type = _sanitize_params(prefix, suffix, dir)
        if not _os.path.exists(dir): raise FileNotFoundError(_errno.ENOENT, "No such file or directory: %r" % dir)
        if not _os.path.isdir(dir): raise NotADirectoryError(_errno.ENOTDIR, "Not a directory: %r" % dir)
    
        names = _get_candidate_names()
        if output_type is bytes:
            names = map(_os.fsencode, names)
        for seq in range(TMP_MAX):
            name = next(names)
            file = _os.path.join(dir, prefix + name + suffix)
            if os.path.exists(file): continue
            _sys.audit("tempfile.mkdtemp", file)
            _os.mkdir(file, 0o700)
            return file
        raise FileExistsError(_errno.EEXIST, "No usable temporary directory name found")

    I recognize that I do not understand the performance/stability/security implications of these changes fully, but on the surface it seems like a better approach than to rely on the try except statements from the previous code. Since the purpose of catching the PermissionError was solely to check if the file already exists, then the os.path.exists should take care of this case, while avoiding the edgecases that os.access caused in the previous code. And if _os.mkdir(file, 0o700) raises a PermissionError, it is now safe to assume that it was caused by missing permissions, and not that the file already exists. Of course there is the very unlikely race condition, where another process creates the file between the check and the _os.mkdir(file, 0o700) call. However, as noted earlier in this thread, this is exceedingly unlikely. Though if this is a genuine concern, it can be handled through a try block like before.

    try:
        _os.mkdir(file, 0o700)
    except FileExistsError:
        continue    # try again
    except PermissionError:
        # This exception is thrown when a directory with the chosen name
        # already exists on windows, or if the process does not have proper
        # permissions to create the directory.
        if (_os.name == 'nt' and _os.path.exists(file)):
            continue
        else:
            raise

    Again, i've removed the os.access check from the if statement, since its purpose is fulfilled more directly by checking if the file actually exists or not.

    @Andrej730
    Copy link
    Contributor

    Andrej730 commented Nov 27, 2024

    Met this issue too. In my case needed TemporaryFile to test whether it's possible to create a file in folder on Windows. Currently came up with this workaround, though probably it's also limited to some cases.

    from pathlib import Path
    
    
    def can_create_file(folder_path: Path) -> bool:
        import tempfile
    
        try:
            tempfile.TemporaryFile(dir=folder_path)
            return True
        except OSError:
            return False
    
    
    def can_create_file_safe(folder_path: Path) -> bool:
        import tempfile
        import os
    
        names = tempfile._get_candidate_names()
        for name in names:
            name: str
            path = folder_path / name
            if path.exists():
                continue
            try:
                path.write_text("")
            except PermissionError:
                return False
            else:
                os.unlink(path)
                return True
    
    
    path = Path(r"\\SHARED-PC\shared")
    print(path, can_create_file_safe(path))

    @Andrej730
    Copy link
    Contributor

    Andrej730 commented Nov 27, 2024

    Can someone please reconsider changing label from "perfomance" to "type-bug"?

    This issue seems to be pretty critical since it can basically make Python process to hang forever while it will be trying to create a temp file for all possible names - which makes creating temporary files with tempfile unreliable on Windows.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.10 only security fixes 3.11 only security fixes 3.12 bugs and security fixes OS-windows performance Performance or resource usage stdlib Python modules in the Lib dir
    Projects
    Status: No status
    Development

    No branches or pull requests

    10 participants