To check the E2BIG error condition in exec

This code probes for the limit a bit crudely, doubling the size of the argument list each time it tests the argument list size. You can refine it if you wish (so it searches in the range between the last success and first failure), but the chances are that it hits the limit correctly first time.

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>

enum { BYTES_PER_KIBIBYTE = 1024 };
enum { BYTES_PER_MEBIBYTE = BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE };
enum { E_GOT_E2BIG =  37 };
enum { E_NOT_E2BIG = 219 };

static void sigchld_handler(int signum)
{
    signal(signum, sigchld_handler);
}

int main(void)
{
    signal(SIGCHLD, sigchld_handler);
    for (int i = 4 * BYTES_PER_KIBIBYTE; i < BYTES_PER_MEBIBYTE; i *= 2)
    {
        fflush(0);
        pid_t pid = fork();
        if (pid < 0)
        {
            fprintf(stderr, "Failed to fork at %d\n", i);
            return 1;
        }
        else if (pid == 0)
        {
            int self = getpid();
            printf("Child: %d\n", self);
            char *args[10] = { "ls" };
            size_t bytes_per_arg = i / 8;
            for (int j = 1; j < 9; j++)
            {
                args[j] = malloc(bytes_per_arg + 1);
                if (args[j] == 0)
                {
                    fprintf(stderr, "Failed to allocate argument space at size %d\n", i);
                    exit(E_NOT_E2BIG);
                }
                memset(args[j], j + '0', bytes_per_arg);
                args[j][bytes_per_arg] = '\0';
            }

            /* Close standard I/O channels so executed command doesn't spew forth */
            int dev_null = open("/dev/null", O_RDWR);
            if (dev_null < 0)
            {
                fprintf(stderr, "Failed to open /dev/null for reading and writing\n");
                exit(E_NOT_E2BIG);
            }
            int dev_stderr = dup(2);
            if (dev_stderr < 0)
            {
                fprintf(stderr, "Failed to dup() standard error\n");
                exit(E_NOT_E2BIG);
            }
            close(0);
            dup(dev_null);
            close(1);
            dup(dev_null);
            close(2);
            dup(dev_null);
            close(dev_null);

            /* Execute ls on big file names -- error is ENAMETOOLONG */
            execvp(args[0], args);

            /* Reinstate standard error so we can report failure */
            dup2(dev_stderr, 2);
            int errnum = errno;
            if (errnum == E2BIG)
            {
                fprintf(stderr, "%d: got E2BIG (%d: %s) at size %d\n", self, errnum, strerror(errnum), i);
                exit(E_GOT_E2BIG);
            }
            fprintf(stderr, "%d: got errno %d (%s) at size %d\n", self, errnum, strerror(errnum), i);
            exit(E_NOT_E2BIG);
        }
        else
        {
            int self = getpid();
            int corpse;
            int status;
            int exit_loop = 0;
            while ((corpse = waitpid(pid, &status, 0)) != -1)
            {
                if (!WIFEXITED(status))
                    printf("%d: child %d died with exit status 0x%.4X", self, corpse, status);
                else
                {
                    int statval = WEXITSTATUS(status);
                    printf("%d: child %d died with exit status %d: ", self, corpse, statval);
                    switch (statval)
                    {
                    case E_GOT_E2BIG:
                        printf("success: got E2BIG");
                        exit_loop = 1;
                        break;
                    case E_NOT_E2BIG:
                        printf("failed: indeterminate error in child");
                        break;
                    case 1:
                        printf("command exited with status 1 - it worked");
                        break;
                    default:
                        printf("unknown: unexpected exit status %d", statval);
                        break;
                    }
                }
                printf(" at size %d (%d KiB)\n", i, i / BYTES_PER_KIBIBYTE);
                fflush(stdout);
            }
            if (exit_loop)
                break;
        }
    }
    return 0;
}

Sample run:

46573: child 46575 died with exit status 1: command exited with status 1 - it worked at size 4096 (4 KiB)
46573: child 46576 died with exit status 1: command exited with status 1 - it worked at size 8192 (8 KiB)
46573: child 46577 died with exit status 1: command exited with status 1 - it worked at size 16384 (16 KiB)
46573: child 46578 died with exit status 1: command exited with status 1 - it worked at size 32768 (32 KiB)
46573: child 46579 died with exit status 1: command exited with status 1 - it worked at size 65536 (64 KiB)
46573: child 46580 died with exit status 1: command exited with status 1 - it worked at size 131072 (128 KiB)
46581: got E2BIG (7: Argument list too long) at size 262144
46573: child 46581 died with exit status 37: success: got E2BIG at size 262144 (256 KiB)

