0

Sometimes Python not only throws exception but also segfaults.

Through many years of my experience with Python I saw many segfaults, half of them where inside binary modules (C libraries, i.e. .so/.pyd files), half of them where inside CPython binary itself.

When segfault is issued then whole Python program finishes with crashdump (or silently). My question is if segfault happens in some block of code or thread is there any chance to catch it as regular Exception through except, and thus preventing whole program from crashing?

It is known that you can use faulthandler, for example through python -q -X faulthandler. Then it creates following dump when segfaults:

>>> import ctypes
>>> ctypes.string_at(0)
Fatal Python error: Segmentation fault

Current thread 0x00007fb899f39700 (most recent call first):
  File "/home/python/cpython/Lib/ctypes/__init__.py", line 486 in string_at
  File "<stdin>", line 1 in <module>
Segmentation fault

But this dump above finishes program entirely. Instead I want to catch this traceback as some standard Exception.


Another question is whether I can catch segfault of Python code inside C API of PyRun_SimpleString() function?

Arty
  • 14,883
  • 6
  • 36
  • 69
  • 4
    You can't reasonably recover from such a fault. If you need to somehow react to it, use a secondary process. – Ulrich Eckhardt Nov 27 '22 at 16:51
  • 3
    Does this answer your question? [Catch Segfault or any other errors/exceptions/signals in C++ like catching exceptions in Java](https://stackoverflow.com/questions/6008470/catch-segfault-or-any-other-errors-exceptions-signals-in-c-like-catching-excep) – Ulrich Eckhardt Nov 27 '22 at 16:53
  • @UlrichEckhardt So you're saying the only way to catch segfault in Python code is to run this Python code as separate process, e.g. through `subprocess.run()`? Can you please tell if it is possible somehow to catch segfault of Python code if I run this code inside C/C++? For example there is [PyRun_SimpleString()](https://docs.python.org/3/c-api/veryhigh.html#c.PyRun_SimpleString) which can be run in C++ code, is it possible to catch any segfault of this `PyRun_SimpleString`? – Arty Nov 27 '22 at 17:05
  • 3
    The thing about a segmentation fault is that it's so serious, there's no way to determine if Python itself is stable enough to keep going. Crashing is really the best option. – Mark Ransom Nov 27 '22 at 17:34
  • @MarkRansom There are two cases when Python can segfault 1) When external `.so/.pyd` module segfaults in some library call like `mod.func(1, 2, 3)`. 2) When CPython binary itself has some bug. In case 1) it is obvious that it is not that serious, because there are millions of external modules in the world, and there is nothing serious about skipping 1 or 2 calls of `mod.func()`, skipping it by catching their segfault as Exception. Of course you can argue that external module can damage some crucial CPython variables, but it didn't damage then it is nice to be able to catch Exception. – Arty Nov 27 '22 at 17:42
  • 3
    The segmentation fault could just be the tip of the iceberg when it happens in an external module. There's no way to know how much damage was done, so I still assert the best course of action is a crash. – Mark Ransom Nov 27 '22 at 18:01
  • @MarkRansom OK, so if crash is the only possibility, is there a way to catch crash somehow? One possibility as I understand is to do `subprocess.run(['python', 'prog.py'], check = True)` and see if it returns non-zero program exit code and if it contains in stdout/err crash lines of `faulthandler` (same as I cited in my Question above). Second option is to run `multiprocessing.Process()`. Can you please tell how to catch segfault of `multiprocessing.Process()` if you know? – Arty Nov 27 '22 at 18:07

1 Answers1

1

The simplest way is to have a "parent" process which launches your app process, and check its exit value. -11 means the process received the signal 11 which is SEGFAULTV (cf)

import subprocess

SEGFAULT_PROCESS_RETURNCODE = -11


segfaulting_code = "import ctypes ; ctypes.string_at(0)"  # https://codegolf.stackexchange.com/a/4694/115779
try:
    subprocess.run(["python3", "-c", segfaulting_code],
                   check=True)
except subprocess.CalledProcessError as err:
    if err.returncode == SEGFAULT_PROCESS_RETURNCODE:
        print("probably segfaulted")
    else:
        print(f"crashed for other reasons: {err.returncode}")
else:
    print("ok")

EDIT: here is a reproducible example with a Python dump using the built-in faulthandler :

# file: parent_handler.py
import subprocess

SEGFAULT_PROCESS_RETURNCODE = -11


try:
    subprocess.run(["python3", "-m", "dangerous_child.py"],
                   check=True)
except subprocess.CalledProcessError as err:
    if err.returncode == SEGFAULT_PROCESS_RETURNCODE:
        print("probably segfaulted")
    else:
        print(f"crashed for other reasons: {err.returncode}")
else:
    print("ok")
# file: dangerous_child.py

import faulthandler
import time

faulthandler.enable()  # by default will dump on sys.stderr, but can also print to a regular file


def cause_segfault():  # https://codegolf.stackexchange.com/a/4694/115779
    import ctypes
    ctypes.string_at(0)


i = 0
while True:
    print("everything is fine ...")
    time.sleep(1)
    i += 1
    if i == 5:
        print("going to segfault!")
        cause_segfault()
everything is fine ...
everything is fine ...
everything is fine ...
everything is fine ...
everything is fine ...
going to segfault!

Fatal Python error: Segmentation fault

Current thread 0x00007f7a9ab35740 (most recent call first):
  File "/usr/lib/python3.8/ctypes/__init__.py", line 514 in string_at
  File "/home/stack_overflow/dangerous_child.py", line 9 in cause_segfault
  File "/home/stack_overflow/dangerous_child.py", line 19 in <module>
  File "<frozen importlib._bootstrap>", line 219 in _call_with_frames_removed
  File "<frozen importlib._bootstrap_external>", line 848 in exec_module
  File "<frozen importlib._bootstrap>", line 671 in _load_unlocked
  File "<frozen importlib._bootstrap>", line 975 in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 991 in _find_and_load
  File "/usr/lib/python3.8/runpy.py", line 111 in _get_module_details
  File "/usr/lib/python3.8/runpy.py", line 185 in _run_module_as_main

probably segfaulted

(outputs from both processes got mixed in my terminal, but you can separate them as you like)

That way you can pinpoint the problem was caused by the Python code ctypes.string_at.

But as Mark indicated in the comments, you should not trust this too much, if the program got killed is because it was doing bad things.

Lenormju
  • 4,078
  • 2
  • 8
  • 22
  • Thanks! Very interesting approach. UpVoted! – Arty Nov 28 '22 at 14:30
  • Do you also know how to get stack trace of whole program? Not only signal -11, but also textual representation of stack. – Arty Nov 28 '22 at 14:33
  • @Arty I added an edit section to answer your question. Does it solve your initial problem ? If so, consider marking it as "accepted" :) – Lenormju Nov 30 '22 at 06:09