1515import uuid
1616import datetime
1717import warnings
18+ import weakref
19+ import sys
1820from typing import List , Union , Any , Optional , Tuple , Sequence , TYPE_CHECKING , Iterable
1921from mssql_python .constants import ConstantsDDBC as ddbc_sql_const , SQLTypes
2022from mssql_python .helpers import check_error
@@ -98,6 +100,13 @@ def __init__(self, connection: "Connection", timeout: int = 0) -> None:
98100 self ._inputsizes : Optional [List [Union [int , Tuple [Any , ...]]]] = None
99101 # self.connection.autocommit = False
100102 self .hstmt : Optional [Any ] = None
103+ self ._handle_freed : bool = False # Track if statement handle has been freed
104+ self ._invalidated : bool = False # Track if cursor was invalidated by connection close
105+
106+ # Store weak reference to connection to check if it's closed
107+ # This prevents segfault when trying to free handles after connection close
108+ self ._connection_ref : Any = weakref .ref (connection )
109+
101110 self ._initialize_cursor ()
102111 self .description : Optional [
103112 List [
@@ -728,14 +737,17 @@ def _reset_cursor(self) -> None:
728737 # Reinitialize the statement handle
729738 self ._initialize_cursor ()
730739
731- def close (self ) -> None :
740+ def close (self , from_del : bool = False ) -> None :
732741 """
733742 Close the connection now (rather than whenever .__del__() is called).
734743 Idempotent: subsequent calls have no effect and will be no-ops.
735744
736745 The cursor will be unusable from this point forward; an InterfaceError
737746 will be raised if any operation (other than close) is attempted with the cursor.
738747 This is a deviation from pyodbc, which raises an exception if the cursor is already closed.
748+
749+ Args:
750+ from_del: Internal flag - when True, handle freeing is skipped to prevent segfaults during GC.
739751 """
740752 if self .closed :
741753 # Do nothing - not calling _check_closed() here since we want this to be idempotent
@@ -751,10 +763,81 @@ def close(self) -> None:
751763 except Exception as e : # pylint: disable=broad-exception-caught
752764 logger .warning ("Error removing cursor from connection tracking: %s" , e )
753765
754- if self .hstmt :
755- self .hstmt .free ()
756- self .hstmt = None
757- logger .debug ("SQLFreeHandle succeeded" )
766+ # Free statement handle with protection against double-free
767+ # CRITICAL SAFETY #0: If called from __del__, do NOTHING - just return
768+ # During GC, ANY object access can trigger segfaults due to partially destroyed state
769+ # Better to leak resources than crash - OS cleans up at process exit
770+ if from_del :
771+ return
772+
773+ if self .hstmt and not self ._handle_freed :
774+
775+ # CRITICAL SAFETY #1: Check if cursor was explicitly invalidated by connection
776+ # This happens when connection.close() invalidates all cursors BEFORE freeing connection handle
777+ if hasattr (self , '_invalidated' ) and self ._invalidated :
778+ self .hstmt = None
779+ self ._handle_freed = True
780+ return
781+
782+ # CRITICAL SAFETY #2: Check if parent connection is closed via _connection attribute
783+ # This is an additional safety check in case weak reference approach fails
784+ try :
785+ if hasattr (self , '_connection' ) and self ._connection :
786+ if hasattr (self ._connection , '_closed' ) and self ._connection ._closed :
787+ # Connection is closed, skip freeing handle
788+ self .hstmt = None
789+ self ._handle_freed = True
790+ return
791+ if hasattr (self ._connection , '_connection_closed' ) and self ._connection ._connection_closed :
792+ # Connection closed flag set, skip freeing handle
793+ self .hstmt = None
794+ self ._handle_freed = True
795+ return
796+ except (AttributeError , ReferenceError ):
797+ # Connection might be in invalid state, skip freeing to be safe
798+ self .hstmt = None
799+ self ._handle_freed = True
800+ return
801+
802+ # CRITICAL SAFETY #3: Skip handle freeing during interpreter shutdown
803+ # During shutdown, ODBC resources may already be freed, causing segfault
804+ if sys .is_finalizing ():
805+ self .hstmt = None
806+ self ._handle_freed = True
807+ return
808+
809+ # Check if parent connection was closed or garbage collected
810+ # First check if we even have a connection reference attribute
811+ if not hasattr (self , '_connection_ref' ):
812+ # Old cursor without weak ref - conservatively skip free
813+ self .hstmt = None
814+ self ._handle_freed = True
815+ return
816+
817+ # Try to get connection from weak reference
818+ try :
819+ conn = self ._connection_ref ()
820+ except Exception :
821+ # Weak ref might be invalid during cleanup - skip free to be safe
822+ self .hstmt = None
823+ self ._handle_freed = True
824+ return
825+
826+ # Skip free if connection is closed or garbage collected
827+ if conn is None or (hasattr (conn , '_connection_closed' ) and conn ._connection_closed ):
828+ # Connection closed/GC'd - ODBC driver already freed handles
829+ self .hstmt = None
830+ self ._handle_freed = True
831+ else :
832+ # Connection is still open - safe to free statement handle
833+ try :
834+ self .hstmt .free ()
835+ self ._handle_freed = True
836+ except Exception :
837+ # Silently handle errors during cleanup
838+ pass
839+ finally :
840+ self .hstmt = None
758841 self ._clear_rownumber ()
759842 self .closed = True
760843
@@ -2760,7 +2843,8 @@ def __del__(self):
27602843 """
27612844 if "closed" not in self .__dict__ or not self .closed :
27622845 try :
2763- self .close ()
2846+ # Pass from_del=True to skip handle freeing (prevents segfaults during GC)
2847+ self .close (from_del = True )
27642848 except Exception as e : # pylint: disable=broad-exception-caught
27652849 # Don't raise an exception in __del__, just log it
27662850 # If interpreter is shutting down, we might not have logging set up
0 commit comments