Visitar URL original
3.13.2: cast in Py_XDECREF causes runtime failures with immortal objects · Issue #135746 · python/cpython · GitHub
Skip to content

3.13.2: cast in Py_XDECREF causes runtime failures with immortal objects #135746

Description

@Jejos

Bug report

Bug description:

In Python 3.13.2 I use

Py_XDECREF(PyObject *op) 

It is called at the end of a function-call to load an image (cv2.imread) which fails, resulting in a None value (not NULL!). For Python 3.9 Py_XDECREF works just fine, without crash. For Python 3.13.2 the program, compiled with VS2022 (64 Bit), raises a "Run-Time Check Failure #1 - A cast to a smaller data type has caused a loss of data." and my application crashes ("/RTC1 = Basic Runtime Checks enabled").

I followed up the calls:

    Py_DECREF
        _Py_IsImmortal
            #elif SIZEOF_VOID_P > 4
                _Py_CAST(PY_INT32_T, op->ob_refcnt)     <-- crash

The value is

op->ob_refcnt == 0x0000000100000000

To fix this problem, I propose, that the type cast to 32 bit should be changed to

_Py_CAST(PY_INT32_T, op->ob_refcnt & 0xFFFFFFFF)

CPython versions tested on:

3.13

Operating systems tested on:

