strncat-like function breaks during test

I’m writing some library code, and one of my functions is invoking undefined behavior. I strongly suspect the error is in the test code, but I could be wrong. Here is a MCVE exhibiting the bug:

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

static inline char *
strncate(char * restrict dest, char const * restrict src, size_t n)
{
    strncat(dest, src, n);
    return dest + n;
}

#define BUFSZ 4096

int main(void){
    char buffer[BUFSZ];
    char *bufp = buffer;
    char const *strings[] = {
        "Hello",
        " ",
        "World",
        "!"
    };
    size_t stringslen = sizeof strings / sizeof *strings;
    assert(stringslen == 4); // always passes

    // 1
    bufp = strncate(bufp, strings[0], 5);
    bufp = strncate(bufp, strings[1], 1);
    bufp = strncate(bufp, strings[2], 5);
    bufp = strncate(bufp, strings[3], 1);

    assert(bufp - buffer == 12); // always passes
    printf("Result:   '%.*s'\n", (int)(bufp - buffer), buffer);
    puts  ("Expected: 'Hello World!'");
    assert(strncmp(buffer, "Hello World!", 12) == 0); // fails
    return 0;
}

Here is for loop that (1) replaces:

for (size_t i = 0; i < stringslen; ++i) {
    size_t len = strlen(strings[i]);
    assert(len == 5 || len == 1);
    size_t currentoffset = bufp - buffer;

    if (currentoffset + len < BUFSZ)
        bufp = strncate(bufp, strings[i], len);
    else
        break;
}

I must be invoking UB at some point, as this is the (well, one possible) output:

$ make -B tstrncatb && ./tstrncatb
cc     tstrncatb.c   -o tstrncatb
Result:   'Hello Wor'
Expected: 'Hello World!'
tstrncatb.c:34: Assertion failed: 'strncmp(buffer, "Hello World!", 12) == 0'
Aborted

The problem is, I’ve been poring over this code for at least an hour now and I don’t see any obvious logic error.

I don’t believe it’s a GCC bug, as clang exhibits the same behavior.

$ CC=clang make -B tstrncatb && ./tstrncatb
clang     tstrncatb.c   -o tstrncatb
Result:   'Hello Wor'
Expected: 'Hello World!'
tstrncatb.c:34: Assertion failed: 'strncmp(buffer, "Hello World!", 12) == 0'
Aborted

That said, the versions of GCC and clang I’m using are:

$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
braden ~/code/git/bradenlib/headers
$ clang --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

Curiously, the code doesn’t raise any warnings nor trip ASan or valgrind

$ gcc -Wall -Wextra -fsanitize=address tstrncatb.c
braden ~/code/git/bradenlib/headers
$ ./a.out
Result:   '07Hello '
Expected: 'Hello World!'
tstrncatb.c:34: Assertion failed: 'strncmp(buffer, "Hello World!", 12) == 0'
Aborted
braden ~/code/git/bradenlib/headers
$ valgrind ./a.out
==3853767== Memcheck, a memory error detector
==3853767== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==3853767== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==3853767== Command: ./a.out
==3853767==
==3853767==ASan runtime does not come first in initial library list; you should either...
==3853767==
==3853767== HEAP SUMMARY:
==3853767==     in use at exit: 0 bytes in 0 blocks
==3853767==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==3853767==
==3853767== All heap blocks were freed -- no leaks are possible
==3853767==
==3853767== For lists of detected and suppressed errors, rerun with: -s
==3853767== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Additionally, when I run the MCVE on the environment at https://repl.it/languages/c, the bug does not reproduce. This is very strange because the MCVE really isn’t much different from the full program. Therefore, since the MCVE may reproduce the problem in some environments and may not in others, here is the entire thing, in its current state which is exhibiting the bug on my platform:

tstrncatb.c (the test program):

#include <stdio.h>

#include "strncatb.h"
#include "sassert.h"

#define BUFSZ 4096

