Is there a way to check for string content after a null-terminating byte?

Viewed 201

I want to pipe an input to my program that only accepts lines that are valid for my regular expression, i.e. a number of max length 3, followed by at least one white space character, followed by a negative or positive number of max length 7.

The following call

echo -e '1 1\n1 1\x00junk' | ./myProgram

gets through my regular expression but shouldn't. I guess its cause of getline(), that my regular expression only gets the content in front of the null-terminating byte and ignores everything after.

Is there a way to check for the content after a null-terminating byte without potentially violating the access of the allocated memory of my string, so that the given call ends in an error ?

#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <regex.h>
#include <errno.h>

int main() {
    int reg;
    regex_t regex;
    char *regexStr = "^[0-9]{1,3} +-?[0-9]{1,7}(\n|\r|\r\n|0a|0d0a)?$";
    
    int size = 16;
    char *buffer = malloc(size * sizeof(char));
    size_t len = size;
    ssize_t nread;
    
    if (regcomp(&regex, regexStr, REG_EXTENDED)) {
        fprintf(stderr, "Couldn't compile regular expression.\n");
        return -1;
    }
    
    while ((nread = getline(&buffer, &len, stdin)) != EOF) {
        if (errno == ENOMEM) {    //  error if getline() couldnt allocate buffer
            fprintf(stderr, "Couldn't allocate enough memory.\n");
            return -1;
        }
        
        reg = regexec(&regex, buffer, 0, NULL, 0);
        
        if (reg == REG_NOMATCH) {     //input invalid if regular expression doesnt match with line
            fprintf(stderr, "Input invalid.\n");
            return -1;
        }
        
        printf("%s", buffer);
    }
    printf("\n");
    printf("Input was valid.\n");
        
    return 0;
}
2 Answers

Matching null bytes with regexec is tricky but could be achieved on some architectures with the optional flag REG_STARTEND as documented by KamilCuk, but this feature is non standard.

There is a simple solution for POSIX systems: unlike fgets(), getline() returns the number of bytes read from the stream, so you can detect if any of these bytes is a null byte by comparing nread with strlen(buffer).

Here is a modified version, with some other fixes:

#define _GNU_SOURCE

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

int main() {
    int reg;
    regex_t regex;
    const char *regexStr = "^[0-9]{1,3} +-?[0-9]{1,7}(\n|\r|\r\n)?$";
    char *buffer = NULL;  // no need to pre-allocate buffer
    size_t size = 0;
    ssize_t nread;
    
    if (regcomp(&regex, regexStr, REG_EXTENDED)) {
        fprintf(stderr, "Couldn't compile regular expression.\n");
        return -1;
    }

    for (;;) {
        errno = 0;  // set errno so we can test it after getline
        nread = getline(&buffer, &size, stdin);
        if (nread < 0) {
            if (errno == ENOMEM) {
                // error if getline() couldnt allocate buffer
                fprintf(stderr, "Couldn't allocate enough memory.\n");
                return -1;
            }
            break;
        }
        if (strlen(buffer) != (size_t)nread) {
            fprintf(stderr, "Invalid input: contains null bytes\n");
            return -1;
        }
        reg = regexec(&regex, buffer, 0, NULL, 0);
        if (reg == REG_NOMATCH) {
            // input invalid if regular expression doesn't match line contents
            fprintf(stderr, "Input invalid.\n");
            return -1;
        }
        printf("%s", buffer);
    }
    printf("\n");
    printf("Input was valid.\n");
    return 0;
}

From man regexec:

   REG_STARTEND
          Use  pmatch[0]  on the input string, starting at byte pmatch[0].rm_so and ending before byte pmatch[0].rm_eo.  This allows
          matching embedded NUL bytes and avoids a strlen(3) on large strings.  It does not use nmatch on input, and does not change
          REG_NOTBOL or REG_NEWLINE processing.  This flag is a BSD extension, not present in POSIX.

Use REG_STARTEND. Like so:

     regmatch_t match[1] = {0};
     match[0].rm_so = 0;
     match[0].rm_eo = nread;
     reg = regexec(&regex, buffer, 0, match, REG_STARTEND);

results in:

$ echo -e '1 1\n1 1\x00junk' | ./a.out
1 1
Input invalid.
Related