Skip to content

Type punning (and strict aliasing) issue in Py_CLEAR() and Py_SETREF() macros: Python --enable-pystats is miscompiled #99701

Description

@mdboom

Bug report

When building with --enable-pystats, the ensurepip step fails with a segmentation fault.
The v3.11.0 tag works on this machine, main is currently broken. I have not yet bisected it.

Cc @markshannon as the primary pystats author.

Your environment

Debian bookworm, WSL

Output during build
if test "xupgrade" != "xno"  ; then \
        case upgrade in \                                                                                                               upgrade) ensurepip="--upgrade" ;; \
                install|*) ensurepip="" ;; \
        esac; \                                                                                                                  ./python -E -m ensurepip \
                $ensurepip --root=/ ; \
fi
Looking in links: /tmp/tmpnrufafo0
Processing /tmp/tmpnrufafo0/setuptools-65.5.0-py3-none-any.whl
Processing /tmp/tmpnrufafo0/pip-22.3.1-py3-none-any.whl
Installing collected packages: setuptools, pip                                                                          Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/mdboom/Work/builds/cpython/Lib/ensurepip/__main__.py", line 5, in <module>                                    sys.exit(ensurepip._main())                                                                                                      ^^^^^^^^^^^^^^^^^
  File "/home/mdboom/Work/builds/cpython/Lib/ensurepip/__init__.py", line 286, in _main                                     return _bootstrap(
           ^^^^^^^^^^^
  File "/home/mdboom/Work/builds/cpython/Lib/ensurepip/__init__.py", line 202, in _bootstrap
    return _run_pip([*args, *_PACKAGE_NAMES], additional_paths)                                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mdboom/Work/builds/cpython/Lib/ensurepip/__init__.py", line 103, in _run_pip
    return subprocess.run(cmd, check=True).returncode                                                                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/mdboom/Work/builds/cpython/Lib/subprocess.py", line 571, in run                                               raise CalledProcessError(retcode, process.args,                                                                     subprocess.CalledProcessError: Command '['/home/mdboom/Work/builds/cpython/python', '-W', 'ignore::DeprecationWarning', '-c', '\nimport runpy\nimport sys\nsys.path = [\'/tmp/tmpnrufafo0/setuptools-65.5.0-py3-none-any.whl\', \'/tmp/tmpnrufafo0/pip-22.3.1-py3-none-any.whl\'] + sys.path\nsys.argv[1:] = [\'install\', \'--no-cache-dir\', \'--no-index\', \'--find-links\', \'/tmp/tmpnrufafo0\', \'--root\', \'/\', \'--upgrade\', \'setuptools\', \'pip\']\nrunpy.run_module("pip", run_name="__main__", alter_sys=True)\n']' died with <Signals.SIGSEGV: 11>.
Backtrace from the segfault
#0  tee_next (to=0x7ffff5ba9040) at ./Modules/itertoolsmodule.c:998
#1  0x0000555555897f66 in filterfalse_next (lz=0x7ffff5c109d0) at ./Modules/itertoolsmodule.c:4198
#2  0x00005555557caf4e in map_next (lz=0x7ffff5c12a10) at Python/bltinmodule.c:1359
#3  0x000055555589b10a in chain_next (lz=0x7ffff5c10640) at ./Include/object.h:135
#4  0x000055555589b10a in chain_next (lz=0x7ffff5c129b0) at ./Include/object.h:135
#5  0x00005555556551d7 in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7ffff7fb9bd8,
    throwflag=<optimized out>) at ./Include/object.h:135
#6  0x00005555556bf0bd in _PyObject_VectorcallTstate (kwnames=0x0, nargsf=3, args=0x7fffffffd120,
    callable=0x7ffff5c311c0, tstate=0x555555b98968 <_PyRuntime+450344>) at ./Include/internal/pycore_call.h:92
#7  method_vectorcall (method=<optimized out>, args=0x7ffff5e11218, nargsf=<optimized out>, kwnames=0x0)
    at Objects/classobject.c:89
