Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-102304: Fix Py_INCREF() stable ABI in debug mode #104763

Merged
merged 1 commit into from
Jun 6, 2023

Conversation

vstinner
Copy link
Member

@vstinner vstinner commented May 22, 2023

When Python is built in debug mode (if the Py_REF_DEBUG macro is defined), the Py_INCREF() and Py_DECREF() function are now always implemented as opaque functions to avoid leaking implementation details like "_Py_RefTotal".

@vstinner
Copy link
Member Author

I suggest backporting the change to the 3.12 branch.

@vstinner
Copy link
Member Author

Previous work on this issue: issue #87854 and commit 3359cab: "The limited C API is now supported if Python is built in debug mode".

@vstinner
Copy link
Member Author

This change is only about fixing the limited C API.

Python itself must not be built with the limited C API. It will still be built with _Py_IncRefTotal_DO_NOT_USE_THIS() and _Py_DecRefTotal_DO_NOT_USE_THIS() functions. But these functions should not be exposed as part of the stable ABI.

@vstinner
Copy link
Member Author

In 2021, I was too shy to fix the limited C API in debug mode for any targeted Python version. I had worries about any potential performance drawbacks, even if the change only concerns Python built in debug mode (Py_DEBUG and Py_REF_DEBUG macros). IMO it's safe to remove this condition and use the opaque function call on all Python versions.

I was never happy about exposing _Py_RefTotal implementation detail as part of the stable ABI :-( Since Python 3.12, _Py_RefTotal became even more complicated: it's now a counter per interpreter, the global variable must no longer be used!

@vstinner
Copy link
Member Author

PR rebased on top of commit 421cbf3.

@vstinner
Copy link
Member Author

Oh, test_enum currently fails on the main branch, as well on my PR.

@vstinner
Copy link
Member Author

Oh, test_enum currently fails on the main branch, as well on my PR.

See #104765

@vstinner
Copy link
Member Author

PR rebased on main to get the test_enum fix.

@vstinner
Copy link
Member Author

@vstinner
Copy link
Member Author

A temporary fix was merged in main for this issue: #104766. This PR removes modified code.

#if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API) && Py_LIMITED_API+0 >= 0x030A0000
// Stable ABI for Python 3.10 built in debug mode.
#if defined(Py_REF_DEBUG) && defined(Py_LIMITED_API)
// Stable ABI for Python built in debug mode
_Py_IncRef(op);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_Py_IncRef was only added in 3.10, so I don't think the Py_LIMITED_API+0 >= 0x030A0000 check can go away.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The #else code path uses op->ob_refcnt_split and _Py_IsImmortal() which were added to Python 3.12. Choose your poison: no code path work on Python 3.9 and older.

I'm convinced that building a C extension targeting an old version of the stable ABI with a new Python never worked because of these subtle implementation details (especially around Py_INCREF/Py_DECREF). I would prefer to always convert Py_INCREF/Py_DECREF to opaque function calls in the stable ABI: the current implementation is badly broken because of that. Honestly, I don't think that relying on union, endianness, accessing direclty PyObject members, is a future proof. Extract of Py_INCREF implementation in Python 3.13:

    // Portable saturated add, branching on the carry flag and set low bits
    PY_UINT32_T cur_refcnt = op->ob_refcnt_split[PY_BIG_ENDIAN];
    PY_UINT32_T new_refcnt = cur_refcnt + 1;
    if (new_refcnt == 0) {
        return;
    }
    op->ob_refcnt_split[PY_BIG_ENDIAN] = new_refcnt;

It's not backward compatible at least.

Moreover, this change only impacts Python built in debug mode. That's really a corner case: nobody should distribute a wheel package with Python built in debug mode, especially when the stable ABI is targeted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That refcnt_split is going to change again anyway - @eduardo-elizondo FYI.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That refcnt_split is going to change again anyway - @eduardo-elizondo FYI.

which makes my point even more relevant :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C extensions built for the stable ABI with Python 3.10 or older don't know about immortal objects :-(

Copy link
Contributor

@eduardo-elizondo eduardo-elizondo Jun 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not backward compatible at least.

@vstinner let's dive a bit more into this! I don't think that this is actually an issue but I want to see if I'm missing something here.

To be more specific do you have an example of what exactly will not be backwards compatible? The semantics did change, but that does not mean that it will be a backwards incompatible change. The updates to the headers is done in a way that it doesn't matter if previous versions don't know about immortal headers, it will still work as intended.

^ Note that this comment is not necessarily about this PR as it totally makes sense to use _Py_IncRef directly in debug mode.

@ericsnowcurrently
Copy link
Member

FYI, I (mostly accidentally) merged gh-104762, even though I had meant to get some sort of resolution here first. At this point I'm going to backport that to 3.12 (and not revert it on main), since it accomplishes what we need for 3.12+.

However, I still think something along the lines of your PR, @vstinner, would be good for 3.12+, since there would be fewer symbols in the stable ABI.

@vstinner
Copy link
Member Author

I rebased my PR on the main branch.

This PR only makes sense in the main branch if it's also backported to Python 3.12.

@vstinner
Copy link
Member Author

@encukou @ericsnowcurrently: Would you mind to review the updated PR?

@vstinner vstinner requested a review from a team as a code owner May 31, 2023 11:53
@vstinner
Copy link
Member Author

vstinner commented May 31, 2023

Oh, _Py_NegativeRefcount() is no longer needed in the limited C API: now it's only exported in the stable ABI.

@ericsnowcurrently
Copy link
Member

I'll try to take a look today.

Copy link
Member

@ericsnowcurrently ericsnowcurrently left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change seems okay, aside from the discussion about the version check. I'd say this is good to go once that is resolved.

Makefile.pre.in Outdated Show resolved Hide resolved
When Python is built in debug mode (if the Py_REF_DEBUG macro is
defined), the Py_INCREF() and Py_DECREF() function are now always
implemented as opaque functions to avoid leaking implementation
details like the "_Py_RefTotal" variable.

* Remove _Py_IncRefTotal_DO_NOT_USE_THIS() and
  _Py_DecRefTotal_DO_NOT_USE_THIS() from the stable ABI.
* Remove _Py_NegativeRefcount() from limited C API.
* Fix script name in Lib/test/test_stable_abi_ctypes.py
@vstinner
Copy link
Member Author

vstinner commented Jun 6, 2023

I wrote a separated PR for limited API / stable ABI doc changes: PR #105345.

@vstinner vstinner merged commit 92022d8 into python:main Jun 6, 2023
@vstinner vstinner deleted the incref_debug branch June 6, 2023 09:15
@vstinner vstinner added the needs backport to 3.12 bug and security fixes label Jun 6, 2023
@miss-islington
Copy link
Contributor

Thanks @vstinner for the PR 🌮🎉.. I'm working now to backport this PR to: 3.12.
🐍🍒⛏🤖

@miss-islington
Copy link
Contributor

Sorry, @vstinner, I could not cleanly backport this to 3.12 due to a conflict.
Please backport using cherry_picker on command line.
cherry_picker 92022d8416d9e175800b65c4d71d4e4fb47adcb0 3.12

@vstinner
Copy link
Member Author

vstinner commented Jun 6, 2023

I merged my PR. I'm not comfortable with losing support for Python 3.9 and older, but IMO it's acceptable for a debug build. Moreover, I documented the change to communicate on it and explain how to work around it if needed.

vstinner added a commit to vstinner/cpython that referenced this pull request Jun 6, 2023
)

