From 8634db59e345563daa3566ab72bb0dbcd61724d9 Mon Sep 17 00:00:00 2001 From: Koolvansh07 Date: Thu, 1 Jan 2026 05:09:41 +0530 Subject: [PATCH 1/7] Fix ctypes.CDLL to honor handle parameter on POSIX systems The handle parameter was being ignored in the POSIX implementation of CDLL._load_library(), causing it to always call _dlopen() even when a valid handle was provided. This was a regression introduced in recent refactoring. This commit adds the missing handle check to match the Windows implementation behavior, and includes a regression test. Fixes gh-143304 --- Lib/ctypes/__init__.py | 2 ++ Lib/test/test_ctypes/test_loading.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index aec92f3aee2472..1c822759eca912 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -458,6 +458,8 @@ def _load_library(self, name, mode, handle, winmode): if name and name.endswith(")") and ".a(" in name: mode |= _os.RTLD_MEMBER | _os.RTLD_NOW self._name = name + if handle is not None: + return handle return _dlopen(name, mode) def __repr__(self): diff --git a/Lib/test/test_ctypes/test_loading.py b/Lib/test/test_ctypes/test_loading.py index 3b8332fbb30928..8bb8599f8f4607 100644 --- a/Lib/test/test_ctypes/test_loading.py +++ b/Lib/test/test_ctypes/test_loading.py @@ -106,6 +106,20 @@ def test_load_without_name_and_with_handle(self): lib = ctypes.WinDLL(name=None, handle=handle) self.assertIs(handle, lib._handle) + @unittest.skipIf(os.name == "nt", 'POSIX-specific test') + def test_load_without_name_and_with_handle_posix(self): + # Test that CDLL honors the handle parameter on POSIX systems + # This is a regression test for gh-143304 + if libc_name is None: + self.skipTest('could not find libc') + # First load a library normally to get a handle + lib1 = CDLL(libc_name) + handle = lib1._handle + # Now create a new CDLL instance with the same handle + lib2 = CDLL(name=None, handle=handle) + # The handle should be used directly, not ignored + self.assertIs(handle, lib2._handle) + @unittest.skipUnless(os.name == "nt", 'Windows-specific test') def test_1703286_A(self): # On winXP 64-bit, advapi32 loads at an address that does From bb4c92eb2d0113205624f1d07133743597ef5c08 Mon Sep 17 00:00:00 2001 From: Koolvansh07 Date: Thu, 1 Jan 2026 05:28:49 +0530 Subject: [PATCH 2/7] Add NEWS entry for gh-143304 --- .../next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst diff --git a/Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst b/Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst new file mode 100644 index 00000000000000..6afdbab7213804 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst @@ -0,0 +1 @@ +Fix :class:`ctypes.CDLL` to honor the ``handle`` parameter on POSIX systems. The parameter was being ignored, causing the library to always call ``_dlopen()`` even when a valid handle was provided. From d8b9ed3686cdb4b9486a16ab0814b8a43fe1567b Mon Sep 17 00:00:00 2001 From: Koolvansh07 Date: Thu, 1 Jan 2026 05:52:50 +0530 Subject: [PATCH 3/7] Address review feedback - Remove AI-generated comments from test - Use skipIf decorator instead of runtime check - Simplify NEWS entry (don't mention private _dlopen) --- Lib/test/test_ctypes/test_loading.py | 8 +-- ...-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst | 2 +- fix_demonstration.py | 51 +++++++++++++++++++ test_handle_fix.py | 25 +++++++++ test_output.txt | 0 5 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 fix_demonstration.py create mode 100644 test_handle_fix.py create mode 100644 test_output.txt diff --git a/Lib/test/test_ctypes/test_loading.py b/Lib/test/test_ctypes/test_loading.py index 8bb8599f8f4607..c2cc7a2827d75d 100644 --- a/Lib/test/test_ctypes/test_loading.py +++ b/Lib/test/test_ctypes/test_loading.py @@ -107,17 +107,11 @@ def test_load_without_name_and_with_handle(self): self.assertIs(handle, lib._handle) @unittest.skipIf(os.name == "nt", 'POSIX-specific test') + @unittest.skipIf(libc_name is None, 'could not find libc') def test_load_without_name_and_with_handle_posix(self): - # Test that CDLL honors the handle parameter on POSIX systems - # This is a regression test for gh-143304 - if libc_name is None: - self.skipTest('could not find libc') - # First load a library normally to get a handle lib1 = CDLL(libc_name) handle = lib1._handle - # Now create a new CDLL instance with the same handle lib2 = CDLL(name=None, handle=handle) - # The handle should be used directly, not ignored self.assertIs(handle, lib2._handle) @unittest.skipUnless(os.name == "nt", 'Windows-specific test') diff --git a/Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst b/Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst index 6afdbab7213804..826b2e9a126d36 100644 --- a/Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst +++ b/Misc/NEWS.d/next/Library/2026-01-01-05-26-00.gh-issue-143304.Kv7x9Q.rst @@ -1 +1 @@ -Fix :class:`ctypes.CDLL` to honor the ``handle`` parameter on POSIX systems. The parameter was being ignored, causing the library to always call ``_dlopen()`` even when a valid handle was provided. +Fix :class:`ctypes.CDLL` to honor the ``handle`` parameter on POSIX systems. diff --git a/fix_demonstration.py b/fix_demonstration.py new file mode 100644 index 00000000000000..6faa556cada9d0 --- /dev/null +++ b/fix_demonstration.py @@ -0,0 +1,51 @@ +# Demonstration of the fix for gh-143304 +# This shows the before and after behavior + +print("=" * 60) +print("BEFORE THE FIX (main branch):") +print("=" * 60) +print(""" +def _load_library(self, name, mode, handle, winmode): + # ... iOS/tvOS/watchOS .fwork handling ... + # ... AIX archive handling ... + self._name = name + return _dlopen(name, mode) # ❌ handle parameter is IGNORED! +""") + +print("\n" + "=" * 60) +print("AFTER THE FIX (our branch):") +print("=" * 60) +print(""" +def _load_library(self, name, mode, handle, winmode): + # ... iOS/tvOS/watchOS .fwork handling ... + # ... AIX archive handling ... + self._name = name + if handle is not None: # ✅ Check if handle was provided + return handle # ✅ Use the provided handle + return _dlopen(name, mode) # Only call _dlopen if no handle +""") + +print("\n" + "=" * 60) +print("COMPARISON WITH WINDOWS VERSION:") +print("=" * 60) +print(""" +Windows _load_library (already correct): + self._name = name + if handle is not None: + return handle + return _LoadLibrary(self._name, winmode) + +POSIX _load_library (now fixed to match): + self._name = name + if handle is not None: # ← This was missing! + return handle # ← This was missing! + return _dlopen(name, mode) +""") + +print("\n" + "=" * 60) +print("VERIFICATION:") +print("=" * 60) +print("✓ The fix adds 2 lines to the POSIX implementation") +print("✓ The fix matches the Windows implementation pattern") +print("✓ The fix allows users to pass an existing handle to CDLL") +print("✓ The fix prevents unnecessary _dlopen() calls when handle is provided") diff --git a/test_handle_fix.py b/test_handle_fix.py new file mode 100644 index 00000000000000..bd8e26d15887fc --- /dev/null +++ b/test_handle_fix.py @@ -0,0 +1,25 @@ +import ctypes +import sys +import os + +print(f"OS name: {os.name}") +print(f"Platform: {sys.platform}") + +if os.name == "nt": + print("\nTesting Windows WinDLL...") + try: + # Get a handle from kernel32 + handle = ctypes.windll.kernel32._handle + print(f"Got kernel32 handle: {handle}") + + # Try to create a new WinDLL with that handle + print("Creating WinDLL with name=None and handle...") + lib = ctypes.WinDLL(name=None, handle=handle) + print(f"Success! Created lib with handle: {lib._handle}") + print(f"Handles match: {handle == lib._handle}") + except Exception as e: + print(f"Error: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() +else: + print("Not on Windows, skipping test") diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 32eb34d574940179b043103220c766a62e368256 Mon Sep 17 00:00:00 2001 From: Koolvansh07 Date: Thu, 1 Jan 2026 05:53:52 +0530 Subject: [PATCH 4/7] Move handle check to beginning to avoid unnecessary computation --- Lib/ctypes/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 1c822759eca912..0d5a9fa4244775 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -440,6 +440,9 @@ def _load_library(self, name, mode, handle, winmode): else: def _load_library(self, name, mode, handle, winmode): + if handle is not None: + self._name = name + return handle # If the filename that has been provided is an iOS/tvOS/watchOS # .fwork file, dereference the location to the true origin of the # binary. @@ -458,8 +461,6 @@ def _load_library(self, name, mode, handle, winmode): if name and name.endswith(")") and ".a(" in name: mode |= _os.RTLD_MEMBER | _os.RTLD_NOW self._name = name - if handle is not None: - return handle return _dlopen(name, mode) def __repr__(self): From 6d95effd556b6a26c53700fbcf7c9ab4d56b5fb1 Mon Sep 17 00:00:00 2001 From: Koolvansh07 Date: Thu, 1 Jan 2026 05:58:43 +0530 Subject: [PATCH 5/7] Remove debug files that were accidentally committed --- fix_demonstration.py | 51 -------------------------------------------- test_handle_fix.py | 25 ---------------------- test_output.txt | 0 3 files changed, 76 deletions(-) delete mode 100644 fix_demonstration.py delete mode 100644 test_handle_fix.py delete mode 100644 test_output.txt diff --git a/fix_demonstration.py b/fix_demonstration.py deleted file mode 100644 index 6faa556cada9d0..00000000000000 --- a/fix_demonstration.py +++ /dev/null @@ -1,51 +0,0 @@ -# Demonstration of the fix for gh-143304 -# This shows the before and after behavior - -print("=" * 60) -print("BEFORE THE FIX (main branch):") -print("=" * 60) -print(""" -def _load_library(self, name, mode, handle, winmode): - # ... iOS/tvOS/watchOS .fwork handling ... - # ... AIX archive handling ... - self._name = name - return _dlopen(name, mode) # ❌ handle parameter is IGNORED! -""") - -print("\n" + "=" * 60) -print("AFTER THE FIX (our branch):") -print("=" * 60) -print(""" -def _load_library(self, name, mode, handle, winmode): - # ... iOS/tvOS/watchOS .fwork handling ... - # ... AIX archive handling ... - self._name = name - if handle is not None: # ✅ Check if handle was provided - return handle # ✅ Use the provided handle - return _dlopen(name, mode) # Only call _dlopen if no handle -""") - -print("\n" + "=" * 60) -print("COMPARISON WITH WINDOWS VERSION:") -print("=" * 60) -print(""" -Windows _load_library (already correct): - self._name = name - if handle is not None: - return handle - return _LoadLibrary(self._name, winmode) - -POSIX _load_library (now fixed to match): - self._name = name - if handle is not None: # ← This was missing! - return handle # ← This was missing! - return _dlopen(name, mode) -""") - -print("\n" + "=" * 60) -print("VERIFICATION:") -print("=" * 60) -print("✓ The fix adds 2 lines to the POSIX implementation") -print("✓ The fix matches the Windows implementation pattern") -print("✓ The fix allows users to pass an existing handle to CDLL") -print("✓ The fix prevents unnecessary _dlopen() calls when handle is provided") diff --git a/test_handle_fix.py b/test_handle_fix.py deleted file mode 100644 index bd8e26d15887fc..00000000000000 --- a/test_handle_fix.py +++ /dev/null @@ -1,25 +0,0 @@ -import ctypes -import sys -import os - -print(f"OS name: {os.name}") -print(f"Platform: {sys.platform}") - -if os.name == "nt": - print("\nTesting Windows WinDLL...") - try: - # Get a handle from kernel32 - handle = ctypes.windll.kernel32._handle - print(f"Got kernel32 handle: {handle}") - - # Try to create a new WinDLL with that handle - print("Creating WinDLL with name=None and handle...") - lib = ctypes.WinDLL(name=None, handle=handle) - print(f"Success! Created lib with handle: {lib._handle}") - print(f"Handles match: {handle == lib._handle}") - except Exception as e: - print(f"Error: {type(e).__name__}: {e}") - import traceback - traceback.print_exc() -else: - print("Not on Windows, skipping test") diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index e69de29bb2d1d6..00000000000000 From d3f65b6f2931eff5d45466a512e59773644649c6 Mon Sep 17 00:00:00 2001 From: Koolvansh07 Date: Thu, 1 Jan 2026 06:06:14 +0530 Subject: [PATCH 6/7] Move handle check to end of function as suggested by reviewer The name parameter may be modified by .fwork and AIX processing, so we need to process it before checking the handle. --- Lib/ctypes/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 0d5a9fa4244775..1c822759eca912 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -440,9 +440,6 @@ def _load_library(self, name, mode, handle, winmode): else: def _load_library(self, name, mode, handle, winmode): - if handle is not None: - self._name = name - return handle # If the filename that has been provided is an iOS/tvOS/watchOS # .fwork file, dereference the location to the true origin of the # binary. @@ -461,6 +458,8 @@ def _load_library(self, name, mode, handle, winmode): if name and name.endswith(")") and ".a(" in name: mode |= _os.RTLD_MEMBER | _os.RTLD_NOW self._name = name + if handle is not None: + return handle return _dlopen(name, mode) def __repr__(self): From cdb8f6ef7af6a4bf742ed5728bfe9983c66ad142 Mon Sep 17 00:00:00 2001 From: Koolvansh07 Date: Thu, 1 Jan 2026 06:07:53 +0530 Subject: [PATCH 7/7] Update test assertion to match (actual, expected) convention --- Lib/test/test_ctypes/test_loading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_loading.py b/Lib/test/test_ctypes/test_loading.py index c2cc7a2827d75d..343f6a07c0a32c 100644 --- a/Lib/test/test_ctypes/test_loading.py +++ b/Lib/test/test_ctypes/test_loading.py @@ -112,7 +112,7 @@ def test_load_without_name_and_with_handle_posix(self): lib1 = CDLL(libc_name) handle = lib1._handle lib2 = CDLL(name=None, handle=handle) - self.assertIs(handle, lib2._handle) + self.assertIs(lib2._handle, handle) @unittest.skipUnless(os.name == "nt", 'Windows-specific test') def test_1703286_A(self):