#8  0x0000555555656173 in do_call_core (use_tracing=<optimized out>, kwdict=0x0, callargs=0x7ffff5e11200,
    func=0x7ffff72d1540, tstate=<optimized out>) at Python/ceval.c:2981
#9  _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7ffff7fb9548, throwflag=<optimized out>)
    at Python/generated_cases.c.h:3510
#10 0x00005555557d3645 in _PyEval_EvalFrame (throwflag=<optimized out>, frame=<optimized out>, tstate=<optimized out>)
    at ./Include/internal/pycore_ceval.h:88
#11 _PyEval_Vector (args=0x0, argcount=0, kwnames=0x0, locals=0x7ffff7fb9308, func=0x7ffff7c65f80,
    tstate=0x555555b98968 <_PyRuntime+450344>) at Python/ceval.c:2059
#12 PyEval_EvalCode (co=co@entry=0x555555d4d720, globals=globals@entry=0x7ffff7a80040,
    locals=locals@entry=0x7ffff7a80040) at Python/ceval.c:585
#13 0x00005555557cc280 in builtin_exec_impl (module=<optimized out>, closure=<optimized out>, locals=0x7ffff7a80040,
    globals=0x7ffff7a80040, source=0x555555d4d720) at Python/bltinmodule.c:1075
#14 builtin_exec (module=<optimized out>, args=<optimized out>, nargs=<optimized out>, kwnames=<optimized out>)
    at Python/clinic/bltinmodule.c.h:543
#15 0x000055555571bd6b in cfunction_vectorcall_FASTCALL_KEYWORDS (func=0x7ffff7c242c0, args=0x7ffff7fb92d0,
    nargsf=<optimized out>, kwnames=<optimized out>) at Objects/methodobject.c:438
#16 0x00005555556bbd5c in _PyObject_VectorcallTstate (kwnames=0x7ffff7a80040, nargsf=<optimized out>,
    args=0xfffc555598798768, callable=0x7ffff7c242c0, tstate=0x555555b98968 <_PyRuntime+450344>)
    at ./Include/internal/pycore_call.h:92
#17 PyObject_Vectorcall (callable=callable@entry=0x7ffff7c242c0, args=args@entry=0x7ffff7fb92d0,
    nargsf=<optimized out>, kwnames=kwnames@entry=0x0) at Objects/call.c:301
#18 0x0000555555651c3b in _PyEval_EvalFrameDefault (tstate=<optimized out>, frame=0x7ffff7fb9228,
    throwflag=<optimized out>) at Python/generated_cases.c.h:2972
#19 0x00005555557d3645 in _PyEval_EvalFrame (throwflag=<optimized out>, frame=<optimized out>, tstate=<optimized out>)
    at ./Include/internal/pycore_ceval.h:88
#20 _PyEval_Vector (args=0x0, argcount=0, kwnames=0x0, locals=0x7ffff7c580ee, func=0x7ffff7c65e40,
    tstate=0x555555b98968 <_PyRuntime+450344>) at Python/ceval.c:2059
#21 PyEval_EvalCode (co=co@entry=0x7ffff7c58030, globals=globals@entry=0x7ffff7c869c0,
    locals=locals@entry=0x7ffff7c869c0) at Python/ceval.c:585
#22 0x000055555582911c in run_eval_code_obj (locals=0x7ffff7c869c0, globals=0x7ffff7c869c0, co=0x7ffff7c58030,
    tstate=0x555555b98968 <_PyRuntime+450344>) at Python/pythonrun.c:1702
#23 run_mod (mod=<optimized out>, filename=filename@entry=0x7ffff7c8b870, globals=globals@entry=0x7ffff7c869c0,
    locals=locals@entry=0x7ffff7c869c0, flags=flags@entry=0x7fffffffd798, arena=arena@entry=0x7ffff7bb3990)
    at Python/pythonrun.c:1723
#24 0x000055555582c192 in pyrun_file (flags=0x7fffffffd798, closeit=1, locals=0x7ffff7c869c0, globals=0x7ffff7c869c0,
    start=257, filename=0x7ffff7c8b870, fp=<optimized out>) at Python/pythonrun.c:1617
