Blog

SET_PDEATHSIG From Python

April 01, 2021

You've got a python process that runs another process and waits for it to finish. Maybe it's some ffmpeg process or something. Everything is great until, one day, you notice that you've got dangling ffmpeg processes.

Reparenting to init

Why does this happen? Your process tree looks something like

init → [stuff] → [more stuff] → python → ffmpeg

where python is the parent of ffmpeg. It's a moderately common misconception that when a parent process dies, its entire child process tree is terminated too. In fact, if python dies, ffmpeg is simply reparented to init:

init → ffmpeg

To see this in action,

import subprocess, sys
subprocess.Popen(['sleep', '3']) 
sys.exit()

Then, ps x | grep sleep.

SET_PDEATHSIG

Enter SET_PDEATHSIG. This tells the kernel to send you a signal when your parent dies.

To call it, we just

#include <stdio.h>
#include <sys/prctl.h>
if (prctl(PR_SET_PDEATHSIG, SIGKILL))
    perror("SET_PDEATHSIG");

Of course, we have python code, so we use ctypes:

import ctypes, ctypes.util, signal
# https://github.com/torvalds/linux/blob/v5.11/include/uapi/linux/prctl.h#L9
PR_SET_PDEATHSIG = 1

def set_pdeathsig():
    libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
    if libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL) != 0:
        raise OSError(ctypes.get_errno(), 'SET_PDEATHSIG')

But wait, how does this help us? Setting the parent death signal of the python process makes it so we get SIGKILL-ed when our parent dies, but the point of this was to kill ffmpeg when python dies.

At first glance, it looks like we need to call prctl from inside the ffmpeg code. But it turns out that PDEATHSIG is inherited across execs, so we simply need to prctl after forking but before execing ffmpeg. Conveniently, subprocess has a preexec_fn for doing precisely this!

import ctypes
import ctypes.util
import signal
import subprocess
import sys

# https://github.com/torvalds/linux/blob/v5.11/include/uapi/linux/prctl.h#L9
PR_SET_PDEATHSIG = 1

def set_pdeathsig():
    libc = ctypes.CDLL(ctypes.util.find_library('c'), use_errno=True)
    if libc.prctl(PR_SET_PDEATHSIG, signal.SIGKILL) != 0:
        raise OSError(ctypes.get_errno(), 'SET_PDEATHSIG')

subprocess.Popen(['sleep', '3'], preexec_fn=set_pdeathsig)
sys.exit()