SIGCHLD handling

The SIGCHLD handling code is irrelevant. This is a more refined version of the code above. It does a binary search for the size of the argument list. On my machine (Mac OS X 10.8.4), allowing for the environment (which counts as part of the argument size), the limit is 256 KiB.

/* SO 18559403: How big an argument list is allowed */

#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

extern char **environ;  /* Sometimes in <unistd.h> */

enum { BYTES_PER_KIBIBYTE = 1024 };
enum { BYTES_PER_MEBIBYTE = BYTES_PER_KIBIBYTE * BYTES_PER_KIBIBYTE };
enum { E_GOT_E2BIG =  37 };
enum { E_NOT_E2BIG = 219 };

enum { R_TOO_LARGE = +1, R_TOO_SMALL = -1 };

static char *print_kib(int size, char *buffer, size_t buflen)
{
    snprintf(buffer, buflen, "%d (%d KiB)", size, size / BYTES_PER_KIBIBYTE);
    return buffer;
}

static int test_arg_size(int size)
{
    char buffer[32];
    int result = R_TOO_SMALL;
    assert(size % 8 == 0);
    fflush(0);
    pid_t pid = fork();
    if (pid < 0)
    {
        fprintf(stderr, "Failed to fork at size %s\n",
                print_kib(size, buffer, sizeof(buffer)));
        exit(1);
    }
    else if (pid == 0)
    {
        int self = getpid();
        printf("Child: %d\n", self);
        char *args[10] = { "ls" };
        size_t bytes_per_arg = size / 8;
        for (int j = 1; j < 9; j++)
        {
            args[j] = malloc(bytes_per_arg);
            if (args[j] == 0)
            {
                fprintf(stderr, "Failed to allocate argument space at size %s\n",
                        print_kib(size, buffer, sizeof(buffer)));
                exit(E_NOT_E2BIG);
            }
            memset(args[j], j + '0', bytes_per_arg - 1);
            args[j][bytes_per_arg - 1] = '\0';
        }

        /* Close standard I/O channels so executed command doesn't spew forth */
        int dev_null = open("/dev/null", O_RDWR);
        if (dev_null < 0)
        {
            fprintf(stderr, "Failed to open /dev/null for reading and writing\n");
            exit(E_NOT_E2BIG);
        }
        int dev_stderr = dup(2);
        if (dev_stderr < 0)
        {
            fprintf(stderr, "Failed to dup() standard error\n");
            exit(E_NOT_E2BIG);
        }
        close(0);
        /*
        ** GCC on Linux generates warnings if you don't pay attention to
        ** the value returned by dup().
        */
        int fd = dup(dev_null);
        assert(fd == 0);
        close(1);
        fd = dup(dev_null);
        assert(fd == 1);
        close(2);
        fd = dup(dev_null);
        assert(fd == 2);
        close(dev_null);

        /* Execute ls on big file names -- error is ENAMETOOLONG */
        execvp(args[0], args);

        /* Reinstate standard error so we can report failure */
        dup2(dev_stderr, 2);
        int errnum = errno;
        if (errnum == E2BIG)
        {
            fprintf(stderr, "%d: got E2BIG (%d: %s) at size %s\n",
                    self, errnum, strerror(errnum),
                    print_kib(size, buffer, sizeof(buffer)));
            exit(E_GOT_E2BIG);
        }
        fprintf(stderr, "%d: got errno %d (%s) at size %s\n",
                self, errnum, strerror(errnum),
                print_kib(size, buffer, sizeof(buffer)));
        exit(E_NOT_E2BIG);
    }
    else
    {
        int self = getpid();
        int corpse;
        int status;
        while ((corpse = waitpid(pid, &status, 0)) != -1)
        {
            if (!WIFEXITED(status))
                printf("%d: child %d died with exit status 0x%.4X",
                       self, corpse, status);
            else
            {
                int statval = WEXITSTATUS(status);
                printf("%d: child %d died with exit status %d: ",
                       self, corpse, statval);
                switch (statval)
                {
                case E_GOT_E2BIG:
                    printf("success: got E2BIG");
                    result = R_TOO_LARGE;
                    break;
                case E_NOT_E2BIG:
                    printf("failed: indeterminate error in child");
                    break;
                    /*
                    ** ls on Mac OS X fails with 1 if it fails to find a
                    ** file.  On Linux, it exits with 1 for 'minor
                    ** problems' (e.g. cannot access subdirectory).
                    ** ls on Linux fails with 2 if it fails with 'serious
                    ** trouble'; (e.g. if it can't find a file)
                    */
                case 1:
                case 2:
                    printf("command exited with status %d - it worked", statval);
                    break;
                default:
                    printf("unknown: unexpected exit status %d", statval);
                    break;
                }
            }
            printf(" at size %s\n", print_kib(size, buffer, sizeof(buffer)));
            fflush(stdout);
        }
    }
    return result;
}

