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.
CodePudding user response:
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.