Repository: kivy/kivy

Issue:
test_clock unit-test is crashing on Windows Python>=3.9 (and still passing)
**Software Versions**
* Python: 3.9-3.11 (but fine earlier)
* OS: Windows, running in GitHub autotests.
* Kivy: Master ([6ae4fc2](https://github.com/kivy/kivy/commit/6ae4fc292373f39ce34563d6c448e8ee7c8fa193))
* Kivy installation method: Part of Github workflow

When the Github action runs the unit tests on Windows unit tests, it works fine on Python 3.7 and 3.8.

On Python 3.9 and later the logs contain a Windows fatal exception, which I understand to correspond to a Heap Corruption.

That's bad, but what is worse is that despite this, the test passes!


	Windows fatal exception: code 0xc0000374

	Thread 0x0000178c (most recent call first):
	<no Python frame>

	Thread 0x0000178c (most recent call first):
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\threading.py", line 312 in wait
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\threading.py", line 581 in wait
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\threading.py", line 904 in start
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pytest_timeout.py", line 282 in pytest_timeout_set_timer
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_callers.py", line 77 in _multicall
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_manager.py", line 115 in _hookexec
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_hooks.py", line 493 in __call__
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pytest_timeout.py", line 160 in pytest_runtest_protocol
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_callers.py", line 62 in _multicall
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_manager.py", line 115 in _hookexec
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_hooks.py", line 493 in __call__
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\_pytest\main.py", line 350 in pytest_runtestloop
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_callers.py", line 77 in _multicall
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_manager.py", line 115 in _hookexec
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_hooks.py", line 493 in __call__
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\_pytest\main.py", line 325 in _main
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\_pytest\main.py", line 271 in wrap_session
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\_pytest\main.py", line 318 in pytest_cmdline_main
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_callers.py", line 77 in _multicall
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_manager.py", line 115 in _hookexec
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pluggy\_hooks.py", line 493 in __call__
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\_pytest\config\__init__.py", line 169 in main
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\_pytest\config\__init__.py", line 192 in console_main
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\site-packages\pytest\__main__.py", line 5 in <module>
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\runpy.py", line 87 in _run_code
	  File "C:\hostedtoolcache\windows\Python\3.9.13\x86\lib\runpy.py", line 197 in _run_module_as_main
	kivy\tests\test_clock.py ..



Discussion:
Been trying to run tests on my own Windows machine. The test in which this fails moves around - if I `--ignore` the one that fails, it dies in the next instead.

I used "--timeout=400 --cov=kivy --cov-branch" paramters because the ci script does. When I removed those, it ran through fine.

(Does that mean something about the params caused it, or they just use up more memory and make the heap corruption trigger earlier? No idea.)





Seems related to coverage, not timeout.

Noted `coverage` was several versions behind. Upgraded from 6.4.2 to 7.3.2. No improvement. Downgraded to 5.5. No improvement.

Managed to get the bug without running coverage. Bah. Not related to coverage at all.

Even if seems to break on `test_clock`, I'm not sure the cause is `test_clock` (or anything related to clock).

Can you try (locally or in the CI) the following?
- Clone or pull latest `master`
- On top of `master`: `git revert adb8344227d3095be026ad27fa9c07e130e7418a -n`
- Run the tests again.
- Does it fail?
Yeah, not `test_clock` - it dies in different positions on different runs.

Tried running the above tests in a totally fresh install, but stumbled at every turn on getting the dependencies and builds to work, like a newb. We don't have instructions for setting up Windows dev environment.

So, I ran it in my existing environment which has been through the wars.

Tests crashed out most of the time before the revert, depending on pytest option.

Tests didn't crash out after running about 5 times after the revert. (Lots of warnings, spurious logs messages and one intemittent failure in test_pyinstaller.py)


> Tests didn't crash out after running about 5 times after the revert. (Lots of warnings, spurious logs messages and one intemittent failure in test_pyinstaller.py)

So that confirms the issue has been introduced on https://github.com/kivy/kivy/pull/8342

> We don't have instructions for setting up Windows dev environment.

Instead this confirms the [need of a hero who talks to Windows PCs ](https://github.com/orgs/kivy/discussions/13)

Resolving patch:
diff --git a/kivy/core/clipboard/clipboard_winctypes.py b/kivy/core/clipboard/clipboard_winctypes.py
--- a/kivy/core/clipboard/clipboard_winctypes.py
+++ b/kivy/core/clipboard/clipboard_winctypes.py
@@ -18,29 +18,52 @@
 c_char_p = ctypes.c_char_p
 c_wchar_p = ctypes.c_wchar_p
 
+GlobalLock = kernel32.GlobalLock
+GlobalLock.argtypes = [wintypes.HGLOBAL]
+GlobalLock.restype = wintypes.LPVOID
+
+GlobalUnlock = kernel32.GlobalUnlock
+GlobalUnlock.argtypes = [wintypes.HGLOBAL]
+GlobalUnlock.restype = wintypes.BOOL
+
+CF_UNICODETEXT = 13
+GMEM_MOVEABLE = 0x0002
+
 
 class ClipboardWindows(ClipboardBase):
 
+    def _copy(self, data):
+        self._ensure_clipboard()
+        self.put(data, self._clip_mime_type)
+
     def get(self, mimetype='text/plain'):
         GetClipboardData = user32.GetClipboardData
         GetClipboardData.argtypes = [wintypes.UINT]
         GetClipboardData.restype = wintypes.HANDLE
 
         user32.OpenClipboard(user32.GetActiveWindow())
-        # Standard Clipboard Format "1" is "CF_TEXT"
-        pcontents = GetClipboardData(13)
+
+        # GetClipboardData returns a HANDLE to the clipboard data
+        # which is a memory object containing the data
+        # See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclipboarddata  # noqa: E501
+        pcontents = GetClipboardData(CF_UNICODETEXT)
 
         # if someone pastes a FILE, the content is None for SCF 13
         # and the clipboard is locked if not closed properly
         if not pcontents:
             user32.CloseClipboard()
             return ''
-        data = c_wchar_p(pcontents).value.encode(self._encoding)
+
+        # The handle returned by GetClipboardData is a memory object
+        # and needs to be locked to get the actual pointer to the data
+        pcontents_locked = GlobalLock(pcontents)
+        data = c_wchar_p(pcontents_locked).value
+        GlobalUnlock(pcontents)
+
         user32.CloseClipboard()
         return data
 
-    def put(self, text, mimetype='text/plain'):
-
+    def put(self, text, mimetype="text/plain"):
         SetClipboardData = user32.SetClipboardData
         SetClipboardData.argtypes = [wintypes.UINT, wintypes.HANDLE]
         SetClipboardData.restype = wintypes.HANDLE
@@ -52,20 +75,31 @@ def put(self, text, mimetype='text/plain'):
         user32.OpenClipboard(user32.GetActiveWindow())
         user32.EmptyClipboard()
 
-        # this allocates memory for the string and returns a handle to it
-        # allocates fixed memory, len + 2 is for the null character
-        # no need to  call GlobalFree here as SetClipboardData will do for you
-        # noqa: E501 see: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata#parameters
-        GMEM_FIXED = 0x0000
-        hCd = GlobalAlloc(GMEM_FIXED, len(text) + 2)
-
-        # copy the string into the allocated memory
-        msvcrt.wcscpy(c_wchar_p(hCd), text)
-
-        # standard clipboard format for unicode text
-        # noqa: E501 see https://learn.microsoft.com/en-us/windows/win32/dataxchg/standard-clipboard-formats#constants
-        CF_UNICODETEXT = 13
-        # set the clipboard data, later used by GetClipboardData()
+        # The wsclen function returns the number of
+        # wide characters in a string (not including the null character)
+        text_len = msvcrt.wcslen(text) + 1
+
+        # According to the docs regarding SetClipboardDatam, if the hMem
+        # parameter identifies a memory object, the object must have
+        # been allocated using the GMEM_MOVEABLE flag.
+        # See: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setclipboarddata  # noqa: E501
+        # The size of the memory object is the number of wide characters in
+        # the string plus one for the terminating null character
+        hCd = GlobalAlloc(
+            GMEM_MOVEABLE, ctypes.sizeof(ctypes.c_wchar) * text_len
+        )
+
+        # Since the memory object is allocated with GMEM_MOVEABLE, should be
+        # locked to get the actual pointer to the data.
+        hCd_locked = GlobalLock(hCd)
+        ctypes.memmove(
+            c_wchar_p(hCd_locked),
+            c_wchar_p(text),
+            ctypes.sizeof(ctypes.c_wchar) * text_len,
+        )
+        GlobalUnlock(hCd)
+
+        # Finally, set the clipboard data (and then close the clipboard)
         SetClipboardData(CF_UNICODETEXT, hCd)
         user32.CloseClipboard()
 


Test patch: