1

I am writing a small program to test the E2BIG error condition in exec sys call. If the arguments passed exceeds the MAX_ARG as specified in the limits.h, the E2BIG error is obtained. In some cases with RLIMIT_STACK also will give error if exceeded the max argument size.

 pid = fork();
 if(0 == pid){ /*CHILD*/
  printf("\n Child : %d \n",getpid());

 getrlimit(RLIMIT_STACK,&limit);
 printf("\n cur limit : %d , max limit : %d \n",(unsigned int)limit.rlim_cur,(unsigned int)limit.rlim_max);

 limit.rlim_cur = 0;
 limit.rlim_max = 0;
 setrlimit(RLIMIT_STACK,&limit);

 printf("\n cur limit : %d , max limit : %d \n",(unsigned int)limit.rlim_cur,(unsigned int)limit.rlim_max);

  execl("/usr/bin/ls","ls",NULL);
  printf("\n Child is done! \n");
 }
 else if(pid > 0){ /*PARENT*/
  waitpid(pid,&status,0);
  printf("\n Parent : %d \n",getpid());
  printf("\n Child : %d exited with exit_status %d \n",pid,status);
 }
 else{ /*ERROR*/
  switch(errno){
        case E2BIG:
                perror("\n E2BIG");
                break;
  }
 }
nneonneo
  • 171,345
  • 36
  • 312
  • 383
Angus
  • 12,133
  • 29
  • 96
  • 151
  • 1
    ...what's your question? – nneonneo Sep 01 '13 at 14:36
  • I couldnt check the E2BIG error condition though I limited the RLIMT_STACK size. – Angus Sep 01 '13 at 14:39
  • I want to check the error condition E2BIG on exec – Angus Sep 01 '13 at 14:39
  • You are checking the errno of *fork* and not the errno associated with *exec*. – pilcrow Sep 01 '13 at 14:44
  • You have to pass some big arguments to the command to get an E2BIG error from `execl()`. You also have to test `errno` after the `execl()` returns (because `execl()` only returns if there is an error, the mere fact of it returning tells you it failed). And you'd have to check the return status from `setrlmit()` to know whether your choice works. And the stack size is substantially unrelated to the size of the argument list. – Jonathan Leffler Sep 01 '13 at 14:48
  • Is there any way to limit the max arg size ? – Angus Sep 01 '13 at 14:59
  • I tried inputing more than 100 arguments to the exec.. But still I couldn't reproduce the error – Angus Sep 01 '13 at 14:59
  • On modern systems, you may need more than a quarter megabyte of data in the arguments to exceed the limit; possibly more. It is not the number of arguments so much as their total length that matters. And the environment also counts against the space (so it is 'arguments + environment are too big'). For large numbers of arguments, you'll use `execv()` or `execve()` or `execvp()` rather than `execl()`, of course. – Jonathan Leffler Sep 01 '13 at 15:10
  • Is there a way to limit this size ? – Angus Sep 01 '13 at 15:27
  • Possible duplicate of [Limit on the number of arguments to `main()` in C](http://stackoverflow.com/questions/3724369/limit-on-the-number-of-arguments-to-main-in-c). It covers some of the same ground, at the least, and the code here demonstrates the limits that are mentioned there. – Jonathan Leffler Dec 21 '13 at 17:30

1 Answers1

2

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)
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Just curious — what does your signal handler accomplish that a SIG_DFL disposition on SIGCHLD would *not*? (Except that it introduces the possibility of more EINTRs.) – pilcrow Sep 02 '13 at 15:37
  • @pilcrow: _Initial (but incorrect) answer — why it is there:_ It allows me to recover the status of the child. Without that, the child would die and the system would not give any useful status for the child. _Subsequent answer:_ Nothing...except cause confusion. If omitted, the code works fine. I'll update my answer shortly. – Jonathan Leffler Sep 02 '13 at 16:15