@@ -1575,6 +1575,12 @@ typedef struct {
15751575} _PyCodeObjectExtra ;
15761576
15771577
1578+ static inline size_t
1579+ code_extra_size (Py_ssize_t n )
1580+ {
1581+ return sizeof (_PyCodeObjectExtra ) + (n - 1 ) * sizeof (void * );
1582+ }
1583+
15781584int
15791585PyUnstable_Code_GetExtra (PyObject * code , Py_ssize_t index , void * * extra )
15801586{
@@ -1583,15 +1589,19 @@ PyUnstable_Code_GetExtra(PyObject *code, Py_ssize_t index, void **extra)
15831589 return -1 ;
15841590 }
15851591
1586- PyCodeObject * o = (PyCodeObject * ) code ;
1587- _PyCodeObjectExtra * co_extra = ( _PyCodeObjectExtra * ) o -> co_extra ;
1592+ PyCodeObject * o = (PyCodeObject * ) code ;
1593+ * extra = NULL ;
15881594
1589- if (co_extra == NULL || index < 0 || co_extra -> ce_size <= index ) {
1590- * extra = NULL ;
1595+ if (index < 0 ) {
15911596 return 0 ;
15921597 }
15931598
1594- * extra = co_extra -> ce_extras [index ];
1599+ // Lock-free read; pairs with release store in SetExtra.
1600+ _PyCodeObjectExtra * co_extra = FT_ATOMIC_LOAD_PTR_ACQUIRE (o -> co_extra );
1601+ if (co_extra != NULL && index < co_extra -> ce_size ) {
1602+ * extra = co_extra -> ce_extras [index ];
1603+ }
1604+
15951605 return 0 ;
15961606}
15971607
@@ -1601,40 +1611,81 @@ PyUnstable_Code_SetExtra(PyObject *code, Py_ssize_t index, void *extra)
16011611{
16021612 PyInterpreterState * interp = _PyInterpreterState_GET ();
16031613
1604- if (!PyCode_Check (code ) || index < 0 ||
1605- index >= interp -> co_extra_user_count ) {
1614+ // co_extra_user_count increases monotonically and is published with a
1615+ // release store, so once an index is valid it remains valid.
1616+ Py_ssize_t user_count = FT_ATOMIC_LOAD_SSIZE_ACQUIRE (
1617+ interp -> co_extra_user_count );
1618+
1619+ if (!PyCode_Check (code ) || index < 0 || index >= user_count ) {
16061620 PyErr_BadInternalCall ();
16071621 return -1 ;
16081622 }
16091623
1610- PyCodeObject * o = (PyCodeObject * ) code ;
1611- _PyCodeObjectExtra * co_extra = ( _PyCodeObjectExtra * ) o -> co_extra ;
1624+ PyCodeObject * o = (PyCodeObject * ) code ;
1625+ int res = 0 ;
16121626
1613- if (co_extra == NULL || co_extra -> ce_size <= index ) {
1614- Py_ssize_t i = (co_extra == NULL ? 0 : co_extra -> ce_size );
1615- co_extra = PyMem_Realloc (
1616- co_extra ,
1617- sizeof (_PyCodeObjectExtra ) +
1618- (interp -> co_extra_user_count - 1 ) * sizeof (void * ));
1619- if (co_extra == NULL ) {
1620- return -1 ;
1621- }
1622- for (; i < interp -> co_extra_user_count ; i ++ ) {
1623- co_extra -> ce_extras [i ] = NULL ;
1624- }
1625- co_extra -> ce_size = interp -> co_extra_user_count ;
1626- o -> co_extra = co_extra ;
1627+ Py_BEGIN_CRITICAL_SECTION (o );
1628+
1629+ _PyCodeObjectExtra * old_extra = (_PyCodeObjectExtra * ) o -> co_extra ;
1630+ Py_ssize_t old_size = (old_extra == NULL ) ? 0 : old_extra -> ce_size ;
1631+
1632+ // user_count > index is checked above.
1633+ Py_ssize_t new_size = old_size > index ? old_size : user_count ;
1634+ assert (new_size > 0 && new_size > index );
1635+
1636+ // Free-threaded builds require copy-on-write to avoid mutating
1637+ // co_extra while lock-free readers in GetExtra may be traversing it.
1638+ // GIL builds could realloc in place, but SetExtra is called rarely
1639+ // and co_extra is small, so use the same path for simplicity.
1640+ _PyCodeObjectExtra * co_extra = PyMem_Malloc (code_extra_size (new_size ));
1641+ if (co_extra == NULL ) {
1642+ PyErr_NoMemory ();
1643+ res = -1 ;
1644+ goto done ;
16271645 }
16281646
1629- if (co_extra -> ce_extras [index ] != NULL ) {
1647+ co_extra -> ce_size = new_size ;
1648+
1649+ // Copy existing extras from the old buffer.
1650+ if (old_size > 0 ) {
1651+ memcpy (co_extra -> ce_extras , old_extra -> ce_extras ,
1652+ old_size * sizeof (void * ));
1653+ }
1654+
1655+ // NULL-initialize new slots.
1656+ for (Py_ssize_t i = old_size ; i < new_size ; i ++ ) {
1657+ co_extra -> ce_extras [i ] = NULL ;
1658+ }
1659+
1660+ if (old_extra != NULL && index < old_size &&
1661+ old_extra -> ce_extras [index ] != NULL )
1662+ {
1663+ // Free the old extra value if a free function was registered.
1664+ // We assume the caller ensures no other thread is concurrently
1665+ // using the old value.
16301666 freefunc free = interp -> co_extra_freefuncs [index ];
16311667 if (free != NULL ) {
1632- free (co_extra -> ce_extras [index ]);
1668+ free (old_extra -> ce_extras [index ]);
16331669 }
16341670 }
16351671
16361672 co_extra -> ce_extras [index ] = extra ;
1637- return 0 ;
1673+
1674+ // Publish pointer and slot writes to lock-free readers.
1675+ FT_ATOMIC_STORE_PTR_RELEASE (o -> co_extra , co_extra );
1676+
1677+ if (old_extra != NULL ) {
1678+ #ifdef Py_GIL_DISABLED
1679+ // Defer container free for lock-free readers.
1680+ _PyMem_FreeDelayed (old_extra , code_extra_size (old_size ));
1681+ #else
1682+ PyMem_Free (old_extra );
1683+ #endif
1684+ }
1685+
1686+ done :;
1687+ Py_END_CRITICAL_SECTION ();
1688+ return res ;
16381689}
16391690
16401691
0 commit comments