Restoring the python dtrace ustack helper

Published at December 31, 2021.

Prior to version r151034, the Python versions shipped with OmniOS included a patch which added support for a small number of dtrace probes around function entry and exit, and also a dtrace ustack helper. A ustack helper allows a stack trace examined in the context of a dtrace probe to be annotated with useful information; in the case of python, information about the python program stack.

When dtrace support was integrated into the official cpython release of Python, in version 3.6, the ustack helper was omitted. This meant that when OmniOS upgraded to Python 3.7 along with the move to using the built-in dtrace support, the ustack helper was no longer available. We recently revisited this and have just completed restoring the feature to Python 3.10 in OmniOS bloody, which will be the basis for the next stable release in May 2022. This is mostly derived from the original Solaris patch for python 3.5. The python patch for OmniOS is being maintained in a github branch.

We have opted to include this in a separate debug variant of the package since there are some performance overheads in providing the helper, even if it is not used. For it to work reliably, python itself needs to be built without optimisations, every loaded module needs to have a second copy of the line number information stored in a format that is usable by dtrace, and every function call needs to go through another stack frame to ensure that the necessary information ends up on the stack in a place that dtrace can find it. There’s a little more on this in John Levon’s blog post on the original python dtrace implementation.

As an example, I was recently debugging a problem with the OmniOS packaging system unexpectedly removing a directory. Reaching for dtrace to check when the rmdir system call is used is not particularly helpful since you only see the stack trace from the cpython process.

% pfexec dtrace -n 'syscall::rmdir:entry{trace(copyinstr(arg0));jstack()}' \
    -c 'pkg image-create -f /tmp/testimg'

  1  246  rmdir:entry   /tmp/testimg/.org.opensolaris,pkg/publisher
   libc.so.1`_syscall6+0x1b
   libpython3.10.so.1.0`os_rmdir+0x102
   libpython3.10.so.1.0`cfunction_vectorcall_FASTCALL_KEYWORDS+0x61
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x57b4
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x57b4
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`method_vectorcall+0x8c
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1663
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`_PyObject_FastCallDictTstate+0x108
   libpython3.10.so.1.0`_PyObject_Call_Prepend+0x73
   libpython3.10.so.1.0`slot_tp_init+0x89
   libpython3.10.so.1.0`type_call+0x10b
   libpython3.10.so.1.0`_PyObject_MakeTpCall+0x126
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x6030
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1663
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x3ef
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x30b0
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x3ef
   libpython3.10.so.1.0`_PyEval_Vector+0x45
   libpython3.10.so.1.0`PyEval_EvalCode+0x96
   libpython3.10.so.1.0`run_eval_code_obj+0x56
   libpython3.10.so.1.0`run_mod+0x6d
   libpython3.10.so.1.0`pyrun_file+0x81
   libpython3.10.so.1.0`_PyRun_SimpleFileObject+0x395
   libpython3.10.so.1.0`_PyRun_AnyFileObject+0x8b
   libpython3.10.so.1.0`Py_RunMain+0x3d6
   libpython3.10.so.1.0`Py_BytesMain+0x3c
   python3.10`_start_crt+0x87
   python3.10`_start+0x18

With this new update, the debug python variant with the ustack helper can easily be switched in.

% pfexec pkg change-variant debug.python=true
            Packages to change:  1
     Variants/Facets to change:  1
       Create boot environment: No
Create backup boot environment: No

DOWNLOAD                                PKGS         FILES    XFER (MB)   SPEED
Completed                                1/1     2556/2556    14.5/14.5      --

PHASE                                          ITEMS
Installing new actions                           2/2
Updating modified actions                  2554/2554
Updating package state database                 Done
Updating package cache                           0/0
Updating image state                            Done
Creating fast lookup database                   Done
Reading search index                            Done
Building new search index                    689/689

and now the same stack trace is annotated with the python program information (the indented parts shown in square brackets).

 10  246  rmdir:entry   /tmp/testimg/.org.opensolaris,pkg/publisher
   libc.so.1`_syscall6+0x1b
   libpython3.10.so.1.0`os_rmdir+0x282
   libpython3.10.so.1.0`cfunction_vectorcall_FASTCALL_KEYWORDS+0x5b
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0xaf11
     [ /usr/lib/python3.10/shutil.py:708 (rmtree) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0xaf11
     [ /usr/lib/python3.10/vendor-packages/pkg/client/image.py:1971 (__set_dirs) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`method_vectorcall+0x185
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0x6dd2
     [ /usr/lib/python3.10/vendor-packages/pkg/client/image.py:711 (__init__) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`_PyObject_FastCallDictTstate+0xfa
   libpython3.10.so.1.0`_PyObject_Call_Prepend+0x112
   libpython3.10.so.1.0`slot_tp_init+0xd9
   libpython3.10.so.1.0`type_call+0x8f
   libpython3.10.so.1.0`_PyObject_MakeTpCall+0xa1
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0x9db5
     [ /usr/lib/python3.10/vendor-packages/pkg/client/api.py:6749 (image_create) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0x6dd2
     [ /usr/bin/pkg:6284 (image_create) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0x756f
     [ /usr/bin/pkg:8042 (main_func) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0x5326
     [ /usr/bin/pkg:6191 (handle_errors) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`_PyEval_EvalFrameDefaultReal+0x756f
     [ /usr/bin/pkg:8182 (<module>) ]
   libpython3.10.so.1.0`_PyEval_EvalFrameDefault+0x1c
   libpython3.10.so.1.0`_PyEval_Vector+0x48
   libpython3.10.so.1.0`PyEval_EvalCode+0x91
   libpython3.10.so.1.0`run_eval_code_obj+0x54
   libpython3.10.so.1.0`run_mod+0x6c
   libpython3.10.so.1.0`pyrun_file+0x80
   libpython3.10.so.1.0`_PyRun_SimpleFileObject+0x124
   libpython3.10.so.1.0`_PyRun_AnyFileObject+0x44
   libpython3.10.so.1.0`Py_RunMain+0x451
   libpython3.10.so.1.0`Py_BytesMain+0x57
   python3.10`_start_crt+0x87
   python3.10`_start+0x18

Here are those lines extracted, and it’s easy to see that the place to start looking is around line 1971 in the __set_dirs function of image.py.

[ /usr/lib/python3.10/shutil.py:708 (rmtree) ]
[ /usr/lib/python3.10/vendor-packages/pkg/client/image.py:1971 (__set_dirs) ]
[ /usr/lib/python3.10/vendor-packages/pkg/client/image.py:711 (__init__) ]
[ /usr/lib/python3.10/vendor-packages/pkg/client/api.py:6749 (image_create) ]
[ /usr/bin/pkg:6284 (image_create) ]
[ /usr/bin/pkg:8042 (main_func) ]
[ /usr/bin/pkg:6191 (handle_errors) ]
[ /usr/bin/pkg:8182 (<module>) ]

To switch back to the production version of python:

% pfexec pkg change-variant debug.python=false