int main(void){
    char buffer[BUFSZ];
    char *bufp = buffer;
    char const *strings[] = {
        "Hello",
        " ",
        "World",
        "!"
    };
    size_t stringslen = sizeof strings / sizeof *strings;
    sassert(stringslen == 4);

#if 0
    for (size_t i = 0; i < stringslen; ++i) {
        size_t len = strlen(strings[i]);
        sassert(len == 5 || len == 1);
        size_t currentoffset = bufp - buffer;

        if (currentoffset + len < BUFSZ)
            bufp = strncate(bufp, strings[i], len);
        else
            break;
    }
#endif
    bufp = strncate(bufp, strings[0], 5);
    bufp = strncate(bufp, strings[1], 1);
    bufp = strncate(bufp, strings[2], 5);
    bufp = strncate(bufp, strings[3], 1);

    sassert(bufp - buffer == 12);
    printf("Result:   '%.*s'\n", (int)(bufp - buffer), buffer);
    puts  ("Expected: 'Hello World!'");
    sassert(strncmp(buffer, "Hello World!", 12) == 0);
    return 0;
}

strncatb.h (the header lib):

/*
 * Name:
 *     strncatb v1.0
 *
 * Synopsis:
 *     size_t  strncatb  (char * restrict dest, char const * restrict src, size_t n)
 *     size_t  strncate  (char * restrict dest, char const * restrict src, size_t n)
 *
 *     #define  strcatb  (dest, src)
 *     #define  strcate  (dest, src)
 *
 * Arguments:
 *     dest: destination string to copy to
 *     src:  source string to copy from
 *     n:    number of bytes to copy
 *
 * Description:
 *     `strncatb` and `strncate` concatenate two strings. Their behavior
 *     is the same as strncat with one exception (see return value)
 *
 *     The `strcatb` and `strcate` macros call their respective functions
 *     with strlen(src) as its third argument.
 *
 *     As in strncat, dest and src MUST NOT overlap. If they do, the
 *     behavior is undefined as per restrict semantics.
 *
 * Return Value:
 *     The b stands for bytes. The e stands for end. These suffixes refer
 *     to the return value
 *
 *     The b functions return the number of bytes copied
 *
 *     The e functions return a pointer to the byte after the last byte
 *     copied, so that calls can be nested or chained.
 *
 * Example:
 *     See tstrncatb.c
 */
#ifndef BRADENLIB_STRNCATB_H
#define BRADENLIB_STRNCATB_H

#include <string.h>

#define strcatb(dest, src) \
    strncatb(dest, src, strlen(src))

#define strcate(dest, src) \
    strncate(dest, src, strlen(src))

static inline size_t
strncatb(char * restrict dest, char const * restrict src, size_t n)
{
    strncat(dest, src, n);
    return n;
}

static inline char *
strncate(char * restrict dest, char const * restrict src, size_t n)
{
    strncat(dest, src, n);
    return dest + n;
}

#endif // BRADENLIB_STRNCATB_H

sassert.h (another header lib–I seriously doubt this is related to the issue):

/*
 * Name:
 *     sassert v1.0
 *
 * Synopsis:
 *     #define sassert(expr)
 *
 * Arguments:
 *     expr: expression to test
 *
 * Description:
 *     Simple assert macro, to replace stdc assert. Checks for NDEBUG
 *     macro
 */
#ifndef BRADENLIB_SASSERT_H
#define BRADENLIB_SASSERT_H

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

#ifdef NDEBUG
#    define sassert(expr) (void)(expr)
#else
#    define sassert(expr) \
        if (!(expr)) { \
            fprintf(stderr, "%s:%u: Assertion failed: '%s'\n", __FILE__, __LINE__, #expr); \
            abort(); \
        }
#endif // NDEBUG

#endif // BRADENLIB_SASSERT_H

I’m happy to answer any questions or provide additional information about my environment. I don’t ask a lot of questions on SO, so forgive me if I’ve committed a faux pas.

>Solution :

A very long question about a very trivial problem.

The solution:

    char buffer[BUFSZ] = {0};

Your buffer is not initialized and you invoke UB when appending the string to it.

https://godbolt.org/z/o6Mfv83M8

e. This is very strange because the MCVE really isn’t much different
from the full program. Therefore, since the MCVE may reproduce the
problem in some environments and may not in others

No, it is not strange – it is exactly how UB may express itself.

Leave a Reply