Securely Killing Processes

Joey Hess wrote on Debian-devel about the problem of init scripts not doing adequate checks before using the data from a PID file under /var/run to determine which process to kill [1]. Unfortunately that still doesn’t quite solve the problem, there is still the issue of a race condition causing a process to die while you are doing the checks and then be replaced by another process.

Below I have included the source code to a little program that will repeatedly fork() until it finds a particular PID and then have it’s child call sleep(). So you can run a command such as “kill -9 1234 ; ./a.out 1234” and then have this program take over the PID 1234.

From testing with this it seems that when you have a shell with it’s current working directory as /proc/1234 then once process 1234 is killed the current directory is empty, “ls -l” returns 0 entries. This isn’t surprising, it’s the standard Unix behavior when the working directory is removed.

So if a program (or even a shell script) changes directory to /proc/1234 it can then verify all attributes of the process (it’s CWD, it’s root directory, the executable used to run it, it’s UID, GID, supplemental groups, it’s SE Linux context, and lots of other things all atomically. The only possibility for confusion is that a process might execute a SETUID or SETGID program or a program that has a label which triggers a SE Linux domain transition. It might also change some attributes without executing a new process, for example by using the setuid(), setgid(), setgroups(), or other similar system calls. For the purposes of killing a process I don’t think that the possibility of it changing it’s own attributes or executing a new program are serious problems, if you want to kill a process then you probably want to kill it after it has called setuid() etc.

It seems to me that it would be useful to have a file named /proc/PID/signal (or something similar) to which you could write a number and then have the kernel send the signal in question to that process. So the commands “kill -9 1234” and “echo 9 > /proc/1234/signal” would give the same result. But you could run the command “cd /proc/1234” and then some time later you could run “echo 9 > signal” and know that you would kill the original process 1234 if it was still running and not some other process that replaced it.

What do you think? Is this worthy of adding a new feature to the proc filesystem?

The Source

Run the following program with a single parameter which is the PID that you want it to take. It will keep looping through the PID space until it gets the one you specify, if the one you specify isn’t available (EG you give it “1” as a parameter) then it will run forever. It will take some time to get a result on a slower system. On my P3-900MHz test system it took up to 72 seconds to get a result.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char **argv)
  if(argc != 2)
    printf("Specify the desired PID on the command-line\n");
    return 1;
  pid_t wanted = (pid_t)atoi(argv[1]);
  int rc;
  int status;
  while((rc = fork()) != wanted && rc != 0)
    if(rc == -1)
      printf("fork error\n");
      return 1;
    if(wait(&status) == -1)
      printf("wait error\n");
  if(rc == 0)
    if(getpid() == wanted)
      printf("Got pid %d, sleeping\n", wanted);
    return 0;
  return 0;

6 comments to Securely Killing Processes

  • The pid racer can be made smarter to avoid needing to take so long on average to get to the target pid. Just have it running beforehand, let it track the current rate of forking on the system, and
    run forward as needed to ensure the newest pid is always a reasonable distance before the target pid.
    In order to exploit the race, you need to have a target program that you can start at will. Probably a suid program, so your pid racer can exec it at just the right time. This program needs to be one that will somehow do something exploitable when sent a TERM. (What?) You also need a way to kill the original pid on demand.
    Both of these requirements seem hard to find real world exploit cases for.
    Do like the signal file idea though.

  • etbe

    I don’t think it’s possible to make the pid racer process run any faster. If you believe that it is possible then I challenge you to provide code. ;)

    I agree that it’s not an easy race to exploit. But I think it’s also not an difficult race to solve either.

    As for a race on sigterm, I agree that’s not a likely condition. I think that a more likely problem is a DOS attack by killing some essential process (EG create a pid file to make the restart of an unimportant daemon cause the LDAP or database server used to authenticate logins be killed).

    One thing I have been considering for some years is implementing a way to make start-stop-daemon take the SE Linux context of a process into account. It wouldn’t be THAT difficult for it to get the context of an executable, the context for running an init script (usually system_u:object_r:initrc_t:s0) and then discover what context should be used for running the daemon. It could then refuse to kill a process with the wrong context.

  • Systemd makes PID files obsolete! :-)

  • Alternatively… I think it would be way easier to open a FD for /proc/1234/signals, and only later write 9 to it.

  • etbe

    Gunnar: Good point. Changing the working directory is a good option if a process only monitors one child process that it might want to kill. If it is to monitor many then keeping a file handle open is the way to go.

    Another possibility is to use the fchdir() system call which then allows keeping the directories open for multiple processes and then changing to whichever one is of interest before interrogating the process state. Blindly killing a process is a fairly rare occurrence, and even when you want to unconditionally kill a daemon you usually want to check for child processes. systemd currently does this through cgroups but it seems that cgroups don’t work for all situations (such as console logins used for running screen).

  • I can trivially prove that your pid racer can be sped up, and no, I don’t have to write code to do it. :-P
    Simply prefork a lot of processes that just wait() forever. Lets say you fork PID_MAX/2 children. Now your pid racer is racing through a space half the size, so it clearly is twice as fast to get to its destination.
    (Regarding exploitability, no fair moving the goal posts back to creating a pid file that causes some other, important daemon to be killed!
    That was what I originally posted about.. Programs that use start-stop-daemon correctly are not vulnerable to that attack, and I already posted about the ones that are vulnerable. )