static int env_size(void)
{
    int size = 0;
    for (char **ep = environ; *ep != 0; ep++)
        size += strlen(*ep) + 1;
    return size;
}

int main(void)
{
    int env = env_size();
    int lo = 0;
    int hi = 4 * BYTES_PER_MEBIBYTE;

    /* Binary search */
    /* The kilobyte slop means termination does not have to be accurate */
    while (lo + 1 * BYTES_PER_KIBIBYTE < hi)
    {
        int mid = (lo + hi) / 2;
        if (test_arg_size(mid) == R_TOO_LARGE)
            hi = mid;
        else
            lo = mid;
    }

    char buffer1[32];
    char buffer2[32];
    printf("Environment size = %d\n", env);
    printf("Best guess: maximum argument size in range %s to %s\n",
           print_kib(lo + env, buffer1, sizeof(buffer1)),
           print_kib(hi + env, buffer2, sizeof(buffer2)));

    return 0;
}

Updated 2014-04-06: compiles and runs better on Linux, where ls distinguishes between minor problems and serious trouble with exit statuses 1 and 2, and where GCC is told to complain if you don’t capture the result of dup(). Also tests up to 4 MiB argument space, up from previous 1 MiB. (Change the initial value of hi in main() to alter the range.)

Note the comment associated with the binary search; normally, you use mid ± 1 to ensure termination of the search, but this search terminates when the range is 1 KiB and not messing with the ±1 keeps the numbers ‘simpler’. Not that the computer minds — I mind, though. That’s also why the start range is 0 MiB .. 1 MiB rather than, say, 4 KiB .. 1 MiB; it keeps the numbers cleaner.

Sample run:

Child: 71822
71822: got E2BIG (7: Argument list too long) at size 524288 (512 KiB)
71821: child 71822 died with exit status 37: success: got E2BIG at size 524288 (512 KiB)
Child: 71823
71823: got E2BIG (7: Argument list too long) at size 262144 (256 KiB)
71821: child 71823 died with exit status 37: success: got E2BIG at size 262144 (256 KiB)
Child: 71824
71821: child 71824 died with exit status 1: command exited with status 1 - it worked at size 131072 (128 KiB)
Child: 71825
71821: child 71825 died with exit status 1: command exited with status 1 - it worked at size 196608 (192 KiB)
Child: 71826
71821: child 71826 died with exit status 1: command exited with status 1 - it worked at size 229376 (224 KiB)
Child: 71827
71821: child 71827 died with exit status 1: command exited with status 1 - it worked at size 245760 (240 KiB)
Child: 71828
71821: child 71828 died with exit status 1: command exited with status 1 - it worked at size 253952 (248 KiB)
Child: 71829
71821: child 71829 died with exit status 1: command exited with status 1 - it worked at size 258048 (252 KiB)
Child: 71830
71830: got E2BIG (7: Argument list too long) at size 260096 (254 KiB)
71821: child 71830 died with exit status 37: success: got E2BIG at size 260096 (254 KiB)
Child: 71831
71821: child 71831 died with exit status 1: command exited with status 1 - it worked at size 259072 (253 KiB)
Environment size = 2124
Best guess: maximum argument size in range 261196 (255 KiB) to 262220 (256 KiB)

Leave a Comment