10

I need to temporarily install a custom signal handler in my code. Afterwards, I need to restore control to the original signal handler. I've seen the following before in other StackOverflow answers:

# Save original signal handler
original_SIGINT_handler = signal.getsignal(signal.SIGINT)

# Install the new SIGINT handler
signal.signal(signal.SIGINT, handle_SIGINT)

# do stuff

# Return control to original signal handler
signal.signal(signal.SIGINT, original_SIGINT_handler)

However, this will not work if the original signal handler was registered from C. According to https://pymotw.com/2/signal/, if the original signal handler was registered from C, signal.getsignal(signal.SIGINT) will return None. Then, when we go to restore the signal with signal.signal(signal.SIGINT, None), the code fails with:

TypeError: signal handler must be signal.SIG_IGN, signal.SIG_DFL, or a callable object

How would I go about installing a temporary signal handler, if the original was defined in C to begin with?

Is it possible to "append" functionality to an existing signal handler? Given this other StackOverflow answer, I feel like there might be a hacky way to do so, since apparently Python signal handlers run after the C signal handler returns.

For more context, I'm writing a lldb Python script, and I need to temporarily capture ctrl+c while inside my script, which I'm doing by temporarily installing my own SIGINT handler. When leaving my script, I need to restore the original ctrl+c functionality, which lldb defined.

dailgez004
  • 123
  • 2
  • 7
  • Could you do something like this to just call sys.exit instead?: `signal.signal(signal.SIGINT, lambda *args: sys.exit(1))` – SurpriseDog Sep 26 '22 at 18:06

1 Answers1

0

It's clear that Python's signal module doesn't provide the necessary tool for accessing raw C function pointers, which are required to save and restore signal handlers installed outside Python. But there's an escape hatch: as a solution of last resort, we can invoke raw libc function signal() directly from Python via ctypes.

Discussion

In the C programming language, what we want is essentially:

/* declare a pointer to the original signal handler function */
void (*sighandler_original)(int);

/*
 * In pure C (without assuming POSIX), it's not possible to get the
 * original signal handler without changing it. So we first change
 * SIGINT's handler to SIG_IGN to get the original handler's pointer,
 * then we change it back.
 */
sighandler_original = signal(SIGINT, SIG_IGN);
signal(SIGINT, sighandler_original);

The equivalent Python code, using ctypes, would be:

import ctypes
import ctypes.util

# On GNU/Linux the result is "libc.so.6"
libc_filename = ctypes.util.find_library("c")
libc = ctypes.cdll.LoadLibrary(libc_filename)

# declare libc signal()'s arguments and return types
# incorrect types declarations may create unpredictable results
libc.signal.argtypes = [ctypes.c_int, ctypes.c_void_p]
libc.signal.restype = ctypes.c_void_p

# C macro SIGINT    2
# C macro SIG_IGN   1
sighandler_original = libc.signal(2, 1)
libc.signal(2, sighandler_original)

After running this code snippet, sighandler_original is now an integer that contains the memory address of the original C signal handler.

>>> print(sighandler_original)
140043040324672

To restore the handler in the future, simply execute libc.signal(2, sighandler_original) again.

You can see that an obvious problem is that ctypes only knows about calling a function via the system's Application Binary Interface (ABI), but it doesn't know anything about C headers or macros. Python also has no idea about the proper input and output types of functions, of which we must explicitly tell Python. Incorrect declarations may produce unpredictable results. This can potential be a portability pitfall when using ctypes for other applications.

Here we makes some specific assumptions of the values of SIGINT and SIG_IGN, which are 2 and 1. They're extremely unlikely to change on Unix systems. We also assume void * and a function pointer are the same thing, which is valid for POSIX.

Full Solution

To make the code more readable in this particular case, we can use the constants already defined in the high-level signal module, so the improved solution is:

import signal
import ctypes
import ctypes.util

# On GNU/Linux the result is "libc.so.6"
libc_filename = ctypes.util.find_library("c")
libc = ctypes.cdll.LoadLibrary(libc_filename)

# declare libc signal()'s arguments and return types
# incorrect type declarations may create unpredictable results
libc.signal.argtypes = [ctypes.c_int, ctypes.c_void_p]
libc.signal.restype = ctypes.c_void_p

sighandler_original = libc.signal(signal.SIGINT, signal.SIG_IGN)
libc.signal(signal.SIGINT, sighandler_original)

# change signal handler via Python
# (assuming handle_SIGINT is already defined in Python)
signal.signal(signal.SIGINT, handle_SIGINT)

# ...
# do things in Python
# ...

# restore signal handler via libc on exit
libc.signal(signal.SIGINT, sighandler_original)

Note that signal.SIGINT and signal.SIG_IGN are custom types, not raw integers or pointers. But since we have libc.signal.argtypes, ctypes automatically type-cast them for us.

Comments

  1. Due to the risks involved, the low-level libc.signal() should strictly be used at the beginning and the end of the Python program for saving and restoring signal handlers. It should not be used for other purposes, and especially do not use it as a replacement of Python's high-level signal.signal().

  2. Possible improvements, such as replacing the deprecated ANSI C signal() with POSIX sigaction() is left as an exercise for the reader.


It's not popularly known, but Python's standard library actually exposes a considerable amount of low-level system details. For example, the os module exposes fork(), exec(), and pipe(), thus it's totally possible to write an asynchronous program in the traditional Unix way instead of using Python's own threading or multiprocessing (historically, Perl programmers were more open to this idea, not Python). Similarly, it's possible to call an arbitrary C function via the module ctypes, which is largely the equivalent of C's dlopen() , allowing access to arbitrary shared libraries. Of course, their use is actively discouraged by Python developers, since the result is a footgun without any safety guarantee from Python, and depends on the specifics of a particular system.

But if you know what you're doing, it's always a choice.

比尔盖子
  • 2,693
  • 5
  • 37
  • 53