#25 _PyRun_SimpleFileObject (fp=fp@entry=0x555555c2f3a0, filename=filename@entry=0x7ffff7c8b870,
    closeit=closeit@entry=1, flags=flags@entry=0x7fffffffd798) at Python/pythonrun.c:439
#26 0x000055555582c810 in _PyRun_AnyFileObject (fp=0x555555c2f3a0, filename=filename@entry=0x7ffff7c8b870,
    closeit=closeit@entry=1, flags=flags@entry=0x7fffffffd798) at Python/pythonrun.c:78

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Nov 22, 2022
  2. mdboom commented on Nov 22, 2022

    @mdboom
    ContributorAuthor

    c03e05c is the first bad commit.

  3. mdboom commented on Nov 22, 2022

    @mdboom
    ContributorAuthor

    Building with --with-pydebug seems to fix the issue (so it's a bit of a Heisenbug, I guess).

  4. mdboom commented on Nov 22, 2022

    @mdboom
    ContributorAuthor

    Reverting Py_SETREF on its own is enough to fix this segfault.

  5. vstinner commented on Nov 23, 2022

    @vstinner
    Member

    Oh, interesting! I can reproduce the crash with the command:

    $ rm ~/pyprefix -rf; git clean -fdx; ./configure --enable-pystats --prefix ~/pyprefix CFLAGS="-O2" && make && make install
    

    But I cannot reproduce the crash using CFLAGS="-O0" nor CFLAGS="-O1". The bug requires at least gcc -O2 optimization level, or gcc -O3.

    Then I can reproduce the crash with the command:

    ./python -E -m ensurepip --upgrade --root=/
    

    The pystats build is very noisy, so I disable the debug output at Python exit using a local change:

    diff --git a/Python/specialize.c b/Python/specialize.c
    index cd09b188b7..98ff406dbc 100644
    --- a/Python/specialize.c
    +++ b/Python/specialize.c
    @@ -205,6 +205,7 @@ _Py_StatsClear(void)
     void
     _Py_PrintSpecializationStats(int to_file)
     {
    +    return;
         if (_py_stats == NULL) {
             return;
         }
  6. vstinner commented on Nov 23, 2022

    @vstinner
    Member

    I can reproduce the crash at commit 0124b5d with the following patch:

    diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c
    index 381ec3b31d..caeba6a3ba 100644
    --- a/Modules/itertoolsmodule.c
    +++ b/Modules/itertoolsmodule.c
    @@ -992,6 +992,14 @@ static PyTypeObject teedataobject_type = {
     };
     
     
    +#define MY_SETREF(dst, src)                                     \
    +    do {                                                        \
    +        PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \
    +        PyObject *_tmp_dst = (*_tmp_dst_ptr);                   \
    +        *_tmp_dst_ptr = _PyObject_CAST(src);                    \
    +        Py_DECREF(_tmp_dst);                                    \
    +    } while (0)
    +
     static PyObject *
     tee_next(teeobject *to)
     {
    @@ -1001,7 +1009,7 @@ tee_next(teeobject *to)
             link = teedataobject_jumplink(to->dataobj);
             if (link == NULL)
                 return NULL;
    -        Py_SETREF(to->dataobj, (teedataobject *)link);
    +        MY_SETREF(to->dataobj, (teedataobject *)link);
             to->index = 0;
         }
         value = teedataobject_getitem(to->dataobj, to->index);
  7. vstinner commented on Nov 23, 2022

    @vstinner
    Member

    Oh. That bug is complicated. It looks like a type punning or strict aliasing bug.

    With the bug, tee_next() is miscomplicated because of a type punning issue related to Py_SETREF().

    With gcc -O2, functions called by tee_next() are inlined in tee_next(): teedataobject_jumplink(), Py_XINCREF(), Py_DECREF() from Py_SETREF(), teedataobject_newinternal(), Py_INCREF() at the end of teedataobject_newinternal().

    The problem is around to->dataobj:

    • At the function entry, the value is stored into a register: mov r12,QWORD PTR [rdi+0x10]
    • Py_SETREF() changes to->dataobj, it writes into the stack: mov QWORD PTR [rbp+0x10],rax -- rbp is a copy of the original rdi value at the function entry
    • To call teedataobject_getitem(to->dataobj, to->index), the old value of to->dataobj is retrieved from r12 register... which is outdated: oops!

    With the old Py_SETREF() implementation, after Py_SETREF(), the old register is no longer used, and the new value of to->dataobj is retried: mov r12,QWORD PTR [rbp+0x10] (here a new r12 register is used).

    IMO this problem is related to type punning or strict aliasing, I'm not sure of the name of the problem.

    • With the old Py_SETREF(), the write was done into type teedataobject*: to->dataobj type is teedataobject*
    • With the new y_SETREF(), the write is done into type PyObject*. But the first register (r13) read to->dataobj as a teedataobject* which is a different type. The compiler gets more freedom in terms of optimizations: "oh, Py_SETREF() uses a different type, we can still use our r13 register, no need to invalidate it!".

    The C language is hard :-(

    That's the problem. I'm not sure what's the best way to solve it. A simple fix is to revert c03e05c: don't fix issue #98724. Another fix would be to avoid the type punning / strict aliasing issue by adding hints to the compiler that it must invalidate its register. Or maybe use the correct type in the macro. Or maybe a type different than PyObject* (and PyObject** for _tmp_dst_ptr) can be used? Anyway, now I need to sleep ;-)

    cc @serge-sans-paille who loves C bugs ;-)

  8. vstinner commented on Nov 23, 2022

    @vstinner
    Member

    A simple fix is to revert c03e05c

    I created PR #99737 for that.

  9. vstinner commented on Nov 23, 2022

    @vstinner
    Member

    I'm now curious if _Py_SET_OPCODE() could be affected by the same sickness?

    #define _Py_SET_OPCODE(word, opcode) \
        do { ((unsigned char *)&(word))[0] = (opcode); } while (0)

    Some macros are using advanced casting, but to read so I understand that they cannot be affected.

    #define DK_ENTRIES(dk) \
        (assert((dk)->dk_kind == DICT_KEYS_GENERAL), \
         (PyDictKeyEntry*)(&((int8_t*)((dk)->dk_indices))[(size_t)1 << (dk)->dk_log2_index_bytes]))
    
    #define DK_UNICODE_ENTRIES(dk) \
        (assert((dk)->dk_kind != DICT_KEYS_GENERAL), \
         (PyDictUnicodeEntry*)(&((int8_t*)((dk)->dk_indices))[(size_t)1 << (dk)->dk_log2_index_bytes]))

    The Py_ARRAY_LENGTH() macro uses a hack using typeof() to trigger a compiler error when the argument is a pointer, rather than an array.

    /* Two gcc extensions.
       &a[0] degrades to a pointer: a different type from an array */
    #define Py_ARRAY_LENGTH(array) \
        (sizeof(array) / sizeof((array)[0]) \
         + Py_BUILD_ASSERT_EXPR(!__builtin_types_compatible_p(typeof(array), \
                                                              typeof(&(array)[0]))))
  10. vstinner commented on Nov 23, 2022

    @vstinner
    Member

    I wrote a PR using __typeof__() to avoid type punning: PR #99739.

  11. mdboom commented on Nov 29, 2022

    @mdboom
    ContributorAuthor

    @vstinner: Thanks for digging so deep on this. This is really interesting.

  12. vstinner commented on Nov 29, 2022

    @vstinner
    Member

    The issue got fixed by commit 3a803bc. I propose to continue discussing type punning in issue #98724.

  13. changed the title [-]Segmentation fault running `ensurepip` during an `--enable-pystats` build[/-] [+]Type punning (and strict aliasing) issue in Py_CLEAR() and Py_SETREF() macros: Python --enable-pystats is miscompiled[/+] on Dec 7, 2022
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

    type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions