Sharp edges

Three well-known threaded-C footguns from the C library -- getenv/setenv races, the process-global rand(), and strncpy's missing NUL -- and the thin public helpers that file them down.

---

Sharp edges

  1. Sharp edges
    1. 1. getenv / setenv are not thread-safe against each other
    2. 2. rand() / random() are process-global and not thread-safe
    3. 3. strncpy does not NUL-terminate
    4. Putting it together

C’s standard library predates threads, and a few of its interfaces are outright hazardous once more than one thread is running. libxtc exposes three small, thread-safe replacements so a consumer never has to reach for the raw, racy primitives. All of them are in xtc.h.

1. getenv / setenv are not thread-safe against each other

setenv(3) may reallocate the environ block that a concurrent getenv(3) is walking. Two threads – one reading, one writing – can therefore crash or read freed memory. Worse, the pointer getenv returns can be invalidated by a later setenv from any thread.

xtc_env_get and xtc_env_set serialize every access on a process-wide lock. xtc_env_get copies the value into a small per-thread buffer under that lock, so the pointer it hands back cannot be pulled out from under you by another thread’s xtc_env_set. The pointer is valid until the next xtc_env_get on the same thread; copy it if you need to keep it.

if (xtc_env_set("XTC_DEMO", "on", /*overwrite=*/1) != XTC_OK)
        /* handle XTC_E_INVAL / XTC_E_NOMEM */;
const char *v = xtc_env_get("XTC_DEMO");   /* NULL if unset */

2. rand() / random() are process-global and not thread-safe

rand(3) and random(3) keep a single hidden state for the whole process. Draw from two threads at once and you get a data race and a correlated, useless stream.

xtc_rand_u64 is a per-thread splitmix64: each thread has its own state, so there is no shared-state race and no contention. xtc_rand_seed makes the calling thread’s stream reproducible – the same seed replays the same sequence, which is exactly what you want for tests and for deterministic jitter/backoff. An un-seeded thread auto-seeds from the clock so distinct threads get distinct default streams.

xtc_rand_seed(42);
uint64_t x = xtc_rand_u64();   /* reproducible from seed 42 */

It is not a cryptographic generator – do not use it for keys or security tokens.

A future release may hook this into the deterministic-simulation clock (DST) so a replayed run draws a reproducible sequence; today it is simply a clean, thread-safe, seedable source.

3. strncpy does not NUL-terminate

strncpy(3) omits the terminator when the source is at least as long as the buffer, leaving an unterminated string that the next strlen/printf walks off the end of. strncat’s count is the number of source bytes, not the buffer size – another classic overflow.

xtc_strlcpy and xtc_strlcat use the BSD contract: they always NUL-terminate when dstsize > 0, and they return the total length they tried to create. A return value >= dstsize is the truncation signal.

char buf[8];
if (xtc_strlcpy(buf, "verylongvalue", sizeof buf) >= sizeof buf)
        /* truncated; buf is still a valid C string */;

Putting it together

The following program is compiled and run by the documentation test gate, so it is guaranteed to build against the current API:


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

#include "xtc.h"

int
main(void)
{
	char path[16];
	uint64_t a, b;

	/* 1. Thread-safe env: set then read back.  Guarded internally so a
	 *    concurrent xtc_env_set from another thread cannot invalidate
	 *    the pointer xtc_env_get returns. */
	if (xtc_env_set("XTC_DEMO", "on", 1) != XTC_OK)
		return 1;
	printf("XTC_DEMO=%s\n", xtc_env_get("XTC_DEMO"));

	/* 2. Seedable per-thread PRNG: the same seed replays the same
	 *    stream (each thread has its own state). */
	xtc_rand_seed(42);
	a = xtc_rand_u64();
	xtc_rand_seed(42);
	b = xtc_rand_u64();
	if (a != b) {
		fprintf(stderr, "seed not reproducible\n");
		return 1;
	}
	printf("rand(42) reproducible: %llu\n", (unsigned long long)a);

	/* 3. Bounded copy: always NUL-terminates; the return is the length
	 *    it TRIED to create, so >= dstsize means truncated. */
	if (xtc_strlcpy(path, "verylongvalue", sizeof path) >= sizeof path)
		printf("truncated to \"%s\"\n", path);

	return 0;
}

Tested source: docs/_includes/snippets/05_sharp_edges.c

See xtc_env(3) for the full contract of each function.