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: GetLastError() may be overwritten by Py_END_ALLOW_THREADS
Type: behavior Stage: test needed
Components: Windows Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: eryksun, izbyshev, paul.moore, steve.dower, tim.golden, zach.ware
Priority: normal Keywords:

Created on 2018-03-08 16:26 by steve.dower, last changed 2022-04-11 14:58 by admin.

Messages (7)
msg313447 - (view) Author: Steve Dower (steve.dower) * (Python committer) Date: 2018-03-08 16:26
Most Win32 API calls are made within Py_BEGIN_ALLOW_THREADS blocks, as they do not access Python objects and so we can release the GIL.

However, in general, error handling occurs after the Py_END_ALLOW_THREADS line. Due to the design of the Win32 API, the pattern looks like this:

    Py_BEGIN_ALLOW_THREADS
    ret = ApiCall(...);
    Py_END_ALLOW_THREADS
    if (FAILED(ret)) {
        error_code = GetLastError();
    }

However, Py_END_ALLOW_THREADS also makes Win32 API calls (to acquire the GIL), and if any of these fail then the error code may be overwritten.

Failures in Py_END_ALLOW_THREADS are either fatal (in which case we don't care about the preceding error any more) or signal a retry (in which case we *do* care about the preceding error), but in the latter case we may have lost the error code.

Further, while Win32 APIs are not _supposed_ to set the last error to ERROR_SUCCESS (0) when they succeed, some occasionally do.

We should update Py_END_ALLOW_THREADS to preserve the last error code when necessary. Ideally, if we don't have to do any work to reacquire the GIL, we shouldn't do any work to preserve the error code either.
msg313449 - (view) Author: Alexey Izbyshev (izbyshev) * (Python triager) Date: 2018-03-08 17:24
FWIW, GetLastError() docs[1] officially scare us:

Most functions that set the thread's last-error code set it when they fail. However, some functions also set the last-error code when they succeed. If the function is not documented to set the last-error code, the value returned by this function is simply the most recent last-error code to have been set; some functions set the last-error code to 0 on success and others do not.

[1] https://msdn.microsoft.com/en-us/library/windows/desktop/ms679360(v=vs.85).aspx
msg313450 - (view) Author: Steve Dower (steve.dower) * (Python committer) Date: 2018-03-08 17:29
> GetLastError() docs officially scare us

I believe this is a case where the docs were updated from intended/correct behavior to actual behavior, which happens from time to time and is never clearly marked.

As I say, they are _supposed_ to leave the error unchanged unless they return a value that indicates an error occurred, but some APIs behave incorrectly. The docs are simply documenting that fact. (It turns out reality is more useful than fantasy ;) though I do wish they were clearer about that)
msg313453 - (view) Author: Alexey Izbyshev (izbyshev) * (Python triager) Date: 2018-03-08 18:42
> Ideally, if we don't have to do any work to reacquire the GIL, we shouldn't do any work to preserve the error code either.

Before take_gil() knows whether it has to do any work, it calls MUTEX_LOCK(_PyRuntime.ceval.gil.mutex), which is either EnterCriticalSection() or AcquireSRWLockExclusive(). So unless we can be sure that those functions can't clobber the last error (or we redesign GIL to have a fast-path like an atomic compare-exchange in non-contended case), I don't see how we can avoid the last error bookkeeping.
msg313457 - (view) Author: Steve Dower (steve.dower) * (Python committer) Date: 2018-03-08 21:30
> Before take_gil() knows whether it has to do any work

I thought we had a check for when the GIL was not even initialized, but that doesn't seem to exist in master any more.

Preserving GetLastError() at the same place we do errno is probably sufficient. There shouldn't be any more of a performance penalty (and maybe we don't have to preserve errno on Windows? That will save a TLS lookup at least)
msg313461 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2018-03-08 22:16
FYI, here's a sampling of successful calls that modify the last error value.

Most Create- functions intentionally set the last error to 0 on success, such as CreateFile, CreateFileMapping, CreateSymbolicLink, CreateJobObject, CreateEvent, CreateMutex, CreateSemaphore, and CreateWaitableTimer. 

Some other functions also intentionally set the last error to 0 on success, such as SetWaitableTimer and the functions that work with security identifiers (SIDs) such as EqualSid, GetLengthSid, GetSidIdentifierAuthority, and GetSidSubAuthority.

The return value of some functions is the low DWORD of a file size, such as GetCompressedFileSize, GetFileSize, and SetFilePointer. In this case 0xFFFFFFFF is either a valid size or indicates an error. The function is forced to clarify this by setting the last error to 0 on success. GetFileSizeEx and SetFilePointerEx are preferred, since they don't have this problem.

Some functions make internal calls to CreateFile or DeviceIoControl. These cases might even set the last error to a non-zero value on success. Examples include GetVolumePathName (e.g. ERROR_MORE_DATA from DeviceIoControl), GetFinalPathNameByHandle (opening  "\\.\MountPointManager"), and GetDriveType (e.g. for junction mountpoints).
msg313462 - (view) Author: Alexey Izbyshev (izbyshev) * (Python triager) Date: 2018-03-08 22:27
@eryksun Very interesting! BTW, I looked at CreateFile docs, and the fact that it may set the last error to zero is even documented there.

@steve.dower
> maybe we don't have to preserve errno on Windows?

There are still places where errno is relied upon. PR 5784 shows one such place: os.[f]truncate(). Others are e.g. os.open() and os.lseek().
History
Date User Action Args
2022-04-11 14:58:58adminsetgithub: 77211
2021-03-19 05:26:18eryksunsetversions: + Python 3.9, Python 3.10, - Python 3.6, Python 3.7
2018-03-08 22:27:25izbyshevsetmessages: + msg313462
2018-03-08 22:16:30eryksunsetnosy: + eryksun
messages: + msg313461
2018-03-08 21:30:30steve.dowersetmessages: + msg313457
2018-03-08 18:42:45izbyshevsetmessages: + msg313453
2018-03-08 17:29:23steve.dowersetmessages: + msg313450
2018-03-08 17:24:40izbyshevsetnosy: + izbyshev
messages: + msg313449
2018-03-08 16:26:01steve.dowercreate