Windows

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Jun 20, 2025
  2. added
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    pendingThe issue will be closed if no feedback is provided
    on Jun 20, 2025
  3. picnixz commented on Jun 20, 2025

    @picnixz
    Member

    Can you check with Python 3.13.5? I think we changed something concerning checks on immortal objects. Also, this build failure hasn't been caught by GHA so I'm surprised.

  4. changed the title [-]Py_XDECREF | Run-Time Check Failure | VS2022 | Python 3.13.2[/-] [+]3.13.2: cast in Py_XDECREF causes build failures with immortal objects[/+] on Jun 20, 2025
  5. Jejos commented on Jun 20, 2025

    @Jejos
    Author

    Hi picnixz, I just updated to 3.13.5, but the run-time check failure still exists and my program throws an exception.

  6. picnixz commented on Jun 20, 2025

    @picnixz
    Member

    Can you provide a PoC please? also, I believe that it's because of warnings treated as errors (I'm actually surprised that Windows complains here because ob_count is a uint32_t so the cast to int32_t should be safe).

    cc @chris-eibl as a Windows expert

  7. Jejos commented on Jun 20, 2025

    @Jejos
    Author

    I believe that it's because of warnings treated as errors

    Yes, that's true for my project.

    On my system, VS2022 (64 Bit) says: op->ob_recnt is __int64.

  8. zooba commented on Jun 20, 2025

    @zooba
    Member

    The runtime checks add some overhead, so performance-focused people turn them off. Having a test run that validates with them on would be a good thing for us to have, even if we do eventually release with them off.

    uint32_t to int32_t isn't any more safe than any other cast without a check (which is why Python checks, and runtime checks can be enabled in MSVC).

    Changing it to _Py_CAST(PY_INT32_T, op->ob_refcnt & 0x7FFFFFFF) (assuming we don't want negative results) would make sense for avoiding the warning, but I admit I'm not up on exactly what is being calculated here.

    @encukou and/or @vstinner should understand the refcounting support well enough to know what the intended result is.

  9. removed
    pendingThe issue will be closed if no feedback is provided
    on Jun 20, 2025
  10. chris-eibl commented on Jun 20, 2025

    @chris-eibl
    Member

    Yeah, as @zooba said, we do not use any run-time error checks in any of our Windows builds. 1

    You mentioned that you are using

    "/RTC1 = Basic Runtime Checks enabled"

    What I do not understand, according to the documentation

    /RTC1 Equivalent to /RTCsu

    this should only enable stack frame run-time error checking (/RTCs) and checking for unitialized variables (/RTCu), but not checking for data losses due to assigning to a smaller data type (/RTCc).

    For /RTCc, it is mentioned, that

    This option can report situations in which you intend to truncate.

    and the suggested workaround would be to

    first mask off the information you need to avoid the run-time error

    as you and @zooba proposed.

    But here in _Py_IsImmortal, this "masking by just casting" is intentional to get the lower bits, and introducing any "real" masking like & 0xFFFFFFFF can result in a performance penalty in potentially hot code.

    @ezio-melotti and later @markshannon tried very hard to keep the performance impact of immortal objects as low as possible.

    Maybe the runtime_checks pragma could be used to turn it off for _Py_IsImmortal?

    Footnotes

    1. Furthermore, it cannot be used when compiling with optimizations enabled ↩

  11. added
    buildThe build process and cross-build
    on Jun 20, 2025
  12. changed the title [-]3.13.2: cast in Py_XDECREF causes build failures with immortal objects[/-] [+]3.13.2: cast in Py_XDECREF causes runtime failures with immortal objects[/+] on Jun 20, 2025
  13. removed
    buildThe build process and cross-build
    on Jun 20, 2025
  14. Jejos commented on Jun 20, 2025

    @Jejos
    Author

    @chris-eibl: Sorry, you're right. I slipped in the line. In my project both, /RTCc and /RTC1 are enabled.

    I tried to make a PoC from scratch, but so far, it doesn't crash, although my development project reproducibly crashes.

    But here in _Py_IsImmortal, this "masking by just casting" is intentional to get the lower bits, and introducing any "real" masking like & 0xFFFFFFFF can result in a performance penalty in potentially hot code.

    As far as I understand the code, you use Py_ssize_t ob_refcnt as well as PY_UINT32_T ob_refcnt_split[2] and as far as I can see ob_refcnt_split[0] contains the lower 32 bit. So maybe another solution would be:

    _Py_CAST(PY_INT32_T, op->ob_refcnt_split[0])

    ?

  15. Jejos commented on Jun 20, 2025

    @Jejos
    Author

    I made a small project that reproduces the problem.

    As an additional dependency the Python library "opencv_python" is needed (see info.txt).

  16. ZeroIntensity commented on Jun 20, 2025

    @ZeroIntensity
    Member

    I might be losing my mind, but looking at the repro and report, reading ob_refcnt (for _Py_IsImmortal) shouldn't be causing a crash, regardless of what the actual reference count is, or what the bitmask is. It will only crash if the object is invalid, such as when it's been freed.

    I think that's probably what's going on here--I'm not a C++ expert, but I suspect the nullptr that you pass to PyObject_CallFunction isn't actually defined as (void *)0 on Windows, so Python doesn't notice (because it checks NULL) and tries to read it as a string. If it does notice and correctly falls back to _PyObject_CallNoArgsTstate, then the nullptr in the variadic arg could also cause problems, because it's left on the stack without being popped off by _PyObject_CallFunctionVa. In both cases, there's bad things going on that could cause weird crashes like this.

    Basically, my theory is that some memory corruption happens, and then the returned pointer is just some non-NULL junk. Does the problem go away when you use PyObject_CallNoArgs?

  17. Jejos commented on Jun 23, 2025

    @Jejos
    Author

    You're probably right and it's a problem in the external library "opencv". I just downloaded it and try to build it to find the bug.

  18. zooba commented on Jun 23, 2025

    @zooba
    Member

    Using a union would be better than casting, for sure. I think there was opposition to that when I last proposed it though (IIRC some compilers didn't like it).

    Passing nullptr should be interpreted as (void *)0 anywhere it crosses into C. It's just for type checking (which in C++ means overload resolution, so it's actually a very useful type).

    If a native module is involved, it's possible that it isn't aware of immortalization and so has been decrementing the refcount of immortal objects - I think you only need to decrement once to get the top bit of the lower DWORD set, which would cause this runtime check to fail. Perhaps we just need to change the check to quickly determine the common case (immortal, unchanged) and also handle the uncommon case (legacy extension has decref'd an immortal) but slower?

  19. encukou commented on Jun 23, 2025

    @encukou
    Member

    @encukou [...] should understand the refcounting support well enough to know what the intended result is.

    Sorry, I'm afraid my understanding is out of date by several optimization passes.

  20. Jejos commented on Jun 23, 2025

    @Jejos
    Author

    The version of OpenCV, that can currently be downloaded at PyPI, is "opencv-python 4.11.0.86".
    To find the bug, I just downloaded the newest source and compiled OpenCV 4.12 (cv2.cp313-win_amd64.pyd).
    When I replace the file Python313/Lib/sitepackages/cv2.pyd with my compilation, the crash vanishes! I can switch between both versions and the older version reproducibly crashes, while the new one doesn't.
    Thanks to the discussion above, I got the understanding, that the return value of the old library (0x0000000100000000) is invalid and should never appear at this position in the Python code and that the library was buggy. Indeed, the new OpenCV version returns 0x00000000ffffffff.

    Thanks a lot for all your support. The bug seems to be fixed, at least it's no problem with Python 3.13.

  21. zooba commented on Jun 24, 2025

    @zooba
    Member

    When I replace the file Python313/Lib/sitepackages/cv2.pyd with my compilation, the crash vanishes!

    Yeah, I guess that cv2.pyd is probably using the limited ABI, which breaksmisbehaves if you use an old version against immortal objects. Rebuilding for the current version is the best/only way to avoid this.

  22. vstinner commented on Jul 1, 2025

    @vstinner
    Member

    Can we close this issue since using a recent OpenVC version works around the issue?

  23. zooba commented on Jul 1, 2025

    @zooba
    Member

    It's not a "recent OpenCV version" - it's compiling without the stable ABI (or at least compiling with stable ABI from after we added immortal object support to the headers and/or made Py_DECREF opaque).

    If you're happy to say that stable ABI pre-3.whenever-that-was is unsupported, fine. I'll quote you on that when we're discussing replacing the stable ABI next, but I'm not opposed to it.

  24. vstinner commented on Jul 8, 2025

    @vstinner
    Member

    Sadly, it seems like building a C extension for the stable ABI with Python 3.11 or older (before PEP 683 – Immortal Objects) and using it on Python 3.12 and newer doesn't work well because of changes in the reference counting (Immortal Objects).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions