Search code examples
cnullpipestrcat

How do strcat() and read() work with '\0' in C


here's my whole code first :

 1. #include <stdio.h>
 2. #include <stdlib.h>
 3. #include <unistd.h>
 4. #include <sys/wait.h>
 5. #include <string.h>
 6. int main(int argc, char *argv[]) {
 7.     int p[2]; // p[0]: file descriptor for read end of pipe
 8.               // p[1]: file descriptor for write end of pipe
 9.     if (pipe(p) < 0) exit(1);
 10.    int rc1 = fork();
 11.    if (rc1 < 0){ fprintf(stderr, "fork error\n"); }
 12.    else if (rc1 == 0){ write(p[1], "1st child output",
 13.                              sizeof("1st child output")); }
 14.    else{
 15.        int rc2 = fork();
 16.        if (rc2 < 0){ fprintf(stderr, "fork error\n"); }
 17.        else if (rc2 == 0){
 18.            printf("2st child output\n");
 19.            char *_1st_child_out;
 20.            read(p[0], _1st_child_out, sizeof("1st child output"));
 21.            strcat(_1st_child_out, ", AFTER PIPE YA FOOL");
 22.            printf("%s\n", _1st_child_out);
 23.        }
 24.    }
 25. }

if i initialize 19:13:

char *_1st_child_out;

with a '\0' or NULL, the string stays empty and 22:13:

printf("%s\n", _1st_child_out);

prints nothing, so how do strcat() and read() work? should i not insert any null terminators before invoking them? what about garbage values?


Solution

  • There are few bugs in your code, here are the my observation.

    Case 1 :- In your code you are calling fork() two times, pipe write end p[1] contains some data 1st child output in first fork() rc1 process, but your code tried to read form p[0] in second rc2 process.

    you should check read() return value whether it's success or not or may be it's reading from wrong/uninitialized file descriptors. This

        char *_1st_child_out = NULL;
        /* for process rc2, p[0] contains nothing, so what read() will read from p[0] ?? */
        int ret = read(p[0], _1st_child_out, sizeof("1st child output"));
        if(ret == -1) {
              perror("read");
              /* error handling */
       }
    

    Since data written into p[1] in rc1 process, not in rc2 process, but here when you tries to read from p[0] it gives you

    read: Bad address

    Case 2 :- To overcome above problem one way is

    int main(int argc, char *argv[]) {
            int p[2]; // p[0]: file descriptor for read end of pipe
            // p[1]: file descriptor for write end of pipe
            if (pipe(p) < 0) exit(1);
            int rc1 = fork();
            if (rc1 < 0){ fprintf(stderr, "fork error\n"); }
            else if (rc1 == 0){
                    write(p[1], "1st child output",sizeof("1st child output"));
            }
            else{
                    char *_1st_child_out = NULL;
    
                    /* read() will read from p[0] and store into _1st_child_out but _1st_child_out not holding any valid memory ? So it causes Undefined behavior */
                    int ret = read(p[0], _1st_child_out, sizeof("1st child output"));
                    if(ret == -1) {
                    perror("read");
                    /* error handling */
                    }
                    strcat(_1st_child_out, ", AFTER PIPE YA FOOL");
                    printf("%s\n", _1st_child_out);
            }
            return 0;
    }
    

    Here _1st_child_out is a pointer and pointers should have valid memory location. you can initialize with NULL which is (void*)0, that's valid but not with \0 as it's just a single character.

    But when you initialize _1st_child_out with NULL and read the data from p[0] & store into _1st_child_out, what it will store into it ? It causes segmentation fault and it's also undefined behavior.

    So it's better to allocate memory dynamically for _1st_child_out and then call read() or create the stack allocated array like

    char _1st_child_out[10];
    

    Here is the sample working code

    int main(int argc, char *argv[]) {
            int p[2]; // p[0]: file descriptor for read end of pipe
            // p[1]: file descriptor for write end of pipe
            if (pipe(p) < 0) exit(1);
            int rc1 = fork();
            if (rc1 < 0){ fprintf(stderr, "fork error\n"); }
            else if (rc1 == 0){
                    write(p[1], "1st child output",sizeof("1st child output"));
            }
            else{
    
                    char *_1st_child_out = malloc(BYTE); /* define BYTE value as how much memory needed, and free the dynamically allocated memory once job is done */
                    int ret = read(p[0], _1st_child_out, sizeof("1st child output"));
                    if(ret == -1) {
                            perror("read");
                            /* error handling */
                    }
                    /* make sure _1st_child_out has enough memory space to concatenate */
                    strcat(_1st_child_out, ", AFTER PIPE YA FOOL");
                    printf("%s\n", _1st_child_out);
            }
            return 0;
    }
    

    Note : Use strncat() instead of strcat(), reason you can find from manual page of strcat() https://linux.die.net/man/3/strcat It says

    The strcat() function appends the src string to the dest string, overwriting the terminating null byte ('\0') at the end of dest, and then adds a terminating null byte. The strings may not overlap, and the dest string must have enough space for the result. If dest is not large enough, program behavior is unpredictable; buffer over‐ runs are a favorite avenue for attacking secure programs.

       The strncat() function is similar, except that
    
       *  it will use at most n bytes from src; and
    
       *  src does not need to be null-terminated if it contains n or more bytes.
    
       As with `strcat()`, the resulting string in dest is always null-terminated.