Files
httrack/tests/threadattrfail.c
Xavier Roche 97d7da5f19 hts_newthread() leaves the pthread attributes object undestroyed when thread creation fails (#807)
* hts_newthread() leaves the pthread attributes object undestroyed when thread creation fails

hts_newthread() folds pthread_attr_init(), pthread_attr_setstacksize() and
pthread_create() into one short-circuit condition, and destroys the attributes
object only on the branch where all three succeeded. If setstacksize or create
fails (EAGAIN under thread or memory exhaustion being the realistic case), attr
stays initialised for good. glibc allocates nothing for a default-initialised
attr, so nothing actually leaks on Linux. This is a portability and hygiene fix
rather than a live bug.

The init has to come out of the chain instead of just gaining a destroy call:
the shared condition cannot tell which of the three failed, and destroying an
object pthread_attr_init() never initialised is undefined.

The imbalance is invisible to a leak checker, so the test counts it directly. A
small LD_PRELOAD shim refuses pthread_create() and records which attributes
objects nobody destroyed afterwards: 1 undestroyed on master, 0 with the fix.

Closes #772

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

* Review fixes: tighter comments, and only attr-bearing refusals count

Condense the two-line note above pthread_attr_init(), fix the "so that is can
be independent" typo on the line the diff moves, and count a refused spawn as
tested only when it carried an attributes object, so a NULL-attr create from
elsewhere cannot satisfy the guard on its own. Note why the shim's fixed slot
count and unlocked counters are enough.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Xavier Roche <roche@httrack.com>

---------

Signed-off-by: Xavier Roche <roche@httrack.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 18:36:53 +00:00

84 lines
2.7 KiB
C

/* Forces pthread_create() to fail for 113_engine-threadattr-leak.test, and
tracks which attributes objects were left undestroyed after that failure
(#772). glibc allocates nothing for a default attr, so the imbalance is
invisible to a leak checker and has to be counted here. */
#define _GNU_SOURCE
#include <dlfcn.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* The tree builds with -fvisibility=hidden, which would hide the interposer. */
#define SHIM_EXPORT __attribute__((visibility("default")))
/* Every create is refused in the only mode this shim runs in, so the process
stays single-threaded: unlocked counters and a fixed 64 slots are enough. */
#define PENDING_MAX 64
static const void *pending[PENDING_MAX];
static unsigned int pending_n;
static unsigned int sabotaged;
SHIM_EXPORT int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start)(void *), void *arg);
SHIM_EXPORT int pthread_attr_destroy(pthread_attr_t *attr);
SHIM_EXPORT int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start)(void *), void *arg) {
static int (*real_create)(pthread_t *, const pthread_attr_t *,
void *(*) (void *), void *) = NULL;
if (getenv("THREADATTRFAIL") != NULL) {
/* only an attr-bearing spawn can leak one, so only those count as tested */
if (attr != NULL) {
sabotaged++;
if (pending_n < PENDING_MAX)
pending[pending_n++] = attr;
}
return EAGAIN;
}
if (real_create == NULL) {
*(void **) &real_create = dlsym(RTLD_NEXT, "pthread_create");
if (real_create == NULL)
return ENOSYS;
}
return real_create(thread, attr, start, arg);
}
SHIM_EXPORT int pthread_attr_destroy(pthread_attr_t *attr) {
static int (*real_destroy)(pthread_attr_t *) = NULL;
unsigned int i;
/* drop one entry, so an address reused by a later init still counts once */
for (i = pending_n; i > 0; i--) {
if (pending[i - 1] == attr) {
memmove(&pending[i - 1], &pending[i],
(pending_n - i) * sizeof(pending[0]));
pending_n--;
break;
}
}
if (real_destroy == NULL) {
*(void **) &real_destroy = dlsym(RTLD_NEXT, "pthread_attr_destroy");
if (real_destroy == NULL)
return ENOSYS;
}
return real_destroy(attr);
}
__attribute__((destructor)) static void threadattrfail_report(void) {
const char *const path = getenv("THREADATTRFAIL_LOG");
FILE *fp;
if (path == NULL)
return;
fp = fopen(path, "wb");
if (fp == NULL)
return;
fprintf(fp, "sabotaged %u\nundestroyed %u\n", sabotaged, pending_n);
fclose(fp);
}