1

I know that a process cannot prevent SIGKILL.

But is there an external way to temporarily prevent SIGKILL to reach a (specific) process? (something like dropping packets by firewalls).

  • Yes, running the process as another user. – mosvy Nov 24 '18 at 19:15
  • but still any root owned process can kill it – gopy Nov 24 '18 at 19:19
  • that's by design. do you want unkillable processes on your machine? – mosvy Nov 24 '18 at 19:25
  • 2
    modify the kernel to implement the behavior you want (this would be a bad idea—why do you want to prevent a KILL signal?) – thrig Nov 24 '18 at 19:25
  • 1
    @mosvy, it should be pretty easy with systemtap. LSMs would be seen as an equivalent of the firewall the OP is mentioning. – Stéphane Chazelas Nov 26 '18 at 22:31
6

You kill a process by invoking the kill() (or tkill()) system call (the kernel can also kill processes/tasks by itself (like the SIGINT sent upon Ctrl-C or SIGKILL sent by the out-of-memory killer). Some signals may be sent as a result of other system calls like ptrace).

When kill() is invoked, it then all happens in the kernel.

Only kernel code stands in between the process sending the signal and the process receiving it (and possibly terminate as a consequence).

Now there are still a few kernel features that stand in the way and that you may be able to use here:

  1. the simple Unix permissions. Quoting the kill(2) man page on Linux:

    For a process to have permission to send a signal, it must either be privileged (under Linux: have the CAP_KILL capability in the user namespace of the target process), or the real or effective user ID of the sending process must equal the real or saved set-user-ID of the target process.

  2. Linux Security Modules. LSMs can (and do at least for Smack, SELinux and apparmor) filter what may send signals to what.

  3. Some processes are immune to killing. That's the case of the process of id 1 (init) on Linux. The root process of other child namespaces is also immune to signals sent by other processes in its namespace. Kernel tasks are also immune to signals.

  4. And then there are kernel instrumentation mechanisms like that used by SystemTap that allows you to affect the behaviour of the kernel and here could be used to hijack signal delivery.

But before getting there, maybe the first thing to try would be to stop whatever is sending that SIGKILL signal from doing it.

If it's to prevent a critical process (for instance one used to support the root file system) to be killed upon shutdown, most init systems will have a way to prevent a given processes to be subject to the killall5 or equivalent that happens then. See the /run/sendsigs.omit.d in some versions of Debian, or the killmode of systemd for instance.

The killer process, whatever it it, must have a way to determine which process to kill. If it's based on the pid of the victim stored in a file (like /run/victim.pid), you can change that file, if it's based on the process name (/proc/pid/task/tid/comm), that's also changeable (by attaching a debugger for instance and call prctl(PR_SET_NAME)), same for the arg list (/proc/pid/cmdline shown by ps -f).

If the killer process is dynamically linked, you could inject code in it to replace the kill() system call with a wrapper function that refuses to do it for a given pid:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#include <errno.h>

int kill(pid_t pid, int sig)
{
  static pid_t pid_to_safeguard = 0;
  static int (*orig_kill)(pid_t, int) = 0;

  if (!orig_kill)
    orig_kill = (int (*)(pid_t, int)) dlsym (RTLD_NEXT, "kill");

  if (!orig_kill) abort();

  if (!pid_to_safeguard) {
    char *env = getenv("NOTME");
    if (env) pid_to_safeguard = atol(env);
  }

  if (pid_to_safeguard && pid == pid_to_safeguard) {
    errno = EPERM;
    return -1;
  }

  return orig_kill(pid, sig);
}

(you may need to do the same for tkill(), and for the pgid of the victim depending on how the killer actually sends the signal).

Compile as:

gcc -fPIC -shared -o notme.so notme.c -ldl

And run the killer command as:

LD_PRELOAD=/path/to/notme.so NOTME=12345 killer args...

Or you could hide the process completely by running it in a pid namespace different from (but not a child of) the one for the rest of the system.

If none of those are an option, then we can go through our list above to prevent the signal from being delivered:

  1. Run the victim as a different user from the killer (assuming the killer is not running as root)
  2. Use a LSM. For instance, when using Smack (boot with security=smack as kernel parameter), setting a different label for the victim process would be enough for other processes not to be able to see it, let alone kill it. For instance:

    sudo zsh -c 'echo unkillable > /proc/self/attr/current && exec sleep 1000'
    

    would have sleep run in that unkillable domain (the name could be anything, the point is there's no currently defined rule that allows anyway to interfere with that domain) and even processes running as the same uid wouldn't be able to kill it. root would though.

  3. If you start your victim process as the leader of a new pid namespace, then it would be immune from its descendants.

    ~$ sudo unshare -p --fork --mount-proc zsh
    ~# kill -s KILL "$$"
    ~#
    

    (still there).

  4. SystemTap could be used here. Note however that you need kernel symbols (linux-image-<version>-dbgsym on Debian) to be able to use it, and the SystemTap or the internal kernel functions that your stap script will hook into are potentially subject to change. So maybe not the most stable of options. The Guru mode should also be used with care (don't try doing anything too fancy).

    With stap, you can inject code at different points in the running kernel. For instance, you could hook into the kernel function that handles the kill() or tkill() system call and tell it change the signal to 0 (harmless) when the pid is that of your victim.

    stap -ge 'probe kernel.function("sys_kill") { if ($pid == 12345) $sig = 0; }'
    

    (here for any signal, you can also check for $sig == 9 if you only want to cover SIGKILL). Now, that doesn't work when tkill() is used or when kill() is called with the process group id of the victim, so we'd need to extend that. That doesn't cover the cases where the signal is sent by the kernel itself.

    But we can also look at the kernel code and see if we can hook ourself in the place where the kernel checks for permission to send the signal.

    stap -ge 'probe kernel.function("check_kill_permission").return {
               if (@entry($t->pid) == 12345) $return = -1; }'
    

    We return -1 (-EPERM) which has also the benefit of letting the killer know its kill() failed, when the requested pid is that of our target (here 12345 as an example).

    ~$ sleep 1000 &
    [1] 8508
    ~$ sudo stap -ge 'probe kernel.function("check_kill_permission").return {
      if (@entry($t->pid) == '"$!"') $return = -1; }' &
    [2] 8510
    ~$ kill -s KILL 8508
    kill: kill 8508 failed: operation not permitted
    

    it also works for some of the cases where the kernel sends a signal by itself, but not all. For that, we'd need to go down to the bottom-most function that does the signal delivery in the kernel code: __send_signal() (at least in current versions of the Linux kernel).

    One way would be to hook into the prepare_signal() function that that __send_signal() calls at the beginning (and bails out if it returns 0);

    stap -ge 'probe kernel.function("prepare_signal").return {
      if (@entry($p->pid) == 12345) $return = 0; }'
    

    Then that pid 12345 would be unkillable as long as that stap process lives.

    Note that the kernel generally assumes SIGKILL would work, so it's not impossible that the above would have unexpected side effects in some corner cases (like the oom-killer being rendered ineffective if it keeps picking your unkillable victim).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.