1212 TracebackAndGotoHandler ,
1313 c_array_initializer ,
1414)
15- from mypyc .common import GENERATOR_ATTRIBUTE_PREFIX , HAVE_IMMORTAL , NATIVE_PREFIX , REG_PREFIX
15+ from mypyc .common import (
16+ GENERATOR_ATTRIBUTE_PREFIX ,
17+ HAVE_IMMORTAL ,
18+ IS_FREE_THREADED ,
19+ NATIVE_PREFIX ,
20+ REG_PREFIX ,
21+ )
1622from mypyc .ir .class_ir import ClassIR
1723from mypyc .ir .func_ir import FUNC_CLASSMETHOD , FUNC_STATICMETHOD , FuncDecl , FuncIR , all_values
1824from mypyc .ir .ops import (
8389 is_int_rprimitive ,
8490 is_none_rprimitive ,
8591 is_pointer_rprimitive ,
92+ is_simple_refcounted_pointer ,
8693 is_tagged ,
8794)
8895
@@ -394,6 +401,35 @@ def get_attr_expr(self, obj: str, op: GetAttr | SetAttr, decl_cl: ClassIR) -> st
394401 cast = f"({ decl_cl .struct_name (self .emitter .names )} *)"
395402 return f"({ cast } { obj } )->{ self .emitter .attr (op .attr )} "
396403
404+ def emit_load_attr_take_ref (
405+ self , dest : str , obj : str , op : GetAttr , cl : ClassIR , attr_rtype : RType , attr_expr : str
406+ ) -> bool :
407+ """Emit the load of a native attribute into 'dest', taking a new reference.
408+
409+ On free-threaded builds, reading a single reference-counted 'PyObject *' field
410+ and taking a new reference must be done atomically to avoid a use-after-free
411+ race with a concurrent setter. CPy_GetAttrRef performs the load and incref
412+ atomically and returns a new reference (or NULL if undefined), so callers must
413+ NOT emit a separate inc_ref. Return True in that case so the caller can skip it.
414+
415+ Final attributes are never rebound (no setter), so there is no concurrent writer
416+ and no use-after-free window; an owned read uses the cheaper CPy_GetAttrRefFinal
417+ (a plain load + incref). Borrowed reads keep the plain load: they are only emitted
418+ for attributes safe to borrow on free-threaded builds (Final and vec attrs -- see
419+ transform_member_expr in irbuild), whose values live as long as their container.
420+ The default (GIL) build always takes the plain-load path and increfs separately.
421+ """
422+ use_get_attr_ref = (
423+ IS_FREE_THREADED and is_simple_refcounted_pointer (attr_rtype ) and not op .is_borrowed
424+ )
425+ if use_get_attr_ref and cl .is_final_attr (op .attr ):
426+ self .emitter .emit_line (f"{ dest } = CPy_GetAttrRefFinal((PyObject **)&{ attr_expr } );" )
427+ elif use_get_attr_ref :
428+ self .emitter .emit_line (f"{ dest } = CPy_GetAttrRef((PyObject **)&{ attr_expr } );" )
429+ else :
430+ self .emitter .emit_line (f"{ dest } = { attr_expr } ;" )
431+ return use_get_attr_ref
432+
397433 def visit_get_attr (self , op : GetAttr ) -> None :
398434 if op .allow_error_value :
399435 self .get_attr_with_allow_error_value (op )
@@ -427,7 +463,9 @@ def visit_get_attr(self, op: GetAttr) -> None:
427463 else :
428464 # Otherwise, use direct or offset struct access.
429465 attr_expr = self .get_attr_expr (obj , op , decl_cl )
430- self .emitter .emit_line (f"{ dest } = { attr_expr } ;" )
466+ use_get_attr_ref = self .emit_load_attr_take_ref (
467+ dest , obj , op , cl , attr_rtype , attr_expr
468+ )
431469 always_defined = cl .is_always_defined (op .attr )
432470 merged_branch = None
433471 if not always_defined :
@@ -458,7 +496,7 @@ def visit_get_attr(self, op: GetAttr) -> None:
458496 )
459497 )
460498
461- if attr_rtype .is_refcounted and not op .is_borrowed :
499+ if attr_rtype .is_refcounted and not op .is_borrowed and not use_get_attr_ref :
462500 if not merged_branch and not always_defined :
463501 self .emitter .emit_line ("} else {" )
464502 self .emitter .emit_inc_ref (dest , attr_rtype )
@@ -480,16 +518,20 @@ def get_attr_with_allow_error_value(self, op: GetAttr) -> None:
480518 cl = rtype .class_ir
481519 attr_rtype , decl_cl = cl .attr_details (op .attr )
482520
483- # Direct struct access without NULL check
484521 attr_expr = self .get_attr_expr (obj , op , decl_cl )
485- self .emitter .emit_line (f"{ dest } = { attr_expr } ;" )
486-
487- # Only emit inc_ref if not NULL
488- if attr_rtype .is_refcounted and not op .is_borrowed :
489- check = self .error_value_check (op , "!=" )
490- self .emitter .emit_line (f"if ({ check } ) {{" )
491- self .emitter .emit_inc_ref (dest , attr_rtype )
492- self .emitter .emit_line ("}" )
522+ # On free-threaded builds this takes a new reference atomically (see
523+ # emit_load_attr_take_ref). CPy_GetAttrRef returns NULL when the field is
524+ # undefined, which is precisely the error value here, so the "NULL without
525+ # AttributeError" behavior is preserved (this op has error_kind ERR_NEVER).
526+ # is_simple_refcounted_pointer excludes tuples/vecs, so error_value_check's
527+ # special cases only matter on the plain-load path (where no ref was taken).
528+ if not self .emit_load_attr_take_ref (dest , obj , op , cl , attr_rtype , attr_expr ):
529+ # Only emit inc_ref if not NULL
530+ if attr_rtype .is_refcounted and not op .is_borrowed :
531+ check = self .error_value_check (op , "!=" )
532+ self .emitter .emit_line (f"if ({ check } ) {{" )
533+ self .emitter .emit_inc_ref (dest , attr_rtype )
534+ self .emitter .emit_line ("}" )
493535
494536 def next_branch (self ) -> Branch | None :
495537 if self .op_index + 1 < len (self .ops ):
@@ -529,6 +571,23 @@ def visit_set_attr(self, op: SetAttr) -> None:
529571 op .attr ,
530572 )
531573 )
574+ elif IS_FREE_THREADED and is_simple_refcounted_pointer (attr_rtype ):
575+ # In free-threaded builds, publishing a single reference-counted
576+ # 'PyObject *' field must be atomic so a concurrent reader (see
577+ # CPy_GetAttrRef) never observes a torn pointer or a freed value.
578+ # Both helpers steal the reference to src.
579+ attr_expr = self .get_attr_expr (obj , op , decl_cl )
580+ if op .is_init :
581+ # The attribute is known to be previously undefined (NULL), so
582+ # there is no old value to reclaim; a relaxed store suffices
583+ # (self's later publication provides the release barrier -- see
584+ # CPy_InitAttrRef).
585+ self .emitter .emit_line (f"CPy_InitAttrRef((PyObject **)&{ attr_expr } , { src } );" )
586+ else :
587+ # Atomically swap in the new value and reclaim the old one.
588+ self .emitter .emit_line (f"CPy_SetAttrRef((PyObject **)&{ attr_expr } , { src } );" )
589+ if op .error_kind == ERR_FALSE :
590+ self .emitter .emit_line (f"{ dest } = 1;" )
532591 else :
533592 # ...and struct access for normal attributes.
534593 attr_expr = self .get_attr_expr (obj , op , decl_cl )
0 commit comments