Linux blocking signals to Python init
Asked Answered
C

2

11

This is a follow up to my other post Installing signal handler with Python. In short, Linux blocks all signals to PID 1 (including SIGKILL) unless Init has installed a signal handler for a particular signal; as to prevent kernel panic if someone were to send a termination signal to PID1. The issue I've been having, is it would seem that the signal module in Python doesn't install signal handlers in a way the system recognises. My Python Init script was seemingly, completely ignoring all signals as I think they were being blocked.

I seem to have found a solution; using ctypes to install the signal handlers with the signal() function in libc (in this case uClibc). Below is a python based test init. It opens a shell on TTY2 from which I can send signals to PID1 for testing. It seems to work in the KVM im using for testing (I'm willing to share the VM with anyone interested)

Is this the best way around this issue? Is there a 'better' way to install the signal handlers without the signal module? (I am not at all concerned with portably)

Is this a bug in Python?

#!/usr/bin/python

import os
import sys
import time

from ctypes import *

def SigHUP():
    print "Caught SIGHUP"
    return 0

def SigCHLD():
    print "Caught SIGCHLD"
    return 0

SIGFUNC = CFUNCTYPE(c_int)
SigHUPFunc = SIGFUNC(SigHUP)
SigCHLDFunc = SIGFUNC(SigCHLD)

libc = cdll.LoadLibrary('libc.so.0')
libc.signal(1, SigHUPFunc) # 1 = SIGHUP
libc.signal(17, SigCHLDFunc) # 17 = SIGCHLD

print "Mounting Proc: %s" % libc.mount(None, "/proc", "proc", 0, None)

print "forking for ash"
cpid = os.fork()
if cpid == 0:
    os.closerange(0, 4)
    sys.stdin = open('/dev/tty2', 'r')
    sys.stdout = open('/dev/tty2', 'w')
    sys.stderr = open('/dev/tty2', 'w')
    os.execv('/bin/ash', ('ash',))

print "ash started on tty2"

print "sleeping"
while True:
    time.sleep(0.01)
Creodont answered 30/4, 2011 at 20:16 Comment(2)
This really belongs on codereview.SE, but +1 for the cool idea of implementing init in Python.Meditate
i thought that might be where i should post, but im not set on this method of doing this and thought there might be plenty of ideas i havent thought of.Creodont
J
8

I did a bit of debugging under KVM and I found that the kernel is delivering signals to pid 1 when the signal handlers are installed by the standard signal module. However, when the signal is received "something" causes a clone of the process to be spawned, rather than printing the expected output.

Here is the strace output when I send HUP to the non-working init.sig-mod:

strace output

Which results in a new process running (pid 23) which is a clone of init.sig-mod:

clone of init as pid 23

I didn't have time to dig deeper into the cause, but this narrows things further. Probably something to do with Python's signal delivery logic (it registers a C hook which invokes your bytecode function when called). The ctypes technique bypasses this. The relevant Python source files are Python/pythonrun.c and Modules/signalmodule.c, in case you want to take a closer look.

Old Info -- I'm not sure this will solve your problem, but might get you closer. I compared these different ways signal handlers are installed:

  • Installing a handler via Python's signal module.
  • Upstart's signal handlers.
  • Using ctypes to call the signal() syscall directly.
  • Some quick tests in C.

Both the ctypes-invoked signal() system call and Upstart's sigaction() syscalls set the SA_RESTART flag when the handler is registered. Setting this flag indicates that when a signal is received while the process is executing or blocking inside certain syscalls (read, write, wait, nanosleep, etc), after the signal handler completes the syscall should be automatically restarted. The application won't be aware of this.

When Python's signal module registers a handler, it zeros the SA_RESTART flag by calling siginterrupt(signum, 1). This says to the system "when a system call is interrupted by a signal, after the signal handler completes set errno to EINTR and return from the syscall". This leaves it up to the developer to handle this and decide whether to restart the system call.

You can set the SA_RESTART flag by registering your signal this way:

import signal
signal.signal(signal.SIGHUP, handler)
signal.siginterrupt(signal.SIGHUP, False)
Jim answered 1/5, 2011 at 2:16 Comment(8)
Im anxious to try this- even if it doesn't solve this problem, this is great reference! i'll re-comment once i've had a chance to test it.Creodont
That didn't work- thanks for the idea though, very insightfulCreodont
OK, but to clarify: your ctypes-based init code is working, right?Jim
@simplebias i linked your post on the project blog (www.teeos.org). i can remove it if you want.Creodont
@Creodont no problem :-) What is still strange to me is that strace shows the only difference between the signals module registered and ctypes-registered versions is the ctypes setting SA_RESTART flag. Do you have a quick recipe for running TeeOS in a VM and debugging?Jim
@simplebias I can send you the VM i am using for testing- but there isn't really much of a development environment around it. (thus far i've just put pieces together from my other embedded platforms) I can compile strace for it though. email me themadcrapper / gmail and i'll send you what i've got.Creodont
@simplebias - maybe the issue is with Pytohn built against uClibc... when i was testing in a chroot, with uclibc, i noticed the same issue but it didn't behave the same way with the python on my laptop- python linked against the full GNU C Lib. I will try again with python compiled again the full clib and see if it behaves the same way.Creodont
The issue is with Python compiled against uClibc :( Python built against glibc doesn't behave the same way. lame.Creodont
C
2

The issue was a compatibility issue with Python compiled against uClibc 0.9.31 with old linux threads. Compiling against 0.9.32-rc3 and using NPTL has fixed the issue.

Creodont answered 6/5, 2011 at 1:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.