-1

I am writing a basic shell program in C for an assignment. I am currently having troubles allocating the memory that will be used to store the command line arguments that will be passed into the program to be parsed.

I am getting a segmentation fault if the input size is greater than or equal to 4 (i.e. "this is a test" will produce a segmentation fault upon program terminations whereas "this is a" will not).

I imagine my issue lies within how I was allocating the memory but the program is capturing each token that I enter and printing them to the screen.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
  char cap[] = "";
  char* cmd;

  const int MAX_ARGS = 16;
  const int MAX_ARG_SIZE = 256;
  char** arglst = (char**)malloc(MAX_ARGS * sizeof(char*));
  for(int i = 0; i < MAX_ARGS; i++)
  {
    arglst[i] = (char*)malloc(MAX_ARG_SIZE * sizeof(char));
  }

  pid_t cpid;
  //implement ls, cd, mkdir, chdir, rm, rmdir
  while(1)
  {
    printf("Enter a command: ");
    scanf("%[^\n]s", cap);

    int index = 0;
    cmd = strtok(cap, " ");
    while( cmd != NULL )
    {
      strcpy(arglst[index], cmd);
      cmd = strtok(NULL, " ");
      index++;
    }

    for(int i = 0; i < index; i++)
    {
      printf("%s\n", arglst[i]);
    }
    printf("%d\n", index);

    /*
    if(strcmp(cap, "quit") == 0) exit(EXIT_SUCCESS);

    if( (cpid = fork()) == -1) perror("fork()");
    else if(cpid == 0)
    {
      if( execvp(cmd, arglst) == -1 )
      {
        errorp("cmd error");
        exit(EXIT_FAILURE);
      }
      exit(EXIT_SUCCESS);
    }
    else
    {
      cpid = wait(NULL);
      strcpy(cmd, "/bin/");
    }
    */
    for(int i = 0; i < index; i++)
    {
      free(arglst[i]);
    }
    free(arglst);
    return 0;
  }

}
11
  • 1
    I think you should add in 1 to the size final of malloc and add "'\0' to your string to indicate end of stream it. Commented Mar 7, 2021 at 20:24
  • 4
    char cap[] = ""; That is a 1 character array. Arrays do not automatically grow in size. Writing any more than an empty string into it later as you do with scanf causes a buffer overflow and undefined behaviour. Commented Mar 7, 2021 at 20:26
  • Fix also the scan format string: you have an 's' too much. Commented Mar 7, 2021 at 20:31
  • char** is a pointer, not an array. It looks like you want to use a 2D array which is not what you allocate. In fact a single allocation would be sufficient for this. There are many dupes about what you are doing and some also provide information how to do what you really want. Commented Mar 7, 2021 at 20:32
  • @AdrianMole: Still this has been asked and solved wrongly and sometimes corrected in comments and answers so many times here. Closing as a dupe is certainly much more helpful than just stating the code behaves correctly (what any code does unless there is a bug in the language implementation. And while asking for X, Y definitively is allocation of a 2D array which is a single malloc. Commented Mar 7, 2021 at 20:39

1 Answer 1

3

There are a number of bugs.

This won't compile (e.g. errorp instead of perror).

cap is too small to contain a line. Better to use (e.g.) char cap[1000];

Doing a preallocate of each arglst[i] once before the main loop is problematic. One of the cells has to get a NULL value so it works with execvp. However, doing so would cause a memory leak. The solution is to use strdup inside the strtok loop so that cells are only allocated when needed.

Also, because arglst[i] is set only once during initialization, doing a loop with free near the bottom causes UB [accessing the buffer after being freed]. This is fixed with the use of strdup below.

The commented out code references variables (e.g. cmd and cap) that should not be relied upon. At that point, cmd will be NULL, causing a segfault.

The return 0; is placed incorrectly. Only one iteration (and thus only one command) will be executed.

The final free of arglst (e.g. free(arglst)) is done inside the outer loop, so referencing it on the second iteration is UB.

There are a few more issues [annotated below]


Here's a refactored version. It fixes the bugs and is heavily annotated.

I've used the preprocessor to show old/original code vs new/fixed code:

#if 0
// old code
#else
// new code
#endif

Likewise, using #if 1 for purely new code.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#if 1
#include <sys/wait.h>
#endif

int
main(void)
{
// NOTE/BUG: this must be large enough to contain a command line
#if 0
    char cap[] = "";
#else
    char cap[1000];
#endif
    char *cmd;

    const int MAX_ARGS = 16;
    char **arglst = malloc(MAX_ARGS * sizeof(*arglst));

// NOTE/BUG: because we need to add a NULL terminator, don't preallocate the
// elements -- we'll leak memory
#if 0
    const int MAX_ARG_SIZE = 256;
    for (int i = 0; i < MAX_ARGS; i++) {
        arglst[i] = (char *) malloc(MAX_ARG_SIZE * sizeof(char));
    }
#endif

    pid_t cpid;

    // implement ls, cd, mkdir, chdir, rm, rmdir
    while (1) {
        printf("Enter a command: ");
// NOTE/BUG: this didn't work too well
#if 0
        scanf("%[^\n]s", cap);
#else
        fgets(cap,sizeof(cap),stdin);
        cap[strcspn(cap,"\n")] = 0;
#endif

        int index = 0;

        cmd = strtok(cap, " ");
        while (cmd != NULL) {
// NOTE/BUG: we should strdup dynamically rather than preallocate -- otherwise,
// we leak memory when we set the necessary NULL pointer below
#if 0
            strcpy(arglst[index], cmd);
#else
            arglst[index] = strdup(cmd);
#endif
            cmd = strtok(NULL, " ");
            index++;
        }

// NOTE/FIX: we have to add a NULL terminator before passing to execvp
#if 1
        arglst[index] = NULL;
#endif

        for (int i = 0; i < index; i++) {
            printf("%s\n", arglst[i]);
        }
        printf("%d\n", index);

// NOTE/BUG: we can't [shouldn't] rely on cap here
#if 0
        if (strcmp(cap, "quit") == 0)
            exit(EXIT_SUCCESS);
#else
        if (strcmp(arglst[0], "quit") == 0)
            exit(EXIT_SUCCESS);
#endif

        if ((cpid = fork()) == -1)
            perror("fork()");
        else if (cpid == 0) {
// NOTE/BUG: cmd will be NULL here
#if 0
            if (execvp(cmd, arglst) == -1) {
                errorp("cmd error");
                exit(EXIT_FAILURE);
            }
#else
            if (execvp(arglst[0], arglst) == -1) {
                perror("cmd error");
                exit(EXIT_FAILURE);
            }
#endif
// NOTE/BUG: this will never be executed
#if 0
            exit(EXIT_SUCCESS);
#endif
        }
        else {
            cpid = wait(NULL);
// NOTE/BUG -- cmd is NULL and this serves no purpose
#if 0
            strcpy(cmd, "/bin/");
#endif
        }

// NOTE/BUG: in the _old_ code that did a single preallocate of these cells
// _before_ the loop, freeing them here is wrong -- they would never be
// reallocated because -- the fix using strdup alleviates the issue
        for (int i = 0; i < index; i++) {
            free(arglst[i]);
        }

// NOTE/BUG: freeing this is wrong because we do the allocation only _once_
// above the outer loop
#if 0
        free(arglst);
#endif

// NOTE/BUG -- this should be placed at the end to allow multiple commands --
// here it stops after the first command is input
#if 0
        return 0;
#endif
    }

// NOTE/FIX: correct placement for the above
#if 1
    free(arglst);

    return 0;
#endif
}

Here's that version cleaned up so that only the fixed code remains:

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

int
main(void)
{
    char cap[1000];
    char *cmd;

    const int MAX_ARGS = 16;
    char **arglst = malloc(MAX_ARGS * sizeof(*arglst));

    pid_t cpid;

    // implement ls, cd, mkdir, chdir, rm, rmdir
    while (1) {
        printf("Enter a command: ");
        fgets(cap,sizeof(cap),stdin);
        cap[strcspn(cap,"\n")] = 0;

        int index = 0;

        cmd = strtok(cap, " ");
        while (cmd != NULL) {
            arglst[index] = strdup(cmd);
            cmd = strtok(NULL, " ");
            index++;
        }

        arglst[index] = NULL;

        for (int i = 0; i < index; i++) {
            printf("%s\n", arglst[i]);
        }
        printf("%d\n", index);

        if (strcmp(arglst[0], "quit") == 0)
            exit(EXIT_SUCCESS);

        if ((cpid = fork()) == -1)
            perror("fork()");
        else if (cpid == 0) {
            if (execvp(arglst[0], arglst) == -1) {
                perror("cmd error");
                exit(EXIT_FAILURE);
            }
        }
        else {
            cpid = wait(NULL);
        }

        for (int i = 0; i < index; i++) {
            free(arglst[i]);
        }
    }

    free(arglst);

    return 0;
}

Note that the above does not check for the number of actual arguments exceeding MAX_ARGS.

While we could add that check, a better way is to use realloc on arglst to dynamically increase it, so an arbitrary limit on the number of arguments isn't needed

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

int
main(void)
{
    char cap[1000];
    char *cmd;

    char **arglst = NULL;
    int argmax = 0;

    pid_t cpid;

    // implement ls, cd, mkdir, chdir, rm, rmdir
    while (1) {
        printf("Enter a command: ");
        fgets(cap,sizeof(cap),stdin);
        cap[strcspn(cap,"\n")] = 0;

        int index = 0;

        cmd = strtok(cap, " ");
        while (cmd != NULL) {
            if (index >= argmax) {
                argmax += 10;
                arglst = realloc(arglst,sizeof(*arglst) * (argmax + 1));
            }

            arglst[index] = strdup(cmd);
            cmd = strtok(NULL, " ");

            index++;
        }

        arglst[index] = NULL;

        for (int i = 0; i < index; i++) {
            printf("%s\n", arglst[i]);
        }
        printf("%d\n", index);

        if (strcmp(arglst[0], "quit") == 0)
            exit(EXIT_SUCCESS);

        if ((cpid = fork()) == -1)
            perror("fork()");
        else if (cpid == 0) {
            if (execvp(arglst[0], arglst) == -1) {
                perror("cmd error");
                exit(EXIT_FAILURE);
            }
        }
        else {
            cpid = wait(NULL);
        }

        for (int i = 0; i < index; i++) {
            free(arglst[i]);
        }
    }

    free(arglst);

    return 0;
}

The original code used malloc and/or strdup on the individual elements of arglst (e.g. arglst[i]).

This makes the code general enough to be used in more complex scenarios. But, as the code is written, the malloc/strdup for the individual elements really isn't necessary.

This is because the cells are fully used [up] at the bottom of the main loop, so we don't need to save them.

We can reuse the cap buffer space on each loop iteration because we do not need any tokens to live on iteration to iteration.

We can simply store the return value from strtok and simplify the code:

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

int
main(void)
{
    char cap[1000];
    char *cmd;

    char **arglst = NULL;
    int argmax = 0;

    pid_t cpid;

    // implement ls, cd, mkdir, chdir, rm, rmdir
    while (1) {
        printf("Enter a command: ");
        fgets(cap,sizeof(cap),stdin);
        cap[strcspn(cap,"\n")] = 0;

        int index = 0;

        cmd = strtok(cap, " ");
        while (cmd != NULL) {
            if (index >= argmax) {
                argmax += 10;
                arglst = realloc(arglst,sizeof(*arglst) * (argmax + 1));
            }

            arglst[index] = cmd;
            cmd = strtok(NULL, " ");

            index++;
        }

        arglst[index] = NULL;

        for (int i = 0; i < index; i++) {
            printf("%s\n", arglst[i]);
        }
        printf("%d\n", index);

        if (strcmp(arglst[0], "quit") == 0)
            exit(EXIT_SUCCESS);

        if ((cpid = fork()) == -1)
            perror("fork()");
        else if (cpid == 0) {
            if (execvp(arglst[0], arglst) == -1) {
                perror("cmd error");
                exit(EXIT_FAILURE);
            }
            exit(EXIT_SUCCESS);
        }
        else {
            cpid = wait(NULL);
        }
    }

    free(arglst);

    return 0;
}
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.