When Python is built in debug mode (if the Py_REF_DEBUG macro is
defined), the Py_INCREF() and Py_DECREF() function are now always
implemented as opaque functions to avoid leaking implementation
details like the "_Py_RefTotal" variable or the
_Py_DecRefTotal_DO_NOT_USE_THIS() function.

* Remove _Py_IncRefTotal_DO_NOT_USE_THIS() and
  _Py_DecRefTotal_DO_NOT_USE_THIS() from the stable ABI.
* Remove _Py_NegativeRefcount() from limited C API.

(cherry picked from commit 92022d8)
@bedevere-bot
Copy link

GH-105352 is a backport of this pull request to the 3.12 branch.

@bedevere-bot bedevere-bot removed the needs backport to 3.12 bug and security fixes label Jun 6, 2023
vstinner added a commit to vstinner/cpython that referenced this pull request Jun 6, 2023
)

When Python is built in debug mode (if the Py_REF_DEBUG macro is
defined), the Py_INCREF() and Py_DECREF() function are now always
implemented as opaque functions to avoid leaking implementation
details like the "_Py_RefTotal" variable or the
_Py_DecRefTotal_DO_NOT_USE_THIS() function.

* Remove _Py_IncRefTotal_DO_NOT_USE_THIS() and
  _Py_DecRefTotal_DO_NOT_USE_THIS() from the stable ABI.
* Remove _Py_NegativeRefcount() from limited C API.

(cherry picked from commit 92022d8)
vstinner added a commit that referenced this pull request Jun 6, 2023
…105352)

gh-102304: Fix Py_INCREF() stable ABI in debug mode (#104763)

When Python is built in debug mode (if the Py_REF_DEBUG macro is
defined), the Py_INCREF() and Py_DECREF() function are now always
implemented as opaque functions to avoid leaking implementation
details like the "_Py_RefTotal" variable or the
_Py_DecRefTotal_DO_NOT_USE_THIS() function.

* Remove _Py_IncRefTotal_DO_NOT_USE_THIS() and
  _Py_DecRefTotal_DO_NOT_USE_THIS() from the stable ABI.
* Remove _Py_NegativeRefcount() from limited C API.

(cherry picked from commit 92022d8)
@encukou
Copy link
Member

encukou commented Jun 6, 2023

I'll ping myself to look at this later: @encukou

@eduardo-elizondo
Copy link
Contributor

Just a small nit that this is not necessarily about Py_INCREF only as it also moved Py_DECREF to use _Py_DecRef in debug mode 🙂

@vstinner
Copy link
Member Author

vstinner commented Jun 9, 2023

Just a small nit that this is not necessarily about Py_INCREF only as it also moved Py_DECREF to use _Py_DecRef in debug mode slightly_smiling_face

Yes, the commit message mentions Py_INCREF and Py_DECREF. For issue and PR titles, I try to make them short, so mention a single function name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants