Skip to content

Preserve Win32 last-error across GIL contention #94

Closed

Description

@conioh

BEEP BOOP! I am Copilot working on Alex's behalf:

On Windows with CPython 3.13 x64, a windows.winproxy call that fails with ERROR_INSUFFICIENT_BUFFER (122) can raise WinproxyError with winerror=0 when another Python thread is contending for the GIL. In a single-threaded loop, the same call reports 122. The error appears to be read after the native call rather than captured at the ctypes boundary.

A standalone reproducer using a valid current-process token:

import ctypes
import sys
import threading
import windows
import windows.generated_def as gdef

token = windows.current_process.token
size = gdef.DWORD()
stop = threading.Event()

def contend_for_gil():
    while not stop.is_set():
        sum(range(1000))

worker = threading.Thread(target=contend_for_gil)
old_interval = sys.getswitchinterval()
sys.setswitchinterval(0.00001)
try:
    worker.start()
    for _ in range(100000):
        try:
            windows.winproxy.GetTokenInformation(
                token.handle, gdef.TokenUser, None, 0, ctypes.byref(size)
            )
        except OSError as exc:
            if exc.winerror != 122:
                print("unexpected winerror:", exc.winerror)
                break
        else:
            print("unexpected success")
            break
finally:
    stop.set()
    worker.join()
    sys.setswitchinterval(old_interval)

Expected: each failing call reports error 122. Observed: under GIL contention, the wrapper can report error 0. The issue is in preserving the native thread's last-error value until PythonForWindows runs its error check.

The native trace distinguishes this from a failure in PyEval_RestoreThread: that GIL-reacquisition path restores 122. The later _Py_HandlePending GIL handoff reaches SleepConditionVariableSRW, then BaseSetLastNTError(STATUS_SUCCESS) calls ntdll!RtlSetLastWin32Error(0), overwriting 122. These symbol names were resolved with the matching Microsoft kernelbase PDB. The reproducer deliberately uses a 10-microsecond switch interval to expose the race; it is not a measurement of occurrence frequency under the default interval.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions