how is strncpy able to copy from source to empty destination?

62 Views Asked by At

I'm trying to implement basic socket program in C which parses HTTP GET requests for now, in parse_http_req function I've declared char* line; and after strncpy call line contains 1st line of HTTP request data. My doubt is I didn't allocate any memory for line before I called strncpy, how is this code working correctly ? Here is my full server.c code (still under development)

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>  
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/in.h>
#include <unistd.h>
#include <asm-generic/socket.h>
#include <string.h>

#define DEFAULT_PORT 8080
#define MAX_REQ_QUEUE_SIZE 10
#define MAX_REQ_SIZE 1024
#define DEFAULT_PROTOCOL "http"

enum Methods {
    GET,
    POST,
    PUT,
    OPTIONS,
    DELETE,
    INVALID_METHOD
};

typedef struct Request {
    enum Methods method;
    char *path;
    char *protocol;
    char *body;
    size_t content_length;
} Request;

enum Methods get_req_method(char *req) {

    if (strcmp(req, "GET")) {
        return GET;
    }
    if (strcmp(req, "POST")) {
        return POST;
    }
    if(strcmp(req, "OPTONS")){
        return OPTIONS;
    }
    if (strcmp(req, "PUT")) {
        return PUT;
    }
    if (strcmp(req, "DELETE")) {
        return DELETE;
    }
    return INVALID_METHOD;    
}

void parse_http_req(char *req, size_t req_size) {
    
    int size = 0;
    
    while ((*(req + size) != '\n') && (size < req_size)) {
        size++;
    }

    printf("size: %d\n", size);

    char *line;

    line = strncpy(line, req, size);
    
    line[size] = '\0';

    printf("First line: %s\n", line);
}

void accept_incoming_request(int socket, struct sockaddr_in *address, socklen_t len) {

    int accept_fd;

    socklen_t socklen = len;
    
    while ((accept_fd  = accept(socket, (struct sockaddr *)address, &len)) > 0) {
        char *buffer = (char *)malloc(sizeof(char) * MAX_REQ_SIZE);

        int recvd_bytes = read(accept_fd, buffer, MAX_REQ_SIZE);

        printf("%s\n%ld\n%d\n\n\n", buffer, strlen(buffer), recvd_bytes);

        parse_http_req(buffer, strlen(buffer));

        free(buffer);

        send(accept_fd, "hello", 5, 0);
        
        close(accept_fd);
    }
    shutdown(socket, SHUT_RDWR);
}

int create_socket(struct sockaddr_in *address, socklen_t len) {
    
    int fd, opt = 1;

    if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket creation failed\n");
        return -1;
    }

    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt,  sizeof(opt))) {  
        perror("setsockopt");  
        return -1;
    } 

    if (bind(fd, (struct sockaddr*)address, len) < 0) {
        perror("Socket bind failed\n");
        return -1;
    }

    return fd;
}

int main(int argc, char *argv[]) {

    short PORT = DEFAULT_PORT;
    
    char *PROTOCOL = DEFAULT_PROTOCOL;

    if (argc > 1) {
        PORT = (short)atoi(argv[1]);
    }

    if (argc > 2) {
        PROTOCOL = argv[2];
    }

    printf("%d %s\n", PORT, PROTOCOL);

    struct sockaddr_in address = {
        sin_family: AF_INET,
        sin_addr: {
            s_addr: INADDR_ANY
        },
        sin_port: htons(PORT)
    };

    int socket;
    
    if ((socket = create_socket(&address, sizeof(address))) < 0) {
        exit(1);
    }

    if (listen(socket, MAX_REQ_QUEUE_SIZE) < 0) {
        perror("Listen failed\n");
        exit(1);
    }

    accept_incoming_request(socket, &address, sizeof(address));

    return 0;
}

If you compile and run server starts at port: 8080 or you can pass desired port as 1st arg like ./server 9000

To test: wget http://localhost:8080/dummy/path

I tried to see if that line strncpy piece of code is valid. Here is my test.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void f1(char *l2, int size) {
    char *l1;
    
    l1 =  strncpy(l1, l2, size);
    
    printf("%s\n", l1);
}

void main() {

    char *l1 = malloc(10);

    char *l2 = "test";

    strncpy(l1, l2, 5);

    printf("%s\n", l1);
     
    free(l1);

    f1(l2, 5);
}

As I expected if I allocate some memory for l1 before calling strncpy like in main it's all good. But, getting Segmentation fault (core dumped) while executing function f1. How is similar piece of code working in my server.c but not in this test.c program

2

There are 2 best solutions below

3
chqrlie On

The code char *line; line = strncpy(line, req, size); has undefined behavior: line is an uninitialized pointer, so you cannot copy anything to it.

My recommendation is you should never use strncpy: it does not do what you think.

In your code, you should instead use char *line = strndup(req, size); which allocates memory and copies the string fragment to it. The memory should be freed after use with free(line).

strndup() first standardized in POSIX finally becomes part of the C Standard in the latest version, so it is available on most systems, but it your target does not have it, it can be defined this way:

#include <stdlib.h>

char *strndup(const char *s, size_t n) {
    char *p;
    size_t i;
    for (i = 0; i < n && s[i] != '\0'; i++)
        continue;
    p = malloc(i + 1);
    if (p != NULL) {
        memcpy(p, s, i);
        p[i] = '\0';
    }
    return p;
}

There are other problems in your code:

  • strcmp(req, "GET") returns 0 is the strings have the same characters, so you should write:
        if (strcmp(req, "GET") == 0) {
            return GET;
        }
    
    or
        if (!strcmp(req, "GET")) {
            return GET;
        }
    
  • you should reverse the order of the tests in while((*(req + size) != '\n') && (size < req_size)) to avoid accessing req[req_size].
  • in accept_incoming_request you should allocate the destination array with an extra byte for the null terminator and set this null byte at the end of the received packet with:

      buffer[recvd_bytes] = '\0';
    
2
John Bode On

In this code

char *line;

line = strncpy(line, req, size);

line is not explicitly initialized to a specific value; since it's an auto (local) variable, its initial value is indeterminate, meaning it could be literally anything. The behavior on writing through an invalid pointer is undefined, meaning that neither the compiler nor the runtime environment are required to handle the situation in any particular way.

In this particular case that indeterminate initial value just happened to correspond to the address of a writable piece of memory, so your code appeared to work without any problems.

However, there's no guarantee you didn't overwrite something important that would have caused a problem later on, or that another thread or process won't write over that memory while you're using it.