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

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

Closed
mdboom opened this issue Nov 22, 2022 · 12 comments
Labels
type-bug An unexpected behavior, bug, or error

Comments

@mdboom
Copy link
Contributor

mdboom commented Nov 22, 2022

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
@mdboom mdboom added the type-bug An unexpected behavior, bug, or error label Nov 22, 2022
@mdboom
Copy link
Contributor Author

mdboom commented Nov 22, 2022

c03e05c is the first bad commit.

@mdboom
Copy link
Contributor Author

mdboom commented Nov 22, 2022

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

@mdboom
Copy link
Contributor Author

mdboom commented Nov 22, 2022

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

@vstinner
Copy link
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;
     }

@vstinner
Copy link
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);

@vstinner
Copy link
Member

vstinner commented Nov 23, 2022

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 ;-)

@vstinner
Copy link
Member

A simple fix is to revert c03e05c

I created PR #99737 for that.

@vstinner
Copy link
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]))))

@vstinner
Copy link
Member

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

@mdboom
Copy link
Contributor Author

mdboom commented Nov 29, 2022

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

@vstinner
Copy link
Member

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

@vstinner vstinner 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 Dec 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

2 participants